weaver-work-cli 0.1.4 → 0.1.6
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/auth/commands.js +4 -1
- 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,591 @@
|
|
|
1
|
+
import { ymbValidationError } from '../errors.js';
|
|
2
|
+
import { schemaOf, WRITE, } from './types.js';
|
|
3
|
+
import { assertImOk, compact, splitIds, str, toUint } from './shared.js';
|
|
4
|
+
import { makeWritePair } from './write-util.js';
|
|
5
|
+
/** 解析 uid 列表为 {cid,uid} 成员,支持 failed 移除。 */
|
|
6
|
+
async function resolveMembers(args, env) {
|
|
7
|
+
const raw = Array.isArray(args.users) ? args.users.map((x) => String(x)) : splitIds(args.users, 'users');
|
|
8
|
+
const map = await env.host.resolveUsersByIds(raw);
|
|
9
|
+
const users = [];
|
|
10
|
+
const failed = [];
|
|
11
|
+
const names = {};
|
|
12
|
+
for (const uid of raw) {
|
|
13
|
+
const p = map[uid];
|
|
14
|
+
const cid = p?.cid || '';
|
|
15
|
+
if (p?.name)
|
|
16
|
+
names[uid] = p.name;
|
|
17
|
+
if (!uid || uid === '0' || !cid || cid === '0') {
|
|
18
|
+
failed.push({ uid, name: p?.name || '' });
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
users.push({ cid, uid });
|
|
22
|
+
}
|
|
23
|
+
return { users, failed, names };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 默认群名:自己的姓名 + 另外最多四个人的姓名(中文顿号连接,自己排第一位)。
|
|
27
|
+
* 姓名优先取调用方传入的 `names`(与 users 顺序一一对应),其次 hrm 解析结果,最后回退 uid。
|
|
28
|
+
*/
|
|
29
|
+
async function defaultGroupName(members, nameOf, selfUid, env) {
|
|
30
|
+
let selfName = selfUid ? nameOf(selfUid) : '';
|
|
31
|
+
if (!selfName) {
|
|
32
|
+
try {
|
|
33
|
+
const self = await env.host.getSelfIdentity();
|
|
34
|
+
if (self.name)
|
|
35
|
+
selfName = self.name;
|
|
36
|
+
}
|
|
37
|
+
catch { /* 回退 uid / 「我」 */ }
|
|
38
|
+
}
|
|
39
|
+
if (!selfName)
|
|
40
|
+
selfName = selfUid || '我';
|
|
41
|
+
const others = [];
|
|
42
|
+
for (const member of members) {
|
|
43
|
+
if (member.uid === selfUid)
|
|
44
|
+
continue; // 跳过自己,避免默认群名重复
|
|
45
|
+
if (others.length >= 4)
|
|
46
|
+
break;
|
|
47
|
+
others.push(nameOf(member.uid));
|
|
48
|
+
}
|
|
49
|
+
return [selfName, ...others].join('、');
|
|
50
|
+
}
|
|
51
|
+
// ── 建群 ──
|
|
52
|
+
const groupCreatePair = makeWritePair({
|
|
53
|
+
action: 'yimiaoban.group.create',
|
|
54
|
+
async plan(args, env) {
|
|
55
|
+
const rawUsers = Array.isArray(args.users) ? args.users.map((x) => String(x)) : splitIds(args.users, 'users');
|
|
56
|
+
const names = Array.isArray(args.names) ? args.names.map((x) => String(x)) : [];
|
|
57
|
+
if (names.length && names.length !== rawUsers.length) {
|
|
58
|
+
throw ymbValidationError('names_mismatch', 'names 数量必须与 users 一致(顺序一一对应)');
|
|
59
|
+
}
|
|
60
|
+
const { users, failed, names: resolvedNames } = await resolveMembers(args, env);
|
|
61
|
+
if (failed.length && args.removeFailed !== true) {
|
|
62
|
+
throw ymbValidationError('members_resolve_failed', `以下成员信息获取失败(cid/uid 缺失或为 0):${failed.map((f) => f.name || f.uid).join('、')};确认移除请传 removeFailed:true`, { details: failed });
|
|
63
|
+
}
|
|
64
|
+
if (users.length === 0)
|
|
65
|
+
throw ymbValidationError('members_empty', '没有可入群的成员');
|
|
66
|
+
// 🔴 群成员必须包含操作者自己(服务端依据 eteamsid 把操作者设为群主)
|
|
67
|
+
const selfUid = str((await env.host.getSession()).userId);
|
|
68
|
+
if (selfUid && !users.some((u) => u.uid === selfUid)) {
|
|
69
|
+
throw ymbValidationError('self_not_included', `不允许创建不包含自己的群聊:users 必须包含操作者自己(uid=${selfUid})`);
|
|
70
|
+
}
|
|
71
|
+
// 默认群名:调用方 names(与 users 顺序对应)优先,其次 hrm 解析姓名,最后回退 uid
|
|
72
|
+
const providedNames = {};
|
|
73
|
+
rawUsers.forEach((uid, index) => {
|
|
74
|
+
if (names[index])
|
|
75
|
+
providedNames[uid] = names[index];
|
|
76
|
+
});
|
|
77
|
+
const nameOf = (uid) => providedNames[uid] || resolvedNames[uid] || uid;
|
|
78
|
+
const name = args.name ? String(args.name) : await defaultGroupName(users, nameOf, selfUid, env);
|
|
79
|
+
// 管理员
|
|
80
|
+
let admins = [];
|
|
81
|
+
if (args.admins) {
|
|
82
|
+
const adminRaw = Array.isArray(args.admins) ? args.admins.map((x) => String(x)) : splitIds(args.admins, 'admins');
|
|
83
|
+
const adminMap = await env.host.resolveUsersByIds(adminRaw);
|
|
84
|
+
const adminFailed = adminRaw.filter((uid) => {
|
|
85
|
+
const cid = adminMap[uid]?.cid || '';
|
|
86
|
+
return !uid || uid === '0' || !cid || cid === '0';
|
|
87
|
+
});
|
|
88
|
+
if (adminFailed.length) {
|
|
89
|
+
throw ymbValidationError('admins_resolve_failed', `管理员信息获取失败(cid/uid 缺失或为 0):${adminFailed.map((uid) => adminMap[uid]?.name || uid).join('、')};请确认后重试(不可静默替换操作人身份)`, { details: adminFailed });
|
|
90
|
+
}
|
|
91
|
+
admins = adminRaw.map((uid) => ({ cid: adminMap[uid]?.cid || '', uid }));
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
summary: {
|
|
95
|
+
action: '创建普通群',
|
|
96
|
+
name,
|
|
97
|
+
memberCount: users.length,
|
|
98
|
+
members: users.map((u) => u.uid),
|
|
99
|
+
adminCount: admins.length,
|
|
100
|
+
removeFailed: args.removeFailed === true ? failed.map((f) => f.uid) : [],
|
|
101
|
+
},
|
|
102
|
+
snapshot: { name, users, admins, failed, removeFailed: args.removeFailed === true },
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
async execute(plan, env) {
|
|
106
|
+
const { name, users, admins } = plan;
|
|
107
|
+
const body = compact({
|
|
108
|
+
name,
|
|
109
|
+
type: 0,
|
|
110
|
+
users,
|
|
111
|
+
attr: admins.length ? { admins } : {},
|
|
112
|
+
});
|
|
113
|
+
const res = await env.host.request({
|
|
114
|
+
method: 'POST',
|
|
115
|
+
path: '/api/em/msg/executeIm/group/groupCreate',
|
|
116
|
+
body,
|
|
117
|
+
operation: 'yimiaoban.group.create',
|
|
118
|
+
});
|
|
119
|
+
assertImOk(res, '创建群');
|
|
120
|
+
// 群 id 可能位于 data / data.data 的 groupId / group_id / id 字段,统一扫描;同时回传服务端原始数据便于核对
|
|
121
|
+
const payload = res?.data ?? {};
|
|
122
|
+
const inner = payload?.data ?? {};
|
|
123
|
+
const groupId = str(inner.groupId ?? inner.group_id ?? inner.id
|
|
124
|
+
?? payload.groupId ?? payload.group_id ?? payload.id
|
|
125
|
+
?? '');
|
|
126
|
+
return { groupId, name, rawData: inner && Object.keys(inner).length ? inner : payload };
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
// ── 邀请入群 ──
|
|
130
|
+
const groupInvitePair = makeWritePair({
|
|
131
|
+
action: 'yimiaoban.group.invite',
|
|
132
|
+
async plan(args, env) {
|
|
133
|
+
const groupId = str(args.groupId);
|
|
134
|
+
if (!groupId || groupId === '0')
|
|
135
|
+
throw ymbValidationError('id_invalid', 'groupId 必须非 0');
|
|
136
|
+
const { users, failed } = await resolveMembers(args, env);
|
|
137
|
+
if (failed.length && args.removeFailed !== true) {
|
|
138
|
+
throw ymbValidationError('members_resolve_failed', `以下成员解析失败:${failed.map((f) => f.name || f.uid).join('、')};确认移除请传 removeFailed:true`);
|
|
139
|
+
}
|
|
140
|
+
if (users.length === 0)
|
|
141
|
+
throw ymbValidationError('members_empty', '没有可邀请的成员');
|
|
142
|
+
return {
|
|
143
|
+
summary: { action: '邀请入群', groupId, inviteCount: users.length, users: users.map((u) => u.uid), removeFailed: args.removeFailed === true ? failed.map((f) => f.uid) : [] },
|
|
144
|
+
snapshot: { groupId, users },
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
async execute(plan, env) {
|
|
148
|
+
const body = { groupId: plan.groupId, users: plan.users };
|
|
149
|
+
const res = await env.host.request({
|
|
150
|
+
method: 'POST',
|
|
151
|
+
path: '/api/em/msg/executeIm/group/inviteGroupUsers',
|
|
152
|
+
body,
|
|
153
|
+
operation: 'yimiaoban.group.invite',
|
|
154
|
+
});
|
|
155
|
+
assertImOk(res, '邀请入群');
|
|
156
|
+
return { groupId: plan.groupId, invited: plan.users.length };
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
// ── 踢出群 ──
|
|
160
|
+
const groupKickPair = makeWritePair({
|
|
161
|
+
action: 'yimiaoban.group.kick',
|
|
162
|
+
async plan(args, env) {
|
|
163
|
+
const groupId = str(args.groupId);
|
|
164
|
+
if (!groupId || groupId === '0')
|
|
165
|
+
throw ymbValidationError('id_invalid', 'groupId 必须非 0');
|
|
166
|
+
const { users, failed } = await resolveMembers(args, env);
|
|
167
|
+
if (failed.length && args.removeFailed !== true) {
|
|
168
|
+
throw ymbValidationError('members_resolve_failed', `以下成员解析失败:${failed.map((f) => f.name || f.uid).join('、')};确认移除请传 removeFailed:true`);
|
|
169
|
+
}
|
|
170
|
+
if (users.length === 0)
|
|
171
|
+
throw ymbValidationError('members_empty', '没有可移除的成员');
|
|
172
|
+
return {
|
|
173
|
+
summary: { action: '踢出群成员', groupId, kickCount: users.length, users: users.map((u) => u.uid) },
|
|
174
|
+
snapshot: { groupId, users },
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
async execute(plan, env) {
|
|
178
|
+
const body = { groupId: plan.groupId, users: plan.users };
|
|
179
|
+
const res = await env.host.request({
|
|
180
|
+
method: 'POST',
|
|
181
|
+
path: '/api/em/msg/executeIm/group/kickGroupUsers',
|
|
182
|
+
body,
|
|
183
|
+
operation: 'yimiaoban.group.kick',
|
|
184
|
+
});
|
|
185
|
+
assertImOk(res, '踢出群成员');
|
|
186
|
+
return { groupId: plan.groupId, kicked: plan.users.length };
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
// ── 退群 ──
|
|
190
|
+
const groupExitPair = makeWritePair({
|
|
191
|
+
action: 'yimiaoban.group.exit',
|
|
192
|
+
async plan(args, env) {
|
|
193
|
+
const groupId = str(args.groupId);
|
|
194
|
+
if (!groupId || groupId === '0')
|
|
195
|
+
throw ymbValidationError('id_invalid', 'groupId 必须非 0');
|
|
196
|
+
return {
|
|
197
|
+
summary: { action: '退出群聊', groupId },
|
|
198
|
+
snapshot: { groupId },
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
async execute(plan, env) {
|
|
202
|
+
const res = await env.host.request({
|
|
203
|
+
method: 'POST',
|
|
204
|
+
path: '/api/em/msg/executeIm/group/exit',
|
|
205
|
+
body: { groupId: plan.groupId },
|
|
206
|
+
operation: 'yimiaoban.group.exit',
|
|
207
|
+
});
|
|
208
|
+
assertImOk(res, '退出群聊');
|
|
209
|
+
return { groupId: plan.groupId };
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
// ── 解散群 ──
|
|
213
|
+
const groupDestroyPair = makeWritePair({
|
|
214
|
+
action: 'yimiaoban.group.destroy',
|
|
215
|
+
async plan(args, env) {
|
|
216
|
+
const groupId = str(args.groupId);
|
|
217
|
+
if (!groupId || groupId === '0')
|
|
218
|
+
throw ymbValidationError('id_invalid', 'groupId 必须非 0');
|
|
219
|
+
return {
|
|
220
|
+
summary: { action: '解散群聊(仅群主可执行,不可恢复)', groupId },
|
|
221
|
+
snapshot: { groupId },
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
async execute(plan, env) {
|
|
225
|
+
const res = await env.host.request({
|
|
226
|
+
method: 'POST',
|
|
227
|
+
path: '/api/em/msg/executeIm/group/destroy',
|
|
228
|
+
body: { groupId: plan.groupId },
|
|
229
|
+
operation: 'yimiaoban.group.destroy',
|
|
230
|
+
});
|
|
231
|
+
assertImOk(res, '解散群聊');
|
|
232
|
+
return { groupId: plan.groupId };
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
// ── 申请入群 ──
|
|
236
|
+
const groupJoinPair = makeWritePair({
|
|
237
|
+
action: 'yimiaoban.group.join',
|
|
238
|
+
async plan(args, env) {
|
|
239
|
+
const groupId = args.groupId !== undefined && args.groupId !== '' ? str(args.groupId) : '';
|
|
240
|
+
const token = args.token !== undefined && args.token !== '' ? String(args.token) : '';
|
|
241
|
+
if (!groupId && !token)
|
|
242
|
+
throw ymbValidationError('target_required', 'groupId 与 token 至少其一');
|
|
243
|
+
if (groupId && groupId === '0')
|
|
244
|
+
throw ymbValidationError('id_invalid', 'groupId 必须非 0');
|
|
245
|
+
if (token && token.length > 500)
|
|
246
|
+
throw ymbValidationError('token_invalid', 'token 过长');
|
|
247
|
+
const mark = args.mark !== undefined && args.mark !== '' ? String(args.mark) : '';
|
|
248
|
+
if (mark.length > 100)
|
|
249
|
+
throw ymbValidationError('mark_too_long', 'mark 不能超过 100 字节');
|
|
250
|
+
return {
|
|
251
|
+
summary: { action: '申请加入群聊', groupId: groupId || '(token)', hasMark: Boolean(mark), needApproval: true },
|
|
252
|
+
snapshot: compact({ groupId: groupId || undefined, token: token || undefined, mark: mark || undefined }),
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
async execute(plan, env) {
|
|
256
|
+
const body = compact({
|
|
257
|
+
trans_id: 0,
|
|
258
|
+
dev: 4,
|
|
259
|
+
rule: 0,
|
|
260
|
+
auto: 0,
|
|
261
|
+
group_id: plan.groupId || undefined,
|
|
262
|
+
mark: plan.mark || undefined,
|
|
263
|
+
token: plan.token || undefined,
|
|
264
|
+
});
|
|
265
|
+
const res = await env.host.request({
|
|
266
|
+
method: 'POST',
|
|
267
|
+
path: '/api/em/msg/executeIm/group/joinGroup',
|
|
268
|
+
body,
|
|
269
|
+
operation: 'yimiaoban.group.join',
|
|
270
|
+
});
|
|
271
|
+
assertImOk(res, '申请入群');
|
|
272
|
+
const flag = Number(res?.data?.data?.flag ?? res?.data?.flag ?? -1);
|
|
273
|
+
return {
|
|
274
|
+
flag,
|
|
275
|
+
note: flag === 1 ? '无需验证,已直接加入' : flag === 0 ? '需管理员/群主审批' : '未知结果',
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
/** --switchs 解析:flag:onoff 逗号分隔。 */
|
|
280
|
+
function parseSwitchs(raw) {
|
|
281
|
+
const text = String(raw ?? '');
|
|
282
|
+
if (!text)
|
|
283
|
+
return [];
|
|
284
|
+
return text.split(',').map((pair) => {
|
|
285
|
+
const [flagRaw, onoffRaw] = pair.split(':');
|
|
286
|
+
const flag = Number(flagRaw);
|
|
287
|
+
const onOff = Number(onoffRaw);
|
|
288
|
+
if (!Number.isInteger(flag) || !Number.isInteger(onOff)) {
|
|
289
|
+
throw ymbValidationError('switchs_invalid', `switchs 必须形如 flag:onoff(如 1:1,2:0),收到:${pair}`);
|
|
290
|
+
}
|
|
291
|
+
return { flag, on_off: onOff };
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
// ── 批量改群属性(batModGroupInfo)──
|
|
295
|
+
const groupModifyPair = makeWritePair({
|
|
296
|
+
action: 'yimiaoban.group.modify',
|
|
297
|
+
async plan(args, env) {
|
|
298
|
+
const groupId = str(args.groupId);
|
|
299
|
+
if (!groupId || groupId === '0')
|
|
300
|
+
throw ymbValidationError('id_invalid', 'groupId 必须非 0');
|
|
301
|
+
if (args.onoff !== undefined) {
|
|
302
|
+
throw ymbValidationError('field_forbidden', 'onoff 字段服务端不处理,请用 switchs');
|
|
303
|
+
}
|
|
304
|
+
const body = { group_id: groupId };
|
|
305
|
+
let mask = 0;
|
|
306
|
+
const set = (bit, key, value) => {
|
|
307
|
+
if (value !== undefined) {
|
|
308
|
+
mask |= bit;
|
|
309
|
+
body[key] = value;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
if (args.num !== undefined)
|
|
313
|
+
set(1 << 1, 'num', toUint(args.num, 'num'));
|
|
314
|
+
if (args.rsonoff !== undefined)
|
|
315
|
+
set(1 << 3, 'rsonoff', toUint(args.rsonoff, 'rsonoff'));
|
|
316
|
+
if (args.history !== undefined)
|
|
317
|
+
set(1 << 5, 'history', toUint(args.history, 'history'));
|
|
318
|
+
if (args.switchs !== undefined) {
|
|
319
|
+
const list = parseSwitchs(args.switchs);
|
|
320
|
+
if (list.length)
|
|
321
|
+
set(1 << 8, 'switchs', list);
|
|
322
|
+
}
|
|
323
|
+
if (args.gtId !== undefined)
|
|
324
|
+
set(1 << 11, 'gt_id', String(args.gtId));
|
|
325
|
+
if (args.copyGroupids) {
|
|
326
|
+
const ids = Array.isArray(args.copyGroupids) ? args.copyGroupids.map((x) => String(x)) : splitIds(args.copyGroupids, 'copyGroupids');
|
|
327
|
+
set(1 << 9, 'copy_groupids', ids.map((x) => Number(x)));
|
|
328
|
+
}
|
|
329
|
+
/** 解析成员并在失败时中止(确认移除后才继续),与源脚本 resolveMemberUsers 语义一致。 */
|
|
330
|
+
const resolveOrFail = async (raw, label) => {
|
|
331
|
+
const { users, failed } = await resolveMembers({ users: raw }, env);
|
|
332
|
+
if (failed.length && args.removeFailed !== true) {
|
|
333
|
+
throw ymbValidationError('members_resolve_failed', `${label}信息获取失败(cid/uid 缺失或为 0):${failed.map((f) => f.name ? `${f.name}(${f.uid})` : f.uid).join('、')};确认移除请传 removeFailed:true`, { details: failed });
|
|
334
|
+
}
|
|
335
|
+
return users;
|
|
336
|
+
};
|
|
337
|
+
if (args.admins) {
|
|
338
|
+
const users = await resolveOrFail(args.admins, '管理员');
|
|
339
|
+
if (users.length === 0)
|
|
340
|
+
throw ymbValidationError('members_empty', '管理员解析为空');
|
|
341
|
+
set(1 << 7, 'admins', users);
|
|
342
|
+
}
|
|
343
|
+
if (args.delAdmins) {
|
|
344
|
+
const users = await resolveOrFail(args.delAdmins, '待移除管理员');
|
|
345
|
+
if (users.length === 0)
|
|
346
|
+
throw ymbValidationError('members_empty', '待移除管理员解析为空');
|
|
347
|
+
// 源 modGroupInfo.js:移除管理员走 del_admins 字段(mask 与 admins 同为 admins 位),不能写进 admins
|
|
348
|
+
set(1 << 7, 'del_admins', users);
|
|
349
|
+
}
|
|
350
|
+
if (args.creator) {
|
|
351
|
+
const users = await resolveOrFail(args.creator, '新群主');
|
|
352
|
+
if (!users.length)
|
|
353
|
+
throw ymbValidationError('members_empty', '新群主解析失败');
|
|
354
|
+
set(1 << 6, 'creator', users[0]);
|
|
355
|
+
}
|
|
356
|
+
if (mask === 0)
|
|
357
|
+
throw ymbValidationError('field_required', '至少修改一项(num/rsonoff/history/switchs/gtId/copyGroupids/admins/delAdmins/creator)');
|
|
358
|
+
body.mask = mask;
|
|
359
|
+
return {
|
|
360
|
+
summary: { action: '批量修改群属性', groupId, mask, changedFields: Object.keys(body).filter((k) => k !== 'group_id' && k !== 'mask') },
|
|
361
|
+
snapshot: { groupId, body },
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
async execute(plan, env) {
|
|
365
|
+
const res = await env.host.request({
|
|
366
|
+
method: 'POST',
|
|
367
|
+
path: '/api/em/msg/executeIm/group/batModGroupInfo',
|
|
368
|
+
body: plan.body,
|
|
369
|
+
operation: 'yimiaoban.group.modify',
|
|
370
|
+
});
|
|
371
|
+
assertImOk(res, '修改群属性');
|
|
372
|
+
return { groupId: plan.groupId };
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
// ── 改群名/单字段(modifyGroupInfo)──
|
|
376
|
+
const groupRenamePair = makeWritePair({
|
|
377
|
+
action: 'yimiaoban.group.rename',
|
|
378
|
+
async plan(args, env) {
|
|
379
|
+
const groupId = str(args.groupId);
|
|
380
|
+
if (!groupId || groupId === '0')
|
|
381
|
+
throw ymbValidationError('id_invalid', 'groupId 必须非 0');
|
|
382
|
+
const forbidden = ['announce', 'type', 'state'];
|
|
383
|
+
for (const key of forbidden) {
|
|
384
|
+
if (args[key] !== undefined)
|
|
385
|
+
throw ymbValidationError('field_forbidden', `${key} 字段禁用:announce 请用公告操作,type/state 会触发高危行为`);
|
|
386
|
+
}
|
|
387
|
+
const fieldKeys = ['name', 'history', 'host', 'display', 'msgSetting', 'mark', 'gtid'].filter((k) => args[k] !== undefined);
|
|
388
|
+
if (fieldKeys.length === 0)
|
|
389
|
+
throw ymbValidationError('field_required', '至少修改一项:name/history/host/display/msgSetting/mark/gtid');
|
|
390
|
+
if (fieldKeys.length > 1)
|
|
391
|
+
throw ymbValidationError('field_conflict', '本接口一次只能改一项');
|
|
392
|
+
const field = fieldKeys[0];
|
|
393
|
+
const maskOrder = { name: 0, history: 8, host: 4, display: 5, msgSetting: 6, mark: 7, gtid: 9 };
|
|
394
|
+
let info = {};
|
|
395
|
+
if (field === 'host') {
|
|
396
|
+
const { users, failed } = await resolveMembers({ users: args.host }, env);
|
|
397
|
+
if (failed.length) {
|
|
398
|
+
throw ymbValidationError('members_resolve_failed', `群主信息获取失败(cid/uid 缺失或为 0):${failed.map((f) => f.name ? `${f.name}(${f.uid})` : f.uid).join('、')}`, { details: failed });
|
|
399
|
+
}
|
|
400
|
+
if (!users.length)
|
|
401
|
+
throw ymbValidationError('members_empty', '新群主解析失败');
|
|
402
|
+
info = { host: users[0] };
|
|
403
|
+
}
|
|
404
|
+
else if (field === 'history' || field === 'display' || field === 'msgSetting' || field === 'mark') {
|
|
405
|
+
info = { [field === 'msgSetting' ? 'msg_setting' : field]: toUint(args[field], field) };
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
info = { [field]: field === 'gtid' ? String(args[field]) : String(args[field]) };
|
|
409
|
+
}
|
|
410
|
+
const data = {
|
|
411
|
+
trans_id: 0,
|
|
412
|
+
group_id: groupId,
|
|
413
|
+
type: 0,
|
|
414
|
+
dev: 4,
|
|
415
|
+
mask: maskOrder[field],
|
|
416
|
+
info,
|
|
417
|
+
};
|
|
418
|
+
return {
|
|
419
|
+
summary: { action: `修改群信息(${field})`, groupId, field, value: info[field === 'msgSetting' ? 'msg_setting' : field] },
|
|
420
|
+
snapshot: { groupId, data },
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
async execute(plan, env) {
|
|
424
|
+
const res = await env.host.request({
|
|
425
|
+
method: 'POST',
|
|
426
|
+
path: '/api/em/msg/executeIm/group/modifyGroupInfo',
|
|
427
|
+
body: { data: plan.data },
|
|
428
|
+
operation: 'yimiaoban.group.rename',
|
|
429
|
+
});
|
|
430
|
+
assertImOk(res, '修改群信息');
|
|
431
|
+
return { groupId: plan.groupId };
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
// ── 群公告 增改删 ──
|
|
435
|
+
const groupAnnounceModifyPair = makeWritePair({
|
|
436
|
+
action: 'yimiaoban.group.announce.modify',
|
|
437
|
+
async plan(args, env) {
|
|
438
|
+
const groupId = str(args.groupId);
|
|
439
|
+
if (!groupId || groupId === '0')
|
|
440
|
+
throw ymbValidationError('id_invalid', 'groupId 必须非 0');
|
|
441
|
+
const isDel = args.del === true || String(args.del) === '1' || String(args.flag) === '1';
|
|
442
|
+
const aid = args.aid !== undefined && args.aid !== '' ? String(args.aid) : '0';
|
|
443
|
+
if (isDel && (!aid || aid === '0'))
|
|
444
|
+
throw ymbValidationError('aid_required', '删除公告必须指定 aid');
|
|
445
|
+
if (!isDel) {
|
|
446
|
+
const annouce = args.annouce !== undefined ? String(args.annouce) : '';
|
|
447
|
+
if (!annouce)
|
|
448
|
+
throw ymbValidationError('annouce_required', '新增/修改公告必须提供 annouce');
|
|
449
|
+
if (annouce.length > 5000)
|
|
450
|
+
throw ymbValidationError('annouce_too_long', '公告内容过长');
|
|
451
|
+
}
|
|
452
|
+
const summary = { action: isDel ? '删除群公告' : (aid && aid !== '0' ? '修改群公告' : '新增群公告'), groupId, aid };
|
|
453
|
+
const snapshot = { groupId, flag: isDel ? 1 : 0, aid };
|
|
454
|
+
if (!isDel) {
|
|
455
|
+
const notice = args.notice !== undefined ? toUint(args.notice, 'notice') : 0;
|
|
456
|
+
const fmark = args.fmark !== undefined ? toUint(args.fmark, 'fmark') : undefined;
|
|
457
|
+
summary.notice = notice;
|
|
458
|
+
summary.mustRead = fmark === 1;
|
|
459
|
+
snapshot.annouce = String(args.annouce);
|
|
460
|
+
snapshot.notice = notice;
|
|
461
|
+
if (fmark !== undefined)
|
|
462
|
+
snapshot.fmark = fmark;
|
|
463
|
+
if (args.related !== undefined)
|
|
464
|
+
snapshot.related = String(args.related);
|
|
465
|
+
}
|
|
466
|
+
return { summary, snapshot };
|
|
467
|
+
},
|
|
468
|
+
async execute(plan, env) {
|
|
469
|
+
const body = compact({
|
|
470
|
+
groupId: plan.groupId,
|
|
471
|
+
flag: plan.flag,
|
|
472
|
+
aid: plan.aid,
|
|
473
|
+
annouce: plan.annouce,
|
|
474
|
+
notice: plan.notice,
|
|
475
|
+
fmark: plan.fmark,
|
|
476
|
+
related: plan.related,
|
|
477
|
+
sensitive_flag: plan.flag === 1 ? undefined : 1,
|
|
478
|
+
});
|
|
479
|
+
const res = await env.host.request({
|
|
480
|
+
method: 'POST',
|
|
481
|
+
path: '/api/em/msg/executeIm/group/modifyAnnouce',
|
|
482
|
+
body,
|
|
483
|
+
operation: 'yimiaoban.group.announce.modify',
|
|
484
|
+
});
|
|
485
|
+
assertImOk(res, '修改群公告');
|
|
486
|
+
const responseAid = str(res?.data?.data?.aid ?? res?.data?.aid ?? '');
|
|
487
|
+
const deleted = plan.flag === 1;
|
|
488
|
+
// 删除后服务端可能回 0/空 aid:对外仍回报被删除的那条公告 id,便于核对
|
|
489
|
+
return {
|
|
490
|
+
groupId: plan.groupId,
|
|
491
|
+
aid: responseAid && responseAid !== '0' ? responseAid : plan.aid,
|
|
492
|
+
deleted,
|
|
493
|
+
...(deleted ? {} : { created: plan.aid === '0' }),
|
|
494
|
+
};
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
function buildWriteDefinition(name, method, summary, inputProps, pair) {
|
|
498
|
+
const requiredInput = Object.keys(inputProps).filter((k) => {
|
|
499
|
+
const prop = inputProps[k];
|
|
500
|
+
return prop?.required === true;
|
|
501
|
+
});
|
|
502
|
+
const prepareSchema = schemaOf({ ...inputProps, removeFailed: { type: 'boolean', description: '确认移除解析失败成员' } }, { required: requiredInput, description: summary });
|
|
503
|
+
return [
|
|
504
|
+
{
|
|
505
|
+
name: `${name}.prepare`,
|
|
506
|
+
resource: 'group',
|
|
507
|
+
method: `${method}.prepare`,
|
|
508
|
+
action: `${name}.prepare`,
|
|
509
|
+
summary: `${summary}(prepare:只读校验 + 差异摘要 + 签发确认)`,
|
|
510
|
+
capability: 'group',
|
|
511
|
+
risk: 'read-before-write',
|
|
512
|
+
requiresConfirmation: true,
|
|
513
|
+
inputSchema: prepareSchema,
|
|
514
|
+
handler: pair.prepare,
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: `${name}.apply`,
|
|
518
|
+
resource: 'group',
|
|
519
|
+
method: `${method}.apply`,
|
|
520
|
+
action: `${name}.apply`,
|
|
521
|
+
summary: `${summary}(apply:需 confirm:true + continuation)`,
|
|
522
|
+
capability: 'group',
|
|
523
|
+
risk: WRITE,
|
|
524
|
+
requiresConfirmation: true,
|
|
525
|
+
inputSchema: schemaOf({
|
|
526
|
+
confirm: { type: 'boolean', description: '必须为 true' },
|
|
527
|
+
continuation: { type: 'string', minLength: 1 },
|
|
528
|
+
}, { required: ['confirm', 'continuation'], description: summary }),
|
|
529
|
+
handler: pair.apply,
|
|
530
|
+
},
|
|
531
|
+
];
|
|
532
|
+
}
|
|
533
|
+
const groupWriteOps = [
|
|
534
|
+
...buildWriteDefinition('yimiaoban.group.create', 'group.create', '创建普通群', {
|
|
535
|
+
users: { type: 'array', items: { type: 'string' }, description: '群成员 uid(逗号串或数组)' },
|
|
536
|
+
name: { type: 'string', description: '群名(缺省自动生成)' },
|
|
537
|
+
names: { type: 'array', items: { type: 'string' }, description: '与 users 顺序对应姓名(用于默认群名)' },
|
|
538
|
+
admins: { type: 'array', items: { type: 'string' }, description: '管理员 uid' },
|
|
539
|
+
}, groupCreatePair),
|
|
540
|
+
...buildWriteDefinition('yimiaoban.group.invite', 'group.invite', '邀请成员入群', {
|
|
541
|
+
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
542
|
+
users: { type: 'array', items: { type: 'string' }, description: '被邀请 uid' },
|
|
543
|
+
}, groupInvitePair),
|
|
544
|
+
...buildWriteDefinition('yimiaoban.group.kick', 'group.kick', '踢出群成员', {
|
|
545
|
+
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
546
|
+
users: { type: 'array', items: { type: 'string' }, description: '被踢 uid' },
|
|
547
|
+
}, groupKickPair),
|
|
548
|
+
...buildWriteDefinition('yimiaoban.group.exit', 'group.exit', '退出群聊', {
|
|
549
|
+
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
550
|
+
}, groupExitPair),
|
|
551
|
+
...buildWriteDefinition('yimiaoban.group.destroy', 'group.destroy', '解散群聊(仅群主,不可恢复)', {
|
|
552
|
+
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
553
|
+
}, groupDestroyPair),
|
|
554
|
+
...buildWriteDefinition('yimiaoban.group.join', 'group.join', '申请加入群聊', {
|
|
555
|
+
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id(token 二选一)' },
|
|
556
|
+
token: { type: 'string', description: '群 token/二维码加入' },
|
|
557
|
+
mark: { type: 'string', maxLength: 100, description: '申请理由' },
|
|
558
|
+
}, groupJoinPair),
|
|
559
|
+
...buildWriteDefinition('yimiaoban.group.modify', 'group.modify', '批量修改群属性(batModGroupInfo)', {
|
|
560
|
+
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
561
|
+
num: { type: 'integer', description: '最大人数' },
|
|
562
|
+
switchs: { type: 'string', description: '群开关(flag:onoff 逗号分隔,如 1:1,8:0)' },
|
|
563
|
+
rsonoff: { type: 'integer', enum: [0, 1], description: '消息阅读状态开关' },
|
|
564
|
+
history: { type: 'integer', description: '新人可看历史范围' },
|
|
565
|
+
admins: { type: 'array', items: { type: 'string' }, description: '新增管理员 uid' },
|
|
566
|
+
delAdmins: { type: 'array', items: { type: 'string' }, description: '移除管理员 uid' },
|
|
567
|
+
creator: { type: 'string', description: '转让群主(单 uid)' },
|
|
568
|
+
copyGroupids: { type: 'array', items: { type: 'string' }, description: '拷贝群列表' },
|
|
569
|
+
gtId: { type: 'string', description: '关联群类型 id' },
|
|
570
|
+
}, groupModifyPair),
|
|
571
|
+
...buildWriteDefinition('yimiaoban.group.rename', 'group.rename', '修改群信息(一次一项:群名/历史/群主/显示/打扰/标记/gtid)', {
|
|
572
|
+
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
573
|
+
name: { type: 'string', description: '改群名' },
|
|
574
|
+
history: { type: 'integer', description: '新人历史消息范围' },
|
|
575
|
+
host: { type: 'string', description: '转让群主(单 uid)' },
|
|
576
|
+
display: { type: 'integer', enum: [0, 1], description: '是否在群列表显示' },
|
|
577
|
+
msgSetting: { type: 'integer', description: '消息打扰设置' },
|
|
578
|
+
mark: { type: 'integer', description: '标记位' },
|
|
579
|
+
gtid: { type: 'string', description: 'GTID' },
|
|
580
|
+
}, groupRenamePair),
|
|
581
|
+
...buildWriteDefinition('yimiaoban.group.announce.modify', 'group.announce.modify', '新增/修改/删除群公告', {
|
|
582
|
+
groupId: { type: 'string', pattern: '^\\d+$', description: '群 id' },
|
|
583
|
+
annouce: { type: 'string', description: '公告内容(删除时不需要)' },
|
|
584
|
+
aid: { type: 'string', description: '公告 id(0/不传=新增;非 0=修改;删除必填)' },
|
|
585
|
+
del: { type: 'boolean', description: '删除开关' },
|
|
586
|
+
notice: { type: 'integer', enum: [0, 1, 2], description: '0 发通知(默认)/1 不发/2 发并@全体' },
|
|
587
|
+
fmark: { type: 'integer', enum: [0, 1], description: '新人必看' },
|
|
588
|
+
related: { type: 'string', description: '关联附件 id' },
|
|
589
|
+
}, groupAnnounceModifyPair),
|
|
590
|
+
];
|
|
591
|
+
export const groupWriteOperations = groupWriteOps;
|