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,139 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { meetingConfirmationError, meetingPartialError, meetingValidationError } from '../errors.js';
|
|
3
|
+
import { meetingSuccess, SCHEMA_ID } from './types.js';
|
|
4
|
+
import { assertMeetingExists, 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
|
+
* 生成一组成对的「prepare(回查会议存在 + continuation)/ apply(真实写)」operation。
|
|
27
|
+
*
|
|
28
|
+
* 权限不在 CLI 侧判定:服务端 MeetingPermissionEvent 会按会议状态与会议基础设置判定,
|
|
29
|
+
* 不通过时返回本地化提示("您没有取消权限!"/"您没有删除权限!"等),CLI 原样透传。
|
|
30
|
+
*/
|
|
31
|
+
function lifecyclePair(spec) {
|
|
32
|
+
const prepareName = `meeting.${spec.action}.prepare`;
|
|
33
|
+
const applyName = `meeting.${spec.action}.apply`;
|
|
34
|
+
const inputSchema = {
|
|
35
|
+
type: 'object',
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
properties: {
|
|
38
|
+
id: { ...SCHEMA_ID, description: '会议 id(必填,长 id 用字符串)' },
|
|
39
|
+
},
|
|
40
|
+
required: ['id'],
|
|
41
|
+
};
|
|
42
|
+
const prepare = {
|
|
43
|
+
name: prepareName,
|
|
44
|
+
resource: spec.resource,
|
|
45
|
+
method: `${spec.action}.prepare`,
|
|
46
|
+
action: prepareName,
|
|
47
|
+
summary: spec.summaryPrepare,
|
|
48
|
+
capability: 'lifecycle',
|
|
49
|
+
risk: 'read-before-write',
|
|
50
|
+
requiresConfirmation: true,
|
|
51
|
+
inputSchema,
|
|
52
|
+
handler: async (args, env) => {
|
|
53
|
+
const id = toIdString(args.id, 'id');
|
|
54
|
+
await assertMeetingExists(id, env.host, prepareName);
|
|
55
|
+
const session = await env.host.getSession();
|
|
56
|
+
const token = await env.continuation.issue({
|
|
57
|
+
operation: applyName,
|
|
58
|
+
baseUrl: session.baseUrl,
|
|
59
|
+
profile: session.profile,
|
|
60
|
+
userId: session.userId,
|
|
61
|
+
payload: { id },
|
|
62
|
+
fingerprint: fingerprint({ id }),
|
|
63
|
+
}, CONTINUATION_TTL_SECONDS * 1000);
|
|
64
|
+
return meetingSuccess(prepareName, {
|
|
65
|
+
status: 'AWAITING_CONFIRMATION',
|
|
66
|
+
preview: { action: spec.action, id, body: spec.bodyOf(id), note: spec.note },
|
|
67
|
+
continuation: token,
|
|
68
|
+
expiresInSeconds: CONTINUATION_TTL_SECONDS,
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const apply = {
|
|
73
|
+
name: applyName,
|
|
74
|
+
resource: spec.resource,
|
|
75
|
+
method: `${spec.action}.apply`,
|
|
76
|
+
action: applyName,
|
|
77
|
+
summary: spec.summaryApply,
|
|
78
|
+
capability: 'lifecycle',
|
|
79
|
+
risk: 'high-risk-write',
|
|
80
|
+
requiresConfirmation: true,
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
properties: {
|
|
85
|
+
confirm: { const: true },
|
|
86
|
+
continuation: { type: 'string', minLength: 1 },
|
|
87
|
+
id: { ...SCHEMA_ID, description: '会议 id(必须与 prepare 时一致)' },
|
|
88
|
+
},
|
|
89
|
+
required: ['confirm', 'continuation', 'id'],
|
|
90
|
+
},
|
|
91
|
+
handler: async (args, env) => {
|
|
92
|
+
requireConfirm(args, applyName);
|
|
93
|
+
const lease = await verifyLease(args, applyName, env);
|
|
94
|
+
const id = toIdString(args.id, 'id');
|
|
95
|
+
if (lease.fingerprint !== fingerprint({ id })) {
|
|
96
|
+
throw meetingValidationError('target_changed', '会议 id 与 prepare 时的快照不一致,请重新执行 prepare');
|
|
97
|
+
}
|
|
98
|
+
let response;
|
|
99
|
+
try {
|
|
100
|
+
response = await env.host.request({
|
|
101
|
+
method: 'POST',
|
|
102
|
+
path: spec.path,
|
|
103
|
+
operation: applyName,
|
|
104
|
+
...(spec.via === 'query' ? { query: spec.bodyOf(id) } : { body: spec.bodyOf(id) }),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (isNetworkError(error)) {
|
|
109
|
+
throw meetingPartialError(`${spec.action} 请求发送后连接中断,结果不确定,禁止自动重试,请通过 meeting.list 或会议详情回查确认后再决定`, { attempted: { id } }, { stage: spec.action });
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
return meetingSuccess(applyName, { status: 'COMPLETE', id, result: response });
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
return [prepare, apply];
|
|
117
|
+
}
|
|
118
|
+
export const meetingLifecycleOperations = [
|
|
119
|
+
...lifecyclePair({
|
|
120
|
+
action: 'cancel',
|
|
121
|
+
resource: 'meeting',
|
|
122
|
+
summaryPrepare: '准备取消会议:回查会议存在性并生成确认预览,不写入(取消后会议进入取消状态)',
|
|
123
|
+
summaryApply: '提交取消会议(operateMeetingByType operateType=CANCEL,body 传参);必须在用户明确确认后调用',
|
|
124
|
+
path: '/api/meeting/detail/operateMeetingByType',
|
|
125
|
+
via: 'body',
|
|
126
|
+
bodyOf: (id) => ({ operateType: 'CANCEL', id }),
|
|
127
|
+
note: '取消会议:会议进入取消状态,占用/回执/签到随之失效;不可逆,需用户明确确认。服务端会按会议状态判定取消权限,无权限时返回“您没有取消权限!”。',
|
|
128
|
+
}),
|
|
129
|
+
...lifecyclePair({
|
|
130
|
+
action: 'delete',
|
|
131
|
+
resource: 'meeting',
|
|
132
|
+
summaryPrepare: '准备删除会议记录:回查会议存在性并生成确认预览,不写入(彻底删除)',
|
|
133
|
+
summaryApply: '提交删除会议记录(deleteMeetingDetail,源码为表单绑定,参数走 query);必须在用户明确确认后调用',
|
|
134
|
+
path: '/api/meeting/detail/deleteMeetingDetail',
|
|
135
|
+
via: 'query',
|
|
136
|
+
bodyOf: (id) => ({ id }),
|
|
137
|
+
note: '删除是彻底移除会议记录(区别于取消);不可逆,需用户明确确认。服务端会按会议状态判定删除权限,无权限时返回“您没有删除权限!”。',
|
|
138
|
+
}),
|
|
139
|
+
];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { meetingSuccess, SCHEMA_DATE } from './types.js';
|
|
2
|
+
import { assertDate, extractRows } from './shared.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// 会议列表(getMeetingList;body 过滤实测不生效,客户端过滤)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/**
|
|
7
|
+
* 与目标日期有重叠:begin < 当天 23:59:59 且 end > 当天 00:00:00。
|
|
8
|
+
* beginDatetime/endDatetime 格式 yyyy-MM-dd HH:mm(缺失视为不过滤该边界)。
|
|
9
|
+
*/
|
|
10
|
+
function overlapsDate(beginDatetime, endDatetime, date) {
|
|
11
|
+
if (typeof beginDatetime !== 'string' && typeof endDatetime !== 'string')
|
|
12
|
+
return true;
|
|
13
|
+
const dayStart = `${date} 00:00`;
|
|
14
|
+
const dayEnd = `${date} 23:59`;
|
|
15
|
+
const begin = typeof beginDatetime === 'string' ? beginDatetime : '';
|
|
16
|
+
const end = typeof endDatetime === 'string' ? endDatetime : '';
|
|
17
|
+
if (begin && end)
|
|
18
|
+
return begin.slice(0, 16) <= dayEnd && end.slice(0, 16) >= dayStart;
|
|
19
|
+
if (begin)
|
|
20
|
+
return begin.slice(0, 10) === date;
|
|
21
|
+
if (end)
|
|
22
|
+
return end.slice(0, 10) === date;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
const meetingList = {
|
|
26
|
+
name: 'meeting.list',
|
|
27
|
+
resource: 'meeting',
|
|
28
|
+
method: 'list',
|
|
29
|
+
action: 'meeting.list',
|
|
30
|
+
summary: '查询当前用户可见的会议列表(getMeetingList 空 body 拉全量 + 客户端按日期过滤)',
|
|
31
|
+
capability: 'meeting',
|
|
32
|
+
risk: 'read',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
properties: {
|
|
37
|
+
filterDate: { ...SCHEMA_DATE, description: '客户端过滤:仅返回与该日期重叠的会议(如今天)' },
|
|
38
|
+
pageSize: { type: 'integer', description: '自动翻页每页条数(默认 50,上限 200)' },
|
|
39
|
+
returnLimit: { type: 'integer', description: '返回条数上限(过滤后截取)' },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
handler: async (args, { host }) => {
|
|
43
|
+
// 实测(standalone skill):getMeetingList body 传日期/参会人过滤不生效,禁止现场试 body。
|
|
44
|
+
// 但分页字段生效(实测默认仅返回 10 条)→ 自动翻页拉全量,再做客户端过滤。
|
|
45
|
+
const pageSize = typeof args.pageSize === 'number' && args.pageSize > 0 ? Math.min(args.pageSize, 200) : 50;
|
|
46
|
+
const maxPages = 10;
|
|
47
|
+
const allRows = [];
|
|
48
|
+
let total;
|
|
49
|
+
for (let current = 1; current <= maxPages; current++) {
|
|
50
|
+
const response = await host.request({
|
|
51
|
+
method: 'POST',
|
|
52
|
+
path: '/api/meeting/search/getMeetingList',
|
|
53
|
+
operation: 'meeting.list',
|
|
54
|
+
body: { current, pageSize },
|
|
55
|
+
});
|
|
56
|
+
const page = extractRows(response);
|
|
57
|
+
total = page.total ?? total;
|
|
58
|
+
allRows.push(...page.rows);
|
|
59
|
+
if (page.rows.length === 0 || (total !== undefined && allRows.length >= total))
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
let filtered = allRows;
|
|
63
|
+
if (args.filterDate !== undefined) {
|
|
64
|
+
const date = assertDate(args.filterDate, 'filterDate');
|
|
65
|
+
filtered = allRows.filter((row) => overlapsDate(row?.beginDatetime, row?.endDatetime, date));
|
|
66
|
+
}
|
|
67
|
+
if (typeof args.returnLimit === 'number' && args.returnLimit > 0) {
|
|
68
|
+
filtered = filtered.slice(0, args.returnLimit);
|
|
69
|
+
}
|
|
70
|
+
return meetingSuccess('meeting.list', { total: filtered.length, sourceTotal: total, meetings: filtered });
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
export const meetingListOperations = [meetingList];
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { meetingPartialError, meetingValidationError } from '../errors.js';
|
|
3
|
+
import { meetingSuccess, SCHEMA_ID } from './types.js';
|
|
4
|
+
import { extractRows, 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 meetingValidationError('confirmation_required', `${action} 需要用户明确确认(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
|
+
// 回执列表(getMeetingReceiptMemberInfoList;isMobile 必传否则 NPE→500)
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
const meetingReceiptList = {
|
|
29
|
+
name: 'meeting.receipt.list',
|
|
30
|
+
resource: 'receipt',
|
|
31
|
+
method: 'receipt.list',
|
|
32
|
+
action: 'meeting.receipt.list',
|
|
33
|
+
summary: '查询会议回执列表(getMeetingReceiptMemberInfoList;isMobile 缺省 false,缺失会导致后端 NPE)',
|
|
34
|
+
capability: 'receipt',
|
|
35
|
+
risk: 'read',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
properties: {
|
|
40
|
+
mtId: { ...SCHEMA_ID, description: '会议 id(必填)' },
|
|
41
|
+
memberType: { type: 'integer', enum: [1, 2, 3], description: '人员类型:1=人员 2=客户 3=其他(默认 1)' },
|
|
42
|
+
isMobile: { type: 'boolean', description: '移动端标记(默认 false)' },
|
|
43
|
+
current: { type: 'integer' },
|
|
44
|
+
pageSize: { type: 'integer' },
|
|
45
|
+
},
|
|
46
|
+
required: ['mtId'],
|
|
47
|
+
},
|
|
48
|
+
handler: async (args, { host }) => {
|
|
49
|
+
const body = {
|
|
50
|
+
mtId: toIdString(args.mtId, 'mtId'),
|
|
51
|
+
isMobile: args.isMobile === undefined ? false : args.isMobile === true,
|
|
52
|
+
current: typeof args.current === 'number' && args.current > 0 ? args.current : 1,
|
|
53
|
+
pageSize: typeof args.pageSize === 'number' && args.pageSize > 0 ? args.pageSize : 50,
|
|
54
|
+
};
|
|
55
|
+
if (args.memberType !== undefined)
|
|
56
|
+
body.memberType = args.memberType;
|
|
57
|
+
const response = await host.request({
|
|
58
|
+
method: 'POST',
|
|
59
|
+
path: '/api/meeting/receiptMember/getMeetingReceiptMemberInfoList',
|
|
60
|
+
operation: 'meeting.receipt.list',
|
|
61
|
+
body,
|
|
62
|
+
});
|
|
63
|
+
const { rows, total } = extractRows(response);
|
|
64
|
+
return meetingSuccess('meeting.receipt.list', { total, receipts: rows });
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// 更新回执(operateMeetingReceiptMember;必须传回执记录 id 而非 memberId)
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
function normalizeReceiptUpdate(args) {
|
|
71
|
+
const mtId = toIdString(args.mtId, 'mtId');
|
|
72
|
+
const memberId = toIdString(args.memberId, 'memberId');
|
|
73
|
+
const attendStatus = args.attendStatus;
|
|
74
|
+
if (attendStatus !== 0 && attendStatus !== 1 && attendStatus !== 2) {
|
|
75
|
+
throw meetingValidationError('attendStatus_invalid', 'attendStatus 仅允许 0=未填写 1=参加 2=不参加(回执"否"传 2)');
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
mtId,
|
|
79
|
+
memberId,
|
|
80
|
+
attendStatus,
|
|
81
|
+
...(args.remark !== undefined ? { remark: String(args.remark) } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** 回查回执列表,按 memberId 定位回执记录 id(建会时后端自动生成,每参会人一条)。 */
|
|
85
|
+
async function locateReceiptId(mtId, memberId, env) {
|
|
86
|
+
const response = await env.host.request({
|
|
87
|
+
method: 'POST',
|
|
88
|
+
path: '/api/meeting/receiptMember/getMeetingReceiptMemberInfoList',
|
|
89
|
+
operation: 'meeting.receipt.update.prepare',
|
|
90
|
+
body: { mtId, memberType: 1, isMobile: false, current: 1, pageSize: 100 },
|
|
91
|
+
});
|
|
92
|
+
const { rows } = extractRows(response);
|
|
93
|
+
const hit = rows.find((row) => String(row?.memberId) === memberId);
|
|
94
|
+
if (!hit || hit.id === undefined) {
|
|
95
|
+
throw meetingValidationError('receipt_not_found', `未找到该参会人(memberId=${memberId})的回执记录,请先通过 meeting.receipt.list 核对`);
|
|
96
|
+
}
|
|
97
|
+
return String(hit.id);
|
|
98
|
+
}
|
|
99
|
+
const meetingReceiptUpdatePrepare = {
|
|
100
|
+
name: 'meeting.receipt.update.prepare',
|
|
101
|
+
resource: 'receipt',
|
|
102
|
+
method: 'receipt.update.prepare',
|
|
103
|
+
action: 'meeting.receipt.update.prepare',
|
|
104
|
+
summary: '准备更新回执:先回查回执记录 id(更新必须传回执主键而非 memberId),生成确认预览,不写入',
|
|
105
|
+
capability: 'receipt',
|
|
106
|
+
risk: 'read-before-write',
|
|
107
|
+
requiresConfirmation: true,
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
additionalProperties: false,
|
|
111
|
+
properties: {
|
|
112
|
+
mtId: { ...SCHEMA_ID, description: '会议 id' },
|
|
113
|
+
memberId: { ...SCHEMA_ID, description: '参会人员 id(用于定位回执记录)' },
|
|
114
|
+
attendStatus: { type: 'integer', enum: [0, 1, 2], description: '0=未填写 1=参加 2=不参加' },
|
|
115
|
+
remark: { type: 'string', description: '备注(如"请假")' },
|
|
116
|
+
},
|
|
117
|
+
required: ['mtId', 'memberId', 'attendStatus'],
|
|
118
|
+
},
|
|
119
|
+
handler: async (args, env) => {
|
|
120
|
+
const normalized = normalizeReceiptUpdate(args);
|
|
121
|
+
const receiptId = await locateReceiptId(String(normalized.mtId), String(normalized.memberId), env);
|
|
122
|
+
const session = await env.host.getSession();
|
|
123
|
+
const payload = { id: receiptId, ...normalized };
|
|
124
|
+
const token = await env.continuation.issue({
|
|
125
|
+
operation: 'meeting.receipt.update.apply',
|
|
126
|
+
baseUrl: session.baseUrl,
|
|
127
|
+
profile: session.profile,
|
|
128
|
+
userId: session.userId,
|
|
129
|
+
payload,
|
|
130
|
+
fingerprint: fingerprint(normalized),
|
|
131
|
+
}, CONTINUATION_TTL_SECONDS * 1000);
|
|
132
|
+
return meetingSuccess('meeting.receipt.update.prepare', {
|
|
133
|
+
status: 'AWAITING_CONFIRMATION',
|
|
134
|
+
preview: payload,
|
|
135
|
+
continuation: token,
|
|
136
|
+
expiresInSeconds: CONTINUATION_TTL_SECONDS,
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
const meetingReceiptUpdateApply = {
|
|
141
|
+
name: 'meeting.receipt.update.apply',
|
|
142
|
+
resource: 'receipt',
|
|
143
|
+
method: 'receipt.update.apply',
|
|
144
|
+
action: 'meeting.receipt.update.apply',
|
|
145
|
+
summary: '提交回执更新(operateMeetingReceiptMember);必须在用户明确确认后调用,要求 prepare 返回的 continuation',
|
|
146
|
+
capability: 'receipt',
|
|
147
|
+
risk: 'high-risk-write',
|
|
148
|
+
requiresConfirmation: true,
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
additionalProperties: false,
|
|
152
|
+
properties: {
|
|
153
|
+
confirm: { const: true },
|
|
154
|
+
continuation: { type: 'string', minLength: 1 },
|
|
155
|
+
mtId: SCHEMA_ID,
|
|
156
|
+
memberId: SCHEMA_ID,
|
|
157
|
+
attendStatus: { type: 'integer', enum: [0, 1, 2] },
|
|
158
|
+
remark: { type: 'string' },
|
|
159
|
+
},
|
|
160
|
+
required: ['confirm', 'continuation', 'mtId', 'memberId', 'attendStatus'],
|
|
161
|
+
},
|
|
162
|
+
handler: async (args, env) => {
|
|
163
|
+
requireConfirm(args, 'meeting.receipt.update.apply');
|
|
164
|
+
const lease = await verifyLease(args, 'meeting.receipt.update.apply', env);
|
|
165
|
+
const normalized = normalizeReceiptUpdate(args);
|
|
166
|
+
if (lease.fingerprint !== fingerprint(normalized)) {
|
|
167
|
+
throw meetingValidationError('target_changed', '回执内容与 prepare 时的快照不一致,请重新执行 prepare');
|
|
168
|
+
}
|
|
169
|
+
const payload = lease.payload;
|
|
170
|
+
let response;
|
|
171
|
+
try {
|
|
172
|
+
response = await env.host.request({
|
|
173
|
+
method: 'POST',
|
|
174
|
+
path: '/api/meeting/receiptMember/operateMeetingReceiptMember',
|
|
175
|
+
operation: 'meeting.receipt.update.apply',
|
|
176
|
+
body: payload,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (isNetworkError(error)) {
|
|
181
|
+
throw meetingPartialError('回执更新请求发送后连接中断,是否已生效不确定,禁止自动重试,请通过 meeting.receipt.list 复核', { attempted: { mtId: normalized.mtId, memberId: normalized.memberId } }, { stage: 'receipt-update' });
|
|
182
|
+
}
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
return meetingSuccess('meeting.receipt.update.apply', {
|
|
186
|
+
status: 'COMPLETE',
|
|
187
|
+
result: response,
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
export const meetingReceiptOperations = [
|
|
192
|
+
meetingReceiptList,
|
|
193
|
+
meetingReceiptUpdatePrepare,
|
|
194
|
+
meetingReceiptUpdateApply,
|
|
195
|
+
];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { meetingValidationError } from '../errors.js';
|
|
2
|
+
import { meetingEnvOperations } from './env.js';
|
|
3
|
+
import { meetingListOperations } from './meetings.js';
|
|
4
|
+
import { meetingRoomOperations } from './rooms.js';
|
|
5
|
+
import { meetingCatalogOperations } from './catalogs.js';
|
|
6
|
+
import { meetingConflictOperations } from './conflicts.js';
|
|
7
|
+
import { meetingChangeOperations } from './change.js';
|
|
8
|
+
import { meetingCreateOperations } from './create.js';
|
|
9
|
+
import { meetingLifecycleOperations } from './lifecycle.js';
|
|
10
|
+
import { meetingReceiptOperations } from './receipts.js';
|
|
11
|
+
import { meetingSignOperations } from './signs.js';
|
|
12
|
+
function buildRegistry() {
|
|
13
|
+
const registry = new Map();
|
|
14
|
+
const definitions = [
|
|
15
|
+
...meetingEnvOperations,
|
|
16
|
+
...meetingListOperations,
|
|
17
|
+
...meetingRoomOperations,
|
|
18
|
+
...meetingCatalogOperations,
|
|
19
|
+
...meetingConflictOperations,
|
|
20
|
+
...meetingCreateOperations,
|
|
21
|
+
...meetingLifecycleOperations,
|
|
22
|
+
...meetingChangeOperations,
|
|
23
|
+
...meetingReceiptOperations,
|
|
24
|
+
...meetingSignOperations,
|
|
25
|
+
];
|
|
26
|
+
for (const definition of definitions) {
|
|
27
|
+
if (registry.has(definition.name)) {
|
|
28
|
+
throw new Error(`duplicate meeting operation: ${definition.name}`);
|
|
29
|
+
}
|
|
30
|
+
registry.set(definition.name, definition);
|
|
31
|
+
}
|
|
32
|
+
return registry;
|
|
33
|
+
}
|
|
34
|
+
const meetingOperationRegistry = buildRegistry();
|
|
35
|
+
export function meetingOperationDefinitions() {
|
|
36
|
+
return [...meetingOperationRegistry.values()];
|
|
37
|
+
}
|
|
38
|
+
export function getMeetingOperation(operation) {
|
|
39
|
+
const definition = meetingOperationRegistry.get(operation);
|
|
40
|
+
if (!definition) {
|
|
41
|
+
throw meetingValidationError('operation_unknown', `会议模块不支持操作:${operation}`);
|
|
42
|
+
}
|
|
43
|
+
return definition;
|
|
44
|
+
}
|
|
45
|
+
export function hasMeetingOperation(operation) {
|
|
46
|
+
return meetingOperationRegistry.has(operation);
|
|
47
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { meetingValidationError } from '../errors.js';
|
|
2
|
+
import { meetingSuccess, SCHEMA_DATETIME_MINUTE, SCHEMA_ID } from './types.js';
|
|
3
|
+
import { assertDateTimeMinute, extractRows, toIdString } from './shared.js';
|
|
4
|
+
/** browser 组件通用 body 骨架(mt_room/mt_type 共用)。 */
|
|
5
|
+
function browserBody(options) {
|
|
6
|
+
const formDatas = {};
|
|
7
|
+
if (typeof options.nameFilter === 'string' && options.nameFilter.trim()) {
|
|
8
|
+
formDatas.name = [{ nameCondition: 'in', name: options.nameFilter.trim() }];
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
browserMultiple: options.multiple ?? true,
|
|
12
|
+
current: typeof options.current === 'number' && options.current > 0 ? options.current : 1,
|
|
13
|
+
pageSize: typeof options.pageSize === 'number' && options.pageSize > 0 ? options.pageSize : 100,
|
|
14
|
+
...options.extra,
|
|
15
|
+
formDatas,
|
|
16
|
+
quickSearchDatas: {},
|
|
17
|
+
type: 'and',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** disabled 归一化:接口可能返回 boolean 或 "true"/"false" 字符串。 */
|
|
21
|
+
function isDisabled(value) {
|
|
22
|
+
return value === true || String(value).toLowerCase() === 'true';
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// 会议室列表(mt_room,权限友好;带时间范围即返回占用判断 disabled)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
const meetingRoomList = {
|
|
28
|
+
name: 'meeting.room.list',
|
|
29
|
+
resource: 'room',
|
|
30
|
+
method: 'room.list',
|
|
31
|
+
action: 'meeting.room.list',
|
|
32
|
+
summary: '查询会议室列表(mt_room browser 组件;带 beginDate/endDate 时返回 disabled 占用判断,禁用 getMeetingRoomInfoList)',
|
|
33
|
+
capability: 'room',
|
|
34
|
+
risk: 'read',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
properties: {
|
|
39
|
+
beginDate: { ...SCHEMA_DATETIME_MINUTE, description: '占用判断起始 yyyy-MM-dd HH:mm;与 endDate 成对提供' },
|
|
40
|
+
endDate: { ...SCHEMA_DATETIME_MINUTE, description: '占用判断结束 yyyy-MM-dd HH:mm;与 beginDate 成对提供' },
|
|
41
|
+
name: { type: 'string', description: '按会议室名称过滤(标准版不支持按地点查询)' },
|
|
42
|
+
totalMember: { type: 'string', description: '参会人数(默认 "0")' },
|
|
43
|
+
current: { type: 'integer', description: '页码,默认 1' },
|
|
44
|
+
pageSize: { type: 'integer', description: '页大小,默认 100' },
|
|
45
|
+
onlyAvailable: { type: 'boolean', description: '仅返回 disabled!=true 的空闲会议室' },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
handler: async (args, { host }) => {
|
|
49
|
+
if ((args.beginDate === undefined) !== (args.endDate === undefined)) {
|
|
50
|
+
throw meetingValidationError('period_incomplete', 'beginDate 与 endDate 必须成对提供(用于占用判断)');
|
|
51
|
+
}
|
|
52
|
+
const extra = {};
|
|
53
|
+
if (args.beginDate !== undefined) {
|
|
54
|
+
extra.beginDate = assertDateTimeMinute(args.beginDate, 'beginDate');
|
|
55
|
+
extra.endDate = assertDateTimeMinute(args.endDate, 'endDate');
|
|
56
|
+
}
|
|
57
|
+
if (args.totalMember !== undefined)
|
|
58
|
+
extra.totalMember = String(args.totalMember);
|
|
59
|
+
const body = browserBody({
|
|
60
|
+
current: args.current,
|
|
61
|
+
pageSize: args.pageSize,
|
|
62
|
+
nameFilter: args.name,
|
|
63
|
+
multiple: true,
|
|
64
|
+
extra,
|
|
65
|
+
});
|
|
66
|
+
const response = await host.request({
|
|
67
|
+
method: 'POST',
|
|
68
|
+
path: '/api/meeting/common/browser/data/mt_room',
|
|
69
|
+
operation: 'meeting.room.list',
|
|
70
|
+
body,
|
|
71
|
+
});
|
|
72
|
+
const { rows, total } = extractRows(response);
|
|
73
|
+
const rooms = rows.map((row) => ({ ...row, disabled: isDisabled(row?.disabled) }));
|
|
74
|
+
const available = rooms.filter((room) => !room.disabled);
|
|
75
|
+
return meetingSuccess('meeting.room.list', {
|
|
76
|
+
total,
|
|
77
|
+
...(args.onlyAvailable === true ? { rooms: available } : { rooms, availableRooms: available }),
|
|
78
|
+
}, {
|
|
79
|
+
warnings: args.beginDate !== undefined
|
|
80
|
+
? ['disabled=true 表示该会议室在时间段内已被占用;预约推荐必须排除']
|
|
81
|
+
: [],
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// 会议室占用视图(getRoomUsageData;无分页,返回全部占用记录)
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
const meetingRoomUsage = {
|
|
89
|
+
name: 'meeting.room.usage',
|
|
90
|
+
resource: 'room',
|
|
91
|
+
method: 'room.usage',
|
|
92
|
+
action: 'meeting.room.usage',
|
|
93
|
+
summary: '查询会议室占用视图(getRoomUsageData):bywhat 2=月 3=周 4=日,可按 roomid 限定',
|
|
94
|
+
capability: 'room',
|
|
95
|
+
risk: 'read',
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
additionalProperties: false,
|
|
99
|
+
properties: {
|
|
100
|
+
roomid: { ...SCHEMA_ID, description: '会议室 id(限定单个会议室)' },
|
|
101
|
+
roomname: { type: 'string', description: '会议室名称筛选' },
|
|
102
|
+
bywhat: { type: 'integer', enum: [2, 3, 4], description: '查询类型:2=月 3=周 4=日' },
|
|
103
|
+
currentdate: { type: 'string', description: '查询日期 yyyy-MM-dd' },
|
|
104
|
+
orgId: SCHEMA_ID,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
handler: async (args, { host }) => {
|
|
108
|
+
const query = {};
|
|
109
|
+
if (args.roomid !== undefined)
|
|
110
|
+
query.roomid = toIdString(args.roomid, 'roomid');
|
|
111
|
+
if (args.roomname !== undefined)
|
|
112
|
+
query.roomname = args.roomname;
|
|
113
|
+
if (args.bywhat !== undefined)
|
|
114
|
+
query.bywhat = args.bywhat;
|
|
115
|
+
if (args.currentdate !== undefined)
|
|
116
|
+
query.currentdate = args.currentdate;
|
|
117
|
+
if (args.orgId !== undefined)
|
|
118
|
+
query.orgId = toIdString(args.orgId, 'orgId');
|
|
119
|
+
const response = await host.request({
|
|
120
|
+
method: 'GET',
|
|
121
|
+
path: '/api/meeting/roomUsage/getRoomUsageData',
|
|
122
|
+
operation: 'meeting.room.usage',
|
|
123
|
+
query,
|
|
124
|
+
});
|
|
125
|
+
const { rows, total } = extractRows(response);
|
|
126
|
+
return meetingSuccess('meeting.room.usage', { total, usages: rows });
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
export const meetingRoomOperations = [
|
|
130
|
+
meetingRoomList,
|
|
131
|
+
meetingRoomUsage,
|
|
132
|
+
];
|