weaver-work-cli 0.1.36 → 0.1.39
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 +4 -2
- package/dist/chunks/{archive-XBVH7JU7.js → archive-ENZYBBME.js} +559 -68
- package/dist/chunks/{archive-oa-MZM3EKFQ.js → archive-oa-YWLEC5V4.js} +4 -3
- package/dist/chunks/{asset-4XO7UOUW.js → asset-OOV6DWPA.js} +8 -7
- package/dist/chunks/asset-idle-RE632MY5.js +670 -0
- package/dist/chunks/{attachment-content-VXSWANAC.js → attachment-content-RWN2FCLL.js} +3 -3
- package/dist/chunks/{blog-PGZKOPSC.js → blog-PEPOJAL7.js} +5 -4
- package/dist/chunks/{caizhilian-BH4ZVGUN.js → caizhilian-WYIQDVPJ.js} +5 -4
- package/dist/chunks/{calendar-MXEMTIO4.js → calendar-CIARBAOX.js} +4 -3
- package/dist/chunks/{chunk-NCOVUAZK.js → chunk-36DIFZAH.js} +4 -3
- package/dist/chunks/{chunk-I6QGIBDM.js → chunk-BJGXMFGD.js} +4 -3
- package/dist/chunks/{chunk-TC6MZAO3.js → chunk-CQHGNCK2.js} +52 -2
- package/dist/chunks/{chunk-WXFPWCXG.js → chunk-EINRJHQ3.js} +3 -3
- package/dist/chunks/{chunk-JUXVYKII.js → chunk-EZGFHJFW.js} +201 -19
- package/dist/chunks/{chunk-VPUTVEMC.js → chunk-F2EQXUEE.js} +436 -290
- package/dist/chunks/{chunk-JYKABYCV.js → chunk-GVPJZXJ2.js} +3 -3
- package/dist/chunks/{chunk-6LLJFKJE.js → chunk-HJYVHXUQ.js} +1 -1
- package/dist/chunks/{chunk-T72N74ZT.js → chunk-I2HB737U.js} +5 -4
- package/dist/chunks/{chunk-VPXOPQJI.js → chunk-K3RUEEOL.js} +4 -2
- package/dist/chunks/{chunk-HXJQI5NM.js → chunk-KTCB5R5H.js} +3 -2
- package/dist/chunks/{chunk-XMGSYD6S.js → chunk-L4N7B7ZD.js} +4 -4
- package/dist/chunks/{chunk-QYGO5EE2.js → chunk-MUQUHKPG.js} +4 -3
- package/dist/chunks/{chunk-QHFANTX7.js → chunk-NLK2RZKG.js} +1348 -90
- package/dist/chunks/{chunk-F746NKNF.js → chunk-P2DAISYW.js} +1 -1
- package/dist/chunks/{chunk-BPE37FPB.js → chunk-T7JDJJ7H.js} +3 -3
- package/dist/chunks/{chunk-JNG42JKG.js → chunk-XXJOSZTQ.js} +6 -6
- package/dist/chunks/{chunk-5DQEEGCC.js → chunk-YS5DPN4T.js} +1 -1
- package/dist/chunks/{chunk-EG6ZU444.js → chunk-ZUX3P6BQ.js} +10 -5
- package/dist/chunks/{chunk-JCSN3JGM.js → chunk-ZXRNHZTE.js} +7 -6
- package/dist/chunks/{conflict-JAB2LJEZ.js → conflict-DRMDOL56.js} +2 -2
- package/dist/chunks/{contract-XI2EEIZU.js → contract-73MERZWN.js} +4 -3
- package/dist/chunks/{cowork-SCPGLBA4.js → cowork-HNGBPD7A.js} +4 -3
- package/dist/chunks/{ebuilder-form-EYLHNOTA.js → ebuilder-form-OY6DVNRK.js} +3 -3
- package/dist/chunks/{ehr-EAP77IMT.js → ehr-RAQNEQO7.js} +4 -3
- package/dist/chunks/{esb-2TGGXEE7.js → esb-6XV7C7YV.js} +4 -4
- package/dist/chunks/{fna-2X7ZE7BU.js → fna-XTSRB7G7.js} +4 -3
- package/dist/chunks/{host-Q4LZGCRU.js → host-3F5UM5GN.js} +3 -3
- package/dist/chunks/{host-XCLDDXKQ.js → host-A7J6GDUP.js} +3 -3
- package/dist/chunks/{host-I4NRK4MX.js → host-VGPVP7YF.js} +3 -3
- package/dist/chunks/{host-C6EL2RKL.js → host-YAQAPYNP.js} +3 -3
- package/dist/chunks/{host-UEI5FGDM.js → host-YMBXWFWX.js} +3 -3
- package/dist/chunks/{hrm-YXERJM76.js → hrm-TLHKC545.js} +3 -3
- package/dist/chunks/{im-A4VZN2TQ.js → im-YOUCFMUK.js} +3 -3
- package/dist/chunks/{invoice-2Z6YPMIS.js → invoice-WNBS2B4C.js} +3 -3
- package/dist/chunks/{jiuchuanhui-AR5GMKZB.js → jiuchuanhui-XABWS5RN.js} +4 -3
- package/dist/chunks/{lazy-dispatch-QXW7P4X3.js → lazy-dispatch-NLZ5UQQI.js} +5 -5
- package/dist/chunks/{mail-7GZ6CPBJ.js → mail-LRYMBFNL.js} +4 -3
- package/dist/chunks/{meeting-3NMGA3MB.js → meeting-ESXL6MFI.js} +13 -12
- package/dist/chunks/{okr-BZ6XUOAG.js → okr-EOV65SSF.js} +4 -3
- package/dist/chunks/{plan-DR5CMUYP.js → plan-YFPVKW6Y.js} +4 -3
- package/dist/chunks/{project-6J63MK33.js → project-MDVZUYQW.js} +4 -3
- package/dist/chunks/{registry-VRTNC6UR.js → registry-QCLZEDAP.js} +8 -8
- package/dist/chunks/{registry-KDD7DHNK.js → registry-VT5Y2HLA.js} +4 -4
- package/dist/chunks/{registry-TKX2E7C7.js → registry-YX4CSEMD.js} +3 -3
- package/dist/chunks/{room-recommend-XCWRMB5P.js → room-recommend-L3MWH4JW.js} +4 -4
- package/dist/chunks/{schedule-IVBICP32.js → schedule-UASAVDVD.js} +4 -4
- package/dist/chunks/{workflow-D74NFK3C.js → workflow-KDGIUCNW.js} +9 -6
- package/dist/core/output.js +16 -6
- package/dist/index.js +33 -28
- package/dist/internal/e10/auth/messages.js +1 -0
- package/dist/internal/e10/auth-recovery.js +127 -7
- package/dist/internal/e10/client.js +16 -2
- package/dist/internal/e10/context.js +16 -3
- package/dist/internal/e10/request-runtime.js +106 -8
- package/dist/shortcuts/archive/host.js +5 -2
- package/dist/shortcuts/archive/manifest.js +1 -1
- package/dist/shortcuts/archive/operations/borrow-list.js +13 -1
- package/dist/shortcuts/archive/operations/flow-url.js +1 -1
- package/dist/shortcuts/archive/operations/prelib.js +472 -0
- package/dist/shortcuts/archive/operations/registry.js +2 -0
- package/dist/shortcuts/archive/operations/upload.js +1 -1
- package/dist/shortcuts/archive-oa/host.js +2 -1
- package/dist/shortcuts/asset/host.js +6 -5
- package/dist/shortcuts/asset-idle/errors.js +44 -0
- package/dist/shortcuts/asset-idle/host.js +161 -0
- package/dist/shortcuts/asset-idle/index.js +114 -0
- package/dist/shortcuts/asset-idle/manifest.js +159 -0
- package/dist/shortcuts/asset-idle/operations/idle.js +47 -0
- package/dist/shortcuts/asset-idle/operations/registry.js +33 -0
- package/dist/shortcuts/asset-idle/operations/shared.js +142 -0
- package/dist/shortcuts/asset-idle/operations/types.js +9 -0
- package/dist/shortcuts/asset-idle/operations.js +15 -0
- 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 +2 -1
- package/dist/shortcuts/ebuilder-form/host.js +3 -1
- package/dist/shortcuts/ehr/host.js +2 -1
- package/dist/shortcuts/esb/host.js +2 -1
- package/dist/shortcuts/fna/host.js +2 -1
- 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 +2 -1
- package/dist/shortcuts/meeting/host.js +3 -2
- package/dist/shortcuts/meeting/invitation/text.js +71 -0
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +1073 -50
- package/dist/shortcuts/meeting/operations/eb/crm-search.js +478 -0
- package/dist/shortcuts/meeting/operations/eb/factory.js +60 -1
- package/dist/shortcuts/meeting/operations/eb/index.js +2 -0
- package/dist/shortcuts/meeting/operations/invitation.js +122 -9
- 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/host.js +3 -0
- package/dist/shortcuts/workflow/index.js +1 -1
- package/dist/shortcuts/workflow/operations/form.js +44 -11
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupRules.js +77 -26
- package/dist/shortcuts/workflow/operations/todoSmartConvert/aigroupStore.js +6 -0
- package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +59 -117
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +44 -7
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +37 -29
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +20 -6
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +22 -18
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +34 -10
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +4 -2
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +9 -2
- package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +110 -20
- package/docs/_catalog.md +3 -0
- package/docs/asset-idle.md +45 -0
- package/docs/e10-auth.md +1 -1
- package/docs/todo-smart-convert/subgroup-rules.md +1 -1
- package/package.json +1 -1
- package/scripts/package-skill.mjs +2 -0
- package/skill-template/business-info.json +14 -1
- package/skill-template/domains/asset-idle.md +13 -0
- package/skill-template/domains/wenshuding.md +3 -0
- package/skills/weaver-e10-archive-oa/SKILL.md +1 -0
- package/skills/weaver-e10-archive-oa/references/my-borrow.md +49 -0
- package/skills/weaver-e10-asset-idle/SKILL.md +73 -0
- package/skills/weaver-e10-asset-idle/references/idle-list.md +62 -0
- package/skills/weaver-e10-asset-idle/references/idle-stats-and-charts.md +65 -0
- package/skills/weaver-e10-asset-idle/references/source-manifest.json +193 -0
- package/skills/weaver-e10-asset-idle/references/widget-style.md +205 -0
- package/skills/weaver-e10-caizhilian/SKILL.md +29 -11
- package/skills/weaver-e10-caizhilian/references/auth-and-install.md +12 -0
- package/skills/weaver-e10-caizhilian/references/files.md +1 -1
- package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +1 -1
- package/skills/weaver-e10-caizhilian/references/source-manifest.json +45 -45
- package/skills/weaver-e10-cowork/SKILL.md +2 -0
- package/skills/weaver-e10-jucailin/SKILL.md +2 -0
- package/skills/weaver-e10-jucailin/references/recruit-read.md +2 -0
- package/skills/weaver-e10-jucailin/references/recruit-write.md +1 -1
- package/skills/weaver-e10-mail/SKILL.md +3 -0
- package/skills/weaver-e10-meeting/SKILL.md +250 -301
- package/skills/weaver-e10-meeting/references/meeting-eb.md +203 -0
- package/skills/weaver-e10-meeting/references/meeting-invitation.md +37 -5
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
- package/skills/weaver-e10-shared/SKILL.md +5 -5
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +3 -3
- package/skills/weaver-e10-wenshuding/SKILL.md +29 -6
- package/skills/weaver-e10-wenshuding/references/archive-borrow-list.md +14 -0
- package/skills/weaver-e10-wenshuding/references/archive-download.md +2 -0
- package/skills/weaver-e10-wenshuding/references/archive-flow.md +1 -1
- package/skills/weaver-e10-wenshuding/references/archive-prelib-arrange.md +76 -0
- package/skills/weaver-e10-wenshuding/references/archive-prelib-list.md +107 -0
- package/skills/weaver-e10-wenshuding/references/archive-prelib-open.md +59 -0
- package/skills/weaver-e10-wenshuding/references/archive-upload.md +4 -1
- package/skills/weaver-e10-wenshuding/references/source-manifest.json +108 -65
- package/skills/weaver-e10-workflow/SKILL.md +5 -2
- package/skills/weaver-e10-workflow/aigroup/default.json +154 -19
- package/skills/weaver-e10-workflow/references/todo-smart-convert.md +59 -27
- package/skills/weaver-e10-workflow/tools/todo-serve.cjs +113 -25
- package/skills/weaver-e10-workflow/tools/todo-summary.cjs +292 -42
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
|
@@ -5,6 +5,7 @@ import { resolve } from 'node:path';
|
|
|
5
5
|
import { meetingSuccess, schemaOf, SCHEMA_DATETIME_MINUTE } from '../types.js';
|
|
6
6
|
import { meetingValidationError } from '../../errors.js';
|
|
7
7
|
import { fetchEbAppSettings, findCommonFreeSlots } from './conflict.js';
|
|
8
|
+
import { EB_BASE, ebBusinessSuccess, writeBody } from './factory.js';
|
|
8
9
|
import { loadRooms, recommendRooms } from './room-recommend.js';
|
|
9
10
|
/**
|
|
10
11
|
* 通过**流程**新建 EB 会议(`meeting.eb.meeting.create.flow`)。
|
|
@@ -34,11 +35,16 @@ const FIELD_ALIASES = {
|
|
|
34
35
|
customize_address: ['自定义会议地点', '自定义地点'],
|
|
35
36
|
hrm_members: ['参会人员', '参会人'],
|
|
36
37
|
total_member: ['参会人数', '人数'],
|
|
38
|
+
/** 参会客户(CustomerComponent 客户浏览按钮):值是客户 id 列表,不是客户名称文本。 */
|
|
39
|
+
crm_members: ['参会客户', '客户'],
|
|
40
|
+
crm_member_count: ['应到客户数', '客户数'],
|
|
37
41
|
caller: ['召集人', '发起人'],
|
|
38
42
|
contacter: ['联系人'],
|
|
39
43
|
mt_type: ['会议类型'],
|
|
40
44
|
other_remark: ['备注', '其他说明'],
|
|
41
45
|
};
|
|
46
|
+
/** 「召集人 / 联系人」两个人员字段的展示名(自动补齐时用来提示用户)。 */
|
|
47
|
+
const PERSON_FIELD_LABELS = { caller: '召集人', contacter: '联系人' };
|
|
42
48
|
function workflowEnv(env) {
|
|
43
49
|
return env;
|
|
44
50
|
}
|
|
@@ -92,37 +98,171 @@ async function resolveEmployeeNames(env, ids) {
|
|
|
92
98
|
}
|
|
93
99
|
return map;
|
|
94
100
|
}
|
|
95
|
-
/**
|
|
96
|
-
|
|
101
|
+
/**
|
|
102
|
+
* 把「员工 id 或姓名」解析成 `{ id, name }`。
|
|
103
|
+
*
|
|
104
|
+
* ⛔ 2026-10-31 修(用户实测反馈「参会人员没代入表单」):以前只拿这些值当 id 去反查姓名,
|
|
105
|
+
* 传姓名时得到的是 `{ id: "王丹丹", name: "王丹丹" }` —— 浏览按钮按 **id** 落值,
|
|
106
|
+
* 服务端解析不出员工,表单里就渲染成空。现在纯数字当 id、其余当姓名去 `hrm.employee.query`
|
|
107
|
+
* 换真实员工 id;同名有歧义(命中多条且没有全等)时不猜,留给调用方提示。
|
|
108
|
+
*/
|
|
109
|
+
async function resolvePersonTokens(env, tokens) {
|
|
110
|
+
const map = new Map();
|
|
111
|
+
const unique = [...new Set(tokens.map((token) => String(token).trim()).filter((token) => token !== ''))];
|
|
112
|
+
if (unique.length === 0)
|
|
113
|
+
return map;
|
|
114
|
+
const idLike = unique.filter((token) => /^\d{8,}$/.test(token));
|
|
115
|
+
const nameLike = unique.filter((token) => !/^\d{8,}$/.test(token));
|
|
116
|
+
if (idLike.length > 0) {
|
|
117
|
+
const names = await resolveEmployeeNames(env, idLike);
|
|
118
|
+
for (const id of idLike)
|
|
119
|
+
map.set(id, { id, name: names.get(id) ?? id });
|
|
120
|
+
}
|
|
121
|
+
if (nameLike.length > 0) {
|
|
122
|
+
try {
|
|
123
|
+
const { hrmEmployeeOperations } = await import('../../../hrm/operations/employee.js');
|
|
124
|
+
const query = hrmEmployeeOperations.find((operation) => operation.name === 'hrm.employee.query');
|
|
125
|
+
for (const name of nameLike) {
|
|
126
|
+
if (!query)
|
|
127
|
+
break;
|
|
128
|
+
const found = await query.handler({ keyword: name, pageSize: 10 }, env).catch(() => null);
|
|
129
|
+
const rows = (found?.data?.rows ?? []);
|
|
130
|
+
const exact = rows.find((row) => String(row.name ?? '').trim() === name);
|
|
131
|
+
const hit = exact ?? (rows.length === 1 ? rows[0] : undefined);
|
|
132
|
+
const id = String(hit?.id ?? '').trim();
|
|
133
|
+
if (id === '')
|
|
134
|
+
continue;
|
|
135
|
+
map.set(name, { id, name: String(hit?.name ?? name) });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// 查不到就保持未解析,由调用方给出 warning
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return map;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 「召集人 / 联系人」的缺省值 = **当前登录人**(2026-10-31 用户要求)。
|
|
146
|
+
*
|
|
147
|
+
* ⛔ 背景:以前这两个字段只有调用方显式传 `caller` / `contacter` 才写值。换一个账号用
|
|
148
|
+
* ("其他人使用")时就是空的 —— 流程单上「召集人」「联系人」整块空白,表里看不出谁召集的,
|
|
149
|
+
* 邀请函也整段落成【待补充】。默认"自己召集、自己联系"才符合建会的常识。
|
|
150
|
+
*
|
|
151
|
+
* ⛔ 只有**能查到姓名**才返回:Employee 浏览按钮认的是员工 id,在不一致的租户(userId ≠ 员工 id)
|
|
152
|
+
* 里硬填会被工作流判成 `NEEDS_CONFIRMATION`,**提交直接卡住** —— 所以查不到宁可留空 +
|
|
153
|
+
* 提示调用方显式传,也不猜。
|
|
154
|
+
*/
|
|
155
|
+
export async function resolveSelfPerson(env) {
|
|
156
|
+
let session = null;
|
|
157
|
+
try {
|
|
158
|
+
session = await env.host?.getSession?.();
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const id = String(session?.userId ?? '').trim();
|
|
164
|
+
if (id === '' || id === 'unknown')
|
|
165
|
+
return null;
|
|
166
|
+
const names = await resolveEmployeeNames(env, [id]);
|
|
167
|
+
const name = String(names.get(id) ?? '').trim();
|
|
168
|
+
if (name === '')
|
|
169
|
+
return null;
|
|
170
|
+
return { id, name };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 「召集人 / 联系人」缺省补齐(纯函数,便于测试):**只补没传的那个字段**,
|
|
174
|
+
* 显式传了就以传的为准(替他人建会 / 指定别的联系人时不覆盖)。
|
|
175
|
+
*/
|
|
176
|
+
export function applySelfPersonDefaults(args, self) {
|
|
177
|
+
const filled = [];
|
|
178
|
+
const missing = ['caller', 'contacter']
|
|
179
|
+
.filter((field) => String(args[field] ?? '').trim() === '');
|
|
180
|
+
if (missing.length === 0)
|
|
181
|
+
return { filled, warning: '' };
|
|
182
|
+
if (!self) {
|
|
183
|
+
// 两个都缺才算问题:只缺一个(比如只指定了召集人)不提示,交给正常流程。
|
|
184
|
+
return {
|
|
185
|
+
filled,
|
|
186
|
+
warning: missing.length === 2
|
|
187
|
+
? '⚠️ 没能确认当前登录用户对应的员工 ——「召集人」「联系人」会留空,'
|
|
188
|
+
+ '请显式传 caller / contacter(员工 id 或姓名)。'
|
|
189
|
+
: '',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
for (const field of missing) {
|
|
193
|
+
args[field] = self.id;
|
|
194
|
+
filled.push({ field, id: self.id, name: self.name });
|
|
195
|
+
}
|
|
196
|
+
return { filled, warning: '' };
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* 拆「会议室」入参的三种常见写法:`123456`(id)、`会议室名`、`会议室名(123456)`。
|
|
200
|
+
*
|
|
201
|
+
* ⛔ 2026-10-31 真机事故:以前只把**纯数字**当房间 id,其它一律按"名称"原样丢给表单。
|
|
202
|
+
* 于是「测试会议室1」这种(用户实际最常说的写法)**既不查存在性、也不查占用**,
|
|
203
|
+
* 直接进确认摘要 —— 用户到流程页面提交时才被打回,或更糟:占用的房间被当成可用。
|
|
204
|
+
*/
|
|
205
|
+
export function parseRoomRef(input) {
|
|
206
|
+
const text = String(input ?? '').trim();
|
|
207
|
+
if (!text)
|
|
208
|
+
return {};
|
|
209
|
+
const withId = /^(.*?)[((]\s*(\d{4,})\s*[))]$/.exec(text);
|
|
210
|
+
if (withId) {
|
|
211
|
+
const name = withId[1].trim();
|
|
212
|
+
const id = withId[2];
|
|
213
|
+
return name ? { id, name } : { id };
|
|
214
|
+
}
|
|
215
|
+
if (/^\d+$/.test(text))
|
|
216
|
+
return { id: text };
|
|
217
|
+
return { name: text };
|
|
218
|
+
}
|
|
219
|
+
/** 把 address 入参解析成会议室台账里的真实记录(id / 名称 / 名称(id) 都能认)。 */
|
|
220
|
+
export async function resolveAddressRoom(env, input) {
|
|
97
221
|
const text = String(input ?? '').trim();
|
|
98
222
|
if (!text)
|
|
99
223
|
return null;
|
|
224
|
+
const ref = parseRoomRef(text);
|
|
100
225
|
const { loadRooms } = await import('./room-recommend.js');
|
|
226
|
+
let rooms = [];
|
|
101
227
|
try {
|
|
102
|
-
|
|
103
|
-
const byId = rooms.find((room) => room.id === text);
|
|
104
|
-
if (byId)
|
|
105
|
-
return { id: byId.id, name: byId.name };
|
|
106
|
-
const byName = rooms.find((room) => room.name === text);
|
|
107
|
-
if (byName)
|
|
108
|
-
return { id: byName.id, name: byName.name };
|
|
228
|
+
rooms = (await loadRooms(env, {})).rooms;
|
|
109
229
|
}
|
|
110
230
|
catch {
|
|
111
|
-
//
|
|
231
|
+
// 台账读不到 ≠ 房间不存在:标记出来,让调用方不要据此判死
|
|
232
|
+
return { id: ref.id ?? '', name: ref.name ?? text, status: 'unknown', lookupFailed: true };
|
|
233
|
+
}
|
|
234
|
+
const hit = (ref.id ? rooms.find((room) => room.id === ref.id) : undefined)
|
|
235
|
+
?? (ref.name ? rooms.find((room) => room.name === ref.name) : undefined);
|
|
236
|
+
if (hit)
|
|
237
|
+
return { id: hit.id, name: hit.name, status: hit.status, statusText: hit.statusText };
|
|
238
|
+
// 纯 id 且台账里没有(刚建/缓存旧)→ 保留 id 继续走,别拦住
|
|
239
|
+
if (ref.id)
|
|
240
|
+
return { id: ref.id, name: ref.name ?? '', status: 'unknown' };
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
/** 会议室 id → {id,name}(优先会议室缓存,缓存没有就查一次详情)。 */
|
|
244
|
+
async function resolveRoomValue(env, input) {
|
|
245
|
+
const text = String(input ?? '').trim();
|
|
246
|
+
if (!text)
|
|
247
|
+
return null;
|
|
248
|
+
const resolved = await resolveAddressRoom(env, text).catch(() => null);
|
|
249
|
+
if (resolved?.id && !resolved.lookupFailed) {
|
|
250
|
+
return { id: resolved.id, name: resolved.name || resolved.id };
|
|
112
251
|
}
|
|
113
|
-
|
|
252
|
+
const ref = parseRoomRef(text);
|
|
253
|
+
if (ref.id) {
|
|
114
254
|
try {
|
|
115
255
|
const { getMeetingOperation } = await import('../registry.js');
|
|
116
|
-
const detail = await getMeetingOperation('meeting.eb.room.detail').handler({ id:
|
|
256
|
+
const detail = await getMeetingOperation('meeting.eb.room.detail').handler({ id: ref.id }, env);
|
|
117
257
|
const main = detail?.data?.mainTable ?? detail?.data?.data?.mainTable ?? {};
|
|
118
|
-
const name = String(main.room_name ?? '').trim();
|
|
258
|
+
const name = String(main.room_name ?? '').trim() || ref.name || '';
|
|
119
259
|
if (name)
|
|
120
|
-
return { id:
|
|
260
|
+
return { id: ref.id, name };
|
|
121
261
|
}
|
|
122
262
|
catch {
|
|
123
263
|
// 详情也拿不到就只传 id
|
|
124
264
|
}
|
|
125
|
-
return { id:
|
|
265
|
+
return { id: ref.id, name: ref.name || ref.id };
|
|
126
266
|
}
|
|
127
267
|
return null;
|
|
128
268
|
}
|
|
@@ -133,7 +273,7 @@ async function resolveRoomValue(env, input) {
|
|
|
133
273
|
* getRelateData 里匹配,匹配不到就返回 NEEDS_CONFIRMATION("无法唯一解析的选项")——
|
|
134
274
|
* 看起来像"流程建会走不通",其实是值格式不对。这里统一补成对象。
|
|
135
275
|
*/
|
|
136
|
-
async function shapeBrowserValues(env, values) {
|
|
276
|
+
export async function shapeBrowserValues(env, values) {
|
|
137
277
|
const notes = [];
|
|
138
278
|
const shaped = { ...values };
|
|
139
279
|
const readable = {};
|
|
@@ -158,9 +298,9 @@ async function shapeBrowserValues(env, values) {
|
|
|
158
298
|
idPool.push(...ids);
|
|
159
299
|
}
|
|
160
300
|
if (idPool.length > 0) {
|
|
161
|
-
const
|
|
162
|
-
const toObject = (
|
|
163
|
-
const toName = (
|
|
301
|
+
const people = await resolvePersonTokens(env, idPool);
|
|
302
|
+
const toObject = (token) => people.get(token) ?? { id: token, name: token };
|
|
303
|
+
const toName = (token) => people.get(token)?.name ?? token;
|
|
164
304
|
if (shaped.hrm_members !== undefined && shaped.hrm_members !== '') {
|
|
165
305
|
const ids = String(shaped.hrm_members).split(/[,;,;\s]+/).map((item) => item.trim()).filter((item) => item !== '');
|
|
166
306
|
shaped.hrm_members = ids.map(toObject);
|
|
@@ -177,6 +317,121 @@ async function shapeBrowserValues(env, values) {
|
|
|
177
317
|
shaped.contacter = toObject(id);
|
|
178
318
|
readable.contacter = toName(id);
|
|
179
319
|
}
|
|
320
|
+
// 解析不出员工的(同名歧义 / 花名册里没有)要点名,别让表单静默留空
|
|
321
|
+
const unresolved = [...new Set(idPool)].filter((token) => !people.has(token));
|
|
322
|
+
if (unresolved.length > 0) {
|
|
323
|
+
notes.push(`⚠️ 有 ${unresolved.length} 个人员没解析到员工:${unresolved.join('、')} —— `
|
|
324
|
+
+ '表单里的「参会人员/召集人/联系人」可能显示不出来。请改传员工 id,或确认姓名与花名册完全一致。');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// 参会客户(CustomerComponent 客户浏览按钮):与人员浏览按钮同形态 —— `[{id,name}]`。
|
|
328
|
+
// 服务端按 id 认客户,name 只用于列表显示,所以只有 id 时也照样提交(不留空行)。
|
|
329
|
+
if (shaped.crm_members !== undefined && shaped.crm_members !== '') {
|
|
330
|
+
const rows = [];
|
|
331
|
+
const push = (idRaw, nameRaw) => {
|
|
332
|
+
const id = String(idRaw ?? '').trim();
|
|
333
|
+
const name = String(nameRaw ?? '').trim();
|
|
334
|
+
if (id === '' && name === '')
|
|
335
|
+
return;
|
|
336
|
+
rows.push({ id, name });
|
|
337
|
+
};
|
|
338
|
+
// 单个 token 的宽容写法:`客户名(客户id)` 取括号里的 id;纯数字(雪花 id)当 id;
|
|
339
|
+
// 其余一律当**客户名**(2026-10-31 修:以前把纯名称整串当 id,导致「只传客户名」这种
|
|
340
|
+
// 最自然的写法反被判 crm_name_required 拒掉)。
|
|
341
|
+
const pushToken = (tokenRaw) => {
|
|
342
|
+
const token = String(tokenRaw ?? '').trim();
|
|
343
|
+
if (token === '')
|
|
344
|
+
return;
|
|
345
|
+
const matched = token.match(/^(.*?)[((]\s*([0-9A-Za-z_-]+)\s*[))]$/);
|
|
346
|
+
if (matched) {
|
|
347
|
+
push(matched[2], matched[1]);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (/^\d{8,}$/.test(token)) {
|
|
351
|
+
push(token, '');
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
push('', token);
|
|
355
|
+
};
|
|
356
|
+
const raw = shaped.crm_members;
|
|
357
|
+
if (Array.isArray(raw)) {
|
|
358
|
+
for (const item of raw) {
|
|
359
|
+
if (item && typeof item === 'object') {
|
|
360
|
+
const obj = item;
|
|
361
|
+
push(obj.id ?? obj.sjID ?? obj.customerId, obj.name ?? obj.mc);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
// ⛔ 数组里的字符串**也要走 `名称(id)` 解析**(2026-10-31 实测漏了这一步:
|
|
365
|
+
// `["客户名(123)"]` 被整串当成 id 提交,服务端认不出客户、静默存空)。
|
|
366
|
+
// 同时容忍 `["c1, c2"]` 这种"一个元素里塞了多个"的写法。
|
|
367
|
+
for (const token of String(item).split(/[,;,;]+/).map((one) => one.trim()).filter((one) => one !== '')) {
|
|
368
|
+
pushToken(token);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else if (typeof raw === 'object') {
|
|
374
|
+
const obj = raw;
|
|
375
|
+
push(obj.id ?? obj.sjID ?? obj.customerId, obj.name ?? obj.mc);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
// "id1,id2" 或 "客户A(123)" 混写
|
|
379
|
+
for (const token of String(raw).split(/[,;,;]+/).map((item) => item.trim()).filter((item) => item !== '')) {
|
|
380
|
+
pushToken(token);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (rows.length > 0) {
|
|
384
|
+
// 只给了客户名、没给 id ⇒ 用 `crm.search` 按名称补 id(「只传客户名」是最常见的用法)。
|
|
385
|
+
// 查不到就报 `crm_not_found` 让人去确认名称,**不静默丢客户**。
|
|
386
|
+
const nameOnly = rows.filter((row) => row.id === '' && row.name !== '');
|
|
387
|
+
if (nameOnly.length > 0) {
|
|
388
|
+
try {
|
|
389
|
+
const { getMeetingOperation } = await import('../registry.js');
|
|
390
|
+
const search = getMeetingOperation('meeting.eb.crm.search');
|
|
391
|
+
for (const row of nameOnly) {
|
|
392
|
+
const found = await search.handler({ keyword: row.name, limit: 5 }, env).catch(() => null);
|
|
393
|
+
const items = (found?.data?.items ?? []);
|
|
394
|
+
const hit = items.find((item) => String(item?.name ?? '').trim() === row.name) ?? items[0];
|
|
395
|
+
const id = String(hit?.customerId ?? '').trim();
|
|
396
|
+
if (id === '')
|
|
397
|
+
continue;
|
|
398
|
+
row.id = id;
|
|
399
|
+
const fullName = String(hit?.name ?? '').trim();
|
|
400
|
+
if (fullName !== '')
|
|
401
|
+
row.name = fullName;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
// 查询失败按"查不到"处理,下面统一报错
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const unresolved = rows.filter((row) => row.id === '');
|
|
409
|
+
if (unresolved.length > 0) {
|
|
410
|
+
throw meetingValidationError('crm_not_found', `参会客户有 ${unresolved.length} 个在 CRM 里查不到:${unresolved.map((row) => row.name).join('、')}。`
|
|
411
|
+
+ '请确认客户名称,或先到客户表建档;也可以改用「客户名(客户id)」或 `[{id,name}]` 传 crm_members。');
|
|
412
|
+
}
|
|
413
|
+
shaped.crm_members = rows;
|
|
414
|
+
readable.crm_members = rows.map((row) => row.name || row.id).join('、');
|
|
415
|
+
notes.push(`参会客户按客户浏览按钮格式提交:${rows.length} 个客户`);
|
|
416
|
+
/**
|
|
417
|
+
* ⛔ 只有 id、没有名称 ⇒ **必须拦住**(2026-10-31 真机实证)。
|
|
418
|
+
*
|
|
419
|
+
* 客户浏览按钮(`CustomerComponent`)写入时服务端按「名称」落 `dataOptions.content`,
|
|
420
|
+
* 只给 id 的写法提交后会**静默存成空值**(单据详情里 `参会客户` 是 "",不报错、不告警)。
|
|
421
|
+
* 之前"服务端按 id 认客户"的说法是错的,用它就会悄悄丢数据。
|
|
422
|
+
* ⇒ 这里直接拒绝,并给出可用写法;客户名可从 `meeting.eb.crm.search` 的 `items[].name` 拿。
|
|
423
|
+
*/
|
|
424
|
+
const missingNames = rows.filter((row) => row.name === '');
|
|
425
|
+
if (missingNames.length > 0) {
|
|
426
|
+
throw meetingValidationError('crm_name_required', `参会客户有 ${missingNames.length} 个只给了 id、没有客户名称:${missingNames.map((row) => row.id).join('、')}。`
|
|
427
|
+
+ '客户浏览按钮必须带名称才能写入(只给 id 会被服务端静默存成空值)。'
|
|
428
|
+
+ '请先用 `meeting.eb.crm.search` 按客户名查到 `{ customerId, name }`,'
|
|
429
|
+
+ '再以 `[{id,name}]` 或「客户名(客户id)」的形式传 `crm_members`。');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
delete shaped.crm_members;
|
|
434
|
+
}
|
|
180
435
|
}
|
|
181
436
|
// 其余标量字段(会议名称/开始时间/结束时间/会议内容/人数…)原样给邀请函用
|
|
182
437
|
for (const [key, raw] of Object.entries(values)) {
|
|
@@ -197,6 +452,253 @@ function normalizeFieldName(name) {
|
|
|
197
452
|
.replace(/[\s\u3000]+/g, '')
|
|
198
453
|
.trim();
|
|
199
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* 「参会客户明细」明细表(2026-10-31 新增)。
|
|
457
|
+
*
|
|
458
|
+
* 结构:宿主表单里是一张明细表(真机 会议申请流程 = `uf_mt_work_chkhmx`,subFormId `1332370499762806784`),
|
|
459
|
+
* 列为 姓名/性别/职位/手机号码/备注。**subFormId 与列字段 id 各租户不同**,所以一律从
|
|
460
|
+
* `workflow.createForm` 的字段清单里按列名反查,不写死 id。
|
|
461
|
+
*
|
|
462
|
+
* ⛔ 明细字段只有在「发起节点布局」里摆上之后才会出现在 `fields` 里;没摆上(或摆在隐藏行)
|
|
463
|
+
* 时这里解析不到,必须如实告警,不能静默丢行。
|
|
464
|
+
*/
|
|
465
|
+
const CRM_DETAIL_COLUMNS = {
|
|
466
|
+
xm: ['姓名', '客户姓名', '参会人', '参会人姓名'],
|
|
467
|
+
xb: ['性别'],
|
|
468
|
+
zw: ['职位', '职务'],
|
|
469
|
+
sjhm: ['手机号码', '手机号', '手机', '电话', '联系电话'],
|
|
470
|
+
bz: ['备注', '说明'],
|
|
471
|
+
};
|
|
472
|
+
/** 从表单字段清单里认出「参会客户明细」那张表(判据:有「姓名」列,且得分最高)。 */
|
|
473
|
+
export function resolveCrmDetailTarget(fields) {
|
|
474
|
+
const groups = new Map();
|
|
475
|
+
for (const field of fields) {
|
|
476
|
+
const sid = String(field.subFormId ?? '').trim();
|
|
477
|
+
if (!sid)
|
|
478
|
+
continue;
|
|
479
|
+
const list = groups.get(sid);
|
|
480
|
+
if (list)
|
|
481
|
+
list.push(field);
|
|
482
|
+
else
|
|
483
|
+
groups.set(sid, [field]);
|
|
484
|
+
}
|
|
485
|
+
let best = null;
|
|
486
|
+
for (const [subFormId, list] of groups) {
|
|
487
|
+
const columns = {};
|
|
488
|
+
const labels = {};
|
|
489
|
+
for (const [semantic, aliases] of Object.entries(CRM_DETAIL_COLUMNS)) {
|
|
490
|
+
const hit = list.find((field) => aliases.includes(normalizeFieldName(field.name)));
|
|
491
|
+
if (!hit)
|
|
492
|
+
continue;
|
|
493
|
+
columns[semantic] = hit.id;
|
|
494
|
+
labels[semantic] = hit.name;
|
|
495
|
+
}
|
|
496
|
+
// 没有「姓名」列的明细表(会议议题/会议服务…)一律不算
|
|
497
|
+
if (!columns.xm)
|
|
498
|
+
continue;
|
|
499
|
+
const score = Object.keys(columns).length + (columns.sjhm ? 1 : 0) + (columns.zw ? 1 : 0);
|
|
500
|
+
if (!best || score > best.score)
|
|
501
|
+
best = { subFormId, columns, labels, score };
|
|
502
|
+
}
|
|
503
|
+
return best ? { subFormId: best.subFormId, columns: best.columns, labels: best.labels } : null;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* 从「会议名称/内容/备注」里认出用户明确点出的客户名(`客户:中国民航` / `客户是xx` / `参会客户为 xx`)。
|
|
507
|
+
*
|
|
508
|
+
* 存在的理由:模型最常见的跑偏是把客户名写进会议名称/内容就直接提交,`crm_members` 一个都没传,
|
|
509
|
+
* 最后「参会客户」浏览按钮是空的(2026-10-31 真机踩到)。这里只做**识别**,不替用户查 CRM 也不自动写入。
|
|
510
|
+
* 只认带分隔符或「是/为」的显式写法,避免把「客户需求/客户满意度」这类普通词当成客户名。
|
|
511
|
+
*/
|
|
512
|
+
export function detectCrmMentionInText(...texts) {
|
|
513
|
+
for (const text of texts) {
|
|
514
|
+
if (typeof text !== 'string')
|
|
515
|
+
continue;
|
|
516
|
+
const value = text.trim();
|
|
517
|
+
if (value === '')
|
|
518
|
+
continue;
|
|
519
|
+
const hit = /(?:参会|拜访|来访)?客户\s*(?:名称)?\s*(?:是|为|[::])\s*([^\s,,。;;、))]{2,40})/.exec(value);
|
|
520
|
+
if (hit?.[1])
|
|
521
|
+
return hit[1].trim();
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
/** 把调用方给的明细行统一成 `{xm,xb,zw,sjhm,bz}`(中英文键都认;姓名与手机号都空的行直接丢)。 */
|
|
526
|
+
export function normalizeCrmDetailRows(raw) {
|
|
527
|
+
if (raw === undefined || raw === null || raw === '')
|
|
528
|
+
return [];
|
|
529
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
530
|
+
const keyAlias = {
|
|
531
|
+
xm: 'xm', name: 'xm', customername: 'xm', 姓名: 'xm', 客户姓名: 'xm',
|
|
532
|
+
xb: 'xb', gender: 'xb', sex: 'xb', 性别: 'xb',
|
|
533
|
+
zw: 'zw', title: 'zw', position: 'zw', job: 'zw', 职位: 'zw', 职务: 'zw',
|
|
534
|
+
sjhm: 'sjhm', mobile: 'sjhm', phone: 'sjhm', tel: 'sjhm', 手机号码: 'sjhm', 手机号: 'sjhm', 电话: 'sjhm', 联系电话: 'sjhm',
|
|
535
|
+
bz: 'bz', remark: 'bz', note: 'bz', 备注: 'bz', 说明: 'bz',
|
|
536
|
+
};
|
|
537
|
+
const rows = [];
|
|
538
|
+
for (const item of list) {
|
|
539
|
+
if (item === undefined || item === null || typeof item !== 'object')
|
|
540
|
+
continue;
|
|
541
|
+
const row = {};
|
|
542
|
+
for (const [key, value] of Object.entries(item)) {
|
|
543
|
+
const semantic = keyAlias[String(key).trim().toLowerCase()] ?? keyAlias[String(key).trim()];
|
|
544
|
+
if (!semantic)
|
|
545
|
+
continue;
|
|
546
|
+
if (value === undefined || value === null)
|
|
547
|
+
continue;
|
|
548
|
+
const text = String(value).trim();
|
|
549
|
+
if (text === '')
|
|
550
|
+
continue;
|
|
551
|
+
row[semantic] = text;
|
|
552
|
+
}
|
|
553
|
+
if (!row.xm && !row.sjhm)
|
|
554
|
+
continue;
|
|
555
|
+
rows.push(row);
|
|
556
|
+
}
|
|
557
|
+
return rows;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* 从 `crm_members` 入参里取出「客户 + 联系人」。
|
|
561
|
+
*
|
|
562
|
+
* 调用方通常把 `meeting.eb.crm.search` 的 `items` 原样传进来(`[{customerId,name,contacts:[…]}]`),
|
|
563
|
+
* 联系人信息就在里面;只传 id/名称时 contacts 为空,由 `resolveAutoCrmDetailRows` 再去查。
|
|
564
|
+
*/
|
|
565
|
+
export function crmMemberEntriesFromArgs(raw) {
|
|
566
|
+
const list = Array.isArray(raw) ? raw : (raw === undefined || raw === null || raw === '' ? [] : [raw]);
|
|
567
|
+
const out = [];
|
|
568
|
+
const push = (idRaw, nameRaw, contactsRaw) => {
|
|
569
|
+
const id = String(idRaw ?? '').trim();
|
|
570
|
+
const name = String(nameRaw ?? '').trim();
|
|
571
|
+
if (id === '' && name === '')
|
|
572
|
+
return;
|
|
573
|
+
const contacts = (Array.isArray(contactsRaw) ? contactsRaw : [])
|
|
574
|
+
.map((item) => {
|
|
575
|
+
const row = (item ?? {});
|
|
576
|
+
return {
|
|
577
|
+
name: String(row.name ?? row.xm ?? '').trim(),
|
|
578
|
+
mobile: String(row.mobile ?? row.sjhm ?? row.phone ?? '').trim(),
|
|
579
|
+
...(String(row.dept ?? row.bm ?? '').trim() === ''
|
|
580
|
+
? {}
|
|
581
|
+
: { dept: String(row.dept ?? row.bm).trim() }),
|
|
582
|
+
};
|
|
583
|
+
})
|
|
584
|
+
.filter((one) => one.name !== '' || one.mobile !== '');
|
|
585
|
+
out.push({ id: id === '' ? name : id, name, contacts });
|
|
586
|
+
};
|
|
587
|
+
for (const item of list) {
|
|
588
|
+
if (item && typeof item === 'object') {
|
|
589
|
+
const obj = item;
|
|
590
|
+
push(obj.customerId ?? obj.sjID ?? obj.id, obj.name ?? obj.mc, obj.contacts ?? obj.lxr);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
// `"客户名(123)"` / `"123"` 两种纯文本写法
|
|
594
|
+
const token = String(item).trim();
|
|
595
|
+
if (token === '')
|
|
596
|
+
continue;
|
|
597
|
+
const matched = token.match(/^(.*?)[((]\s*([0-9A-Za-z_-]+)\s*[))]$/);
|
|
598
|
+
if (matched)
|
|
599
|
+
push(matched[2], matched[1], undefined);
|
|
600
|
+
else
|
|
601
|
+
push(token, '', undefined);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return out;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* 联系人 → 「参会客户明细」行(姓名 / 手机号 / 备注=部门)。
|
|
608
|
+
*
|
|
609
|
+
* ⛔ 2026-10-31 用户要求:「带出了三个联系人,为什么要让我选,不能直接代入到流程里吗」。
|
|
610
|
+
* 明细表本来就是一人一行,所以**全部带入**,不再纠结"选哪一位";要只留某一位,
|
|
611
|
+
* 调用方显式传 `crm_member_details` 覆盖即可。
|
|
612
|
+
*/
|
|
613
|
+
export function crmContactsToDetailRows(entries) {
|
|
614
|
+
const rows = [];
|
|
615
|
+
const seen = new Set();
|
|
616
|
+
for (const entry of entries) {
|
|
617
|
+
for (const contact of entry.contacts) {
|
|
618
|
+
const xm = contact.name.trim();
|
|
619
|
+
const sjhm = contact.mobile.trim();
|
|
620
|
+
if (xm === '' && sjhm === '')
|
|
621
|
+
continue;
|
|
622
|
+
const key = `${xm}|${sjhm}`;
|
|
623
|
+
if (seen.has(key))
|
|
624
|
+
continue;
|
|
625
|
+
seen.add(key);
|
|
626
|
+
const row = { xm: xm === '' ? sjhm : xm };
|
|
627
|
+
if (sjhm !== '')
|
|
628
|
+
row.sjhm = sjhm;
|
|
629
|
+
if (contact.dept && contact.dept.trim() !== '')
|
|
630
|
+
row.bz = `部门:${contact.dept.trim()}`;
|
|
631
|
+
rows.push(row);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return rows;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* 把「已选客户」的联系人自动补成明细行:入参自带就用自带的,没带就回查一次 `crm.search`。
|
|
638
|
+
*
|
|
639
|
+
* 为什么回查:模型经常只把 `customerId/name` 传过来(把 contacts 丢了),
|
|
640
|
+
* 结果明细表空着、又反过来问用户"联系人是谁"——多一轮对话,用户明确说不要这样。
|
|
641
|
+
*/
|
|
642
|
+
async function resolveAutoCrmDetailRows(env, raw) {
|
|
643
|
+
const entries = crmMemberEntriesFromArgs(raw);
|
|
644
|
+
const missing = [];
|
|
645
|
+
/**
|
|
646
|
+
* ⛔ 2026-10-31 修:以前只在「入参一个联系人都没带」时才回查,结果调用方(小E)只传了
|
|
647
|
+
* 用户挑中的那一位时,会议明细里就真只有一位 —— 而客户在 CRM 里其实有好几位。
|
|
648
|
+
* 用户的要求是「有几位就全带几位」,所以现在**一律回查一次,按 CRM 实时数据补全**(取并集,
|
|
649
|
+
* 已带的额外字段如性别/备注保留)。只有 CRM 也查不到时才留空。
|
|
650
|
+
*/
|
|
651
|
+
if (entries.length > 0) {
|
|
652
|
+
try {
|
|
653
|
+
const { getMeetingOperation } = await import('../registry.js');
|
|
654
|
+
const search = getMeetingOperation('meeting.eb.crm.search');
|
|
655
|
+
for (const entry of entries) {
|
|
656
|
+
const keyword = (entry.name || entry.id).trim();
|
|
657
|
+
if (keyword === '') {
|
|
658
|
+
missing.push(entry.id);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
const searchOnce = async (kw) => {
|
|
662
|
+
const found = await search.handler({ keyword: kw, limit: 5 }, env).catch(() => null);
|
|
663
|
+
const items = (found?.data?.items ?? []);
|
|
664
|
+
const hit = items.find((item) => String(item?.customerId ?? '') === entry.id) ?? items[0];
|
|
665
|
+
return (hit?.contacts ?? []);
|
|
666
|
+
};
|
|
667
|
+
let live = await searchOnce(keyword);
|
|
668
|
+
// 客户全名常带「--子公司、专项」后缀,动作流按全名可能一个都匹配不到;
|
|
669
|
+
// 这时退回「--」前的简称再搜一次(仍要按 customerId 认,别拿首个匹配凑数)。
|
|
670
|
+
if (live.length === 0 && keyword.includes('--')) {
|
|
671
|
+
const short = keyword.split('--')[0].trim();
|
|
672
|
+
if (short !== '')
|
|
673
|
+
live = await searchOnce(short);
|
|
674
|
+
}
|
|
675
|
+
if (live.length === 0) {
|
|
676
|
+
if (entry.contacts.length === 0)
|
|
677
|
+
missing.push(entry.name || entry.id);
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
const keyOf = (one) => `${String(one.name ?? '').trim()}|${String(one.mobile ?? '').trim()}`;
|
|
681
|
+
const merged = new Map();
|
|
682
|
+
for (const one of live)
|
|
683
|
+
merged.set(keyOf(one), one);
|
|
684
|
+
for (const one of entry.contacts) {
|
|
685
|
+
const key = keyOf(one);
|
|
686
|
+
const base = merged.get(key);
|
|
687
|
+
merged.set(key, base ? { ...base, ...one } : one);
|
|
688
|
+
}
|
|
689
|
+
entry.contacts = [...merged.values()];
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
// 查不到就沿用入参自带的联系人,由调用方按 warning 处理,不阻断建会
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return {
|
|
697
|
+
rows: crmContactsToDetailRows(entries),
|
|
698
|
+
customers: entries.map((entry) => entry.name || entry.id).filter((one) => one !== ''),
|
|
699
|
+
missing,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
200
702
|
/**
|
|
201
703
|
* 按别名把会议字段映射到表单字段 **datakey**(fieldId)。
|
|
202
704
|
*
|
|
@@ -263,6 +765,125 @@ export function mapFields(fields, values, preferred = {}) {
|
|
|
263
765
|
}
|
|
264
766
|
return { main, mapped, unmapped, resolved };
|
|
265
767
|
}
|
|
768
|
+
/**
|
|
769
|
+
* 把「表单字段 → 即将提交的值」渲染成人话 Markdown 表格,用于**提交前让用户核对**。
|
|
770
|
+
*
|
|
771
|
+
* ⛔ 2026-10-31 用户诉求:「为什么没有直接发起流程表单让我确认重填后让我点击提交」。
|
|
772
|
+
* 浏览器填单那条路走不通(浏览按钮不赋值,见 submit_mode 说明),所以改用"把要提交的内容
|
|
773
|
+
* 原样列出来给用户过一眼"来替代:值里的 `{id,name}` 一律显示姓名,数组顿号连接,空值写 `—`。
|
|
774
|
+
*/
|
|
775
|
+
/** 预览时要换成姓名的字段(表单里的中文名)。 */
|
|
776
|
+
const PREVIEW_PERSON_FIELD_RE = /^(召集人|联系人|参会人员|会议创建人|主讲人|负责人|费用承担人)$/;
|
|
777
|
+
/**
|
|
778
|
+
* 把要展示的 `main` 里「纯 id」的人员字段换成姓名。
|
|
779
|
+
*
|
|
780
|
+
* 为什么不直接指望上游已经是 `{id,name}`:`caller`/`contacter` 是 CLI 自己补的默认值,
|
|
781
|
+
* `mapFields` 之后仍是裸 id(只有 `hrm_members` 走过 `{id,name}` 成型)。不换的话
|
|
782
|
+
* 提交前的核对表会写成「联系人 | 4980990112251498198」—— 用户看到的是一串数字,
|
|
783
|
+
* 2026-10-31 就因此被投诉过"联系人怎么显示成 id"。
|
|
784
|
+
*/
|
|
785
|
+
async function decoratePreviewPersonNames(env, main, fields, known = new Map()) {
|
|
786
|
+
const targets = fields.filter((field) => PREVIEW_PERSON_FIELD_RE.test(String(field?.name ?? '').trim()));
|
|
787
|
+
if (targets.length === 0)
|
|
788
|
+
return main;
|
|
789
|
+
const ID_ONLY_RE = /^\d{8,}$/;
|
|
790
|
+
/** 这一格是不是「只有 id、没有真名」——字符串形态,或 `{id,name}` 里 name 为空/等于 id。 */
|
|
791
|
+
const missingNameId = (value) => {
|
|
792
|
+
if (typeof value === 'string')
|
|
793
|
+
return ID_ONLY_RE.test(value.trim()) ? value.trim() : null;
|
|
794
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
795
|
+
const obj = value;
|
|
796
|
+
const id = String(obj.id ?? '').trim();
|
|
797
|
+
const name = String(obj.name ?? '').trim();
|
|
798
|
+
if (id !== '' && (name === '' || name === id))
|
|
799
|
+
return id;
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
};
|
|
803
|
+
const ids = new Set();
|
|
804
|
+
for (const field of targets) {
|
|
805
|
+
const value = main[field.id];
|
|
806
|
+
const one = missingNameId(value);
|
|
807
|
+
if (one)
|
|
808
|
+
ids.add(one);
|
|
809
|
+
else if (Array.isArray(value))
|
|
810
|
+
for (const item of value) {
|
|
811
|
+
const hit = missingNameId(item);
|
|
812
|
+
if (hit)
|
|
813
|
+
ids.add(hit);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (ids.size === 0)
|
|
817
|
+
return main;
|
|
818
|
+
let names = new Map(known);
|
|
819
|
+
const unknown = [...ids].filter((id) => !names.has(id));
|
|
820
|
+
if (unknown.length > 0) {
|
|
821
|
+
try {
|
|
822
|
+
const resolved = await resolveEmployeeNames(env, unknown);
|
|
823
|
+
for (const [id, name] of resolved)
|
|
824
|
+
names.set(id, name);
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
// 查不到就用已知的(至少调用方显式给的姓名能显示)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (names.size === 0)
|
|
831
|
+
return main;
|
|
832
|
+
const rename = (value) => {
|
|
833
|
+
const one = missingNameId(value);
|
|
834
|
+
if (one)
|
|
835
|
+
return names.get(one) ?? value;
|
|
836
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
837
|
+
const obj = value;
|
|
838
|
+
const id = String(obj.id ?? '').trim();
|
|
839
|
+
const name = names.get(id);
|
|
840
|
+
if (name)
|
|
841
|
+
return { ...obj, name };
|
|
842
|
+
}
|
|
843
|
+
return value;
|
|
844
|
+
};
|
|
845
|
+
const decorated = { ...main };
|
|
846
|
+
for (const field of targets) {
|
|
847
|
+
const value = decorated[field.id];
|
|
848
|
+
decorated[field.id] = Array.isArray(value) ? value.map(rename) : rename(value);
|
|
849
|
+
}
|
|
850
|
+
return decorated;
|
|
851
|
+
}
|
|
852
|
+
export function renderFormPreview(fields, main) {
|
|
853
|
+
const fmt = (value) => {
|
|
854
|
+
if (value === undefined || value === null)
|
|
855
|
+
return '—';
|
|
856
|
+
if (Array.isArray(value)) {
|
|
857
|
+
const parts = value.map((one) => fmt(one)).filter((one) => one !== '—');
|
|
858
|
+
return parts.length > 0 ? parts.join('、') : '—';
|
|
859
|
+
}
|
|
860
|
+
if (typeof value === 'object') {
|
|
861
|
+
const obj = value;
|
|
862
|
+
const name = String(obj.name ?? obj.mc ?? '').trim();
|
|
863
|
+
if (name !== '')
|
|
864
|
+
return name.replace(/\|/gu, '/');
|
|
865
|
+
const id = String(obj.id ?? obj.customerId ?? obj.sjID ?? '').trim();
|
|
866
|
+
return id === '' ? '—' : id;
|
|
867
|
+
}
|
|
868
|
+
const text = String(value).trim();
|
|
869
|
+
return text === '' ? '—' : text.replace(/\|/gu, '/').replace(/\r?\n/gu, ' ');
|
|
870
|
+
};
|
|
871
|
+
const lines = ['| 字段 | 值 |', '| --- | --- |'];
|
|
872
|
+
const known = new Set();
|
|
873
|
+
for (const field of fields) {
|
|
874
|
+
known.add(field.id);
|
|
875
|
+
if (!(field.id in main))
|
|
876
|
+
continue;
|
|
877
|
+
lines.push(`| ${String(field.name).replace(/\|/gu, '/')} | ${fmt(main[field.id])} |`);
|
|
878
|
+
}
|
|
879
|
+
// 字段清单里没有、但确实要提交的(datakey 直填的历史字段)也要列出来,别悄悄带上去
|
|
880
|
+
for (const [id, value] of Object.entries(main)) {
|
|
881
|
+
if (known.has(id))
|
|
882
|
+
continue;
|
|
883
|
+
lines.push(`| ${id} | ${fmt(value)} |`);
|
|
884
|
+
}
|
|
885
|
+
return lines.join('\n');
|
|
886
|
+
}
|
|
266
887
|
function flowFieldCacheDir(env) {
|
|
267
888
|
const configured = env?.ctx?.config?.path?.();
|
|
268
889
|
const root = configured
|
|
@@ -501,7 +1122,9 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
501
1122
|
method: 'meeting.eb.meeting.create.flow.prepare',
|
|
502
1123
|
action: 'meeting.eb.meeting.create.flow.prepare',
|
|
503
1124
|
summary: '流程建会(准备):用固化流程候选逐个探测发起权限 —— 仅 1 个可发起就自动用它、多个都可发起则返回选项让用户选、'
|
|
504
|
-
+ '都不可发起报「没有发起权限」;不传 begin_datetime
|
|
1125
|
+
+ '都不可发起报「没有发起权限」;不传 begin_datetime 时自动推荐「人+会议室都空闲」的时段。只出预览与确认票据,不提交。'
|
|
1126
|
+
+ '⚠️ 用户提到客户名(「客户是 X」)时**必须先** meeting.eb.crm.search {"keyword":"X"} 拿 customerId,'
|
|
1127
|
+
+ '再传 crm_members:[{id,name}];只把客户名写进会议名称/内容,表单「参会客户」就是空的',
|
|
505
1128
|
capability: 'eb.meeting',
|
|
506
1129
|
risk: 'read-before-write',
|
|
507
1130
|
requiresConfirmation: true,
|
|
@@ -521,8 +1144,30 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
521
1144
|
customize_address: { type: 'string', description: '自定义会议地点' },
|
|
522
1145
|
hrm_members: { type: 'string', description: '参会人员(逗号分隔员工 id 或姓名,按流程表单要求)' },
|
|
523
1146
|
total_member: { type: 'number', description: '参会人数' },
|
|
524
|
-
|
|
525
|
-
|
|
1147
|
+
crm_members: {
|
|
1148
|
+
oneOf: [{ type: 'string' }, { type: 'array', items: { oneOf: [{ type: 'string' }, { type: 'object' }] } }],
|
|
1149
|
+
description: '参会客户:**必须带客户名称**,写法 `[{id,name}]` 或「客户名(客户id)」或逗号分隔的这两种。'
|
|
1150
|
+
+ '客户先从 `meeting.eb.crm.search` 按客户名查出(入参 custom_name,出参 items[].customerId / name),'
|
|
1151
|
+
+ '该查询**一次只回首个匹配** ⇒ exact=false 时必须先让用户确认客户名再写这个字段。'
|
|
1152
|
+
+ '⛔ 只给 id 不给名称会被拒绝(客户浏览按钮按名称落值,只给 id 会静默存成空)',
|
|
1153
|
+
},
|
|
1154
|
+
crm_member_count: { type: 'number', description: '应到客户数;不传则按 crm_members 条数自动算' },
|
|
1155
|
+
crm_member_details: {
|
|
1156
|
+
type: 'array',
|
|
1157
|
+
description: '参会客户明细(宿主表单里的明细表,列 姓名/性别/职位/手机号码/备注):'
|
|
1158
|
+
+ '每行 `{"xm":"张三","xb":"男","zw":"经理","sjhm":"138...","bz":""}`,也接受中文键(姓名/性别/职位/手机号码/备注)。'
|
|
1159
|
+
+ '列字段 id 由 CLI 从表单里按列名反查,不用传。'
|
|
1160
|
+
+ '⚠️ 该明细表必须在发起节点布局里(否则 CLI 会提示"表单里没有参会客户明细"并忽略这些行)'
|
|
1161
|
+
+ '⛔ 默认不生效:客户联系人由 CLI 按 CRM 全量自动带入,这里只在 crm_detail_mode:"only" 时才用来"只写这几位"',
|
|
1162
|
+
items: { type: 'object' },
|
|
1163
|
+
},
|
|
1164
|
+
crm_detail_mode: {
|
|
1165
|
+
type: 'string',
|
|
1166
|
+
description: '明细行口径:不传(默认)= 按 CRM 把该客户**全部**联系人自动写入「参会客户明细」(一人一行,不用问用户选哪位);'
|
|
1167
|
+
+ '传 "only" = 只写 crm_member_details 里点名的这几位',
|
|
1168
|
+
},
|
|
1169
|
+
caller: { type: 'string', description: '召集人(员工 id 或姓名)。缺省 = 当前登录人,不用传;只有指定别人当召集人才传' },
|
|
1170
|
+
contacter: { type: 'string', description: '联系人(员工 id 或姓名)。缺省 = 当前登录人,不用传;只有指定别人当联系人才传' },
|
|
526
1171
|
mt_type: {
|
|
527
1172
|
type: 'string',
|
|
528
1173
|
description: '会议类型:可传类型名称或 id(先用 `meeting.eb.type.list` 查有哪些)。'
|
|
@@ -731,6 +1376,14 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
731
1376
|
enrichFailures.push(`${label}未完成:${String(error?.message ?? error).slice(0, 120)}`);
|
|
732
1377
|
return null;
|
|
733
1378
|
};
|
|
1379
|
+
// 「召集人 / 联系人」缺省 = 当前登录人(2026-10-31 用户要求)。
|
|
1380
|
+
// ⛔ 以前只有调用方显式传值才写这两个字段:换个账号用(别人用)就整块空白,
|
|
1381
|
+
// 流程单上「召集人」「联系人」空着、邀请函也落成【待补充】。现在默认取当前登录人,
|
|
1382
|
+
// 只有显式传了别的值才以传的为准(各自独立兜底,只缺一个就只补一个)。
|
|
1383
|
+
const needSelfPerson = String(args.caller ?? '').trim() === '' || String(args.contacter ?? '').trim() === '';
|
|
1384
|
+
const autoPerson = applySelfPersonDefaults(args, needSelfPerson ? await resolveSelfPerson(env) : null);
|
|
1385
|
+
const autoFilledPerson = autoPerson.filled;
|
|
1386
|
+
const selfPersonWarning = autoPerson.warning;
|
|
734
1387
|
// ⛔ 会议人数要含召集人:`hrm_members` 通常只列"其他参会人",召集人是自己。
|
|
735
1388
|
// 以前直接用 `memberIds.length`,1 个参会人就按 1 人算 → 容量下限 2 人的房间把它判死,
|
|
736
1389
|
// 出现「可用 1 间却被过滤成 0 候选」(2026-10-30 真机:易鹏 1 人 → 测试会议室1 1/2 被排除)。
|
|
@@ -751,16 +1404,25 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
751
1404
|
: Promise.resolve(null);
|
|
752
1405
|
// ⛔ 指定了会议室时,**单独查它在该时段是否已被占用**(query_roomusage_list)。
|
|
753
1406
|
// 以前 `!addressInput` 直接跳过整段 → "指定了一间被占用的房"照样往下走,用户到页面上才发现。
|
|
1407
|
+
// ⛔ 2026-10-31 修:以前只有"纯数字 id"才查,传名称(用户最常说的「测试会议室1」)整段被跳过。
|
|
1408
|
+
// 现在统一先解析成台账里的真实房间(id/名称/名称(id) 都认),再查占用 + 存在性 + 封存状态。
|
|
754
1409
|
const addressId = String(addressInput ?? '').trim();
|
|
755
|
-
const addressIsRoomId = /^\d+$/.test(addressId);
|
|
756
1410
|
// ④ 会议室闸门:与时间同理 —— 传了 address **不等于**用户确认过这间房。
|
|
757
1411
|
// 只有 `roomConfirmed: true`(用户明确说了哪间,或在选项里点选后把 value 原样回传)才认;
|
|
758
1412
|
// 否则返回房间选项让用户点。2026-10-30 同一事故:agent 把推荐房间写进确认摘要当定论。
|
|
759
1413
|
const roomConfirmed = args.roomConfirmed === true;
|
|
760
|
-
const roomConflictTask = (enrich && beginText && endText &&
|
|
1414
|
+
const roomConflictTask = (enrich && beginText && endText && addressId !== '')
|
|
761
1415
|
? (async () => {
|
|
1416
|
+
const room = await resolveAddressRoom(env, addressId);
|
|
1417
|
+
// 名称对不上任何房间 → 直接判"没这间房",别把一串文本丢进表单等提交时才报错
|
|
1418
|
+
if (!room)
|
|
1419
|
+
return { unresolved: true, input: addressId };
|
|
1420
|
+
// 台账读失败 / 纯 id 台账里没有 → 没法判占用,但不拦(避免网络抖动被当成"房间不存在")
|
|
1421
|
+
if (!room.id || room.lookupFailed || room.status !== '0')
|
|
1422
|
+
return { room };
|
|
762
1423
|
const { getMeetingOperation } = await import('../registry.js');
|
|
763
|
-
|
|
1424
|
+
const result = await getMeetingOperation('meeting.eb.conflict.room').handler({ beginDatetime: beginText, endDatetime: endText, address: room.id }, env);
|
|
1425
|
+
return { room, ...(result?.data ?? {}) };
|
|
764
1426
|
})().catch(noteFailure('会议室**占用**检测'))
|
|
765
1427
|
: Promise.resolve(null);
|
|
766
1428
|
// ⛔ 这三路检测失败**必须留下痕迹**:以前统一 `.catch(() => null)`,
|
|
@@ -826,12 +1488,65 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
826
1488
|
reason: candidate.reasons.join(';'),
|
|
827
1489
|
}));
|
|
828
1490
|
// ① 指定的会议室该时段已被占用 → 不替用户改,但必须拦下来并给替代候选。
|
|
829
|
-
const roomConflictData = roomConflict
|
|
1491
|
+
const roomConflictData = roomConflict;
|
|
1492
|
+
const resolvedAddress = roomConflictData?.room ?? null;
|
|
1493
|
+
const resolvedAddressId = resolvedAddress?.id || addressId;
|
|
1494
|
+
const addressDisplay = resolvedAddress?.name || addressId;
|
|
1495
|
+
// ①-a 名称对不上任何房间。
|
|
1496
|
+
// ⛔ 2026-10-31 真机:以前这种会一路走到 AWAITING_CONFIRMATION,地点按纯文本塞进表单,
|
|
1497
|
+
// 用户到流程页面提交时才被工作流打回「请按名称选择」—— 报错位置离真实原因太远。
|
|
1498
|
+
if (roomConflictData?.unresolved === true) {
|
|
1499
|
+
decisions.roomNotFound = { input: addressId, candidates: roomOptions.length };
|
|
1500
|
+
if (roomOptions.length > 0)
|
|
1501
|
+
decisions.roomOptions = roomOptions;
|
|
1502
|
+
return meetingSuccess('meeting.eb.meeting.create.flow.prepare', {
|
|
1503
|
+
status: 'NEEDS_SELECTION',
|
|
1504
|
+
selectionType: 'room',
|
|
1505
|
+
ui: { component: 'choice', multiple: false, recommendedKey: 'R1' },
|
|
1506
|
+
message: `没找到名为「${addressId}」的会议室(会议室台账里没有这个名字),本次**没有提交**。`
|
|
1507
|
+
+ (roomOptions.length > 0
|
|
1508
|
+
? `请从下面 ${roomOptions.length} 个在该时段空闲的会议室里选一间,或核对一下会议室名称`
|
|
1509
|
+
: '该时段也没有可用的空闲会议室,请改会议时间或改线上会议(allowNoRoom: true)'),
|
|
1510
|
+
workflowPending: true,
|
|
1511
|
+
options: roomOptions,
|
|
1512
|
+
recommended: roomOptions[0]?.value ?? null,
|
|
1513
|
+
roomNotFound: decisions.roomNotFound,
|
|
1514
|
+
hint: '选定后把 value **整体**原样带回(含 roomConfirmed: true);'
|
|
1515
|
+
+ '核对名称可用 `meeting.eb.room.list` 看全量清单',
|
|
1516
|
+
}, {
|
|
1517
|
+
warnings: [
|
|
1518
|
+
`会议室「${addressId}」在台账里不存在,已拦下未提交。`,
|
|
1519
|
+
'⛔ 不要把它当成"用户指定的房间"写进确认摘要 —— 这间房根本没有被解析出来。',
|
|
1520
|
+
],
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
// ①-b 查到了,但已封存(status=1):不能拿去建会。
|
|
1524
|
+
if (resolvedAddress && resolvedAddress.status === '1') {
|
|
1525
|
+
decisions.roomSealed = { id: resolvedAddress.id, name: resolvedAddress.name };
|
|
1526
|
+
if (roomOptions.length > 0)
|
|
1527
|
+
decisions.roomOptions = roomOptions;
|
|
1528
|
+
return meetingSuccess('meeting.eb.meeting.create.flow.prepare', {
|
|
1529
|
+
status: 'NEEDS_SELECTION',
|
|
1530
|
+
selectionType: 'room',
|
|
1531
|
+
ui: { component: 'choice', multiple: false, recommendedKey: 'R1' },
|
|
1532
|
+
message: `会议室「${resolvedAddress.name}」当前是**封存**状态(不可预约),本次**没有提交**。`
|
|
1533
|
+
+ (roomOptions.length > 0
|
|
1534
|
+
? '请改用下面空闲的会议室'
|
|
1535
|
+
: '该时段没有其它可用会议室,请改会议时间'),
|
|
1536
|
+
workflowPending: true,
|
|
1537
|
+
options: roomOptions,
|
|
1538
|
+
recommended: roomOptions[0]?.value ?? null,
|
|
1539
|
+
roomSealed: decisions.roomSealed,
|
|
1540
|
+
}, {
|
|
1541
|
+
warnings: [`会议室「${resolvedAddress.name}」(${resolvedAddress.id})已封存,不可预约,已拦下未提交。`],
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
830
1544
|
// 只有"用户已确认这间房"时才按占用拦截;未确认的只是候选,交给下面的选房分支处理。
|
|
831
1545
|
if (addressInput && roomConfirmed && roomConflictData?.hasConflict === true) {
|
|
832
1546
|
const busy = Array.isArray(roomConflictData.conflicts) ? roomConflictData.conflicts : [];
|
|
833
1547
|
decisions.roomConflict = {
|
|
834
|
-
address:
|
|
1548
|
+
address: resolvedAddressId,
|
|
1549
|
+
addressName: addressDisplay,
|
|
835
1550
|
hasConflict: true,
|
|
836
1551
|
conflicts: busy,
|
|
837
1552
|
verdict: roomConflictData.verdict ?? 'conflict',
|
|
@@ -842,16 +1557,17 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
842
1557
|
status: 'NEEDS_SELECTION',
|
|
843
1558
|
selectionType: 'room_conflict',
|
|
844
1559
|
ui: { component: 'choice', multiple: false, recommendedKey: 'R1' },
|
|
845
|
-
message:
|
|
846
|
-
+
|
|
1560
|
+
message: `你指定的会议室「${addressDisplay}」在 ${beginText}~${endText} 已被占用`
|
|
1561
|
+
+ `(${busy.length} 条占用记录),本次**没有提交**。请改用下面空闲的会议室,或调整会议时间`,
|
|
847
1562
|
workflowPending: true,
|
|
848
1563
|
options: roomOptions,
|
|
849
1564
|
recommended: roomOptions[0]?.value ?? null,
|
|
850
1565
|
conflicts: busy,
|
|
1566
|
+
roomConflict: decisions.roomConflict,
|
|
851
1567
|
hint: '选定后把 value.id 作为 address 重新调用;要改时间就同时传新的 begin_datetime/end_datetime',
|
|
852
1568
|
}, {
|
|
853
1569
|
warnings: [
|
|
854
|
-
|
|
1570
|
+
`会议室「${addressDisplay}」(${resolvedAddressId})在该时段已有 ${busy.length} 条占用,已拦下未提交。`,
|
|
855
1571
|
roomOptions.length > 0
|
|
856
1572
|
? `已按人数/常用/空闲给出 ${roomOptions.length} 个替代会议室(R1 为首选)。`
|
|
857
1573
|
: '该时段没有可替代的空闲会议室,建议调整会议时间或改线上会议(allowNoRoom: true)。',
|
|
@@ -875,16 +1591,17 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
875
1591
|
const needRoomPick = !addressInput || !roomConfirmed;
|
|
876
1592
|
let pickOptions = roomOptions;
|
|
877
1593
|
if (addressInput && !roomConfirmed) {
|
|
878
|
-
|
|
1594
|
+
// 用**解析后的真实房间 id** 去匹配候选:传名称时以前匹配不上,会被错标成"未解析的指定项"。
|
|
1595
|
+
const hit = roomOptions.find((item) => String(item.value?.id) === resolvedAddressId);
|
|
879
1596
|
const specified = hit
|
|
880
1597
|
? { ...hit, label: `${hit.label}(你指定的,待确认)`, value: { ...hit.value, roomConfirmed: true } }
|
|
881
1598
|
: {
|
|
882
1599
|
key: 'R0',
|
|
883
|
-
label: `${
|
|
884
|
-
value: { ...(
|
|
1600
|
+
label: `${addressDisplay}(你指定的,待确认)`,
|
|
1601
|
+
value: { ...(resolvedAddressId ? { id: resolvedAddressId } : {}), name: addressDisplay, roomConfirmed: true },
|
|
885
1602
|
reason: '你传入的会议室,未提交、未确认',
|
|
886
1603
|
};
|
|
887
|
-
pickOptions = [specified, ...roomOptions.filter((item) => String(item.value?.id) !==
|
|
1604
|
+
pickOptions = [specified, ...roomOptions.filter((item) => String(item.value?.id) !== resolvedAddressId)]
|
|
888
1605
|
.map((item, index) => ({ ...item, key: `R${index + 1}` }));
|
|
889
1606
|
}
|
|
890
1607
|
if (needRoomPick && !autoRoom && pickOptions.length > 0 && args.allowNoRoom !== true) {
|
|
@@ -893,7 +1610,7 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
893
1610
|
selectionType: 'room',
|
|
894
1611
|
ui: { component: 'choice', multiple: false, recommendedKey: 'R1' },
|
|
895
1612
|
message: addressInput
|
|
896
|
-
? `你传入的会议室「${
|
|
1613
|
+
? `你传入的会议室「${addressDisplay}」**未经用户确认**(没带 roomConfirmed),已把它和其它候选一起列出来`
|
|
897
1614
|
+ ' —— ⛔ 必须让用户在选项里点选,不要直接提交'
|
|
898
1615
|
: `未指定会议室,请从 ${pickOptions.length} 个候选里选择(已按人数/常用/时段占用排序)`,
|
|
899
1616
|
// 固化自动选的流程要带出来:调用方下一轮带 address 重调时不用再猜用的是哪条流程。
|
|
@@ -906,7 +1623,7 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
906
1623
|
}, {
|
|
907
1624
|
warnings: [
|
|
908
1625
|
addressInput
|
|
909
|
-
? `会议室「${
|
|
1626
|
+
? `会议室「${addressDisplay}」未经用户确认:本次**没有**提交。`
|
|
910
1627
|
: '未指定会议室:本次**没有**提交,也没有替用户挑房间。',
|
|
911
1628
|
'如需直接建会并沿用推荐,可传 autoRoom: true;不需要会议室则传 allowNoRoom: true。',
|
|
912
1629
|
...(earlyWorkflowNote ? [earlyWorkflowNote] : []),
|
|
@@ -969,8 +1686,8 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
969
1686
|
if (enrich && addressInput && !roomConfirmed && roomOptions.length === 0 && args.allowNoRoom !== true) {
|
|
970
1687
|
const specified = {
|
|
971
1688
|
key: 'R1',
|
|
972
|
-
label: `${
|
|
973
|
-
value: { ...(
|
|
1689
|
+
label: `${addressDisplay}(你指定的,待确认)`,
|
|
1690
|
+
value: { ...(resolvedAddressId ? { id: resolvedAddressId } : {}), name: addressDisplay, roomConfirmed: true },
|
|
974
1691
|
reason: '你传入的会议室,未提交、未确认',
|
|
975
1692
|
};
|
|
976
1693
|
return meetingSuccess('meeting.eb.meeting.create.flow.prepare', {
|
|
@@ -994,11 +1711,14 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
994
1711
|
// 到这里四要素已经齐了(时间带 timeConfirmed、地点带 roomConfirmed),只剩流程没定。
|
|
995
1712
|
const resolved = await resolveWorkflowId(env, args);
|
|
996
1713
|
// 地点还原成**人看得懂的名字**(address 入参可能是 id):用户看确认摘要不能是一串数字。
|
|
997
|
-
|
|
998
|
-
const
|
|
1714
|
+
// 名称/名称(id) 传进来时用解析出来的真实房间名,别再回显用户那串原文。
|
|
1715
|
+
const confirmedAddressId = resolvedAddressId;
|
|
1716
|
+
const confirmedAddressLabel = addressId === ''
|
|
999
1717
|
? ''
|
|
1000
|
-
: (
|
|
1001
|
-
|
|
1718
|
+
: (resolvedAddress?.name
|
|
1719
|
+
|| String(roomOptions.find((item) => String(item.value?.id ?? '') === confirmedAddressId)
|
|
1720
|
+
?.value?.name ?? '')
|
|
1721
|
+
|| addressId);
|
|
1002
1722
|
const confirmedWhen = `${beginText}~${String(endText).slice(11) || endText}`;
|
|
1003
1723
|
let candidates = resolved.candidates;
|
|
1004
1724
|
workflowId = resolved.workflowId;
|
|
@@ -1134,6 +1854,21 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
1134
1854
|
}
|
|
1135
1855
|
// 参会人冲突 / 空闲时段已在选房之前处理完(见上方 conflictBrief)。
|
|
1136
1856
|
decisions.timings = timings;
|
|
1857
|
+
// 参会客户:选了客户就自动算「应到客户数」,不让用户再手填一遍(填了以用户的为准)。
|
|
1858
|
+
const crmMemberCount = (() => {
|
|
1859
|
+
if (args.crm_member_count !== undefined && args.crm_member_count !== '')
|
|
1860
|
+
return args.crm_member_count;
|
|
1861
|
+
const raw = args.crm_members;
|
|
1862
|
+
if (raw === undefined || raw === null || raw === '')
|
|
1863
|
+
return undefined;
|
|
1864
|
+
if (Array.isArray(raw))
|
|
1865
|
+
return raw.length > 0 ? raw.length : undefined;
|
|
1866
|
+
const text = String(raw).trim();
|
|
1867
|
+
if (!text)
|
|
1868
|
+
return undefined;
|
|
1869
|
+
const count = text.split(/[,;,;]+/).map((item) => item.trim()).filter((item) => item !== '').length;
|
|
1870
|
+
return count > 0 ? count : undefined;
|
|
1871
|
+
})();
|
|
1137
1872
|
const shaped = await shapeBrowserValues(env, {
|
|
1138
1873
|
mt_name: args.mt_name,
|
|
1139
1874
|
begin_datetime: args.begin_datetime,
|
|
@@ -1143,6 +1878,8 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
1143
1878
|
customize_address: args.customize_address,
|
|
1144
1879
|
hrm_members: args.hrm_members,
|
|
1145
1880
|
total_member: args.total_member,
|
|
1881
|
+
crm_members: args.crm_members,
|
|
1882
|
+
...(crmMemberCount === undefined ? {} : { crm_member_count: crmMemberCount }),
|
|
1146
1883
|
caller: args.caller,
|
|
1147
1884
|
contacter: args.contacter,
|
|
1148
1885
|
other_remark: args.other_remark,
|
|
@@ -1157,11 +1894,113 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
1157
1894
|
const { main, mapped, unmapped, resolved: resolvedFields } = mapFields(form.fields, shaped.values, preferredFields);
|
|
1158
1895
|
await writeFlowFieldMap(env, workflowId, form.workflowName, resolvedFields);
|
|
1159
1896
|
const warnings = [];
|
|
1897
|
+
if (autoFilledPerson.length > 0) {
|
|
1898
|
+
const labels = autoFilledPerson.map((one) => `「${PERSON_FIELD_LABELS[one.field]}」`).join('');
|
|
1899
|
+
warnings.push(`${labels}没有传值,已按**当前登录人**补齐:${autoFilledPerson[0].name}(${autoFilledPerson[0].id})。`
|
|
1900
|
+
+ '要换成别人(例如替他人建会、指定别的联系人)请显式传 caller / contacter。');
|
|
1901
|
+
}
|
|
1902
|
+
else if (selfPersonWarning) {
|
|
1903
|
+
warnings.push(selfPersonWarning);
|
|
1904
|
+
}
|
|
1160
1905
|
// 固化自动选流程要留痕:调用方/用户能看到"用的是哪条、还有哪些可选"。
|
|
1161
1906
|
if (flowPickNote)
|
|
1162
1907
|
warnings.push(flowPickNote);
|
|
1163
1908
|
for (const note of shaped.notes)
|
|
1164
1909
|
warnings.push(note);
|
|
1910
|
+
// 「提到客户却没传 crm_members」是最常见的一次跑偏:客户名被写进名称/内容就直接往下走,
|
|
1911
|
+
// 结果「参会客户」浏览按钮是空的,单子看着正常、客户却没进表。这里点名提示。
|
|
1912
|
+
const crmMentioned = detectCrmMentionInText(args.mt_name, args.mt_desc, args.other_remark);
|
|
1913
|
+
if (crmMentioned && !args.crm_members) {
|
|
1914
|
+
const hasCrmField = form.fields.some((field) => /参会客户/.test(String(field?.label ?? field?.name ?? field?.fieldName ?? '')));
|
|
1915
|
+
warnings.push(`会议名称/内容里提到客户「${crmMentioned}」,但本次没传 crm_members`
|
|
1916
|
+
+ (hasCrmField
|
|
1917
|
+
? ',该流程表单**有「参会客户」字段,现在会是空的**。请先 '
|
|
1918
|
+
+ `meeting.eb.crm.search {"keyword":"${crmMentioned}"} 拿 items[].customerId,`
|
|
1919
|
+
+ '再用 crm_members:[{id,name}] 重新提交(必须带客户名称)。'
|
|
1920
|
+
: '(该流程表单里没有「参会客户」字段,本条可忽略)。'));
|
|
1921
|
+
}
|
|
1922
|
+
// 传了客户就自动把**全部**联系人写成「参会客户明细」行(5 列:姓名/性别/职位/手机号码/备注),
|
|
1923
|
+
// 不需要调用方去"选一位"(2026-10-31 用户纠正:有几位带几位)。
|
|
1924
|
+
const crmMembersGiven = Array.isArray(args.crm_members)
|
|
1925
|
+
? args.crm_members.length > 0
|
|
1926
|
+
: String(args.crm_members ?? '').trim() !== '';
|
|
1927
|
+
// 「参会客户」是 `Ebuilder` 组件 —— **这是正常配置,别劝人去改表单**:同一张表单的「会议地点」
|
|
1928
|
+
// 也是 Ebuilder,一直写得好好的。它落到会议表那一列由 **ESB 字段映射**负责(用户已配好),
|
|
1929
|
+
// CLI 只负责把客户 id/名称写进流程单据,不再自己补写会议表。
|
|
1930
|
+
if (crmMembersGiven) {
|
|
1931
|
+
const crmFieldId = String(resolvedFields.crm_members ?? '').trim();
|
|
1932
|
+
const crmField = form.fields.find((field) => String(field?.id ?? '') === crmFieldId);
|
|
1933
|
+
const componentKey = String(crmField?.componentKey ?? '').toLowerCase();
|
|
1934
|
+
if (crmField && componentKey !== '' && componentKey !== 'customer' && componentKey !== 'customercomponent') {
|
|
1935
|
+
warnings.push(`「参会客户」字段(${crmFieldId})是 ${crmField.componentKey} 组件:客户 id 与名称照常写进流程单据,`
|
|
1936
|
+
+ '再由 ESB 的字段映射同步到会议表 —— 正常配置,不需要改表单。');
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
// 客户联系人:能自动带就**全部自动带进明细**,不再问用户"选哪一位"
|
|
1940
|
+
// (2026-10-31 用户原话:「带出了三个联系人,为什么要让我选择,不能直接代入到流程里面去吗」)。
|
|
1941
|
+
const providedCrmDetailRows = normalizeCrmDetailRows(args.crm_member_details);
|
|
1942
|
+
// ⛔ 默认按 CRM **全量**补全;只有显式 crm_detail_mode:"only" 才只写用户点名的那几位。
|
|
1943
|
+
// 2026-10-31 用户反馈「流程里只带出第一个联系人」——原因是调用方只把挑中的一位传了进来,
|
|
1944
|
+
// 旧逻辑一看"入参已有联系人"就不再回查,于是明细永远只有一位。
|
|
1945
|
+
const onlyDetailMode = String(args.crm_detail_mode ?? '').trim().toLowerCase() === 'only';
|
|
1946
|
+
let crmDetailRows = onlyDetailMode ? providedCrmDetailRows : [];
|
|
1947
|
+
if (crmMembersGiven && !onlyDetailMode) {
|
|
1948
|
+
const detailTarget = resolveCrmDetailTarget(form.fields);
|
|
1949
|
+
if (detailTarget) {
|
|
1950
|
+
const auto = await resolveAutoCrmDetailRows(env, args.crm_members);
|
|
1951
|
+
if (auto.rows.length > 0) {
|
|
1952
|
+
crmDetailRows = auto.rows;
|
|
1953
|
+
// 入参里显式给的额外列(性别/备注等)按姓名补回去,不全丢
|
|
1954
|
+
if (providedCrmDetailRows.length > 0) {
|
|
1955
|
+
const byName = new Map(providedCrmDetailRows.map((row) => [String(row.xm ?? '').trim(), row]));
|
|
1956
|
+
crmDetailRows = crmDetailRows.map((row) => {
|
|
1957
|
+
const given = byName.get(String(row.xm ?? '').trim());
|
|
1958
|
+
if (!given)
|
|
1959
|
+
return row;
|
|
1960
|
+
const merged = { ...row };
|
|
1961
|
+
for (const [key, value] of Object.entries(given)) {
|
|
1962
|
+
if (String(value ?? '').trim() !== '')
|
|
1963
|
+
merged[key] = String(value);
|
|
1964
|
+
}
|
|
1965
|
+
return merged;
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
warnings.push(`已自动把「${auto.customers.join('、')}」的 ${auto.rows.length} 位联系人写进「参会客户明细」:`
|
|
1969
|
+
+ `${auto.rows.map((row) => `${row.xm}${row.sjhm ? ` ${row.sjhm}` : ''}`).join('、')}`
|
|
1970
|
+
+ '(明细表本来就一人一行,不需要让用户挑)。确实只想要其中几位时,'
|
|
1971
|
+
+ '传 crm_detail_mode:"only" 再配 crm_member_details 才覆盖本次自动带入。');
|
|
1972
|
+
}
|
|
1973
|
+
else if (auto.missing.length > 0) {
|
|
1974
|
+
warnings.push(`已写「参会客户」,但没能自动带出「${auto.missing.join('、')}」的联系人(CRM 里查不到)。`
|
|
1975
|
+
+ '请把已查到的联系人直接告给用户核对;确实没有联系人时问一次姓名+手机号,'
|
|
1976
|
+
+ '再以 crm_member_details:[{"xm":"张三","sjhm":"13800000000"}] 重新 prepare。');
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
// 参会客户明细(明细表):行内用**列字段 id** 作 key,服务端按 id 认(名称列名各租户不同,不写死)。
|
|
1981
|
+
let detailPayload;
|
|
1982
|
+
if (crmDetailRows.length > 0) {
|
|
1983
|
+
const target = resolveCrmDetailTarget(form.fields);
|
|
1984
|
+
if (!target) {
|
|
1985
|
+
warnings.push(`传了 crm_member_details(${crmDetailRows.length} 行),但该建会流程的**发起节点表单里没有「参会客户明细」明细表**`
|
|
1986
|
+
+ '(或它被摆在隐藏行上),明细无法写入,已忽略这些行;主表的「参会客户」照常写入。');
|
|
1987
|
+
}
|
|
1988
|
+
else {
|
|
1989
|
+
detailPayload = {
|
|
1990
|
+
[target.subFormId]: crmDetailRows.map((row) => {
|
|
1991
|
+
const line = {};
|
|
1992
|
+
for (const [semantic, fieldId] of Object.entries(target.columns)) {
|
|
1993
|
+
const value = row[semantic];
|
|
1994
|
+
if (value !== undefined && value !== '')
|
|
1995
|
+
line[fieldId] = value;
|
|
1996
|
+
}
|
|
1997
|
+
return line;
|
|
1998
|
+
}),
|
|
1999
|
+
};
|
|
2000
|
+
warnings.push(`参会客户明细:${crmDetailRows.length} 行写入明细表 ${target.subFormId}`
|
|
2001
|
+
+ `(列:${Object.values(target.labels).join('/')})`);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
1165
2004
|
// datakey 直填(2026-10-30 用户建议):按表单字段的 **datakey** 直接给值,跳过中文名匹配。
|
|
1166
2005
|
// 用在「字段名带提示语、别名表猜不到」或「租户自建字段」场景,比按中文名匹配稳。
|
|
1167
2006
|
if (args.fieldValues && typeof args.fieldValues === 'object' && !Array.isArray(args.fieldValues)) {
|
|
@@ -1211,7 +2050,9 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
1211
2050
|
}
|
|
1212
2051
|
// mt_name 不算"漏填":表单没有「会议名称」字段时它已经用作流程标题(下面单独说明),
|
|
1213
2052
|
// 混在 unmapped 里会让 agent 以为还需要额外补字段。
|
|
1214
|
-
|
|
2053
|
+
// crm_member_count 同理:它是**按客户条数自动派生**的(用户没手填),表单没有「应到客户数」
|
|
2054
|
+
// 时静默忽略即可 —— 列出来会让 agent 以为要再补一个字段。
|
|
2055
|
+
const unmappedReal = unmapped.filter((name) => name !== 'mt_name' && name !== 'crm_member_count');
|
|
1215
2056
|
if (unmappedReal.length > 0) {
|
|
1216
2057
|
warnings.push(`以下字段没有在建会流程表单里找到同名字段,未自动填写:${unmappedReal.join('、')}`
|
|
1217
2058
|
+ `(表单现有字段:${form.fields.map((field) => field.name).join('、')})`);
|
|
@@ -1221,6 +2062,10 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
1221
2062
|
warnings.push('该建会流程的表单没有「会议名称」字段(部分流程用流程标题代替会议名称,属正常配置,不是流程配错)。'
|
|
1222
2063
|
+ '如需会议名称独立成字段,请改选带该字段的流程(如「会议申请流程(NEW)」)。');
|
|
1223
2064
|
}
|
|
2065
|
+
// 客户 id 随凭证带到 apply:默认只用于邀请函/回显,**不再补写会议表**(那边由 ESB 字段映射负责)。
|
|
2066
|
+
const crmPatchIds = (Array.isArray(shaped.values.crm_members) ? shaped.values.crm_members : [])
|
|
2067
|
+
.map((row) => String(row?.id ?? '').trim())
|
|
2068
|
+
.filter((id) => id !== '');
|
|
1224
2069
|
const session = await env.host.getSession();
|
|
1225
2070
|
const continuation = await env.continuation.issue({
|
|
1226
2071
|
operation: 'meeting.eb.meeting.create.flow.apply',
|
|
@@ -1231,12 +2076,22 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
1231
2076
|
workflowId,
|
|
1232
2077
|
requestName: String(args.mt_name),
|
|
1233
2078
|
mtName: String(args.mt_name),
|
|
1234
|
-
data: { main },
|
|
2079
|
+
data: { main, ...(detailPayload ? { details: detailPayload } : {}) },
|
|
1235
2080
|
surfaceContinuation: form.continuation,
|
|
1236
2081
|
// 供 apply 在「会议还没生成」时按申请单信息预生成邀请函(预置函)
|
|
1237
2082
|
invitationFields: shaped.readable,
|
|
2083
|
+
// 提交后回显要用「字段名 → 值」,字段清单先存下来(apply 阶段拿不到 createForm 结果)
|
|
2084
|
+
fieldLabels: form.fields.map((field) => ({ id: String(field.id), name: String(field.name) })),
|
|
2085
|
+
// 已确认的「人员 id → 姓名」,供 apply 回显时把裸 id 换成姓名
|
|
2086
|
+
...(autoFilledPerson.length > 0
|
|
2087
|
+
? { personLabels: Object.fromEntries(autoFilledPerson.map((one) => [one.id, one.name])) }
|
|
2088
|
+
: {}),
|
|
2089
|
+
// 会议记录的客户要靠 apply 补写(表单「参会客户」是 Ebuilder,单据↔会议表不同步)
|
|
2090
|
+
...(crmPatchIds.length > 0
|
|
2091
|
+
? { crmPatch: { ids: crmPatchIds, count: crmMemberCount === undefined ? crmPatchIds.length : crmMemberCount } }
|
|
2092
|
+
: {}),
|
|
1238
2093
|
},
|
|
1239
|
-
fingerprint: JSON.stringify({ workflowId, main }),
|
|
2094
|
+
fingerprint: JSON.stringify({ workflowId, main, ...(detailPayload ? { details: detailPayload } : {}) }),
|
|
1240
2095
|
}, 10 * 60 * 1000);
|
|
1241
2096
|
return meetingSuccess('meeting.eb.meeting.create.flow.prepare', {
|
|
1242
2097
|
status: 'AWAITING_CONFIRMATION',
|
|
@@ -1250,6 +2105,17 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
1250
2105
|
requestName: String(args.mt_name),
|
|
1251
2106
|
fieldMapping: mapped,
|
|
1252
2107
|
formData: { main },
|
|
2108
|
+
// 人话版「即将提交的内容」—— 提交前必须先贴给用户核对(见 SKILL 建会章节)。
|
|
2109
|
+
formPreview: renderFormPreview(form.fields, await decoratePreviewPersonNames(env, main, form.fields, new Map(autoFilledPerson.map((one) => [one.id, one.name])))),
|
|
2110
|
+
// 召集人/联系人是不是"替用户默认填的"要说清楚,用户才知道能不能改。
|
|
2111
|
+
...(autoFilledPerson.length > 0
|
|
2112
|
+
? {
|
|
2113
|
+
autoFilled: Object.fromEntries(autoFilledPerson.map((one) => [PERSON_FIELD_LABELS[one.field], `${one.name}(${one.id})`])),
|
|
2114
|
+
}
|
|
2115
|
+
: {}),
|
|
2116
|
+
...(detailPayload
|
|
2117
|
+
? { detailRows: { subFormId: Object.keys(detailPayload)[0], count: crmDetailRows.length, rows: crmDetailRows } }
|
|
2118
|
+
: {}),
|
|
1253
2119
|
formFieldCount: form.fields.length,
|
|
1254
2120
|
...(Object.keys(decisions).length > 0 ? { decisions } : {}),
|
|
1255
2121
|
},
|
|
@@ -1258,6 +2124,77 @@ const ebMeetingCreateFlowPrepare = {
|
|
|
1258
2124
|
}, warnings.length > 0 ? { warnings } : {});
|
|
1259
2125
|
},
|
|
1260
2126
|
};
|
|
2127
|
+
/**
|
|
2128
|
+
* 补写客户后的观察窗(毫秒)—— 每一轮「写 → 等 → 回读」,被覆盖就再补一次。
|
|
2129
|
+
* 会议刚生成时 ESB 会异步整行写主表,覆盖掉刚补的 `crm_members`。
|
|
2130
|
+
*/
|
|
2131
|
+
const CRM_PATCH_SETTLE_MS = [4000, 6000];
|
|
2132
|
+
/**
|
|
2133
|
+
* 把「参会客户」补写进**会议记录**(会议表 `crm_members` 存客户 id,多客户逗号分隔)。
|
|
2134
|
+
*
|
|
2135
|
+
* ⛔ 2026-10-31 用户纠正「Ebuilder 组件这个就是对的」—— 我之前判"组件错、要换回 Customer"是错的:
|
|
2136
|
+
* 同一张表单的「会议地点」也是 Ebuilder,一直写得好好的;Ebuilder 就是吃 `{id,name}` 的浏览按钮。
|
|
2137
|
+
* 真正的原因是**流程单据 → 会议表的字段映射不覆盖「参会客户」**:单据里客户关联正常
|
|
2138
|
+
* (`workflow.detail` 的 fieldValues 有「名(id)」),会议记录 `crm_members` 却一直是空
|
|
2139
|
+
* ⇒ 邀请函「客户单位」、客户统计、会议详情全拿不到客户。
|
|
2140
|
+
*
|
|
2141
|
+
* 会议表本身是 eBuilder 表单,可以绕过流程映射直接 `updateFormData/change_meeting` 写。
|
|
2142
|
+
* 写接口只回 `dataIds`(服务端对字段不做校验),所以**必须回读 `crm_members` 确认落库**。
|
|
2143
|
+
*/
|
|
2144
|
+
export async function patchMeetingCrmMembers(env, meetingId, ids, count, settleMsList = CRM_PATCH_SETTLE_MS) {
|
|
2145
|
+
const list = ids.map((id) => String(id ?? '').trim()).filter((id) => id !== '');
|
|
2146
|
+
if (String(meetingId).trim() === '' || list.length === 0) {
|
|
2147
|
+
return { ok: false, error: '没有会议 id 或客户 id' };
|
|
2148
|
+
}
|
|
2149
|
+
const mainTable = {
|
|
2150
|
+
id: String(meetingId).trim(),
|
|
2151
|
+
operate_type: 'UPDATE',
|
|
2152
|
+
crm_members: list.join(','),
|
|
2153
|
+
};
|
|
2154
|
+
const total = Number(count);
|
|
2155
|
+
if (Number.isFinite(total) && total > 0)
|
|
2156
|
+
mainTable.crm_total_member = String(total);
|
|
2157
|
+
const write = async () => {
|
|
2158
|
+
const session = await env.host.getSession();
|
|
2159
|
+
await env.host.request({
|
|
2160
|
+
method: 'POST',
|
|
2161
|
+
path: `${EB_BASE}/updateFormData/change_meeting`,
|
|
2162
|
+
query: { id: String(meetingId).trim() },
|
|
2163
|
+
operation: 'meeting.eb.meeting.create.flow.apply',
|
|
2164
|
+
body: writeBody(String(session.userId), { mainTable, details: {}, operateType: {} }),
|
|
2165
|
+
acceptBusiness: ebBusinessSuccess,
|
|
2166
|
+
});
|
|
2167
|
+
};
|
|
2168
|
+
const readBack = async () => {
|
|
2169
|
+
const { getMeetingOperation } = await import('../registry.js');
|
|
2170
|
+
const detail = getMeetingOperation('meeting.eb.meeting.detail');
|
|
2171
|
+
const read = await detail.handler({ id: String(meetingId).trim() }, env);
|
|
2172
|
+
return String(read?.data?.data?.crm_members ?? '').trim();
|
|
2173
|
+
};
|
|
2174
|
+
// ⛔ 写完立刻回读会「假成功」(2026-10-31 真机踩到):会议刚生成时回执/占用等 ESB 还在异步写主表,
|
|
2175
|
+
// 整行覆盖会把刚补的客户冲掉 —— 第一次回读非空、8 秒后再读又空了。所以每一轮都「写 → 等 → 再读」,
|
|
2176
|
+
// 被覆盖就再补一次。两次等待合计 10s,只在真的带了客户时才有这段开销。
|
|
2177
|
+
let lastError = '';
|
|
2178
|
+
for (const settleMs of settleMsList) {
|
|
2179
|
+
try {
|
|
2180
|
+
await write();
|
|
2181
|
+
}
|
|
2182
|
+
catch (error) {
|
|
2183
|
+
return { ok: false, error: String(error?.message ?? error).slice(0, 160) };
|
|
2184
|
+
}
|
|
2185
|
+
await new Promise((resolve) => setTimeout(resolve, settleMs));
|
|
2186
|
+
try {
|
|
2187
|
+
const value = await readBack();
|
|
2188
|
+
if (value !== '')
|
|
2189
|
+
return { ok: true, value };
|
|
2190
|
+
lastError = '补写后回读 crm_members 仍为空(多半被会议初始化的后续写入覆盖了)';
|
|
2191
|
+
}
|
|
2192
|
+
catch (error) {
|
|
2193
|
+
lastError = `补写成功但回读失败:${String(error?.message ?? error).slice(0, 120)}`;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
return { ok: false, error: lastError };
|
|
2197
|
+
}
|
|
1261
2198
|
/**
|
|
1262
2199
|
* 按申请单已知信息预生成邀请函(**预置函**)。
|
|
1263
2200
|
*
|
|
@@ -1281,6 +2218,9 @@ export async function renderPreInvitation(payload, env) {
|
|
|
1281
2218
|
caller: String(fields.caller ?? '').trim(),
|
|
1282
2219
|
contacter: String(fields.contacter ?? '').trim(),
|
|
1283
2220
|
total_member: String(fields.total_member ?? '').trim(),
|
|
2221
|
+
// 客户单位(邀请函开头「尊敬的 XXX」):建会时填了参会客户就直接带过去,
|
|
2222
|
+
// 否则整句永远是【待补充】—— 用户看到的就是"客户明明填了,邀请函里却没有"。
|
|
2223
|
+
extra: { guest_org: String(fields.crm_members ?? '').trim() },
|
|
1284
2224
|
}, env);
|
|
1285
2225
|
// 用 pending 标记「这份是按申请单预生成的,不是会议最终数据」:
|
|
1286
2226
|
// 调用方据此提示用户"会议生成后可用 mtId 重新生成一版拿完整字段"。
|
|
@@ -1313,6 +2253,13 @@ const ebMeetingCreateFlowApply = {
|
|
|
1313
2253
|
inputSchema: schemaOf({
|
|
1314
2254
|
continuation: { type: 'string', description: 'prepare 返回的确认票据(必填)' },
|
|
1315
2255
|
confirm: { type: 'boolean', description: '必须显式传 true' },
|
|
2256
|
+
submit_mode: {
|
|
2257
|
+
type: 'string',
|
|
2258
|
+
enum: ['direct', 'browser'],
|
|
2259
|
+
description: '提交方式,默认 browser:在小e 里**弹出流程表单**,字段已预填,用户核对/修改后自己点提交。'
|
|
2260
|
+
+ 'direct = 直接写服务端(会议当场生成、邀请函用真实数据,但没有人工核对表单这一步);'
|
|
2261
|
+
+ '只有用户明确说"不用我确认,直接建"时才传 direct',
|
|
2262
|
+
},
|
|
1316
2263
|
waitSeconds: { type: 'number', description: '提交后等待会议生成的最长秒数(默认 60,0 = 不等待)' },
|
|
1317
2264
|
}, { required: ['continuation', 'confirm'] }),
|
|
1318
2265
|
handler: async (args, env) => {
|
|
@@ -1335,6 +2282,10 @@ const ebMeetingCreateFlowApply = {
|
|
|
1335
2282
|
};
|
|
1336
2283
|
const submitStartedAt = Date.now();
|
|
1337
2284
|
const submit = getWorkflowOperation('workflow.createRequest');
|
|
2285
|
+
// 默认走**浏览器填单**(2026-10-31 用户纠正):小e 的填单页能按 id 把人员/客户带进去
|
|
2286
|
+
// (`xiaoEUrlParamScalar` 对浏览按钮取 `id`),弹出表单让用户核对/修改/自己点提交才是要的体验。
|
|
2287
|
+
// 传 submit_mode:"direct" 才改为服务端直提(会议当场生成,但没有人工确认这一步)。
|
|
2288
|
+
const submitMode = String(args.submit_mode ?? 'browser').trim();
|
|
1338
2289
|
const submitted = await submit.handler({
|
|
1339
2290
|
// 工作流的入参名是 context(createForm 返回的 continuation 就是它的 context 句柄)
|
|
1340
2291
|
context: payload.surfaceContinuation,
|
|
@@ -1342,6 +2293,7 @@ const ebMeetingCreateFlowApply = {
|
|
|
1342
2293
|
action: 'submit',
|
|
1343
2294
|
requestName: payload.requestName,
|
|
1344
2295
|
confirm: true,
|
|
2296
|
+
...(submitMode === 'browser' ? {} : { forceServerSubmit: true }),
|
|
1345
2297
|
}, wfEnv);
|
|
1346
2298
|
const submittedData = submitted?.data ?? {};
|
|
1347
2299
|
const requestId = String(submittedData.requestId ?? submittedData.id ?? '').trim();
|
|
@@ -1439,13 +2391,41 @@ const ebMeetingCreateFlowApply = {
|
|
|
1439
2391
|
}
|
|
1440
2392
|
timings.pollMs = Date.now() - pollStart;
|
|
1441
2393
|
}
|
|
2394
|
+
// 「参会客户」怎么落到会议表:**由 ESB 的字段映射负责**(流程单据 → 会议表 `ufd_crm_members_new`)。
|
|
2395
|
+
// 2026-10-31 用户确认:「你发起流程后,字段映射是靠 ESB 来的,这个我已经配置好了」。
|
|
2396
|
+
// ⛔ 所以 CLI **不再自己补写** `crm_members`:会议表里客户那一列的 dataKey 不是 `crm_members`
|
|
2397
|
+
// (写它既落不到页面,回读也永远为空),只会多出一条「客户没关联上」的假警报,把验收带偏。
|
|
2398
|
+
// 真要回退老行为时置 WEAVER_MEETING_CRM_PATCH=1。
|
|
2399
|
+
let crmLinked;
|
|
2400
|
+
const crmPatch = payload?.crmPatch;
|
|
2401
|
+
const crmPatchEnabled = process.env.WEAVER_MEETING_CRM_PATCH === '1';
|
|
2402
|
+
if (meetingId && crmPatchEnabled && Array.isArray(crmPatch?.ids) && crmPatch.ids.length > 0) {
|
|
2403
|
+
const patched = await patchMeetingCrmMembers(env, meetingId, crmPatch.ids, crmPatch.count);
|
|
2404
|
+
crmLinked = {
|
|
2405
|
+
ok: patched.ok,
|
|
2406
|
+
requested: crmPatch.ids.length,
|
|
2407
|
+
...(patched.value ? { readBack: patched.value } : {}),
|
|
2408
|
+
...(patched.error ? { error: patched.error } : {}),
|
|
2409
|
+
};
|
|
2410
|
+
if (patched.ok) {
|
|
2411
|
+
warnings.push(`「参会客户」已补写到会议记录(crm_members=${patched.value})。`);
|
|
2412
|
+
}
|
|
2413
|
+
else {
|
|
2414
|
+
warnings.push(`「参会客户」没有回读到(${patched.error ?? '未知原因'}):当前走的是 CLI 手工补写(WEAVER_MEETING_CRM_PATCH=1),`
|
|
2415
|
+
+ '不依赖 ESB 字段映射时才需要它。');
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
1442
2418
|
let invitation = { pending: true };
|
|
1443
2419
|
if (meetingId) {
|
|
1444
2420
|
const invitationStart = Date.now();
|
|
1445
2421
|
try {
|
|
1446
2422
|
const { getMeetingOperation } = await import('../registry.js');
|
|
1447
2423
|
const render = getMeetingOperation('meeting.invitation.render');
|
|
1448
|
-
const rendered = await render.handler({
|
|
2424
|
+
const rendered = await render.handler({
|
|
2425
|
+
mtId: meetingId,
|
|
2426
|
+
// 同理:会议主表不存客户名,用建会时填的参会客户补「客户单位」
|
|
2427
|
+
extra: { guest_org: String(payload?.invitationFields?.crm_members ?? '').trim() },
|
|
2428
|
+
}, env);
|
|
1449
2429
|
invitation = { pending: false, ...(rendered?.data ?? {}) };
|
|
1450
2430
|
timings.invitationMs = Date.now() - invitationStart;
|
|
1451
2431
|
}
|
|
@@ -1476,6 +2456,11 @@ const ebMeetingCreateFlowApply = {
|
|
|
1476
2456
|
requestId,
|
|
1477
2457
|
requestName: payload.requestName,
|
|
1478
2458
|
meetingId: meetingId || null,
|
|
2459
|
+
// 参会客户有没有真的写进会议记录(表单侧是 Ebuilder,需 apply 补写;回读确认才算 ok)
|
|
2460
|
+
...(crmLinked ? { crmLinked } : {}),
|
|
2461
|
+
// 提交后回显「实际提交了什么」:字段名 → 值(人员/客户显示姓名,不显示 id)。
|
|
2462
|
+
// 用户要的就是"能看见内容",替代走不通的浏览器填单核对。
|
|
2463
|
+
submittedPreview: renderFormPreview(payload?.fieldLabels ?? [], await decoratePreviewPersonNames(env, (payload?.data?.main ?? {}), payload?.fieldLabels ?? [], new Map(Object.entries((payload?.personLabels ?? {}))))),
|
|
1479
2464
|
invitation,
|
|
1480
2465
|
result: submitted?.data ?? null,
|
|
1481
2466
|
}, warnings.length > 0 ? { warnings } : {});
|
|
@@ -1496,7 +2481,13 @@ const ebMeetingCreateFlowQuick = {
|
|
|
1496
2481
|
resource: 'eb',
|
|
1497
2482
|
method: 'meeting.eb.meeting.create.flow.quick',
|
|
1498
2483
|
action: 'meeting.eb.meeting.create.flow.quick',
|
|
1499
|
-
summary: '一步流程建会(prepare+apply
|
|
2484
|
+
summary: '一步流程建会(prepare+apply 合并):内部并行做会议室推荐/冲突检测/空闲时段,默认**弹浏览器表单**给用户核对提交'
|
|
2485
|
+
+ '(submit_mode=browser,小e 里预填人员/客户后由用户点提交;非小e 环境会自动写服务端);'
|
|
2486
|
+
+ '传 submit_mode=direct 才是服务端直提(拿 requestId + meetingId,会议当场生成、邀请函用真实数据)。'
|
|
2487
|
+
+ '多流程或需选择时不提交,返回选项。'
|
|
2488
|
+
+ '⚠️ 用户提到客户名时**必须先** meeting.eb.crm.search {"keyword":"X"} 拿 customerId 再传 crm_members:[{id,name}];'
|
|
2489
|
+
+ '只把客户名写进会议名称/内容而不传 crm_members 会被直接拒绝(crm_member_required)。'
|
|
2490
|
+
+ '⛔ 不要用 submit_mode=browser —— 小e 填单页不给浏览按钮赋值,参会人员/参会客户会整段丢失',
|
|
1500
2491
|
capability: 'eb.meeting',
|
|
1501
2492
|
risk: 'high-risk-write',
|
|
1502
2493
|
requiresConfirmation: true,
|
|
@@ -1516,8 +2507,8 @@ const ebMeetingCreateFlowQuick = {
|
|
|
1516
2507
|
customize_address: { type: 'string', description: '自定义会议地点' },
|
|
1517
2508
|
hrm_members: { type: 'string', description: '参会人员 id,逗号分隔' },
|
|
1518
2509
|
total_member: { type: 'number', description: '参会人数' },
|
|
1519
|
-
caller: { type: 'string', description: '
|
|
1520
|
-
contacter: { type: 'string', description: '
|
|
2510
|
+
caller: { type: 'string', description: '召集人(员工 id 或姓名)。缺省 = 当前登录人,不用传;只有指定别人当召集人才传' },
|
|
2511
|
+
contacter: { type: 'string', description: '联系人(员工 id 或姓名)。缺省 = 当前登录人,不用传;只有指定别人当联系人才传' },
|
|
1521
2512
|
mt_type: {
|
|
1522
2513
|
type: 'string',
|
|
1523
2514
|
description: '会议类型:可传类型名称或 id(先用 `meeting.eb.type.list` 查有哪些)。'
|
|
@@ -1569,14 +2560,41 @@ const ebMeetingCreateFlowQuick = {
|
|
|
1569
2560
|
description: '多个建会流程**都有发起权限**时:默认(不传 / ask)= 返回选项让用户挑;'
|
|
1570
2561
|
+ 'auto = 自动取优先级最高的。只有 1 个有权限时直接发起',
|
|
1571
2562
|
},
|
|
2563
|
+
crm_skip: {
|
|
2564
|
+
type: 'boolean',
|
|
2565
|
+
description: '显式声明「这次不填参会客户」。'
|
|
2566
|
+
+ '会议名称/内容里出现 `客户:X`、`客户是X` 这类字样却**没传 `crm_members`**时,'
|
|
2567
|
+
+ '一步建会会直接拒绝(crm_member_required)——确认这些字样不是要上会表的客户,才传 true',
|
|
2568
|
+
},
|
|
2569
|
+
submit_mode: {
|
|
2570
|
+
type: 'string',
|
|
2571
|
+
enum: ['direct', 'browser'],
|
|
2572
|
+
description: '提交方式,默认 browser:在小e 里**弹出流程表单**(字段已预填含人员/客户),用户核对修改后自己点提交。'
|
|
2573
|
+
+ 'direct = 直接写服务端(会议当场生成、邀请函用真实数据,但没有人工核对表单这一步);'
|
|
2574
|
+
+ '只有用户明确说"不用我确认,直接建"时才传 direct',
|
|
2575
|
+
},
|
|
1572
2576
|
}, { required: ['mt_name', 'confirm'] }),
|
|
1573
2577
|
handler: async (args, env) => {
|
|
1574
2578
|
if (args.confirm !== true) {
|
|
1575
2579
|
throw meetingValidationError('confirmation_required', '一步建会会直接提交流程,必须显式传 confirm=true;如只想看方案,请用 meeting.eb.meeting.create.flow.prepare');
|
|
1576
2580
|
}
|
|
2581
|
+
// 客户名写在名称/内容里、却没传 crm_members ⇒ 一步建会**直接拒绝**。
|
|
2582
|
+
// quick 没有预览环节,放过去就是一张「参会客户」空着的单子(2026-10-31 真机踩到),
|
|
2583
|
+
// 事后清理比在这儿拦一下贵得多。确认不需要填客户时传 crm_skip:true 放行。
|
|
2584
|
+
const crmMentioned = detectCrmMentionInText(args.mt_name, args.mt_desc, args.other_remark);
|
|
2585
|
+
const crmSkipped = args.crm_skip === true;
|
|
2586
|
+
if (crmMentioned && !args.crm_members && !crmSkipped) {
|
|
2587
|
+
throw meetingValidationError('crm_member_required', `会议名称/内容里提到客户「${crmMentioned}」,但没传 crm_members —— 提交后「参会客户」会是空的。`
|
|
2588
|
+
+ `先 meeting.eb.crm.search {"keyword":"${crmMentioned}"} 拿 items[].customerId,`
|
|
2589
|
+
+ '再把 crm_members:[{"id":"<customerId>","name":"<完整客户名>"}] 一起传'
|
|
2590
|
+
+ '(必须带名称,只给 id 会被静默存空);确实不需要填参会客户就传 crm_skip:true。');
|
|
2591
|
+
}
|
|
1577
2592
|
const { getMeetingOperation } = await import('../registry.js');
|
|
1578
2593
|
const prepareStartedAt = Date.now();
|
|
1579
|
-
const
|
|
2594
|
+
const { submit_mode: _submitMode, confirm: _confirm, ...prepareArgs } = args;
|
|
2595
|
+
const prepared = await getMeetingOperation('meeting.eb.meeting.create.flow.prepare').handler(
|
|
2596
|
+
// submit_mode 只属于提交阶段,prepare 的 schema 不接受它(additionalProperties:false)
|
|
2597
|
+
prepareArgs, env);
|
|
1580
2598
|
if (prepared?.data?.status === 'NEEDS_SELECTION') {
|
|
1581
2599
|
return prepared; // 让用户先选流程,不提交
|
|
1582
2600
|
}
|
|
@@ -1589,6 +2607,9 @@ const ebMeetingCreateFlowQuick = {
|
|
|
1589
2607
|
const applied = await getMeetingOperation('meeting.eb.meeting.create.flow.apply').handler({
|
|
1590
2608
|
continuation,
|
|
1591
2609
|
confirm: true,
|
|
2610
|
+
...(args.submit_mode
|
|
2611
|
+
? { submit_mode: args.submit_mode }
|
|
2612
|
+
: {}),
|
|
1592
2613
|
...(args.waitSeconds !== undefined ? { waitSeconds: args.waitSeconds } : {}),
|
|
1593
2614
|
}, env);
|
|
1594
2615
|
const warnings = [
|
|
@@ -1601,6 +2622,8 @@ const ebMeetingCreateFlowQuick = {
|
|
|
1601
2622
|
requestId: applied?.data?.requestId,
|
|
1602
2623
|
meetingId: applied?.data?.meetingId ?? null,
|
|
1603
2624
|
invitation: applied?.data?.invitation ?? null,
|
|
2625
|
+
// 已提交内容回显(字段名 → 值),提交后可直接贴给用户看"到底填进去了什么"
|
|
2626
|
+
submittedPreview: applied?.data?.submittedPreview ?? null,
|
|
1604
2627
|
// 小e 环境(BROWSER_FILL)不写服务端,表单靠这段 <browsertool> 在浏览器里填 —— 必须一起透出,
|
|
1605
2628
|
// 否则宿主拿不到表单,用户看到的就是"提交了但没反应"。
|
|
1606
2629
|
...(applied?.data?.contextId ? { contextId: (applied?.data).contextId } : {}),
|