weaver-work-cli 0.1.29 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/archive-UMGX72K6.js +2537 -0
- package/dist/chunks/archive-oa-N2IYBLF3.js +1317 -0
- package/dist/chunks/asset-GLEGKFG5.js +2109 -0
- package/dist/chunks/attachment-content-AQNX5N3S.js +23 -0
- package/dist/chunks/blog-E44OTVDA.js +2157 -0
- package/dist/chunks/caizhilian-DQUFZXZL.js +10087 -0
- package/dist/chunks/calendar-JKPIRKFD.js +1195 -0
- package/dist/chunks/chunk-2GA5ZIQL.js +1615 -0
- package/dist/chunks/chunk-4YC43QQ4.js +403 -0
- package/dist/chunks/chunk-6EFWDL7Y.js +161 -0
- package/dist/chunks/chunk-6RKD4DRZ.js +5556 -0
- package/dist/chunks/chunk-AJ62BGAW.js +29 -0
- package/dist/chunks/chunk-BUSC6UAE.js +43 -0
- package/dist/chunks/chunk-C6R3B2ML.js +358 -0
- package/dist/chunks/chunk-CVZ2JOSW.js +62 -0
- package/dist/chunks/chunk-DW6AUUKS.js +508 -0
- package/dist/chunks/chunk-E4Z23GHM.js +6962 -0
- package/dist/chunks/chunk-EG6ZU444.js +654 -0
- package/dist/chunks/chunk-EZALDOQH.js +383 -0
- package/dist/chunks/chunk-FMPJJYYI.js +106 -0
- package/dist/chunks/chunk-G2JSIUWF.js +769 -0
- package/dist/chunks/chunk-G3QJULO2.js +387 -0
- package/dist/chunks/chunk-GCG2NMWR.js +703 -0
- package/dist/chunks/chunk-GPMFD4RD.js +75 -0
- package/dist/chunks/chunk-H6XSTSLM.js +2357 -0
- package/dist/chunks/chunk-H7J4DC5R.js +464 -0
- package/dist/chunks/chunk-IURUFGZK.js +477 -0
- package/dist/chunks/chunk-LUSPKZKY.js +53 -0
- package/dist/chunks/chunk-NIAJ5ZCG.js +73 -0
- package/dist/chunks/chunk-NOODWMCL.js +584 -0
- package/dist/chunks/chunk-OOUTBLZ4.js +51 -0
- package/dist/chunks/chunk-P26ZWSKU.js +73 -0
- package/dist/chunks/chunk-QWY6PGL6.js +3472 -0
- package/dist/chunks/chunk-WKI2FBIQ.js +15606 -0
- package/dist/chunks/chunk-WSKBSQ2U.js +231 -0
- package/dist/chunks/chunk-YLCYX7LE.js +371 -0
- package/dist/chunks/chunk-YRVDW4WN.js +205 -0
- package/dist/chunks/chunk-ZEDWSH32.js +68 -0
- package/dist/chunks/continuation-2CKSBLA6.js +11 -0
- package/dist/chunks/continuation-HZIZRZBX.js +13 -0
- package/dist/chunks/continuation-NLECEJPX.js +11 -0
- package/dist/chunks/ebuilder-form-V5FG2EHC.js +2276 -0
- package/dist/chunks/ehr-HMFY6VIL.js +5241 -0
- package/dist/chunks/employee-SAK2QYXX.js +11 -0
- package/dist/chunks/esb-ZYSVB7SG.js +141 -0
- package/dist/chunks/fna-B7RKPAZD.js +1444 -0
- package/dist/chunks/host-45WED2KK.js +21 -0
- package/dist/chunks/host-I4Z6BFYX.js +23 -0
- package/dist/chunks/host-KNZ52LJZ.js +17 -0
- package/dist/chunks/host-OCH2BCCV.js +19 -0
- package/dist/chunks/hrm-2KAZGUHI.js +244 -0
- package/dist/chunks/im-ACJJWH4U.js +252 -0
- package/dist/chunks/invoice-ZK3WCX2U.js +5179 -0
- package/dist/chunks/jiuchuanhui-5PQD3IL2.js +1940 -0
- package/dist/chunks/lazy-dispatch-JQXBW3GM.js +28 -0
- package/dist/chunks/mail-S2HDDXO2.js +2695 -0
- package/dist/chunks/meeting-QVN3I37L.js +504 -0
- package/dist/chunks/msg-parse-4GSQ2WTO.js +41 -0
- package/dist/chunks/okr-QNG4VC2Q.js +2482 -0
- package/dist/chunks/plan-A25EMW6U.js +2025 -0
- package/dist/chunks/project-G6XV66DZ.js +1361 -0
- package/dist/chunks/registry-2VLUAESA.js +14 -0
- package/dist/chunks/registry-7C5UMIPL.js +19 -0
- package/dist/chunks/registry-DC6KR3NH.js +18 -0
- package/dist/chunks/registry-NYPYMZXG.js +16 -0
- package/dist/chunks/registry-TI3CIUXF.js +19 -0
- package/dist/chunks/room-recommend-WRTHO6Y2.js +20 -0
- package/dist/chunks/schedule-KFIITGSK.js +24 -0
- package/dist/chunks/state-Y2XBP6YE.js +15 -0
- package/dist/chunks/workflow-DJ7SYWAF.js +245 -0
- package/dist/cmd/argv.js +45 -0
- package/dist/cmd/docs/index.js +3 -3
- package/dist/cmd/lazy-dispatch.js +20 -0
- package/dist/cmd/root.js +2 -4
- package/dist/cmd/setup/index.js +2 -2
- package/dist/cmd/skills/index.js +2 -2
- package/dist/core/package-paths.js +61 -0
- package/dist/index.js +163 -7
- package/dist/internal/e10/auth/xiaoe.js +3 -3
- package/dist/internal/e10/request-runtime.js +18 -62
- package/dist/internal/skills/detector.js +2 -2
- package/dist/shortcuts/contract/data/performance-catalog.js +1306 -0
- package/dist/shortcuts/contract/host.js +91 -35
- package/dist/shortcuts/contract/index.js +213 -0
- package/dist/shortcuts/contract/manifest.js +928 -0
- package/dist/shortcuts/contract/ooxml/docx.js +295 -0
- package/dist/shortcuts/contract/ooxml/xlsx.js +221 -0
- package/dist/shortcuts/contract/ooxml/zip.js +158 -0
- package/dist/shortcuts/contract/operations/assistant.js +1156 -0
- package/dist/shortcuts/contract/operations/clause-library.js +522 -0
- package/dist/shortcuts/contract/operations/compare.js +193 -0
- package/dist/shortcuts/contract/operations/credit-page.js +34 -0
- package/dist/shortcuts/contract/operations/flow.js +98 -0
- package/dist/shortcuts/contract/operations/page.js +66 -0
- package/dist/shortcuts/contract/operations/performance.js +733 -0
- package/dist/shortcuts/contract/operations/quote.js +557 -0
- package/dist/shortcuts/contract/operations/recommend.js +78 -0
- package/dist/shortcuts/contract/operations/registry.js +18 -0
- package/dist/shortcuts/contract/operations/review.js +83 -0
- package/dist/shortcuts/contract/operations/shared.js +37 -0
- package/dist/shortcuts/contract/operations/upload.js +108 -0
- package/dist/shortcuts/contract/operations/write.js +3 -2
- package/dist/shortcuts/index.js +140 -40
- package/dist/shortcuts/invoice/manifest.js +34 -11
- package/dist/shortcuts/invoice/operations/reim-auto.js +9 -0
- package/dist/shortcuts/invoice/operations/reim.js +81 -5
- package/dist/shortcuts/invoice/operations/shared.js +12 -5
- package/dist/shortcuts/meeting/host.js +45 -3
- package/dist/shortcuts/meeting/invitation/docx.js +442 -0
- package/dist/shortcuts/meeting/operations/attachment-content.js +418 -0
- package/dist/shortcuts/meeting/operations/attachment.js +442 -0
- package/dist/shortcuts/meeting/operations/eb/cache.js +57 -0
- package/dist/shortcuts/meeting/operations/eb/catalog.js +343 -28
- package/dist/shortcuts/meeting/operations/eb/conflict.js +372 -11
- package/dist/shortcuts/meeting/operations/eb/create-flow.js +710 -0
- package/dist/shortcuts/meeting/operations/eb/decision.js +47 -1
- package/dist/shortcuts/meeting/operations/eb/factory.js +129 -11
- package/dist/shortcuts/meeting/operations/eb/index.js +122 -2
- package/dist/shortcuts/meeting/operations/eb/room-recommend.js +378 -0
- package/dist/shortcuts/meeting/operations/eb/room-summary.js +84 -0
- package/dist/shortcuts/meeting/operations/eb/schedule.js +450 -0
- package/dist/shortcuts/meeting/operations/from-group-chat.js +576 -0
- package/dist/shortcuts/meeting/operations/invitation.js +458 -0
- package/dist/shortcuts/meeting/operations/minute-content.js +266 -0
- package/dist/shortcuts/meeting/operations/pipeline.js +489 -0
- package/dist/shortcuts/meeting/operations/qrcode.js +131 -0
- package/dist/shortcuts/meeting/operations/receipts.js +1 -1
- package/dist/shortcuts/meeting/operations/registry.js +18 -0
- package/dist/shortcuts/meeting/operations/summary.js +276 -0
- package/dist/shortcuts/meeting/operations/task-notify.js +325 -0
- package/dist/shortcuts/meeting/operations.js +42 -1
- package/dist/shortcuts/meeting/qr/encoder.js +427 -0
- package/dist/shortcuts/meeting/qr/render.js +103 -0
- package/dist/shortcuts/workflow/operations/form.js +17 -5
- package/docs/contract.md +39 -4
- package/docs/invoice.md +5 -5
- package/package.json +3 -2
- package/scripts/bundle.mjs +63 -0
- package/skill-template/business-info.json +25 -4
- package/skill-template/domains/jinchengda.md +16 -3
- package/skill-template/domains/yepiaotong.md +1 -1
- package/skills/weaver-e10-jinchengda/SKILL.md +111 -31
- package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +7 -13
- package/skills/weaver-e10-jinchengda/references/contract-ai-assistant.md +150 -0
- package/skills/weaver-e10-jinchengda/references/contract-ai-review.md +87 -0
- package/skills/weaver-e10-jinchengda/references/contract-clause-library-crud.md +111 -0
- package/skills/weaver-e10-jinchengda/references/contract-clause-recommender.md +0 -15
- package/skills/weaver-e10-jinchengda/references/contract-clause-reference-save.md +0 -13
- package/skills/weaver-e10-jinchengda/references/contract-credit-page.md +50 -0
- package/skills/weaver-e10-jinchengda/references/contract-disabled-capabilities.md +4 -2
- package/skills/weaver-e10-jinchengda/references/contract-doc-compare.md +74 -0
- package/skills/weaver-e10-jinchengda/references/contract-flow-launch.md +84 -0
- package/skills/weaver-e10-jinchengda/references/contract-performance-query.md +29 -0
- package/skills/weaver-e10-jinchengda/references/contract-quote-to-contract.md +28 -0
- package/skills/weaver-e10-jinchengda/references/contract-request-status.md +2 -21
- package/skills/weaver-e10-jinchengda/references/contract-request-write.md +0 -24
- package/skills/weaver-e10-jinchengda/references/contract-signing-subjects.md +0 -15
- package/skills/weaver-e10-jinchengda/references/contract-subject-create.md +0 -17
- package/skills/weaver-e10-jinchengda/references/contract-template-recommend.md +70 -0
- package/skills/weaver-e10-jinchengda/references/contract-template-selector.md +0 -15
- package/skills/weaver-e10-jinchengda/references/contract-workflow-discovery.md +0 -15
- package/skills/weaver-e10-jinchengda/references/source-manifest.json +1168 -152
- package/skills/weaver-e10-meeting/SKILL.md +493 -17
- package/skills/weaver-e10-meeting/references/meeting-change.md +4 -4
- package/skills/weaver-e10-meeting/references/meeting-create.md +6 -6
- package/skills/weaver-e10-meeting/references/meeting-eb.md +503 -27
- package/skills/weaver-e10-meeting/references/meeting-invitation.md +113 -0
- package/skills/weaver-e10-meeting/references/meeting-lifecycle.md +6 -6
- package/skills/weaver-e10-meeting/references/meeting-minute-file.md +425 -0
- package/skills/weaver-e10-meeting/references/meeting-queries.md +4 -4
- package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +15 -15
- package/skills/weaver-e10-meeting/references/meeting-verification.md +19 -11
- package/skills/weaver-e10-meeting.zip +0 -0
- package/skills/weaver-e10-yepiaotong/SKILL.md +9 -5
- package/skills/weaver-e10-yepiaotong/references/invoice-enterprise-list.md +1 -1
- package/skills/weaver-e10-yepiaotong/references/invoice-personal-list.md +6 -5
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +14 -11
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +97 -56
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +84 -60
- package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +142 -105
- package/skills/weaver-e10-yepiaotong/references/reim-matching-rules.md +53 -194
- package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +7 -7
- package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +164 -72
- package/skills/weaver-e10-yepiaotong/references/source-manifest.json +6 -0
- package/templates/meeting/invitation.docx +0 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import { meetingSuccess, schemaOf, SCHEMA_DATETIME_MINUTE } from '../types.js';
|
|
2
|
+
import { meetingValidationError } from '../../errors.js';
|
|
3
|
+
import { fetchEbAppSettings, findCommonFreeSlots } from './conflict.js';
|
|
4
|
+
import { recommendRooms } from './room-recommend.js';
|
|
5
|
+
/**
|
|
6
|
+
* 通过**流程**新建 EB 会议(`meeting.eb.meeting.create.flow`)。
|
|
7
|
+
*
|
|
8
|
+
* 为什么需要(2026-10-28 用户明确要求):会议应用设置里勾了「只允许通过流程新建会议」
|
|
9
|
+
* (`only_flow_create=1`)时,直连 `create_meeting` 是被禁止的;此前的做法是直接抛错让调用方
|
|
10
|
+
* "去用别的 skill",等于把活推回去。正确做法是**顺着管控走流程**:载入建会流程表单 → 填会议字段 →
|
|
11
|
+
* 提交审批 → 会议由流程/ESB 创建 → 会后再补邀请函。
|
|
12
|
+
*
|
|
13
|
+
* 链路:`workflow.createForm`(按 mt_new_wf 载入表单)→ 字段名映射 → `workflow.createRequest`
|
|
14
|
+
* (action=submit)→ 轮询会议列表确认会议已生成 → `meeting.invitation.render` 出邀请函。
|
|
15
|
+
*
|
|
16
|
+
* 口径提醒:
|
|
17
|
+
* - 流程走完(或审批通过)后会议才会真正出现在会议表里,所以**邀请函在确认到会议 id 之后才生成**;
|
|
18
|
+
* 拿不到会议时返回 `invitation.pending=true`,不要假装已经生成。
|
|
19
|
+
* - 字段按**中文名**映射(会议名称/开始时间/…),租户改了字段名就在 `fieldAliases` 里补;
|
|
20
|
+
* 映射不到的字段会原样回到 `unmapped` 里,由调用方决定是否补充。
|
|
21
|
+
* - 不猜必填项:`workflow.createRequest` 报缺字段时原样返回,由调用方向用户确认。
|
|
22
|
+
*/
|
|
23
|
+
/** 建会流程表单字段的常见别名(租户之间字段名会有出入,按名称匹配,不用 fieldId 写死)。 */
|
|
24
|
+
const FIELD_ALIASES = {
|
|
25
|
+
mt_name: ['会议名称', '会议主题', '主题'],
|
|
26
|
+
begin_datetime: ['开始时间', '会议开始时间'],
|
|
27
|
+
end_datetime: ['结束时间', '会议结束时间'],
|
|
28
|
+
mt_desc: ['会议内容', '会议说明', '会议描述'],
|
|
29
|
+
address: ['会议地点', '会议室'],
|
|
30
|
+
customize_address: ['自定义会议地点', '自定义地点'],
|
|
31
|
+
hrm_members: ['参会人员', '参会人'],
|
|
32
|
+
total_member: ['参会人数', '人数'],
|
|
33
|
+
caller: ['召集人', '发起人'],
|
|
34
|
+
contacter: ['联系人'],
|
|
35
|
+
mt_type: ['会议类型'],
|
|
36
|
+
other_remark: ['备注', '其他说明'],
|
|
37
|
+
};
|
|
38
|
+
function workflowEnv(env) {
|
|
39
|
+
return env;
|
|
40
|
+
}
|
|
41
|
+
/** 载入建会流程表单(拿 contextId/continuation/字段清单)。 */
|
|
42
|
+
async function loadCreateForm(env, workflowId) {
|
|
43
|
+
const { createWorkflowHost } = await import('../../../workflow/host.js');
|
|
44
|
+
const { createWorkflowContinuationSigner } = await import('../../../workflow/continuation.js');
|
|
45
|
+
const { getWorkflowOperation } = await import('../../../workflow/operations/registry.js');
|
|
46
|
+
const wfEnv = {
|
|
47
|
+
...workflowEnv(env),
|
|
48
|
+
host: createWorkflowHost(env.ctx),
|
|
49
|
+
continuation: createWorkflowContinuationSigner(env.ctx),
|
|
50
|
+
};
|
|
51
|
+
const operation = getWorkflowOperation('workflow.createForm');
|
|
52
|
+
const result = await operation.handler({ workflow: [workflowId] }, wfEnv);
|
|
53
|
+
const data = result?.data ?? {};
|
|
54
|
+
if (data.status !== 'FORM_READY') {
|
|
55
|
+
throw meetingValidationError('flow_form_unavailable', `建会流程表单载入失败(status=${String(data.status)}):${String(data.clarificationQuestion ?? data.note ?? '').slice(0, 200)}`, { workflowId });
|
|
56
|
+
}
|
|
57
|
+
const wf = data.workflow ?? {};
|
|
58
|
+
return {
|
|
59
|
+
fields: (data.fields ?? []),
|
|
60
|
+
continuation: data.continuation,
|
|
61
|
+
requestName: data.requestName,
|
|
62
|
+
contextId: data.contextId,
|
|
63
|
+
workflowName: String(wf.name ?? wf.workflowName ?? '').trim() || undefined,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** 取员工姓名(hrm.employee.batch,一次批量,失败返回空表不阻塞)。 */
|
|
67
|
+
async function resolveEmployeeNames(env, ids) {
|
|
68
|
+
const map = new Map();
|
|
69
|
+
const unique = [...new Set(ids.map((id) => String(id).trim()).filter((id) => id !== ''))];
|
|
70
|
+
if (unique.length === 0)
|
|
71
|
+
return map;
|
|
72
|
+
try {
|
|
73
|
+
const { hrmEmployeeOperations } = await import('../../../hrm/operations/employee.js');
|
|
74
|
+
const batch = hrmEmployeeOperations.find((operation) => operation.name === 'hrm.employee.batch');
|
|
75
|
+
if (!batch)
|
|
76
|
+
return map;
|
|
77
|
+
const result = await batch.handler({ ids: unique }, env);
|
|
78
|
+
const rows = (result?.data?.rows ?? result?.data?.userList ?? []);
|
|
79
|
+
for (const row of rows) {
|
|
80
|
+
const id = String(row.id ?? '').trim();
|
|
81
|
+
const name = String(row.name ?? '').trim();
|
|
82
|
+
if (id && name)
|
|
83
|
+
map.set(id, name);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// 查不到姓名就退回「以 id 当名称」,工作流多数能按 id 直接命中
|
|
88
|
+
}
|
|
89
|
+
return map;
|
|
90
|
+
}
|
|
91
|
+
/** 会议室 id → {id,name}(优先会议室缓存,缓存没有就查一次详情)。 */
|
|
92
|
+
async function resolveRoomValue(env, input) {
|
|
93
|
+
const text = String(input ?? '').trim();
|
|
94
|
+
if (!text)
|
|
95
|
+
return null;
|
|
96
|
+
const { loadRooms } = await import('./room-recommend.js');
|
|
97
|
+
try {
|
|
98
|
+
const { rooms } = await loadRooms(env, {});
|
|
99
|
+
const byId = rooms.find((room) => room.id === text);
|
|
100
|
+
if (byId)
|
|
101
|
+
return { id: byId.id, name: byId.name };
|
|
102
|
+
const byName = rooms.find((room) => room.name === text);
|
|
103
|
+
if (byName)
|
|
104
|
+
return { id: byName.id, name: byName.name };
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// 缓存读不到就退到详情
|
|
108
|
+
}
|
|
109
|
+
if (/^\d+$/.test(text)) {
|
|
110
|
+
try {
|
|
111
|
+
const { getMeetingOperation } = await import('../registry.js');
|
|
112
|
+
const detail = await getMeetingOperation('meeting.eb.room.detail').handler({ id: text }, env);
|
|
113
|
+
const main = detail?.data?.mainTable ?? detail?.data?.data?.mainTable ?? {};
|
|
114
|
+
const name = String(main.room_name ?? '').trim();
|
|
115
|
+
if (name)
|
|
116
|
+
return { id: text, name };
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// 详情也拿不到就只传 id
|
|
120
|
+
}
|
|
121
|
+
return { id: text, name: text };
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 浏览按钮字段要传 **{id,name}**(不能只传 id 或只传名字)。
|
|
127
|
+
*
|
|
128
|
+
* 2026-10-28 真机踩坑:把「会议地点/参会人员/召集人」当普通文本传 id,工作流把它们当"名称"去
|
|
129
|
+
* getRelateData 里匹配,匹配不到就返回 NEEDS_CONFIRMATION("无法唯一解析的选项")——
|
|
130
|
+
* 看起来像"流程建会走不通",其实是值格式不对。这里统一补成对象。
|
|
131
|
+
*/
|
|
132
|
+
async function shapeBrowserValues(env, values) {
|
|
133
|
+
const notes = [];
|
|
134
|
+
const shaped = { ...values };
|
|
135
|
+
if (shaped.address !== undefined && shaped.address !== '') {
|
|
136
|
+
const room = await resolveRoomValue(env, String(shaped.address));
|
|
137
|
+
if (room) {
|
|
138
|
+
shaped.address = room;
|
|
139
|
+
notes.push(`会议地点按浏览按钮格式提交:${room.name}(${room.id})`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const personFields = ['hrm_members', 'caller', 'contacter'];
|
|
143
|
+
const idPool = [];
|
|
144
|
+
for (const key of personFields) {
|
|
145
|
+
const raw = shaped[key];
|
|
146
|
+
if (raw === undefined || raw === '')
|
|
147
|
+
continue;
|
|
148
|
+
const ids = String(raw).split(/[,;,;\s]+/).map((item) => item.trim()).filter((item) => item !== '');
|
|
149
|
+
idPool.push(...ids);
|
|
150
|
+
}
|
|
151
|
+
if (idPool.length > 0) {
|
|
152
|
+
const names = await resolveEmployeeNames(env, idPool);
|
|
153
|
+
const toObject = (id) => ({ id, name: names.get(id) ?? id });
|
|
154
|
+
if (shaped.hrm_members !== undefined && shaped.hrm_members !== '') {
|
|
155
|
+
const ids = String(shaped.hrm_members).split(/[,;,;\s]+/).map((item) => item.trim()).filter((item) => item !== '');
|
|
156
|
+
shaped.hrm_members = ids.map(toObject);
|
|
157
|
+
notes.push(`参会人员按浏览按钮格式提交:${ids.length} 人`);
|
|
158
|
+
}
|
|
159
|
+
if (shaped.caller !== undefined && shaped.caller !== '') {
|
|
160
|
+
const id = String(shaped.caller).trim();
|
|
161
|
+
shaped.caller = toObject(id);
|
|
162
|
+
}
|
|
163
|
+
if (shaped.contacter !== undefined && shaped.contacter !== '') {
|
|
164
|
+
const id = String(shaped.contacter).trim();
|
|
165
|
+
shaped.contacter = toObject(id);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { values: shaped, notes };
|
|
169
|
+
}
|
|
170
|
+
/** 按别名把会议字段映射到表单字段 id。 */
|
|
171
|
+
function mapFields(fields, values) {
|
|
172
|
+
const byName = new Map(fields.map((field) => [field.name.trim(), field]));
|
|
173
|
+
const main = {};
|
|
174
|
+
const mapped = {};
|
|
175
|
+
const unmapped = [];
|
|
176
|
+
for (const [key, raw] of Object.entries(values)) {
|
|
177
|
+
if (raw === undefined || raw === null || raw === '')
|
|
178
|
+
continue;
|
|
179
|
+
const aliases = FIELD_ALIASES[key] ?? [];
|
|
180
|
+
const hit = aliases.map((name) => byName.get(name)).find((field) => field !== undefined);
|
|
181
|
+
if (!hit) {
|
|
182
|
+
unmapped.push(key);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
main[hit.id] = raw;
|
|
186
|
+
mapped[key] = `${hit.name}(${hit.id})`;
|
|
187
|
+
}
|
|
188
|
+
return { main, mapped, unmapped };
|
|
189
|
+
}
|
|
190
|
+
/** 解析要用的建会流程 id:入参优先,其次取应用设置里的 mt_new_wf(逗号分隔取第一个)。 */
|
|
191
|
+
async function resolveWorkflowId(env, args) {
|
|
192
|
+
const explicit = String(args.workflowId ?? '').trim();
|
|
193
|
+
if (explicit)
|
|
194
|
+
return { workflowId: explicit, candidates: [explicit] };
|
|
195
|
+
const settings = await fetchEbAppSettings(env, 'meeting.eb.meeting.create.flow.prepare');
|
|
196
|
+
const candidates = String(settings.mtNewWf ?? '')
|
|
197
|
+
.split(/[,;,;\s]+/)
|
|
198
|
+
.map((item) => item.trim())
|
|
199
|
+
.filter((item) => item !== '');
|
|
200
|
+
if (candidates.length === 0) {
|
|
201
|
+
throw meetingValidationError('flow_not_configured', '应用设置里没有配置「新建会议工作流」(mt_new_wf 为空),无法走流程建会;请管理员在会议应用设置里配置,或关闭「只允许通过流程新建会议」');
|
|
202
|
+
}
|
|
203
|
+
// 多个流程时先不拍板:由调用方探测"当前人员对每个流程有没有发起权限",再决定是否让用户选。
|
|
204
|
+
return { workflowId: candidates.length === 1 ? candidates[0] : '', candidates };
|
|
205
|
+
}
|
|
206
|
+
const ebMeetingCreateFlowPrepare = {
|
|
207
|
+
name: 'meeting.eb.meeting.create.flow.prepare',
|
|
208
|
+
resource: 'eb',
|
|
209
|
+
method: 'meeting.eb.meeting.create.flow.prepare',
|
|
210
|
+
action: 'meeting.eb.meeting.create.flow.prepare',
|
|
211
|
+
summary: '流程建会(准备):按应用设置 mt_new_wf 载入建会流程表单,把会议字段按中文名映射成表单值,返回预览与确认票据(不提交)',
|
|
212
|
+
capability: 'eb.meeting',
|
|
213
|
+
risk: 'read-before-write',
|
|
214
|
+
requiresConfirmation: true,
|
|
215
|
+
inputSchema: schemaOf({
|
|
216
|
+
mt_name: { type: 'string', description: '会议名称(必填)' },
|
|
217
|
+
begin_datetime: { ...SCHEMA_DATETIME_MINUTE, description: '开始时间(必填,yyyy-MM-dd HH:mm)' },
|
|
218
|
+
end_datetime: { ...SCHEMA_DATETIME_MINUTE, description: '结束时间(必填,yyyy-MM-dd HH:mm)' },
|
|
219
|
+
mt_desc: { type: 'string', description: '会议内容/说明' },
|
|
220
|
+
address: { type: 'string', description: '会议室(名称或 id,按流程表单字段要求)' },
|
|
221
|
+
customize_address: { type: 'string', description: '自定义会议地点' },
|
|
222
|
+
hrm_members: { type: 'string', description: '参会人员(逗号分隔员工 id 或姓名,按流程表单要求)' },
|
|
223
|
+
total_member: { type: 'number', description: '参会人数' },
|
|
224
|
+
caller: { type: 'string', description: '召集人' },
|
|
225
|
+
contacter: { type: 'string', description: '联系人' },
|
|
226
|
+
other_remark: { type: 'string', description: '备注' },
|
|
227
|
+
workflowId: { type: 'string', description: '指定建会流程 id(默认取应用设置 mt_new_wf 的第一个)' },
|
|
228
|
+
autoRoom: { type: 'boolean', description: '默认 false:未指定会议室时返回候选让用户选;传 true 才自动采用推荐' },
|
|
229
|
+
allowNoRoom: { type: 'boolean', description: '确实不需要会议室(线上会等)时传 true,避免被“请选择会议室”拦住' },
|
|
230
|
+
}, { required: ['mt_name', 'begin_datetime', 'end_datetime'] }),
|
|
231
|
+
handler: async (args, env) => {
|
|
232
|
+
const resolved = await resolveWorkflowId(env, args);
|
|
233
|
+
const candidates = resolved.candidates;
|
|
234
|
+
let workflowId = resolved.workflowId;
|
|
235
|
+
const probes = [];
|
|
236
|
+
const loadedForms = new Map();
|
|
237
|
+
if (!workflowId && candidates.length > 1) {
|
|
238
|
+
// 应用设置里配了多个建会流程:**逐个探测当前人员能否发起**(createForm 能载入 = 有发起权限),
|
|
239
|
+
// 不能默认拿第一个就提交 —— 用户可能对第一个没有发起权限(2026-10-28 用户要求)。
|
|
240
|
+
// 并行探测:每个流程一次 createForm(内含多次 OA 请求),串行会成倍放大等待
|
|
241
|
+
const probed = await Promise.all(candidates.map(async (id) => {
|
|
242
|
+
try {
|
|
243
|
+
const probeForm = await loadCreateForm(env, id);
|
|
244
|
+
return {
|
|
245
|
+
id,
|
|
246
|
+
probeForm,
|
|
247
|
+
probe: {
|
|
248
|
+
workflowId: id,
|
|
249
|
+
...(probeForm.workflowName ? { name: probeForm.workflowName } : {}),
|
|
250
|
+
launchable: true,
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
return {
|
|
256
|
+
id,
|
|
257
|
+
probeForm: null,
|
|
258
|
+
probe: {
|
|
259
|
+
workflowId: id,
|
|
260
|
+
launchable: false,
|
|
261
|
+
reason: String(error?.message ?? error).slice(0, 160),
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}));
|
|
266
|
+
for (const item of probed) {
|
|
267
|
+
if (item.probeForm)
|
|
268
|
+
loadedForms.set(item.id, item.probeForm);
|
|
269
|
+
probes.push(item.probe);
|
|
270
|
+
}
|
|
271
|
+
const launchable = probes.filter((probe) => probe.launchable);
|
|
272
|
+
if (launchable.length === 0) {
|
|
273
|
+
throw meetingValidationError('flow_not_launchable', `应用设置配置了 ${candidates.length} 个建会流程,但当前账号一个都发起不了(无发起权限或流程已停用):`
|
|
274
|
+
+ probes.map((probe) => `${probe.workflowId}(${probe.reason ?? '不可发起'})`).join(';'), { probes });
|
|
275
|
+
}
|
|
276
|
+
if (launchable.length > 1) {
|
|
277
|
+
// 仍有多个可选 → 交给用户挑,不替用户决定(用户明确要求"以选择的方式")
|
|
278
|
+
return meetingSuccess('meeting.eb.meeting.create.flow.prepare', {
|
|
279
|
+
status: 'NEEDS_SELECTION',
|
|
280
|
+
message: `当前账号可发起 ${launchable.length} 个建会流程,请让用户选择要触发哪一个`,
|
|
281
|
+
options: launchable.map((probe, index) => ({
|
|
282
|
+
key: `W${index + 1}`,
|
|
283
|
+
label: probe.name ? `${probe.name}(${probe.workflowId})` : probe.workflowId,
|
|
284
|
+
value: { workflowId: probe.workflowId },
|
|
285
|
+
reason: '当前账号有发起权限',
|
|
286
|
+
})),
|
|
287
|
+
probes,
|
|
288
|
+
}, {
|
|
289
|
+
warnings: [
|
|
290
|
+
`检测到 ${candidates.length} 个建会流程,其中 ${launchable.length} 个当前账号可发起;`
|
|
291
|
+
+ '请把这几个流程做成选项让用户挑一个,选定后带 workflowId 重新 prepare。',
|
|
292
|
+
...probes.filter((probe) => !probe.launchable)
|
|
293
|
+
.map((probe) => `流程 ${probe.workflowId} 当前账号无法发起(${probe.reason ?? '无权限'}),已排除`),
|
|
294
|
+
],
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
workflowId = launchable[0].workflowId;
|
|
298
|
+
}
|
|
299
|
+
const form = loadedForms.get(workflowId) ?? await loadCreateForm(env, workflowId);
|
|
300
|
+
if (!form.continuation) {
|
|
301
|
+
throw meetingValidationError('flow_context_missing', '建会流程表单没有返回 continuation,无法继续提交');
|
|
302
|
+
}
|
|
303
|
+
// 走流程也要有"选会议室 / 选时间"的体验(2026-10-28 用户要求)。
|
|
304
|
+
// ⚠️ 性能:会议室推荐、参会人冲突、共同空闲时段三件事**互相独立**,必须并行发;
|
|
305
|
+
// 串行会把 3 段网络耗时叠加成"建个会要等好几分钟"(用户 2026-10-28 反馈)。
|
|
306
|
+
const decisions = {};
|
|
307
|
+
// enrich=false:只做"建会必需"的事(载入表单 + 填字段 + 提交),跳过会议室推荐/冲突检测/空闲时段。
|
|
308
|
+
// 适用场景:人、时间、房间都已经确定,只想尽快把会建出来(可省掉 3 组额外请求与一次 ESB 调用)。
|
|
309
|
+
const enrich = args.enrich !== false;
|
|
310
|
+
const beginText = String(args.begin_datetime ?? '').trim();
|
|
311
|
+
const endText = String(args.end_datetime ?? '').trim();
|
|
312
|
+
const memberIds = String(args.hrm_members ?? '')
|
|
313
|
+
.split(/[,;,;\s]+/)
|
|
314
|
+
.map((item) => item.trim())
|
|
315
|
+
.filter((item) => item !== '');
|
|
316
|
+
let addressInput = args.address;
|
|
317
|
+
const day = beginText.slice(0, 10);
|
|
318
|
+
const durationMinutes = beginText && endText
|
|
319
|
+
? Math.max(15, Math.round((Date.parse(endText.replace(' ', 'T')) - Date.parse(beginText.replace(' ', 'T'))) / 60000) || 60)
|
|
320
|
+
: 60;
|
|
321
|
+
const timings = {};
|
|
322
|
+
const startedAt = Date.now();
|
|
323
|
+
const roomTask = (enrich && !addressInput && beginText && endText)
|
|
324
|
+
? recommendRooms(env, {
|
|
325
|
+
beginDatetime: beginText,
|
|
326
|
+
endDatetime: endText,
|
|
327
|
+
...(memberIds.length > 0 ? { memberCount: memberIds.length } : {}),
|
|
328
|
+
topN: 3,
|
|
329
|
+
}).catch(() => null)
|
|
330
|
+
: Promise.resolve(null);
|
|
331
|
+
const conflictTask = (enrich && beginText && endText && memberIds.length > 0)
|
|
332
|
+
? (async () => {
|
|
333
|
+
const { getMeetingOperation } = await import('../registry.js');
|
|
334
|
+
return getMeetingOperation('meeting.eb.conflict.member').handler({ beginDatetime: beginText, endDatetime: endText, hrmMembers: memberIds.join(',') }, env);
|
|
335
|
+
})().catch(() => null)
|
|
336
|
+
: Promise.resolve(null);
|
|
337
|
+
const availabilityTask = (enrich && beginText && endText && memberIds.length > 0)
|
|
338
|
+
? findCommonFreeSlots(env, {
|
|
339
|
+
rangeStart: `${day} 00:00`,
|
|
340
|
+
rangeEnd: `${day} 23:59`,
|
|
341
|
+
durationMinutes,
|
|
342
|
+
members: memberIds,
|
|
343
|
+
maxSlots: 3,
|
|
344
|
+
maxDays: 1,
|
|
345
|
+
includeSchedule: true,
|
|
346
|
+
}).catch(() => null)
|
|
347
|
+
: Promise.resolve(null);
|
|
348
|
+
const [roomRec, memberConflict, free] = await Promise.all([roomTask, conflictTask, availabilityTask]);
|
|
349
|
+
timings.parallelMs = Date.now() - startedAt;
|
|
350
|
+
if (roomRec) {
|
|
351
|
+
const roomOptions = roomRec.candidates.map((candidate, index) => ({
|
|
352
|
+
key: `R${index + 1}`,
|
|
353
|
+
label: `${candidate.name}${candidate.capacity.max > 0 ? `(可容 ${candidate.capacity.max} 人)` : ''}`
|
|
354
|
+
+ `${index === 0 ? ' · 推荐' : ''}`,
|
|
355
|
+
value: { id: candidate.id, name: candidate.name },
|
|
356
|
+
reason: candidate.reasons.join(';'),
|
|
357
|
+
}));
|
|
358
|
+
decisions.roomOptions = roomOptions;
|
|
359
|
+
decisions.autoRoom = {
|
|
360
|
+
picked: roomRec.best
|
|
361
|
+
? { id: roomRec.best.id, name: roomRec.best.name, score: roomRec.best.score, reasons: roomRec.best.reasons }
|
|
362
|
+
: null,
|
|
363
|
+
candidates: roomRec.candidates,
|
|
364
|
+
meta: roomRec.meta,
|
|
365
|
+
};
|
|
366
|
+
// 会议室**不替用户决定**(用户明确要求:即使是常用会议室,也必须推荐出来让选择)。
|
|
367
|
+
// 只有显式 `autoRoom: true` 才自动选中;否则返回 NEEDS_SELECTION,把候选交给调用方去问。
|
|
368
|
+
const autoRoom = args.autoRoom === true;
|
|
369
|
+
if (!addressInput && !autoRoom && roomOptions.length > 0 && args.allowNoRoom !== true) {
|
|
370
|
+
return meetingSuccess('meeting.eb.meeting.create.flow.prepare', {
|
|
371
|
+
status: 'NEEDS_SELECTION',
|
|
372
|
+
selectionType: 'room',
|
|
373
|
+
message: `未指定会议室,请从 ${roomOptions.length} 个候选里选择(已按人数/常用/时段占用排序)`,
|
|
374
|
+
options: roomOptions,
|
|
375
|
+
recommended: roomOptions[0]?.value ?? null,
|
|
376
|
+
hint: '选定后把 value.id 作为 address 重新调用(quick 或 prepare);确实不需要会议室请传 allowNoRoom: true',
|
|
377
|
+
}, {
|
|
378
|
+
warnings: [
|
|
379
|
+
'未指定会议室:本次**没有**提交,也没有替用户挑房间。',
|
|
380
|
+
'如需直接建会并沿用推荐,可传 autoRoom: true;不需要会议室则传 allowNoRoom: true。',
|
|
381
|
+
],
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (addressInput) {
|
|
385
|
+
// 调用方已指定(或显式 autoRoom)
|
|
386
|
+
}
|
|
387
|
+
else if (autoRoom && roomRec.best) {
|
|
388
|
+
addressInput = roomRec.best.id;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const conflicts = (memberConflict?.data?.conflicts ?? []);
|
|
392
|
+
if (conflicts.length > 0)
|
|
393
|
+
decisions.memberConflicts = conflicts;
|
|
394
|
+
if (free) {
|
|
395
|
+
decisions.scheduleItems = free.scheduleItems;
|
|
396
|
+
decisions.availabilityCost = free.cost;
|
|
397
|
+
if (free.slots.length > 0) {
|
|
398
|
+
decisions.suggestedSlots = free.slots;
|
|
399
|
+
decisions.slotOptions = free.slots.map((slot, index) => ({
|
|
400
|
+
key: `T${index + 1}`,
|
|
401
|
+
label: `${slot.begin.slice(11)}~${slot.end.slice(11)}(${day}) · 全部参会人空闲`,
|
|
402
|
+
value: { begin: slot.begin, end: slot.end },
|
|
403
|
+
reason: '该时段所有参会人都没有会议与日程占用(已同时核对会议回执与日程)',
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
else if (free.blockingSummary) {
|
|
407
|
+
decisions.dayBlocked = { day, summary: free.blockingSummary };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
decisions.timings = timings;
|
|
411
|
+
const shaped = await shapeBrowserValues(env, {
|
|
412
|
+
mt_name: args.mt_name,
|
|
413
|
+
begin_datetime: args.begin_datetime,
|
|
414
|
+
end_datetime: args.end_datetime,
|
|
415
|
+
mt_desc: args.mt_desc,
|
|
416
|
+
address: addressInput,
|
|
417
|
+
customize_address: args.customize_address,
|
|
418
|
+
hrm_members: args.hrm_members,
|
|
419
|
+
total_member: args.total_member,
|
|
420
|
+
caller: args.caller,
|
|
421
|
+
contacter: args.contacter,
|
|
422
|
+
other_remark: args.other_remark,
|
|
423
|
+
mt_type: args.mt_type,
|
|
424
|
+
});
|
|
425
|
+
const { main, mapped, unmapped } = mapFields(form.fields, shaped.values);
|
|
426
|
+
const warnings = [];
|
|
427
|
+
for (const note of shaped.notes)
|
|
428
|
+
warnings.push(note);
|
|
429
|
+
const picked = decisions.autoRoom?.picked;
|
|
430
|
+
if (picked) {
|
|
431
|
+
warnings.push(`未指定会议室 → 已自动选择「${picked.name}」(${(picked.reasons ?? []).join(';')});`
|
|
432
|
+
+ `另有候选 ${(decisions.roomOptions ?? []).slice(1).map((item) => item.label).join(' / ') || '无'}`);
|
|
433
|
+
}
|
|
434
|
+
const slotOptions = decisions.slotOptions ?? [];
|
|
435
|
+
if (slotOptions.length > 0) {
|
|
436
|
+
warnings.push(`参会人时间有冲突;当天所有人都空的时段:${slotOptions.map((item) => `[${item.key}] ${item.label}`).join(';')}`
|
|
437
|
+
+ '——请用选项方式让用户挑(选定后重新 prepare 并改时间)。');
|
|
438
|
+
}
|
|
439
|
+
else if (decisions.dayBlocked?.summary) {
|
|
440
|
+
warnings.push(String(decisions.dayBlocked.summary));
|
|
441
|
+
}
|
|
442
|
+
if (unmapped.length > 0) {
|
|
443
|
+
warnings.push(`以下字段没有在建会流程表单里找到同名字段,未自动填写:${unmapped.join('、')}`
|
|
444
|
+
+ `(表单现有字段:${form.fields.map((field) => field.name).join('、')})`);
|
|
445
|
+
}
|
|
446
|
+
if (!main[form.fields.find((field) => field.name === '会议名称')?.id ?? ''] && !mapped.mt_name) {
|
|
447
|
+
warnings.push('建会流程表单里没找到「会议名称」字段,请人工确认流程是否配错');
|
|
448
|
+
}
|
|
449
|
+
const session = await env.host.getSession();
|
|
450
|
+
const continuation = await env.continuation.issue({
|
|
451
|
+
operation: 'meeting.eb.meeting.create.flow.apply',
|
|
452
|
+
baseUrl: session.baseUrl,
|
|
453
|
+
profile: session.profile,
|
|
454
|
+
userId: session.userId,
|
|
455
|
+
payload: {
|
|
456
|
+
workflowId,
|
|
457
|
+
requestName: String(args.mt_name),
|
|
458
|
+
mtName: String(args.mt_name),
|
|
459
|
+
data: { main },
|
|
460
|
+
surfaceContinuation: form.continuation,
|
|
461
|
+
},
|
|
462
|
+
fingerprint: JSON.stringify({ workflowId, main }),
|
|
463
|
+
}, 10 * 60 * 1000);
|
|
464
|
+
return meetingSuccess('meeting.eb.meeting.create.flow.prepare', {
|
|
465
|
+
status: 'AWAITING_CONFIRMATION',
|
|
466
|
+
preview: {
|
|
467
|
+
workflowId,
|
|
468
|
+
workflowCandidates: candidates,
|
|
469
|
+
...(probes.length > 0 ? { workflowProbes: probes } : {}),
|
|
470
|
+
requestName: String(args.mt_name),
|
|
471
|
+
fieldMapping: mapped,
|
|
472
|
+
formData: { main },
|
|
473
|
+
formFieldCount: form.fields.length,
|
|
474
|
+
...(Object.keys(decisions).length > 0 ? { decisions } : {}),
|
|
475
|
+
},
|
|
476
|
+
continuation,
|
|
477
|
+
expiresInSeconds: 600,
|
|
478
|
+
}, warnings.length > 0 ? { warnings } : {});
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
const ebMeetingCreateFlowApply = {
|
|
482
|
+
name: 'meeting.eb.meeting.create.flow.apply',
|
|
483
|
+
resource: 'eb',
|
|
484
|
+
method: 'meeting.eb.meeting.create.flow.apply',
|
|
485
|
+
action: 'meeting.eb.meeting.create.flow.apply',
|
|
486
|
+
summary: '流程建会(提交):用确认票据提交建会流程;提交后轮询会议列表确认会议是否已生成,生成则自动出邀请函',
|
|
487
|
+
capability: 'eb.meeting',
|
|
488
|
+
risk: 'high-risk-write',
|
|
489
|
+
requiresConfirmation: true,
|
|
490
|
+
inputSchema: schemaOf({
|
|
491
|
+
continuation: { type: 'string', description: 'prepare 返回的确认票据(必填)' },
|
|
492
|
+
confirm: { type: 'boolean', description: '必须显式传 true' },
|
|
493
|
+
waitSeconds: { type: 'number', description: '提交后等待会议生成的最长秒数(默认 60,0 = 不等待)' },
|
|
494
|
+
}, { required: ['continuation', 'confirm'] }),
|
|
495
|
+
handler: async (args, env) => {
|
|
496
|
+
if (args.confirm !== true) {
|
|
497
|
+
throw meetingValidationError('confirmation_required', '流程建会属于高影响写操作,必须显式传 confirm=true');
|
|
498
|
+
}
|
|
499
|
+
const lease = await env.continuation.verify(String(args.continuation ?? ''), 'meeting.eb.meeting.create.flow.apply');
|
|
500
|
+
const session = await env.host.getSession();
|
|
501
|
+
if (lease.baseUrl !== session.baseUrl || lease.userId !== session.userId) {
|
|
502
|
+
throw meetingValidationError('context_mismatch', '确认上下文已变化(租户或登录账号不同),请重新执行 prepare');
|
|
503
|
+
}
|
|
504
|
+
const payload = lease?.payload ?? {};
|
|
505
|
+
const { createWorkflowHost } = await import('../../../workflow/host.js');
|
|
506
|
+
const { createWorkflowContinuationSigner } = await import('../../../workflow/continuation.js');
|
|
507
|
+
const { getWorkflowOperation } = await import('../../../workflow/operations/registry.js');
|
|
508
|
+
const wfEnv = {
|
|
509
|
+
...workflowEnv(env),
|
|
510
|
+
host: createWorkflowHost(env.ctx),
|
|
511
|
+
continuation: createWorkflowContinuationSigner(env.ctx),
|
|
512
|
+
};
|
|
513
|
+
const submitStartedAt = Date.now();
|
|
514
|
+
const submit = getWorkflowOperation('workflow.createRequest');
|
|
515
|
+
const submitted = await submit.handler({
|
|
516
|
+
// 工作流的入参名是 context(createForm 返回的 continuation 就是它的 context 句柄)
|
|
517
|
+
context: payload.surfaceContinuation,
|
|
518
|
+
data: payload.data,
|
|
519
|
+
action: 'submit',
|
|
520
|
+
requestName: payload.requestName,
|
|
521
|
+
confirm: true,
|
|
522
|
+
}, wfEnv);
|
|
523
|
+
const submittedData = submitted?.data ?? {};
|
|
524
|
+
const requestId = String(submittedData.requestId ?? submittedData.id ?? '').trim();
|
|
525
|
+
if (!requestId && String(submittedData.status ?? '') === 'NEEDS_CONFIRMATION') {
|
|
526
|
+
// 浏览器字段(会议地点/参会人员/召集人…)解析不出唯一目标时,工作流会停下来要「确认」。
|
|
527
|
+
// 这不是失败,也不该吞掉:把服务端给的原话、已解析值、候选原样交回去,由调用方向用户确认后重提。
|
|
528
|
+
return meetingSuccess('meeting.eb.meeting.create.flow.apply', {
|
|
529
|
+
status: 'NEEDS_CONFIRMATION',
|
|
530
|
+
workflowId: payload.workflowId,
|
|
531
|
+
requestName: payload.requestName,
|
|
532
|
+
message: String(submittedData.message ?? ''),
|
|
533
|
+
resolvedValues: submittedData.resolvedValues ?? [],
|
|
534
|
+
unresolved: submittedData.unresolved ?? submittedData.candidates ?? [],
|
|
535
|
+
submit: submittedData,
|
|
536
|
+
}, {
|
|
537
|
+
warnings: [
|
|
538
|
+
'建会流程未提交:服务端有字段无法唯一解析(多为会议地点/参会人员/召集人等浏览按钮字段),'
|
|
539
|
+
+ '请按 message / unresolved 与用户确认取值后重新 prepare(把这些字段改成服务端认得的格式,如「名称(ID)」)。',
|
|
540
|
+
],
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
if (!requestId) {
|
|
544
|
+
// 没拿到 requestId 时**不能**说"流程已提交":常见原因是必填项缺失(工作流只在 saveFormData 前校验),
|
|
545
|
+
// 也可能只是存了草稿。把缺失项原样交回去,让调用方补字段,而不是含糊地说"已提交"。
|
|
546
|
+
const missing = submittedData.missingFields ?? submittedData.requiredMissing ?? submittedData.missing ?? [];
|
|
547
|
+
throw meetingValidationError('flow_submit_incomplete', `建会流程未提交成功(status=${String(submittedData.status ?? submitted?.status ?? '未知')}):`
|
|
548
|
+
+ (Array.isArray(missing) && missing.length > 0
|
|
549
|
+
? `缺少必填项 ${missing.map((item) => item?.name ?? item).join('、')}`
|
|
550
|
+
: '服务端未返回 requestId,请查看 result 排查'), { submit: submittedData, missing });
|
|
551
|
+
}
|
|
552
|
+
const warnings = [];
|
|
553
|
+
// 会议由流程/ESB 创建:提交后不一定立刻出现,轮询一下再决定要不要出邀请函。
|
|
554
|
+
// 默认只等 20s:流程多走审批,干等会把"建个会"拖成好几分钟(用户 2026-10-28 反馈);
|
|
555
|
+
// 没等到就给 pending,让调用方稍后补邀请函。
|
|
556
|
+
const waitSeconds = args.waitSeconds === undefined ? 20 : Math.max(0, Math.min(180, Number(args.waitSeconds)));
|
|
557
|
+
let meetingId = '';
|
|
558
|
+
const timings = { submitMs: Date.now() - submitStartedAt };
|
|
559
|
+
if (waitSeconds > 0) {
|
|
560
|
+
// 轮询策略:**先立即查一次**(多数情况下会议是同步生成的),没有就 5 秒后再查一次;
|
|
561
|
+
// 仍未生成说明还在审批,直接返回 pending —— 不再按 waitSeconds 死等(用户体感"建会好几分钟"多半是这里)。
|
|
562
|
+
const { getMeetingOperation } = await import('../registry.js');
|
|
563
|
+
const list = getMeetingOperation('meeting.eb.meeting.list');
|
|
564
|
+
const deadline = Date.now() + Math.min(waitSeconds, 30) * 1000;
|
|
565
|
+
const pollStart = Date.now();
|
|
566
|
+
let waited = 0;
|
|
567
|
+
while (Date.now() <= deadline) {
|
|
568
|
+
try {
|
|
569
|
+
const page = await list.handler({ mt_name: payload.mtName, pageSize: 5 }, env);
|
|
570
|
+
const rows = (page?.data?.rows ?? []);
|
|
571
|
+
const hit = rows
|
|
572
|
+
.map((row) => row?.mainTable ?? row ?? {})
|
|
573
|
+
.find((main) => String(main.mt_name ?? '').trim() === String(payload.mtName).trim());
|
|
574
|
+
if (hit) {
|
|
575
|
+
meetingId = String(hit.id ?? '').trim();
|
|
576
|
+
if (meetingId)
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
// 查询失败不中断,继续按节奏重试
|
|
582
|
+
}
|
|
583
|
+
if (waited >= 5000)
|
|
584
|
+
break; // 第二次仍未命中:不再等,交给调用方稍后补邀请函
|
|
585
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
586
|
+
waited += 5000;
|
|
587
|
+
}
|
|
588
|
+
timings.pollMs = Date.now() - pollStart;
|
|
589
|
+
}
|
|
590
|
+
let invitation = { pending: true };
|
|
591
|
+
if (meetingId) {
|
|
592
|
+
const invitationStart = Date.now();
|
|
593
|
+
try {
|
|
594
|
+
const { getMeetingOperation } = await import('../registry.js');
|
|
595
|
+
const render = getMeetingOperation('meeting.invitation.render');
|
|
596
|
+
const rendered = await render.handler({ mtId: meetingId }, env);
|
|
597
|
+
invitation = { pending: false, ...(rendered?.data ?? {}) };
|
|
598
|
+
timings.invitationMs = Date.now() - invitationStart;
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
invitation = {
|
|
602
|
+
pending: false,
|
|
603
|
+
ok: false,
|
|
604
|
+
error: String(error?.message ?? error).slice(0, 200),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
warnings.push(`流程已提交(requestId=${requestId || '未返回'}),但 ${waitSeconds}s 内没在会议表里查到「${payload.mtName}」——`
|
|
610
|
+
+ '多数情况是还在审批中。审批通过后(或后台建会完成后)再跑一次 meeting.invitation.render 出邀请函;'
|
|
611
|
+
+ '本 operation 不重复提交流程。');
|
|
612
|
+
}
|
|
613
|
+
return meetingSuccess('meeting.eb.meeting.create.flow.apply', {
|
|
614
|
+
status: 'COMPLETE',
|
|
615
|
+
timings,
|
|
616
|
+
workflowId: payload.workflowId,
|
|
617
|
+
requestId,
|
|
618
|
+
requestName: payload.requestName,
|
|
619
|
+
meetingId: meetingId || null,
|
|
620
|
+
invitation,
|
|
621
|
+
result: submitted?.data ?? null,
|
|
622
|
+
}, warnings.length > 0 ? { warnings } : {});
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
/**
|
|
626
|
+
* 一步建会(prepare + apply 合并成一次调用)。
|
|
627
|
+
*
|
|
628
|
+
* 为什么需要(2026-10-28 用户反馈「创建一个会议要好几分钟」):
|
|
629
|
+
* CLI 单次调用其实只有 1~2 秒(prepare)/ 5 秒左右(apply),慢的是**对话里来回**——
|
|
630
|
+
* 先 prepare、再等模型确认、再 apply、中间还要补邀请函/查冲突,每轮都有模型推理与大 JSON 进出上下文。
|
|
631
|
+
* 把这两步合成一个 operation 后,无歧义情况下**一次调用就能建完**(含会议室、冲突提示、邀请函)。
|
|
632
|
+
*
|
|
633
|
+
* 仍保留人工选择:需要选流程(多流程可发起)时不提交,原样返回 NEEDS_SELECTION 的选项。
|
|
634
|
+
*/
|
|
635
|
+
const ebMeetingCreateFlowQuick = {
|
|
636
|
+
name: 'meeting.eb.meeting.create.flow.quick',
|
|
637
|
+
resource: 'eb',
|
|
638
|
+
method: 'meeting.eb.meeting.create.flow.quick',
|
|
639
|
+
action: 'meeting.eb.meeting.create.flow.quick',
|
|
640
|
+
summary: '一步流程建会(prepare+apply 合并):内部并行做会议室推荐/冲突检测/空闲时段,提交后自动出邀请函;多流程或需选择时不提交,返回选项',
|
|
641
|
+
capability: 'eb.meeting',
|
|
642
|
+
risk: 'high-risk-write',
|
|
643
|
+
requiresConfirmation: true,
|
|
644
|
+
inputSchema: schemaOf({
|
|
645
|
+
mt_name: { type: 'string', description: '会议名称(必填)' },
|
|
646
|
+
begin_datetime: { ...SCHEMA_DATETIME_MINUTE, description: '开始时间(必填,yyyy-MM-dd HH:mm)' },
|
|
647
|
+
end_datetime: { ...SCHEMA_DATETIME_MINUTE, description: '结束时间(必填,yyyy-MM-dd HH:mm)' },
|
|
648
|
+
mt_desc: { type: 'string', description: '会议内容/说明' },
|
|
649
|
+
address: { type: 'string', description: '会议室(id 或名称;不传则返回候选让用户选,不会自动决定)' },
|
|
650
|
+
customize_address: { type: 'string', description: '自定义会议地点' },
|
|
651
|
+
hrm_members: { type: 'string', description: '参会人员 id,逗号分隔' },
|
|
652
|
+
total_member: { type: 'number', description: '参会人数' },
|
|
653
|
+
caller: { type: 'string', description: '召集人' },
|
|
654
|
+
contacter: { type: 'string', description: '联系人' },
|
|
655
|
+
other_remark: { type: 'string', description: '备注' },
|
|
656
|
+
workflowId: { type: 'string', description: '指定建会流程 id(多流程时用来跳过选择)' },
|
|
657
|
+
autoRoom: { type: 'boolean', description: '默认 false:不指定会议室时返回候选让用户选;传 true 才自动采用推荐会议室' },
|
|
658
|
+
allowNoRoom: { type: 'boolean', description: '确实不需要会议室(线上会等)时传 true' },
|
|
659
|
+
confirm: { type: 'boolean', description: '必须显式 true 才会真正提交' },
|
|
660
|
+
waitSeconds: { type: 'number', description: '提交后等待会议生成的秒数(默认 20;算法是立即查一次 + 5s 再查一次,仍未生成就返回 pending)' },
|
|
661
|
+
enrich: { type: 'boolean', description: '是否顺带做会议室推荐/冲突检测/空闲时段(默认 true;人、时间、房间都已确定时传 false 可显著加快)' },
|
|
662
|
+
}, { required: ['mt_name', 'begin_datetime', 'end_datetime', 'confirm'] }),
|
|
663
|
+
handler: async (args, env) => {
|
|
664
|
+
if (args.confirm !== true) {
|
|
665
|
+
throw meetingValidationError('confirmation_required', '一步建会会直接提交流程,必须显式传 confirm=true;如只想看方案,请用 meeting.eb.meeting.create.flow.prepare');
|
|
666
|
+
}
|
|
667
|
+
const { getMeetingOperation } = await import('../registry.js');
|
|
668
|
+
const prepareStartedAt = Date.now();
|
|
669
|
+
const prepared = await getMeetingOperation('meeting.eb.meeting.create.flow.prepare').handler({ ...args, confirm: undefined }, env);
|
|
670
|
+
if (prepared?.data?.status === 'NEEDS_SELECTION') {
|
|
671
|
+
return prepared; // 让用户先选流程,不提交
|
|
672
|
+
}
|
|
673
|
+
const continuation = String(prepared?.data?.continuation ?? '');
|
|
674
|
+
if (!continuation) {
|
|
675
|
+
throw meetingValidationError('flow_prepare_failed', '建会流程准备阶段没有返回确认票据,未提交');
|
|
676
|
+
}
|
|
677
|
+
const prepareMs = Date.now() - prepareStartedAt;
|
|
678
|
+
const applyStartedAt = Date.now();
|
|
679
|
+
const applied = await getMeetingOperation('meeting.eb.meeting.create.flow.apply').handler({
|
|
680
|
+
continuation,
|
|
681
|
+
confirm: true,
|
|
682
|
+
...(args.waitSeconds !== undefined ? { waitSeconds: args.waitSeconds } : {}),
|
|
683
|
+
}, env);
|
|
684
|
+
const warnings = [
|
|
685
|
+
...(prepared?.warnings ?? []),
|
|
686
|
+
...(applied?.warnings ?? []),
|
|
687
|
+
];
|
|
688
|
+
return meetingSuccess('meeting.eb.meeting.create.flow.quick', {
|
|
689
|
+
status: applied?.data?.status ?? 'COMPLETE',
|
|
690
|
+
workflowId: applied?.data?.workflowId,
|
|
691
|
+
requestId: applied?.data?.requestId,
|
|
692
|
+
meetingId: applied?.data?.meetingId ?? null,
|
|
693
|
+
invitation: applied?.data?.invitation ?? null,
|
|
694
|
+
decisions: prepared?.data?.preview?.decisions ?? null,
|
|
695
|
+
timings: {
|
|
696
|
+
...(prepared?.data?.preview?.decisions?.timings ?? {}),
|
|
697
|
+
// apply 侧的分段耗时(提交/轮询/邀请函)也要带出来,否则"到底慢在哪"还是看不清
|
|
698
|
+
...(applied?.data?.timings ?? {}),
|
|
699
|
+
prepareMs,
|
|
700
|
+
applyMs: Date.now() - applyStartedAt,
|
|
701
|
+
totalMs: Date.now() - prepareStartedAt,
|
|
702
|
+
},
|
|
703
|
+
}, warnings.length > 0 ? { warnings } : {});
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
export const meetingEbCreateFlowOperations = [
|
|
707
|
+
ebMeetingCreateFlowPrepare,
|
|
708
|
+
ebMeetingCreateFlowApply,
|
|
709
|
+
ebMeetingCreateFlowQuick,
|
|
710
|
+
];
|