@jiexiaoyin/wecom-api 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +228 -0
- package/config.example.json +7 -0
- package/config.js +76 -0
- package/docs/approval-templates.example.json +11 -0
- package/docs/nginx-mirror.md +193 -0
- package/openclaw.plugin.json +15 -0
- package/package.json +34 -0
- package/plugin.cjs +172 -0
- package/plugin.ts +136 -0
- package/skills/wecom-api/SKILL.md +40 -0
- package/skills/wecom-api/index.js +288 -0
- package/skills/wecom-api/openclaw.plugin.json +10 -0
- package/src/callback-helper.js +198 -0
- package/src/config.cjs +286 -0
- package/src/core/permission.js +479 -0
- package/src/crypto.js +130 -0
- package/src/index.js +199 -0
- package/src/modules/addressbook/index.js +413 -0
- package/src/modules/addressbook_cache/index.js +365 -0
- package/src/modules/advanced/index.js +159 -0
- package/src/modules/app/index.js +102 -0
- package/src/modules/approval/index.js +146 -0
- package/src/modules/auth/index.js +103 -0
- package/src/modules/callback/index.js +1180 -0
- package/src/modules/chain/index.js +193 -0
- package/src/modules/checkin/index.js +142 -0
- package/src/modules/checkin_rules/index.js +251 -0
- package/src/modules/contact/index.js +481 -0
- package/src/modules/contact_stats/index.js +349 -0
- package/src/modules/custom/index.js +140 -0
- package/src/modules/customer/index.js +51 -0
- package/src/modules/disk/index.js +245 -0
- package/src/modules/document/index.js +282 -0
- package/src/modules/hr/index.js +93 -0
- package/src/modules/intelligence/index.js +346 -0
- package/src/modules/kf/index.js +74 -0
- package/src/modules/live/index.js +122 -0
- package/src/modules/media/index.js +183 -0
- package/src/modules/meeting/index.js +665 -0
- package/src/modules/message/index.js +402 -0
- package/src/modules/messenger/index.js +208 -0
- package/src/modules/moments/index.js +161 -0
- package/src/modules/msgaudit/index.js +24 -0
- package/src/modules/notify/index.js +81 -0
- package/src/modules/oceanengine/index.js +199 -0
- package/src/modules/openchat/index.js +197 -0
- package/src/modules/phone/index.js +45 -0
- package/src/modules/room/index.js +178 -0
- package/src/modules/schedule/index.js +246 -0
- package/src/modules/school/index.js +199 -0
- package/src/modules/security/index.js +223 -0
- package/src/modules/sensitive/index.js +170 -0
- package/src/modules/thirdparty/index.js +145 -0
- package/src/sdk/index.js +269 -0
- package/src/utils/callback-helper.js +198 -0
- package/test/callback-crypto.test.js +55 -0
- package/test/crypto.test.js +85 -0
- package/test/permission.test.js +115 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 权限控制核心模块
|
|
3
|
+
*
|
|
4
|
+
* 权限来源优先级:
|
|
5
|
+
* 1. 自定义映射表(roleMapping)
|
|
6
|
+
* 2. 企微部门负责人字段
|
|
7
|
+
* 3. 默认规则(admin > manager > staff)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const ROLE = {
|
|
11
|
+
ADMIN: 'admin',
|
|
12
|
+
MANAGER: 'manager',
|
|
13
|
+
STAFF: 'staff'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// 角色等级(数值越大权限越高)
|
|
17
|
+
const ROLE_LEVEL = {
|
|
18
|
+
[ROLE.STAFF]: 1,
|
|
19
|
+
[ROLE.MANAGER]: 2,
|
|
20
|
+
[ROLE.ADMIN]: 3
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// 模块权限矩阵
|
|
24
|
+
// format: module.action = [allowed roles]
|
|
25
|
+
const PERMISSION_MATRIX = {
|
|
26
|
+
// 通讯录
|
|
27
|
+
addressbook: {
|
|
28
|
+
getDepartmentList: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
29
|
+
getDepartmentDetail: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
30
|
+
getDepartmentUsers: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
31
|
+
getUser: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
32
|
+
getTagList: [ROLE.ADMIN, ROLE.MANAGER],
|
|
33
|
+
createUser: [ROLE.ADMIN],
|
|
34
|
+
updateUser: [ROLE.ADMIN],
|
|
35
|
+
deleteUser: [ROLE.ADMIN],
|
|
36
|
+
batchDeleteUser: [ROLE.ADMIN],
|
|
37
|
+
importUsers: [ROLE.ADMIN],
|
|
38
|
+
exportUsers: [ROLE.ADMIN],
|
|
39
|
+
convertToUserId: [ROLE.ADMIN, ROLE.MANAGER],
|
|
40
|
+
getUserIdByName: [ROLE.ADMIN, ROLE.MANAGER]
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// 审批
|
|
44
|
+
approval: {
|
|
45
|
+
getApprovalDetail: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
46
|
+
getApprovalList: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
47
|
+
getApprovalTemplate: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
48
|
+
getSelfApprovalList: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
49
|
+
submitApproval: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
50
|
+
cancelApproval: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
51
|
+
approveApproval: [ROLE.ADMIN, ROLE.MANAGER],
|
|
52
|
+
getHolidayBalance: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF]
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// 客户联系
|
|
56
|
+
contact: {
|
|
57
|
+
getClientList: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
58
|
+
getClientDetail: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
59
|
+
getClientTags: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
60
|
+
getGroupList: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
61
|
+
getGroupMemberList: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
62
|
+
getUnassignedList: [ROLE.ADMIN, ROLE.MANAGER],
|
|
63
|
+
transferClient: [ROLE.ADMIN, ROLE.MANAGER],
|
|
64
|
+
getContactStats: [ROLE.ADMIN, ROLE.MANAGER]
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
// 考勤
|
|
68
|
+
checkin: {
|
|
69
|
+
getCheckinData: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
70
|
+
getCheckinSummary: [ROLE.ADMIN, ROLE.MANAGER],
|
|
71
|
+
getCheckinMonthReport: [ROLE.ADMIN, ROLE.MANAGER],
|
|
72
|
+
getScheduleList: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
73
|
+
getExceptionList: [ROLE.ADMIN, ROLE.MANAGER]
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// 消息推送
|
|
77
|
+
message: {
|
|
78
|
+
sendMessage: [ROLE.ADMIN, ROLE.MANAGER],
|
|
79
|
+
sendTextMessage: [ROLE.ADMIN, ROLE.MANAGER],
|
|
80
|
+
sendImageMessage: [ROLE.ADMIN, ROLE.MANAGER],
|
|
81
|
+
sendFileMessage: [ROLE.ADMIN, ROLE.MANAGER],
|
|
82
|
+
sendMpNewsMessage: [ROLE.ADMIN, ROLE.MANAGER],
|
|
83
|
+
sendChatMessage: [ROLE.ADMIN, ROLE.MANAGER],
|
|
84
|
+
getChatList: [ROLE.ADMIN, ROLE.MANAGER],
|
|
85
|
+
getChatDetail: [ROLE.ADMIN, ROLE.MANAGER]
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// 应用管理
|
|
89
|
+
app: {
|
|
90
|
+
getAppInfo: [ROLE.ADMIN],
|
|
91
|
+
setAppInfo: [ROLE.ADMIN],
|
|
92
|
+
getMenu: [ROLE.ADMIN, ROLE.MANAGER],
|
|
93
|
+
createMenu: [ROLE.ADMIN],
|
|
94
|
+
deleteMenu: [ROLE.ADMIN]
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// 素材管理
|
|
98
|
+
media: {
|
|
99
|
+
uploadMedia: [ROLE.ADMIN, ROLE.MANAGER],
|
|
100
|
+
getMedia: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
101
|
+
getMediaList: [ROLE.ADMIN, ROLE.MANAGER],
|
|
102
|
+
deleteMedia: [ROLE.ADMIN]
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
// 会议
|
|
106
|
+
meeting: {
|
|
107
|
+
getMeetingList: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
108
|
+
getMeetingDetail: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
109
|
+
scheduleMeeting: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
110
|
+
cancelMeeting: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
111
|
+
getMeetingParticipants: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF]
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
// 日程
|
|
115
|
+
schedule: {
|
|
116
|
+
getScheduleList: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
117
|
+
getScheduleDetail: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
118
|
+
createSchedule: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
119
|
+
updateSchedule: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
120
|
+
deleteSchedule: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF]
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// 文档
|
|
124
|
+
document: {
|
|
125
|
+
getDocumentList: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
126
|
+
getDocumentDetail: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
127
|
+
createDocument: [ROLE.ADMIN, ROLE.MANAGER],
|
|
128
|
+
updateDocument: [ROLE.ADMIN, ROLE.MANAGER],
|
|
129
|
+
deleteDocument: [ROLE.ADMIN]
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// 微盘
|
|
133
|
+
disk: {
|
|
134
|
+
getFileList: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
135
|
+
getFileDetail: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
136
|
+
uploadFile: [ROLE.ADMIN, ROLE.MANAGER],
|
|
137
|
+
downloadFile: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
138
|
+
deleteFile: [ROLE.ADMIN, ROLE.MANAGER]
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// 客服
|
|
142
|
+
custom: {
|
|
143
|
+
getAccountList: [ROLE.ADMIN],
|
|
144
|
+
getSessionList: [ROLE.ADMIN, ROLE.MANAGER],
|
|
145
|
+
getSessionDetail: [ROLE.ADMIN, ROLE.MANAGER],
|
|
146
|
+
getMessageList: [ROLE.ADMIN, ROLE.MANAGER],
|
|
147
|
+
sendMessage: [ROLE.ADMIN, ROLE.MANAGER]
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// 直播
|
|
151
|
+
live: {
|
|
152
|
+
getLivingList: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
153
|
+
getLivingDetail: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
154
|
+
getLivingStat: [ROLE.ADMIN, ROLE.MANAGER]
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// 智能会话
|
|
158
|
+
intelligence: {
|
|
159
|
+
getRecordList: [ROLE.ADMIN, ROLE.MANAGER],
|
|
160
|
+
getRecordDetail: [ROLE.ADMIN, ROLE.MANAGER],
|
|
161
|
+
getChatData: [ROLE.ADMIN]
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// 通讯录同步
|
|
165
|
+
addressbook_cache: {
|
|
166
|
+
syncDepartmentTree: [ROLE.ADMIN],
|
|
167
|
+
syncUserList: [ROLE.ADMIN],
|
|
168
|
+
getDepartmentTree: [ROLE.ADMIN, ROLE.MANAGER],
|
|
169
|
+
searchUser: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF]
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
// 客户统计
|
|
173
|
+
contactstats: {
|
|
174
|
+
getUserStats: [ROLE.ADMIN, ROLE.MANAGER],
|
|
175
|
+
getDepartmentStats: [ROLE.ADMIN, ROLE.MANAGER],
|
|
176
|
+
getTrendStats: [ROLE.ADMIN, ROLE.MANAGER]
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// 安全相关
|
|
180
|
+
security: {
|
|
181
|
+
getLoginDeviceList: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
182
|
+
getOperationLog: [ROLE.ADMIN],
|
|
183
|
+
getMessageRemindConfig: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF]
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// 人事
|
|
187
|
+
hr: {
|
|
188
|
+
getStaffDetail: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
189
|
+
getStaffList: [ROLE.ADMIN, ROLE.MANAGER]
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
// 会议室
|
|
193
|
+
room: {
|
|
194
|
+
getRoomList: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
195
|
+
getRoomDetail: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
196
|
+
bookRoom: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF],
|
|
197
|
+
cancelRoom: [ROLE.ADMIN, ROLE.MANAGER, ROLE.STAFF]
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
// 回调
|
|
201
|
+
callback: {
|
|
202
|
+
getEventHistory: [ROLE.ADMIN],
|
|
203
|
+
getEventById: [ROLE.ADMIN],
|
|
204
|
+
exportEventHistory: [ROLE.ADMIN],
|
|
205
|
+
clearEventHistory: [ROLE.ADMIN]
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
class Permission {
|
|
210
|
+
constructor(config, addressbookModule) {
|
|
211
|
+
// 权限映射配置
|
|
212
|
+
this.roleMapping = config.roleMapping || {};
|
|
213
|
+
// 企微部门树模块(用于获取部门负责人)
|
|
214
|
+
this.addressbook = addressbookModule;
|
|
215
|
+
// 缓存
|
|
216
|
+
this._userRoleCache = new Map();
|
|
217
|
+
this._departmentHeadCache = new Map();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 获取用户的角色
|
|
222
|
+
* @param {string} userId 用户ID
|
|
223
|
+
* @returns {Promise<string>} 角色
|
|
224
|
+
*/
|
|
225
|
+
async getUserRole(userId) {
|
|
226
|
+
// 先检查缓存
|
|
227
|
+
if (this._userRoleCache.has(userId)) {
|
|
228
|
+
return this._userRoleCache.get(userId);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let role = null;
|
|
232
|
+
|
|
233
|
+
// 1. 先查自定义映射表
|
|
234
|
+
role = await this._getRoleFromMapping(userId);
|
|
235
|
+
if (role) {
|
|
236
|
+
this._userRoleCache.set(userId, role);
|
|
237
|
+
return role;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 2. 查企微部门负责人
|
|
241
|
+
role = await this._getRoleFromDepartmentHead(userId);
|
|
242
|
+
if (role) {
|
|
243
|
+
this._userRoleCache.set(userId, role);
|
|
244
|
+
return role;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 3. 默认 staff
|
|
248
|
+
role = ROLE.STAFF;
|
|
249
|
+
this._userRoleCache.set(userId, role);
|
|
250
|
+
return role;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 从自定义映射表获取角色
|
|
255
|
+
*/
|
|
256
|
+
async _getRoleFromMapping(userId) {
|
|
257
|
+
const mapping = this.roleMapping;
|
|
258
|
+
|
|
259
|
+
// 检查 admin
|
|
260
|
+
if (mapping.admin?.users?.includes(userId)) {
|
|
261
|
+
return ROLE.ADMIN;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 检查 manager
|
|
265
|
+
if (mapping.manager?.users?.includes(userId)) {
|
|
266
|
+
return ROLE.MANAGER;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 检查 staff
|
|
270
|
+
if (mapping.staff?.users?.includes(userId)) {
|
|
271
|
+
return ROLE.STAFF;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 从企微部门负责人获取角色
|
|
279
|
+
*/
|
|
280
|
+
async _getRoleFromDepartmentHead(userId) {
|
|
281
|
+
if (!this.addressbook) return null;
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
// 获取用户的部门信息
|
|
285
|
+
const userInfo = await this.addressbook.getUser(userId);
|
|
286
|
+
if (!userInfo || !userInfo.department) return null;
|
|
287
|
+
|
|
288
|
+
const departments = Array.isArray(userInfo.department)
|
|
289
|
+
? userInfo.department
|
|
290
|
+
: [userInfo.department];
|
|
291
|
+
|
|
292
|
+
// 检查是否为某些部门的负责人
|
|
293
|
+
for (const deptId of departments) {
|
|
294
|
+
if (this._departmentHeadCache.has(deptId)) {
|
|
295
|
+
const headUserId = this._departmentHeadCache.get(deptId);
|
|
296
|
+
if (headUserId === userId) {
|
|
297
|
+
return ROLE.MANAGER;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 从企微获取部门负责人
|
|
303
|
+
for (const deptId of departments) {
|
|
304
|
+
try {
|
|
305
|
+
const deptDetail = await this.addressbook.getDepartmentDetail(deptId);
|
|
306
|
+
if (deptDetail && deptDetail.department_chairman_userid === userId) {
|
|
307
|
+
this._departmentHeadCache.set(deptId, userId);
|
|
308
|
+
return ROLE.MANAGER;
|
|
309
|
+
}
|
|
310
|
+
} catch (e) {
|
|
311
|
+
// 忽略单个部门的错误
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} catch (e) {
|
|
315
|
+
// 忽略错误
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 检查用户是否有权限
|
|
323
|
+
* @param {string} userId 用户ID
|
|
324
|
+
* @param {string} module 模块名
|
|
325
|
+
* @param {string} action 操作名
|
|
326
|
+
* @returns {Promise<boolean>}
|
|
327
|
+
*/
|
|
328
|
+
async checkPermission(userId, module, action) {
|
|
329
|
+
// 管理员拥有所有权限
|
|
330
|
+
const userRole = await this.getUserRole(userId);
|
|
331
|
+
if (userRole === ROLE.ADMIN) return true;
|
|
332
|
+
|
|
333
|
+
// 查找权限矩阵
|
|
334
|
+
const modulePerm = PERMISSION_MATRIX[module];
|
|
335
|
+
if (!modulePerm) {
|
|
336
|
+
// 模块未定义权限矩阵,默认允许
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const allowedRoles = modulePerm[action];
|
|
341
|
+
if (!allowedRoles) {
|
|
342
|
+
// 操作未定义,默认允许
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return allowedRoles.includes(userRole);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* 检查权限并抛出异常
|
|
351
|
+
* @param {string} userId 用户ID
|
|
352
|
+
* @param {string} module 模块名
|
|
353
|
+
* @param {string} action 操作名
|
|
354
|
+
*/
|
|
355
|
+
async requirePermission(userId, module, action) {
|
|
356
|
+
const hasPermission = await this.checkPermission(userId, module, action);
|
|
357
|
+
if (!hasPermission) {
|
|
358
|
+
const error = new Error(`无权限: ${module}.${action}`);
|
|
359
|
+
error.code = 'PERMISSION_DENIED';
|
|
360
|
+
error.details = { userId, module, action };
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 获取用户在某个模块的数据权限范围
|
|
367
|
+
* @param {string} userId 用户ID
|
|
368
|
+
* @param {string} module 模块名
|
|
369
|
+
* @returns {Promise<Object>} 数据权限范围
|
|
370
|
+
*/
|
|
371
|
+
async getDataScope(userId, module) {
|
|
372
|
+
const userRole = await this.getUserRole(userId);
|
|
373
|
+
|
|
374
|
+
switch (userRole) {
|
|
375
|
+
case ROLE.ADMIN:
|
|
376
|
+
// 管理员可访问所有数据
|
|
377
|
+
return { scope: 'all', departmentIds: null };
|
|
378
|
+
|
|
379
|
+
case ROLE.MANAGER:
|
|
380
|
+
// 经理获取管辖部门
|
|
381
|
+
const depts = await this._getManagerDepartments(userId);
|
|
382
|
+
return { scope: 'department', departmentIds: depts };
|
|
383
|
+
|
|
384
|
+
case ROLE.STAFF:
|
|
385
|
+
default:
|
|
386
|
+
// 员工只能看自己的数据
|
|
387
|
+
return { scope: 'self', userId };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* 获取经理管辖的部门列表
|
|
393
|
+
*/
|
|
394
|
+
async _getManagerDepartments(userId) {
|
|
395
|
+
const mapping = this.roleMapping;
|
|
396
|
+
|
|
397
|
+
// 优先用映射表
|
|
398
|
+
if (mapping.manager?.departments?.length > 0) {
|
|
399
|
+
return mapping.manager.departments;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 从企微获取
|
|
403
|
+
if (!this.addressbook) return [];
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const userInfo = await this.addressbook.getUser(userId);
|
|
407
|
+
if (!userInfo || !userInfo.department) return [];
|
|
408
|
+
return Array.isArray(userInfo.department)
|
|
409
|
+
? userInfo.department
|
|
410
|
+
: [userInfo.department];
|
|
411
|
+
} catch (e) {
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* 过滤数据(根据权限范围)
|
|
418
|
+
* @param {Array} data 数据列表
|
|
419
|
+
* @param {string} userId 用户ID
|
|
420
|
+
* @param {string} dataField 数据中用于判断的字段名
|
|
421
|
+
* @returns {Promise<Array>} 过滤后的数据
|
|
422
|
+
*/
|
|
423
|
+
async filterData(data, userId, dataField = 'userId') {
|
|
424
|
+
const scope = await this.getDataScope(userId);
|
|
425
|
+
|
|
426
|
+
if (scope.scope === 'all') {
|
|
427
|
+
return data;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (scope.scope === 'self') {
|
|
431
|
+
return data.filter(item => item[dataField] === scope.userId);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (scope.scope === 'department') {
|
|
435
|
+
// 如果数据包含 department 字段,过滤
|
|
436
|
+
if (data.length > 0 && data[0].department !== undefined) {
|
|
437
|
+
return data.filter(item =>
|
|
438
|
+
scope.departmentIds.includes(item.department)
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return data;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* 清除缓存
|
|
448
|
+
*/
|
|
449
|
+
clearCache() {
|
|
450
|
+
this._userRoleCache.clear();
|
|
451
|
+
this._departmentHeadCache.clear();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* 获取权限矩阵
|
|
456
|
+
*/
|
|
457
|
+
getPermissionMatrix() {
|
|
458
|
+
return PERMISSION_MATRIX;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* 获取角色定义
|
|
463
|
+
*/
|
|
464
|
+
static getRoles() {
|
|
465
|
+
return ROLE;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* 获取角色等级
|
|
470
|
+
*/
|
|
471
|
+
static getRoleLevel() {
|
|
472
|
+
return ROLE_LEVEL;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
module.exports = Permission;
|
|
477
|
+
module.exports.ROLE = ROLE;
|
|
478
|
+
module.exports.ROLE_LEVEL = ROLE_LEVEL;
|
|
479
|
+
module.exports.PERMISSION_MATRIX = PERMISSION_MATRIX;
|
package/src/crypto.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 企业微信加密解密工具
|
|
3
|
+
* 与官方 @openclaw/wecom 插件保持一致
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 解码 AES Key
|
|
10
|
+
*/
|
|
11
|
+
function decodeEncodingAESKey(encodingAESKey) {
|
|
12
|
+
const trimmed = encodingAESKey.trim();
|
|
13
|
+
if (!trimmed) throw new Error("encodingAESKey missing");
|
|
14
|
+
const withPadding = trimmed.endsWith("=") ? trimmed : `${trimmed}=`;
|
|
15
|
+
const key = Buffer.from(withPadding, "base64");
|
|
16
|
+
if (key.length !== 32) {
|
|
17
|
+
throw new Error(`invalid encodingAESKey (expected 32 bytes after base64 decode, got ${key.length})`);
|
|
18
|
+
}
|
|
19
|
+
return key;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const WECOM_PKCS7_BLOCK_SIZE = 32;
|
|
23
|
+
|
|
24
|
+
function pkcs7Pad(buf, blockSize) {
|
|
25
|
+
const mod = buf.length % blockSize;
|
|
26
|
+
const pad = mod === 0 ? blockSize : blockSize - mod;
|
|
27
|
+
const padByte = Buffer.from([pad]);
|
|
28
|
+
return Buffer.concat([buf, Buffer.alloc(pad, padByte[0])]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function pkcs7Unpad(buf, blockSize) {
|
|
32
|
+
if (buf.length === 0) throw new Error("invalid pkcs7 payload");
|
|
33
|
+
const pad = buf[buf.length - 1];
|
|
34
|
+
if (pad < 1 || pad > blockSize) {
|
|
35
|
+
throw new Error("invalid pkcs7 padding");
|
|
36
|
+
}
|
|
37
|
+
if (pad > buf.length) {
|
|
38
|
+
throw new Error("invalid pkcs7 payload");
|
|
39
|
+
}
|
|
40
|
+
for (let i = 0; i < pad; i++) {
|
|
41
|
+
if (buf[buf.length - 1 - i] !== pad) {
|
|
42
|
+
throw new Error("invalid pkcs7 padding");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return buf.subarray(0, buf.length - pad);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sha1Hex(input) {
|
|
49
|
+
return crypto.createHash("sha1").update(input).digest("hex");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function computeWecomMsgSignature(params) {
|
|
53
|
+
const parts = [params.token, params.timestamp, params.nonce, params.encrypt]
|
|
54
|
+
.map((v) => String(v ?? ""))
|
|
55
|
+
.sort();
|
|
56
|
+
return sha1Hex(parts.join(""));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function verifyWecomSignature(params) {
|
|
60
|
+
const expected = computeWecomMsgSignature({
|
|
61
|
+
token: params.token,
|
|
62
|
+
timestamp: params.timestamp,
|
|
63
|
+
nonce: params.nonce,
|
|
64
|
+
encrypt: params.encrypt,
|
|
65
|
+
});
|
|
66
|
+
return expected === params.signature;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function decryptWecomEncrypted(params) {
|
|
70
|
+
const aesKey = decodeEncodingAESKey(params.encodingAESKey);
|
|
71
|
+
const iv = aesKey.subarray(0, 16);
|
|
72
|
+
const decipher = crypto.createDecipheriv("aes-256-cbc", aesKey, iv);
|
|
73
|
+
decipher.setAutoPadding(false);
|
|
74
|
+
const decryptedPadded = Buffer.concat([
|
|
75
|
+
decipher.update(Buffer.from(params.encrypt, "base64")),
|
|
76
|
+
decipher.final(),
|
|
77
|
+
]);
|
|
78
|
+
const decrypted = pkcs7Unpad(decryptedPadded, WECOM_PKCS7_BLOCK_SIZE);
|
|
79
|
+
|
|
80
|
+
if (decrypted.length < 20) {
|
|
81
|
+
throw new Error(`invalid decrypted payload (expected at least 20 bytes, got ${decrypted.length})`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const msgLen = decrypted.readUInt32BE(16);
|
|
85
|
+
const msgStart = 20;
|
|
86
|
+
const msgEnd = msgStart + msgLen;
|
|
87
|
+
if (msgEnd > decrypted.length) {
|
|
88
|
+
throw new Error(`invalid decrypted msg length (msgEnd=${msgEnd}, payloadLength=${decrypted.length})`);
|
|
89
|
+
}
|
|
90
|
+
const msg = decrypted.subarray(msgStart, msgEnd).toString("utf8");
|
|
91
|
+
|
|
92
|
+
const receiveId = params.receiveId ?? "";
|
|
93
|
+
if (receiveId) {
|
|
94
|
+
const trailing = decrypted.subarray(msgEnd).toString("utf8");
|
|
95
|
+
if (trailing !== receiveId) {
|
|
96
|
+
throw new Error(`receiveId mismatch (expected "${receiveId}", got "${trailing}")`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return msg;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function encryptWecomPlaintext(params) {
|
|
104
|
+
const aesKey = decodeEncodingAESKey(params.encodingAESKey);
|
|
105
|
+
const iv = aesKey.subarray(0, 16);
|
|
106
|
+
const random16 = crypto.randomBytes(16);
|
|
107
|
+
const msg = Buffer.from(params.plaintext ?? "", "utf8");
|
|
108
|
+
const msgLen = Buffer.alloc(4);
|
|
109
|
+
msgLen.writeUInt32BE(msg.length, 0);
|
|
110
|
+
const receiveId = Buffer.from(params.receiveId ?? "", "utf8");
|
|
111
|
+
|
|
112
|
+
const raw = Buffer.concat([random16, msgLen, msg, receiveId]);
|
|
113
|
+
const padded = pkcs7Pad(raw, WECOM_PKCS7_BLOCK_SIZE);
|
|
114
|
+
const cipher = crypto.createCipheriv("aes-256-cbc", aesKey, iv);
|
|
115
|
+
cipher.setAutoPadding(false);
|
|
116
|
+
const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
|
|
117
|
+
return encrypted.toString("base64");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
decodeEncodingAESKey,
|
|
122
|
+
WECOM_PKCS7_BLOCK_SIZE,
|
|
123
|
+
pkcs7Pad,
|
|
124
|
+
pkcs7Unpad,
|
|
125
|
+
sha1Hex,
|
|
126
|
+
computeWecomMsgSignature,
|
|
127
|
+
verifyWecomSignature,
|
|
128
|
+
decryptWecomEncrypted,
|
|
129
|
+
encryptWecomPlaintext
|
|
130
|
+
};
|