weaver-work-cli 0.1.4 → 0.1.5
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/internal/e10/request-runtime.js +3 -2
- package/dist/shortcuts/index.js +4 -0
- package/dist/shortcuts/meeting/continuation.js +70 -0
- package/dist/shortcuts/meeting/errors.js +63 -0
- package/dist/shortcuts/meeting/host.js +167 -0
- package/dist/shortcuts/meeting/index.js +123 -0
- package/dist/shortcuts/meeting/manifest.js +64 -0
- package/dist/shortcuts/meeting/operations/catalogs.js +58 -0
- package/dist/shortcuts/meeting/operations/change.js +242 -0
- package/dist/shortcuts/meeting/operations/conflicts.js +142 -0
- package/dist/shortcuts/meeting/operations/create.js +349 -0
- package/dist/shortcuts/meeting/operations/env.js +191 -0
- package/dist/shortcuts/meeting/operations/lifecycle.js +139 -0
- package/dist/shortcuts/meeting/operations/meetings.js +73 -0
- package/dist/shortcuts/meeting/operations/receipts.js +195 -0
- package/dist/shortcuts/meeting/operations/registry.js +47 -0
- package/dist/shortcuts/meeting/operations/rooms.js +132 -0
- package/dist/shortcuts/meeting/operations/shared.js +112 -0
- package/dist/shortcuts/meeting/operations/signs.js +41 -0
- package/dist/shortcuts/meeting/operations/types.js +25 -0
- package/dist/shortcuts/meeting/operations.js +19 -0
- package/dist/shortcuts/yimiaoban/continuation.js +70 -0
- package/dist/shortcuts/yimiaoban/errors.js +61 -0
- package/dist/shortcuts/yimiaoban/host.js +469 -0
- package/dist/shortcuts/yimiaoban/index.js +271 -0
- package/dist/shortcuts/yimiaoban/manifest.js +28 -0
- package/dist/shortcuts/yimiaoban/operations/file-user.js +288 -0
- package/dist/shortcuts/yimiaoban/operations/group-read.js +477 -0
- package/dist/shortcuts/yimiaoban/operations/group-write.js +591 -0
- package/dist/shortcuts/yimiaoban/operations/msg-parse.js +429 -0
- package/dist/shortcuts/yimiaoban/operations/msg-sync.js +519 -0
- package/dist/shortcuts/yimiaoban/operations/msg-write.js +593 -0
- package/dist/shortcuts/yimiaoban/operations/person.js +102 -0
- package/dist/shortcuts/yimiaoban/operations/registry.js +38 -0
- package/dist/shortcuts/yimiaoban/operations/session.js +437 -0
- package/dist/shortcuts/yimiaoban/operations/shared.js +236 -0
- package/dist/shortcuts/yimiaoban/operations/types.js +27 -0
- package/dist/shortcuts/yimiaoban/operations/write-util.js +78 -0
- package/dist/shortcuts/yimiaoban/operations.js +8 -0
- package/docs/_catalog.md +6 -0
- package/docs/meeting.md +48 -0
- package/docs/yimiaoban.md +86 -0
- package/package.json +1 -1
- package/skill-template/business-info.json +4 -1
- package/skill-template/domains/meeting.md +3 -0
- package/skill-template/domains/yimiaoban.md +21 -0
- package/skills/weaver-e10-calendar/SKILL.md +1 -0
- package/skills/weaver-e10-esb/SKILL.md +1 -0
- package/skills/weaver-e10-esb/references/source-manifest.json +3 -3
- package/skills/weaver-e10-hrm/SKILL.md +1 -0
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +1 -0
- package/skills/weaver-e10-jucailin/SKILL.md +1 -0
- package/skills/weaver-e10-mail/SKILL.md +1 -0
- package/skills/weaver-e10-meeting/SKILL.md +106 -0
- package/skills/weaver-e10-meeting/product.json +8 -0
- package/skills/weaver-e10-meeting/references/meeting-change.md +67 -0
- package/skills/weaver-e10-meeting/references/meeting-create.md +52 -0
- package/skills/weaver-e10-meeting/references/meeting-lifecycle.md +57 -0
- package/skills/weaver-e10-meeting/references/meeting-queries.md +61 -0
- package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +48 -0
- package/skills/weaver-e10-meeting/references/meeting-update.md +7 -0
- package/skills/weaver-e10-meeting/references/source-manifest.json +50 -0
- package/skills/weaver-e10-plan/SKILL.md +1 -0
- package/skills/weaver-e10-plan/references/source-manifest.json +3 -3
- package/skills/weaver-e10-qiyecheng/SKILL.md +1 -0
- package/skills/weaver-e10-qiyecheng/references/source-manifest.json +3 -3
- package/skills/weaver-e10-shared/SKILL.md +4 -0
- package/skills/weaver-e10-skill-maker/SKILL.md +1 -0
- package/skills/weaver-e10-skill-maker/references/weaver-skill-style.md +1 -1
- package/skills/weaver-e10-wenshuding/SKILL.md +1 -0
- package/skills/weaver-e10-wenshuding/references/source-manifest.json +3 -3
- package/skills/weaver-e10-yepiaotong/references/source-manifest.json +5 -5
- package/skills/weaver-e10-yimiaoban/SKILL.md +104 -0
- package/skills/weaver-e10-yimiaoban/product.json +8 -0
- package/skills/weaver-e10-yimiaoban/references/ding-write.md +68 -0
- package/skills/weaver-e10-yimiaoban/references/error-codes.md +85 -0
- package/skills/weaver-e10-yimiaoban/references/field-resolution.md +46 -0
- package/skills/weaver-e10-yimiaoban/references/file-user-i18n.md +54 -0
- package/skills/weaver-e10-yimiaoban/references/group-read.md +55 -0
- package/skills/weaver-e10-yimiaoban/references/group-write.md +72 -0
- package/skills/weaver-e10-yimiaoban/references/msg-read.md +59 -0
- package/skills/weaver-e10-yimiaoban/references/msg-write.md +64 -0
- package/skills/weaver-e10-yimiaoban/references/person.md +44 -0
- package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +61 -0
- package/skills/weaver-e10-yimiaoban/references/session-sysmsg.md +47 -0
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +485 -0
- package/skills/weaver-e10-ziguanjia/SKILL.md +1 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { meetingConflictError, meetingConfirmationError, meetingPartialError, meetingValidationError } from '../errors.js';
|
|
3
|
+
import { meetingSuccess, SCHEMA_DATETIME_MINUTE, SCHEMA_ID } from './types.js';
|
|
4
|
+
import { assertDateTimeMinute, extractConflict, toIdCsv, toIdString, } from './shared.js';
|
|
5
|
+
const CONTINUATION_TTL_SECONDS = 600;
|
|
6
|
+
function fingerprint(value) {
|
|
7
|
+
return createHash('sha256').update(JSON.stringify(value ?? null)).digest('hex').slice(0, 32);
|
|
8
|
+
}
|
|
9
|
+
function requireConfirm(args, action) {
|
|
10
|
+
if (args.confirm !== true) {
|
|
11
|
+
throw meetingConfirmationError(action, { hint: '重新调用时传入 confirm=true 与 prepare 返回的 continuation' });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function verifyLease(args, action, env) {
|
|
15
|
+
const lease = await env.continuation.verify(args.continuation, action);
|
|
16
|
+
const session = await env.host.getSession();
|
|
17
|
+
if (lease.baseUrl !== session.baseUrl || lease.profile !== session.profile || lease.userId !== session.userId) {
|
|
18
|
+
throw meetingValidationError('context_mismatch', '确认上下文已变化(租户或登录账号不同),请重新执行 prepare');
|
|
19
|
+
}
|
|
20
|
+
return lease;
|
|
21
|
+
}
|
|
22
|
+
function isNetworkError(error) {
|
|
23
|
+
return error instanceof Error && error.type === 'network';
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// MeetingDetailBean 归一化(纯函数,prepare/apply 共用,保证 fingerprint 一致)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
const REPEAT_TYPE_LABELS = { 1: '天', 2: '周', 4: '月', 5: '年', 6: '自定义周期' };
|
|
29
|
+
const BYDAY_LABELS = {
|
|
30
|
+
MO: '周一', TU: '周二', WE: '周三', TH: '周四', FR: '周五', SA: '周六', SU: '周日',
|
|
31
|
+
};
|
|
32
|
+
/** 周期概述 repeatDesc:后端不生成,按前端 calculateRepeatDesc 规则拼接。 */
|
|
33
|
+
function buildRepeatDesc(args) {
|
|
34
|
+
const interval = typeof args.interval === 'number' && args.interval > 0 ? args.interval : 1;
|
|
35
|
+
const frequent = typeof args.frequent === 'number' ? args.frequent : 1;
|
|
36
|
+
const freqLabel = frequent === 1 ? '天' : frequent === 2 ? '周' : frequent === 3 ? '月' : '年';
|
|
37
|
+
const cadence = interval === 1 ? `每 1 ${freqLabel}` : `每 ${interval} ${freqLabel}`;
|
|
38
|
+
const byDay = typeof args.byDay === 'string' && args.byDay.trim()
|
|
39
|
+
? args.byDay.split(',').map((item) => BYDAY_LABELS[item.trim().toUpperCase()] || item.trim()).join(' ')
|
|
40
|
+
: '';
|
|
41
|
+
const finishLabel = args.finishAt === 1 && args.count !== undefined
|
|
42
|
+
? `重复${args.count}次`
|
|
43
|
+
: args.until !== undefined ? `截止${args.until}` : '';
|
|
44
|
+
return [cadence, byDay, finishLabel, '非工作日正常召开'].filter(Boolean).join(' ');
|
|
45
|
+
}
|
|
46
|
+
export function normalizeCreateInput(args, currentUserId) {
|
|
47
|
+
const name = typeof args.name === 'string' ? args.name.trim() : '';
|
|
48
|
+
if (!name)
|
|
49
|
+
throw meetingValidationError('name_required', 'name(会议名称)必填');
|
|
50
|
+
const beginDatetime = assertDateTimeMinute(args.beginDatetime, 'beginDatetime');
|
|
51
|
+
const endDatetime = assertDateTimeMinute(args.endDatetime, 'endDatetime');
|
|
52
|
+
const body = {
|
|
53
|
+
operateType: 'SUBMIT',
|
|
54
|
+
id: '',
|
|
55
|
+
name,
|
|
56
|
+
beginDatetime,
|
|
57
|
+
endDatetime,
|
|
58
|
+
caller: toIdString(args.caller ?? currentUserId, 'caller'),
|
|
59
|
+
contacter: toIdString(args.contacter ?? currentUserId, 'contacter'),
|
|
60
|
+
};
|
|
61
|
+
if (args.address !== undefined)
|
|
62
|
+
body.address = toIdString(args.address, 'address');
|
|
63
|
+
if (args.customizeAddress !== undefined)
|
|
64
|
+
body.customizeAddress = String(args.customizeAddress);
|
|
65
|
+
if (args.mtType !== undefined)
|
|
66
|
+
body.mtType = toIdString(args.mtType, 'mtType');
|
|
67
|
+
const hrmMembers = toIdCsv(args.hrmMembers, 'hrmMembers');
|
|
68
|
+
if (hrmMembers !== undefined)
|
|
69
|
+
body.hrmMembers = hrmMembers;
|
|
70
|
+
const orgMembers = toIdCsv(args.orgMembers, 'orgMembers');
|
|
71
|
+
if (orgMembers !== undefined)
|
|
72
|
+
body.orgMembers = orgMembers;
|
|
73
|
+
if (args.otherMembers !== undefined)
|
|
74
|
+
body.otherMembers = String(args.otherMembers);
|
|
75
|
+
const crmMembers = toIdCsv(args.crmMembers, 'crmMembers');
|
|
76
|
+
if (crmMembers !== undefined)
|
|
77
|
+
body.crmMembers = crmMembers;
|
|
78
|
+
if (args.totalMember !== undefined)
|
|
79
|
+
body.totalMember = args.totalMember;
|
|
80
|
+
// 周期字段(有任一即视为周期会议)
|
|
81
|
+
const repeatKeys = ['repeatType', 'frequent', 'interval', 'finishAt', 'count', 'until', 'byDay', 'byMonthDay', 'byMonth', 'byWeekNo', 'byDateType'];
|
|
82
|
+
const hasRepeat = repeatKeys.some((key) => args[key] !== undefined);
|
|
83
|
+
if (hasRepeat) {
|
|
84
|
+
if (args.repeatType === undefined) {
|
|
85
|
+
throw meetingValidationError('repeatType_required', '周期会议必须提供 repeatType(1=每天 2=每周 4=每月 5=每年 6=自定义)');
|
|
86
|
+
}
|
|
87
|
+
body.repeatType = args.repeatType;
|
|
88
|
+
if (args.frequent !== undefined)
|
|
89
|
+
body.frequent = args.frequent;
|
|
90
|
+
if (args.interval !== undefined)
|
|
91
|
+
body.interval = args.interval;
|
|
92
|
+
// finishAt 周期会议必填(当前租户 options 仅 1=次数 / 2=日期,禁止 0=永不)
|
|
93
|
+
if (args.finishAt === undefined) {
|
|
94
|
+
throw meetingValidationError('finishAt_required', '周期会议必须提供 finishAt(1=重复次数 2=结束日期);禁止传 0(永不)');
|
|
95
|
+
}
|
|
96
|
+
if (args.finishAt !== 1 && args.finishAt !== 2) {
|
|
97
|
+
throw meetingValidationError('finishAt_invalid', 'finishAt 仅允许 1=重复次数 或 2=结束日期');
|
|
98
|
+
}
|
|
99
|
+
body.finishAt = args.finishAt;
|
|
100
|
+
if (args.finishAt === 1 && args.count === undefined) {
|
|
101
|
+
throw meetingValidationError('count_required', 'finishAt=1(按次数)时必须提供 count');
|
|
102
|
+
}
|
|
103
|
+
if (args.finishAt === 2 && args.until === undefined) {
|
|
104
|
+
throw meetingValidationError('until_required', 'finishAt=2(按日期)时必须提供 until(yyyy-MM-dd)');
|
|
105
|
+
}
|
|
106
|
+
if (args.count !== undefined)
|
|
107
|
+
body.count = args.count;
|
|
108
|
+
if (args.until !== undefined) {
|
|
109
|
+
if (typeof args.until !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(args.until)) {
|
|
110
|
+
throw meetingValidationError('until_invalid', 'until 必须是 yyyy-MM-dd 格式');
|
|
111
|
+
}
|
|
112
|
+
body.until = args.until;
|
|
113
|
+
}
|
|
114
|
+
if (args.byDay !== undefined)
|
|
115
|
+
body.byDay = String(args.byDay);
|
|
116
|
+
if (args.byMonthDay !== undefined)
|
|
117
|
+
body.byMonthDay = String(args.byMonthDay);
|
|
118
|
+
if (args.byMonth !== undefined)
|
|
119
|
+
body.byMonth = String(args.byMonth);
|
|
120
|
+
if (args.byWeekNo !== undefined)
|
|
121
|
+
body.byWeekNo = String(args.byWeekNo);
|
|
122
|
+
if (args.byDateType !== undefined)
|
|
123
|
+
body.byDateType = args.byDateType;
|
|
124
|
+
// repeatDesc 后端不生成:用户未提供时按规则自动拼接
|
|
125
|
+
body.repeatDesc = typeof args.repeatDesc === 'string' && args.repeatDesc.trim()
|
|
126
|
+
? args.repeatDesc.trim()
|
|
127
|
+
: buildRepeatDesc(args);
|
|
128
|
+
}
|
|
129
|
+
return body;
|
|
130
|
+
}
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// prepare 前置检查:建会路径(onlyFlowCreate/周期工作流)与强制冲突检测
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
async function assertCreatePathAllowed(args, env) {
|
|
135
|
+
const isRepeat = ['repeatType', 'finishAt', 'byDay', 'until', 'count'].some((key) => args[key] !== undefined);
|
|
136
|
+
const response = await env.host.request({
|
|
137
|
+
method: 'GET',
|
|
138
|
+
path: '/api/meeting/detail/getCalendarBaseData',
|
|
139
|
+
operation: 'meeting.create.prepare',
|
|
140
|
+
});
|
|
141
|
+
const base = response?.data?.createBaseData ?? {};
|
|
142
|
+
const repeatWorkflowIds = typeof base.repeatWorkFlowBaseIds === 'string' ? base.repeatWorkFlowBaseIds.trim() : '';
|
|
143
|
+
if (isRepeat && repeatWorkflowIds) {
|
|
144
|
+
throw meetingConflictError('conflict_rejected', '该租户为周期会议配置了工作流(repeatWorkFlowBaseIds 非空),周期会议必须走流程建会;CLI v1 未集成工作流发起,请通过工作流流程创建', { repeatWorkFlowBaseIds: repeatWorkflowIds });
|
|
145
|
+
}
|
|
146
|
+
if (!isRepeat && Number(base.onlyFlowCreate) === 1) {
|
|
147
|
+
throw meetingConflictError('conflict_rejected', '该租户 onlyFlowCreate=1:会议只能通过流程创建;CLI v1 未集成工作流发起,请通过工作流流程创建', { workFlowBaseIds: base.workFlowBaseIds ?? '' });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** 强制冲突检测:接口失败视为冲突(拒绝建会),cansub=false 拒绝提交。 */
|
|
151
|
+
async function runMandatoryConflictChecks(body, args, env) {
|
|
152
|
+
const warnings = [];
|
|
153
|
+
const beginDatetime = String(body.beginDatetime);
|
|
154
|
+
const endDatetime = String(body.endDatetime);
|
|
155
|
+
const common = {
|
|
156
|
+
beginDatetime,
|
|
157
|
+
endDatetime,
|
|
158
|
+
...(args.mtId !== undefined ? { mtId: args.mtId } : {}),
|
|
159
|
+
};
|
|
160
|
+
if (body.address !== undefined) {
|
|
161
|
+
let roomConflict;
|
|
162
|
+
try {
|
|
163
|
+
const response = await env.host.request({
|
|
164
|
+
method: 'POST',
|
|
165
|
+
path: '/api/meeting/detail/meetingChkRoom',
|
|
166
|
+
operation: 'meeting.create.prepare',
|
|
167
|
+
body: { ...common, address: body.address, ...(body.frequent !== undefined ? { frequent: body.frequent } : {}), ...(body.repeatRuleEx !== undefined ? { repeatRuleEx: body.repeatRuleEx } : {}) },
|
|
168
|
+
});
|
|
169
|
+
roomConflict = extractConflict(response);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
throw meetingConflictError('conflict_check_failed', `会议室冲突检测失败(视为冲突,拒绝建会):${error instanceof Error ? error.message : String(error)}`);
|
|
173
|
+
}
|
|
174
|
+
if (roomConflict.cansub === false) {
|
|
175
|
+
throw meetingConflictError('conflict_rejected', `会议室冲突,禁止提交:${roomConflict.message || '该时间段会议室已被占用'}`);
|
|
176
|
+
}
|
|
177
|
+
if (roomConflict.message)
|
|
178
|
+
warnings.push(`会议室冲突提醒(可提交):${roomConflict.message}`);
|
|
179
|
+
}
|
|
180
|
+
if (body.hrmMembers !== undefined) {
|
|
181
|
+
let memberConflict;
|
|
182
|
+
try {
|
|
183
|
+
const response = await env.host.request({
|
|
184
|
+
method: 'POST',
|
|
185
|
+
path: '/api/meeting/detail/meetingChkMember',
|
|
186
|
+
operation: 'meeting.create.prepare',
|
|
187
|
+
body: { ...common, hrmids: body.hrmMembers, ...(body.totalMember !== undefined ? { totalmember: body.totalMember } : {}) },
|
|
188
|
+
});
|
|
189
|
+
memberConflict = extractConflict(response);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
throw meetingConflictError('conflict_check_failed', `参会人员冲突检测失败(视为冲突,拒绝建会):${error instanceof Error ? error.message : String(error)}`);
|
|
193
|
+
}
|
|
194
|
+
if (memberConflict.cansub === false) {
|
|
195
|
+
throw meetingConflictError('conflict_rejected', `参会人员冲突,禁止提交:${memberConflict.message || '参会人在该时间段已有其他会议'}`);
|
|
196
|
+
}
|
|
197
|
+
if (memberConflict.message)
|
|
198
|
+
warnings.push(`参会人员冲突提醒(可提交):${memberConflict.message}`);
|
|
199
|
+
}
|
|
200
|
+
return warnings;
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// 预约会议(标准版 operateMeetingByType SUBMIT,后端自动生成占用/回执/签到)
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
const meetingCreatePrepare = {
|
|
206
|
+
name: 'meeting.create.prepare',
|
|
207
|
+
resource: 'meeting',
|
|
208
|
+
method: 'create.prepare',
|
|
209
|
+
action: 'meeting.create.prepare',
|
|
210
|
+
summary: '准备预约会议:归一化 MeetingDetailBean、执行建会路径检查(onlyFlowCreate/周期工作流)与强制冲突检测(cansub=false 拒绝),生成确认预览,不写入',
|
|
211
|
+
capability: 'meeting',
|
|
212
|
+
risk: 'read-before-write',
|
|
213
|
+
requiresConfirmation: true,
|
|
214
|
+
inputSchema: {
|
|
215
|
+
type: 'object',
|
|
216
|
+
additionalProperties: false,
|
|
217
|
+
properties: {
|
|
218
|
+
name: { type: 'string', minLength: 1, description: '会议名称(必填)' },
|
|
219
|
+
beginDatetime: { ...SCHEMA_DATETIME_MINUTE, description: '开始时间(必填)' },
|
|
220
|
+
endDatetime: { ...SCHEMA_DATETIME_MINUTE, description: '结束时间(必填)' },
|
|
221
|
+
address: { ...SCHEMA_ID, description: '会议室 id(长 id 用字符串)' },
|
|
222
|
+
customizeAddress: { type: 'string', description: '自定义地点' },
|
|
223
|
+
mtType: { ...SCHEMA_ID, description: '会议类型 id(先通过 meeting.type.list 获取)' },
|
|
224
|
+
hrmMembers: { type: 'string', description: '参会人员 id,逗号分隔字符串' },
|
|
225
|
+
orgMembers: { type: 'string', description: '参会部门 id,逗号分隔字符串' },
|
|
226
|
+
otherMembers: { type: 'string', description: '其他参会人员(文本)' },
|
|
227
|
+
crmMembers: { type: 'string', description: '参会客户 id,逗号分隔字符串' },
|
|
228
|
+
totalMember: { type: 'integer', description: '参会人数' },
|
|
229
|
+
caller: { ...SCHEMA_ID, description: '召集人 id,缺省当前用户(必须显式传)' },
|
|
230
|
+
contacter: { ...SCHEMA_ID, description: '联系人 id,缺省当前用户(必须显式传,后端无兜底)' },
|
|
231
|
+
repeatType: { type: 'integer', enum: [1, 2, 4, 5, 6], description: '周期类型:1=每天 2=每周 4=每月 5=每年 6=自定义' },
|
|
232
|
+
frequent: { type: 'integer', enum: [1, 2, 3, 4], description: '重复频率:1=天 2=周 3=月 4=年' },
|
|
233
|
+
interval: { type: 'integer', description: '重复间隔(默认 1)' },
|
|
234
|
+
finishAt: { type: 'integer', enum: [1, 2], description: '结束重复于:1=次数 2=日期(周期会议必填,禁止 0)' },
|
|
235
|
+
count: { type: 'integer', description: '重复次数(finishAt=1 时必填)' },
|
|
236
|
+
until: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}$', description: '重复结束日期(finishAt=2 时必填)' },
|
|
237
|
+
byDay: { type: 'string', description: '一周某天 MO,TU,WE,TH,FR,SA,SU(每周重复时必填)' },
|
|
238
|
+
byMonthDay: { type: 'string', description: '一月的第几天(1-31 或 -31~-1)' },
|
|
239
|
+
byMonth: { type: 'string', description: '一年中的第几个月(1-12)' },
|
|
240
|
+
byWeekNo: { type: 'string', description: '一年的第几周(1-53)' },
|
|
241
|
+
byDateType: { type: 'integer', enum: [0, 1], description: '按月/年重复:0=按日期 1=按星期' },
|
|
242
|
+
repeatDesc: { type: 'string', description: '周期概述;缺省按规则自动拼接' },
|
|
243
|
+
serviceItems: { type: 'string', description: '会议服务需求(提供时触发会议室属性检测)' },
|
|
244
|
+
},
|
|
245
|
+
required: ['name', 'beginDatetime', 'endDatetime'],
|
|
246
|
+
},
|
|
247
|
+
handler: async (args, env) => {
|
|
248
|
+
const session = await env.host.getSession();
|
|
249
|
+
const body = normalizeCreateInput(args, session.userId);
|
|
250
|
+
// 建会路径检查(周期工作流 / onlyFlowCreate)
|
|
251
|
+
await assertCreatePathAllowed(args, env);
|
|
252
|
+
// 强制冲突检测(cansub=false / 检测失败 → 拒绝)
|
|
253
|
+
const warnings = await runMandatoryConflictChecks(body, args, env);
|
|
254
|
+
const token = await env.continuation.issue({
|
|
255
|
+
operation: 'meeting.create.apply',
|
|
256
|
+
baseUrl: session.baseUrl,
|
|
257
|
+
profile: session.profile,
|
|
258
|
+
userId: session.userId,
|
|
259
|
+
payload: body,
|
|
260
|
+
fingerprint: fingerprint(body),
|
|
261
|
+
}, CONTINUATION_TTL_SECONDS * 1000);
|
|
262
|
+
return meetingSuccess('meeting.create.prepare', {
|
|
263
|
+
status: 'AWAITING_CONFIRMATION',
|
|
264
|
+
preview: body,
|
|
265
|
+
continuation: token,
|
|
266
|
+
expiresInSeconds: CONTINUATION_TTL_SECONDS,
|
|
267
|
+
}, { warnings });
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
const meetingCreateApply = {
|
|
271
|
+
name: 'meeting.create.apply',
|
|
272
|
+
resource: 'meeting',
|
|
273
|
+
method: 'create.apply',
|
|
274
|
+
action: 'meeting.create.apply',
|
|
275
|
+
summary: '提交预约会议(operateMeetingByType SUBMIT + id 留空);必须在用户明确确认后调用,要求 prepare 返回的 continuation',
|
|
276
|
+
capability: 'meeting',
|
|
277
|
+
risk: 'high-risk-write',
|
|
278
|
+
requiresConfirmation: true,
|
|
279
|
+
inputSchema: {
|
|
280
|
+
type: 'object',
|
|
281
|
+
additionalProperties: false,
|
|
282
|
+
properties: {
|
|
283
|
+
confirm: { const: true },
|
|
284
|
+
continuation: { type: 'string', minLength: 1 },
|
|
285
|
+
name: { type: 'string', minLength: 1 },
|
|
286
|
+
beginDatetime: SCHEMA_DATETIME_MINUTE,
|
|
287
|
+
endDatetime: SCHEMA_DATETIME_MINUTE,
|
|
288
|
+
address: SCHEMA_ID,
|
|
289
|
+
customizeAddress: { type: 'string' },
|
|
290
|
+
mtType: SCHEMA_ID,
|
|
291
|
+
hrmMembers: { type: 'string' },
|
|
292
|
+
orgMembers: { type: 'string' },
|
|
293
|
+
otherMembers: { type: 'string' },
|
|
294
|
+
crmMembers: { type: 'string' },
|
|
295
|
+
totalMember: { type: 'integer' },
|
|
296
|
+
caller: SCHEMA_ID,
|
|
297
|
+
contacter: SCHEMA_ID,
|
|
298
|
+
repeatType: { type: 'integer', enum: [1, 2, 4, 5, 6] },
|
|
299
|
+
frequent: { type: 'integer', enum: [1, 2, 3, 4] },
|
|
300
|
+
interval: { type: 'integer' },
|
|
301
|
+
finishAt: { type: 'integer', enum: [1, 2] },
|
|
302
|
+
count: { type: 'integer' },
|
|
303
|
+
until: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}$' },
|
|
304
|
+
byDay: { type: 'string' },
|
|
305
|
+
byMonthDay: { type: 'string' },
|
|
306
|
+
byMonth: { type: 'string' },
|
|
307
|
+
byWeekNo: { type: 'string' },
|
|
308
|
+
byDateType: { type: 'integer', enum: [0, 1] },
|
|
309
|
+
repeatDesc: { type: 'string' },
|
|
310
|
+
serviceItems: { type: 'string' },
|
|
311
|
+
},
|
|
312
|
+
required: ['confirm', 'continuation', 'name', 'beginDatetime', 'endDatetime'],
|
|
313
|
+
},
|
|
314
|
+
handler: async (args, env) => {
|
|
315
|
+
requireConfirm(args, 'meeting.create.apply');
|
|
316
|
+
const lease = await verifyLease(args, 'meeting.create.apply', env);
|
|
317
|
+
const session = await env.host.getSession();
|
|
318
|
+
const body = normalizeCreateInput(args, session.userId);
|
|
319
|
+
if (lease.fingerprint !== fingerprint(body)) {
|
|
320
|
+
throw meetingValidationError('target_changed', '会议内容与 prepare 时的快照不一致,请重新执行 prepare');
|
|
321
|
+
}
|
|
322
|
+
let response;
|
|
323
|
+
try {
|
|
324
|
+
response = await env.host.request({
|
|
325
|
+
method: 'POST',
|
|
326
|
+
path: '/api/meeting/detail/operateMeetingByType',
|
|
327
|
+
operation: 'meeting.create.apply',
|
|
328
|
+
body,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
if (isNetworkError(error)) {
|
|
333
|
+
throw meetingPartialError('建会请求发送后连接中断,会议是否已创建不确定,禁止自动重试,请通过 meeting.list 复核后再决定', { attempted: { name: body.name, beginDatetime: body.beginDatetime } }, { stage: 'create' });
|
|
334
|
+
}
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
const value = response;
|
|
338
|
+
const meetingId = value?.id ?? value?.data?.id ?? value?.data?.meetingId ?? null;
|
|
339
|
+
return meetingSuccess('meeting.create.apply', {
|
|
340
|
+
status: 'COMPLETE',
|
|
341
|
+
meetingId: meetingId === null ? undefined : String(meetingId),
|
|
342
|
+
result: response,
|
|
343
|
+
});
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
export const meetingCreateOperations = [
|
|
347
|
+
meetingCreatePrepare,
|
|
348
|
+
meetingCreateApply,
|
|
349
|
+
];
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { meetingValidationError } from '../errors.js';
|
|
6
|
+
import { meetingSuccess, SCHEMA_ID } from './types.js';
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// 环境模式探测(EB 版 / 标准版)——按 baseUrl 固化缓存,对齐 standalone 行为
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const EB_SMOKE_PATH = '/api/ebuilder/form/formdata/v2/getFormDataList/query_meeting_baseset';
|
|
11
|
+
const STD_CALENDAR_PATH = '/api/meeting/detail/getCalendarBaseData';
|
|
12
|
+
function stateRoot(ctx) {
|
|
13
|
+
const configured = ctx?.config?.path();
|
|
14
|
+
if (configured)
|
|
15
|
+
return resolve(configured, '..');
|
|
16
|
+
return process.env.WEAVER_WORK_CLI_HOME || process.env.WORK_CLI_HOME || resolve(homedir(), '.weaver-work-cli');
|
|
17
|
+
}
|
|
18
|
+
function cacheFile(ctx) {
|
|
19
|
+
return resolve(stateRoot(ctx), 'meeting', 'env-mode.json');
|
|
20
|
+
}
|
|
21
|
+
function emptyCache() {
|
|
22
|
+
return { schemaVersion: 1, environments: {} };
|
|
23
|
+
}
|
|
24
|
+
async function readCache(ctx) {
|
|
25
|
+
const file = cacheFile(ctx);
|
|
26
|
+
if (!existsSync(file))
|
|
27
|
+
return emptyCache();
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(await readFile(file, 'utf8'));
|
|
30
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.environments)
|
|
31
|
+
return emptyCache();
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return emptyCache();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** 原子性要求低(诊断缓存),写失败不阻断业务:返回 warning 文本。 */
|
|
39
|
+
async function writeCache(ctx, baseUrl, mode, smokePath) {
|
|
40
|
+
try {
|
|
41
|
+
const file = cacheFile(ctx);
|
|
42
|
+
await mkdir(resolve(file, '..'), { recursive: true });
|
|
43
|
+
const cache = await readCache(ctx);
|
|
44
|
+
cache.environments[baseUrl] = { mode, smokePath, detectedAt: new Date().toISOString() };
|
|
45
|
+
await writeFile(file, JSON.stringify(cache, null, 2), 'utf8');
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return `环境模式缓存写入失败(不影响本次结果,下次会重新冒烟):${error instanceof Error ? error.message : String(error)}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const meetingEnvDetect = {
|
|
53
|
+
name: 'meeting.env.detect',
|
|
54
|
+
resource: 'env',
|
|
55
|
+
method: 'detect',
|
|
56
|
+
action: 'meeting.env.detect',
|
|
57
|
+
summary: '探测当前环境会议模式:默认按 baseUrl 读缓存;未命中或 refresh=true 时 EB 版冒烟优先、失败回退标准版,成功后固化缓存',
|
|
58
|
+
capability: 'env',
|
|
59
|
+
risk: 'read',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
additionalProperties: false,
|
|
63
|
+
properties: {
|
|
64
|
+
refresh: { type: 'boolean', description: 'true=忽略缓存强制重新冒烟并更新缓存(用户说"重新探测/刷新模式"时使用)' },
|
|
65
|
+
profileId: SCHEMA_ID,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
handler: async (args, { host, ctx }) => {
|
|
69
|
+
const session = await host.getSession();
|
|
70
|
+
// ① 默认先读缓存(按 baseUrl 固化);refresh=true 才强制重新冒烟。
|
|
71
|
+
if (args.refresh !== true) {
|
|
72
|
+
const cache = await readCache(ctx);
|
|
73
|
+
const cached = cache.environments[session.baseUrl];
|
|
74
|
+
if (cached && (cached.mode === 'eb' || cached.mode === 'standard')) {
|
|
75
|
+
return meetingSuccess('meeting.env.detect', {
|
|
76
|
+
mode: cached.mode,
|
|
77
|
+
smokePath: cached.smokePath,
|
|
78
|
+
fromCache: true,
|
|
79
|
+
detectedAt: cached.detectedAt,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ② 第 1 步:EB 版冒烟(EB 优先)。接口 404/业务失败均视为「EB 未配置」。
|
|
84
|
+
let ebError;
|
|
85
|
+
try {
|
|
86
|
+
await host.request({
|
|
87
|
+
method: 'POST',
|
|
88
|
+
path: EB_SMOKE_PATH,
|
|
89
|
+
operation: 'meeting.env.detect',
|
|
90
|
+
body: { datajson: { header: { operator: session.userId } } },
|
|
91
|
+
});
|
|
92
|
+
const warning = await writeCache(ctx, session.baseUrl, 'eb', EB_SMOKE_PATH);
|
|
93
|
+
return meetingSuccess('meeting.env.detect', { mode: 'eb', smokePath: EB_SMOKE_PATH, fromCache: false }, warning ? { warnings: [warning] } : undefined);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
ebError = error;
|
|
97
|
+
}
|
|
98
|
+
// ③ 第 2 步:标准版冒烟(仅当 EB 冒烟未通过)。
|
|
99
|
+
try {
|
|
100
|
+
await host.request({
|
|
101
|
+
method: 'GET',
|
|
102
|
+
path: STD_CALENDAR_PATH,
|
|
103
|
+
operation: 'meeting.env.detect',
|
|
104
|
+
});
|
|
105
|
+
const ebDetail = ebError instanceof Error
|
|
106
|
+
? `${ebError.type ?? 'api'}/${ebError.subtype ?? 'error'}: ${ebError.message}`
|
|
107
|
+
: String(ebError);
|
|
108
|
+
const warning = await writeCache(ctx, session.baseUrl, 'standard', STD_CALENDAR_PATH);
|
|
109
|
+
return meetingSuccess('meeting.env.detect', {
|
|
110
|
+
mode: 'standard',
|
|
111
|
+
smokePath: STD_CALENDAR_PATH,
|
|
112
|
+
fromCache: false,
|
|
113
|
+
ebSmokeFailed: true,
|
|
114
|
+
ebSmokeError: ebDetail,
|
|
115
|
+
}, warning ? { warnings: [warning] } : undefined);
|
|
116
|
+
}
|
|
117
|
+
catch (stdError) {
|
|
118
|
+
// 两步都不通 → 不写缓存,提示接口未配置完整。
|
|
119
|
+
throw meetingValidationError('env_unavailable', '当前环境 EB 版与标准版会议接口冒烟均未通过,请确认会议应用已安装且接口已配置完整', {
|
|
120
|
+
eb: ebError instanceof Error ? ebError.message : String(ebError),
|
|
121
|
+
standard: stdError instanceof Error ? stdError.message : String(stdError),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// 日历/建会基础信息(建会流程判断唯一依据)
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
const meetingCalendarBase = {
|
|
130
|
+
name: 'meeting.calendar.base',
|
|
131
|
+
resource: 'env',
|
|
132
|
+
method: 'calendar.base',
|
|
133
|
+
action: 'meeting.calendar.base',
|
|
134
|
+
summary: '查询日历/建会基础信息(getCalendarBaseData):onlyFlowCreate / workFlowBaseIds / repeatWorkFlowBaseIds / existWorkFlow',
|
|
135
|
+
capability: 'env',
|
|
136
|
+
risk: 'read',
|
|
137
|
+
inputSchema: { type: 'object', additionalProperties: false, properties: {} },
|
|
138
|
+
handler: async (_args, { host }) => {
|
|
139
|
+
const response = await host.request({
|
|
140
|
+
method: 'GET',
|
|
141
|
+
path: STD_CALENDAR_PATH,
|
|
142
|
+
operation: 'meeting.calendar.base',
|
|
143
|
+
});
|
|
144
|
+
const createBaseData = response?.data?.createBaseData ?? response?.createBaseData ?? null;
|
|
145
|
+
return meetingSuccess('meeting.calendar.base', { createBaseData, raw: response });
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// 会议详情表单结构(提醒方式/周期结束条件等 options 来源)
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
const meetingDetailField = {
|
|
152
|
+
name: 'meeting.detail.field',
|
|
153
|
+
resource: 'meeting',
|
|
154
|
+
method: 'detail.field',
|
|
155
|
+
action: 'meeting.detail.field',
|
|
156
|
+
summary: '查询会议详情表单结构与 options(getMeetingDetailField):id / operateType / fromWorkflow / isReadOnly',
|
|
157
|
+
capability: 'meeting',
|
|
158
|
+
risk: 'read',
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
properties: {
|
|
163
|
+
id: SCHEMA_ID,
|
|
164
|
+
operateType: { type: 'string', description: '操作类型,如 SUBMIT/UPDATE/CHANGE' },
|
|
165
|
+
fromWorkflow: { type: 'integer', enum: [0, 1] },
|
|
166
|
+
isReadOnly: { type: 'integer', enum: [0, 1] },
|
|
167
|
+
},
|
|
168
|
+
required: ['id'],
|
|
169
|
+
},
|
|
170
|
+
handler: async (args, { host }) => {
|
|
171
|
+
const query = { id: String(args.id) };
|
|
172
|
+
if (args.operateType !== undefined)
|
|
173
|
+
query.operateType = args.operateType;
|
|
174
|
+
if (args.fromWorkflow !== undefined)
|
|
175
|
+
query.fromWorkflow = args.fromWorkflow;
|
|
176
|
+
if (args.isReadOnly !== undefined)
|
|
177
|
+
query.isReadOnly = args.isReadOnly;
|
|
178
|
+
const response = await host.request({
|
|
179
|
+
method: 'GET',
|
|
180
|
+
path: '/api/meeting/detail/getMeetingDetailField',
|
|
181
|
+
operation: 'meeting.detail.field',
|
|
182
|
+
query,
|
|
183
|
+
});
|
|
184
|
+
return meetingSuccess('meeting.detail.field', { data: response?.data ?? response });
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
export const meetingEnvOperations = [
|
|
188
|
+
meetingEnvDetect,
|
|
189
|
+
meetingCalendarBase,
|
|
190
|
+
meetingDetailField,
|
|
191
|
+
];
|