@abtnode/core 1.17.4-beta-20251202-034514-637cd8e2 → 1.17.4-beta-20251202-122551-267b614d
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/lib/api/team.js +108 -0
- package/lib/blocklet/migration-dist/migration.cjs +1 -0
- package/lib/blocklet/webhook/event-bus.js +2 -2
- package/lib/blocklet/webhook/index.js +2 -2
- package/lib/blocklet/webhook/queues.js +49 -4
- package/lib/event/index.js +1 -1
- package/lib/index.js +10 -2
- package/lib/states/user.js +47 -0
- package/lib/states/webhook-endpoint.js +8 -0
- package/lib/states/webhook.js +65 -1
- package/lib/util/webhook.js +65 -0
- package/lib/webhook/index.js +67 -1
- package/lib/webhook/sender/api/index.js +2 -0
- package/lib/webhook/sender/slack/index.js +2 -0
- package/package.json +24 -24
package/lib/api/team.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { EventEmitter } = require('events');
|
|
2
2
|
const axios = require('@abtnode/util/lib/axios');
|
|
3
3
|
const pick = require('lodash/pick');
|
|
4
|
+
const omit = require('lodash/omit');
|
|
4
5
|
const defaults = require('lodash/defaults');
|
|
5
6
|
const isEmpty = require('lodash/isEmpty');
|
|
6
7
|
const throttle = require('lodash/throttle');
|
|
@@ -21,6 +22,7 @@ const {
|
|
|
21
22
|
EVENTS,
|
|
22
23
|
NOTIFICATION_SEND_CHANNEL,
|
|
23
24
|
USER_SESSION_STATUS,
|
|
25
|
+
WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD,
|
|
24
26
|
} = require('@abtnode/constant');
|
|
25
27
|
const { isValid: isValidDid } = require('@arcblock/did');
|
|
26
28
|
const { BlockletEvents, TeamEvents, BlockletInternalEvents } = require('@blocklet/constant');
|
|
@@ -857,6 +859,112 @@ class TeamAPI extends EventEmitter {
|
|
|
857
859
|
}
|
|
858
860
|
}
|
|
859
861
|
|
|
862
|
+
async createWebhookDisabledNotification({ teamDid, userDid, webhook, action = '', locale = 'en' }) {
|
|
863
|
+
const notification = {
|
|
864
|
+
en: {
|
|
865
|
+
title: 'Your webhook has been temporarily disabled',
|
|
866
|
+
description: `Your Webhook has been temporarily disabled after ${WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD} consecutive failed message delivery attempts. Please check its availability and you can click the button below to reactivate it if needed.`,
|
|
867
|
+
failureCount: 'Number of failures',
|
|
868
|
+
failureUrl: 'Failed Webhook URL',
|
|
869
|
+
},
|
|
870
|
+
zh: {
|
|
871
|
+
title: '您的 webhook 已被暂时禁用',
|
|
872
|
+
description: `您的 Webhook 在连续 ${WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD} 次消息投递失败后已被暂时禁用。请检查其可用性,如果需要重新激活,请点击下方按钮。`,
|
|
873
|
+
failureCount: '失败次数',
|
|
874
|
+
failureUrl: '失败的 Webhook URL',
|
|
875
|
+
},
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
try {
|
|
879
|
+
const nodeInfo = await this.node.read();
|
|
880
|
+
// server 的 webhook 页面地址
|
|
881
|
+
const actionPath = '/settings/integration';
|
|
882
|
+
let _action =
|
|
883
|
+
process.env.NODE_ENV === 'production' ? joinURL(nodeInfo.routing.adminPath, actionPath) : actionPath;
|
|
884
|
+
const channels = [NOTIFICATION_SEND_CHANNEL.WALLET, NOTIFICATION_SEND_CHANNEL.PUSH];
|
|
885
|
+
if (teamDid && !this.teamManager.isNodeTeam(teamDid)) {
|
|
886
|
+
channels.push(NOTIFICATION_SEND_CHANNEL.EMAIL);
|
|
887
|
+
_action = action || `${WELLKNOWN_SERVICE_PATH_PREFIX}/user/settings`; // 默认返回用户中心页面设置页面
|
|
888
|
+
}
|
|
889
|
+
await this.teamManager.createNotification({
|
|
890
|
+
title: notification[locale].title,
|
|
891
|
+
description: notification[locale].description,
|
|
892
|
+
attachments: [
|
|
893
|
+
{
|
|
894
|
+
type: 'section',
|
|
895
|
+
fields: [
|
|
896
|
+
{
|
|
897
|
+
type: 'text',
|
|
898
|
+
data: {
|
|
899
|
+
type: 'plain',
|
|
900
|
+
color: '#9397A1',
|
|
901
|
+
text: notification[locale].failureUrl,
|
|
902
|
+
},
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
type: 'text',
|
|
906
|
+
data: {
|
|
907
|
+
type: 'plain',
|
|
908
|
+
color: '#9397A1',
|
|
909
|
+
text: webhook.url,
|
|
910
|
+
},
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
type: 'text',
|
|
914
|
+
data: {
|
|
915
|
+
type: 'plain',
|
|
916
|
+
color: '#9397A1',
|
|
917
|
+
text: notification[locale].failureCount,
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
type: 'text',
|
|
922
|
+
data: {
|
|
923
|
+
type: 'plain',
|
|
924
|
+
color: '#9397A1',
|
|
925
|
+
text: `${WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD}`,
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
],
|
|
929
|
+
},
|
|
930
|
+
],
|
|
931
|
+
source: 'system',
|
|
932
|
+
severity: 'warning',
|
|
933
|
+
action: _action,
|
|
934
|
+
channels,
|
|
935
|
+
...(teamDid ? { teamDid } : {}),
|
|
936
|
+
...(userDid ? { receiver: userDid } : {}),
|
|
937
|
+
});
|
|
938
|
+
} catch (error) {
|
|
939
|
+
logger.error('Failed to create webhook disabled notification', { teamDid, userDid, webhook, error });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
async updateWebHookState({ teamDid, userDids, webhook, enabled, consecutiveFailures }) {
|
|
944
|
+
const state = await this.getUserState(teamDid);
|
|
945
|
+
return Promise.all(
|
|
946
|
+
userDids.map(async (userDid) => {
|
|
947
|
+
const webhookParams =
|
|
948
|
+
consecutiveFailures === undefined
|
|
949
|
+
? omit(
|
|
950
|
+
{
|
|
951
|
+
...webhook,
|
|
952
|
+
enabled: enabled ?? webhook.enabled,
|
|
953
|
+
},
|
|
954
|
+
['consecutiveFailures']
|
|
955
|
+
)
|
|
956
|
+
: {
|
|
957
|
+
...webhook,
|
|
958
|
+
enabled: enabled ?? webhook.enabled,
|
|
959
|
+
consecutiveFailures,
|
|
960
|
+
};
|
|
961
|
+
await state.updateWebhook(userDid, webhookParams, (updated) => {
|
|
962
|
+
this.createWebhookDisabledNotification({ teamDid, userDid, webhook: updated });
|
|
963
|
+
});
|
|
964
|
+
})
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
|
|
860
968
|
async updateUser({ teamDid, user }) {
|
|
861
969
|
const state = await this.getUserState(teamDid);
|
|
862
970
|
const exist = await state.getUserByDid(user.did);
|
|
@@ -19,8 +19,8 @@ const validateBlockletStatus = (blocklet, appDid) => {
|
|
|
19
19
|
return true;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
const init = ({ states, teamManager }) => {
|
|
23
|
-
const { webhookEventQueue } = endpointQueueInit.init({ states, teamManager });
|
|
22
|
+
const init = ({ states, teamManager, teamAPI }) => {
|
|
23
|
+
const { webhookEventQueue } = endpointQueueInit.init({ states, teamManager, teamAPI });
|
|
24
24
|
|
|
25
25
|
const handleEventBusEvent = async (data) => {
|
|
26
26
|
logger.info('init webhook event bus');
|
|
@@ -20,7 +20,7 @@ const validatePaging = (paging = {}) => {
|
|
|
20
20
|
const getUserDid = (context) => get(context, 'user.did', '');
|
|
21
21
|
|
|
22
22
|
class WebhooksAPI extends EventEmitter {
|
|
23
|
-
constructor({ states, teamManager }) {
|
|
23
|
+
constructor({ states, teamManager, teamAPI }) {
|
|
24
24
|
super();
|
|
25
25
|
|
|
26
26
|
this.states = states;
|
|
@@ -28,7 +28,7 @@ class WebhooksAPI extends EventEmitter {
|
|
|
28
28
|
this.cache = new Map();
|
|
29
29
|
this.TTL = 5 * 60 * 1000;
|
|
30
30
|
|
|
31
|
-
const { webhookEndpointQueue } = endpointQueueInit.init({ states, teamManager });
|
|
31
|
+
const { webhookEndpointQueue } = endpointQueueInit.init({ states, teamManager, teamAPI });
|
|
32
32
|
this.webhookEndpointQueue = webhookEndpointQueue;
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD, WELLKNOWN_BLOCKLET_ADMIN_PATH } = require('@abtnode/constant');
|
|
1
2
|
const logger = require('@abtnode/logger')('@abtnode/core:webhook:queues');
|
|
2
3
|
const axios = require('@abtnode/util/lib/axios');
|
|
3
4
|
const { Op } = require('sequelize');
|
|
@@ -8,7 +9,42 @@ const { getWebhookJobId, STATUS } = require('./util');
|
|
|
8
9
|
const MAX_RETRY_COUNT = 2; // 重试一次
|
|
9
10
|
const AXIOS_TIMEOUT = 1000 * 60 * 3;
|
|
10
11
|
|
|
11
|
-
const createWebhookEndpointQueue = ({ states, teamManager }) => {
|
|
12
|
+
const createWebhookEndpointQueue = ({ states, teamManager, teamAPI }) => {
|
|
13
|
+
const updateWebhookEndpointState = async ({ webhook, teamDid, consecutiveFailures, webhookEndpointState }) => {
|
|
14
|
+
try {
|
|
15
|
+
if (!webhook.url) {
|
|
16
|
+
logger.warn('Webhook not found', { webhook });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const updates = {
|
|
21
|
+
metadata: {
|
|
22
|
+
...(webhook.metadata || {}),
|
|
23
|
+
consecutiveFailures,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
if (consecutiveFailures >= WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD) {
|
|
27
|
+
updates.status = 'disabled';
|
|
28
|
+
teamAPI
|
|
29
|
+
.createWebhookDisabledNotification({
|
|
30
|
+
teamDid,
|
|
31
|
+
webhook: {
|
|
32
|
+
url: webhook.url,
|
|
33
|
+
enabled: false,
|
|
34
|
+
consecutiveFailures,
|
|
35
|
+
},
|
|
36
|
+
action: `${WELLKNOWN_BLOCKLET_ADMIN_PATH}/integrations/webhooks`,
|
|
37
|
+
})
|
|
38
|
+
.catch((err) => {
|
|
39
|
+
logger.error('create webhook disabled notification error', err);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await webhookEndpointState.updateWebhook(webhook.id, updates);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
logger.error('update webhook endpoint state error', err, { consecutiveFailures, webhook });
|
|
46
|
+
}
|
|
47
|
+
};
|
|
12
48
|
// https://stripe.com/docs/webhooks
|
|
13
49
|
/**
|
|
14
50
|
*
|
|
@@ -52,7 +88,16 @@ const createWebhookEndpointQueue = ({ states, teamManager }) => {
|
|
|
52
88
|
|
|
53
89
|
try {
|
|
54
90
|
// verify similar to component call, but supports external urls
|
|
55
|
-
|
|
91
|
+
let response;
|
|
92
|
+
try {
|
|
93
|
+
response = await axios.post(webhook.url, event.request, { timeout: AXIOS_TIMEOUT });
|
|
94
|
+
updateWebhookEndpointState({ webhook, teamDid: job.appDid, consecutiveFailures: 0, webhookEndpointState });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// 需要更新 webhook 的 consecutiveFailures
|
|
97
|
+
const consecutiveFailures = (webhook.metadata?.consecutiveFailures || 0) + 1;
|
|
98
|
+
updateWebhookEndpointState({ webhook, teamDid: job.appDid, consecutiveFailures, webhookEndpointState });
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
56
101
|
|
|
57
102
|
if (job.attemptId) {
|
|
58
103
|
await webhookAttemptState.update(
|
|
@@ -242,8 +287,8 @@ const createWebhookEventQueue = ({ states, teamManager }, webhookEndpointQueue)
|
|
|
242
287
|
};
|
|
243
288
|
|
|
244
289
|
module.exports = {
|
|
245
|
-
init: ({ states, teamManager }) => {
|
|
246
|
-
const webhookEndpointQueue = createWebhookEndpointQueue({ states, teamManager });
|
|
290
|
+
init: ({ states, teamManager, teamAPI }) => {
|
|
291
|
+
const webhookEndpointQueue = createWebhookEndpointQueue({ states, teamManager, teamAPI });
|
|
247
292
|
const webhookEventQueue = createWebhookEventQueue({ states, teamManager }, webhookEndpointQueue);
|
|
248
293
|
|
|
249
294
|
return { webhookEndpointQueue, webhookEventQueue };
|
package/lib/event/index.js
CHANGED
|
@@ -723,7 +723,7 @@ module.exports = ({
|
|
|
723
723
|
events.handleBlockletEvent = handleBlockletEvent;
|
|
724
724
|
|
|
725
725
|
if (daemon) {
|
|
726
|
-
const { handleEventBusEvent } = eventBusHandler({ states, teamManager });
|
|
726
|
+
const { handleEventBusEvent } = eventBusHandler({ states, teamManager, teamAPI });
|
|
727
727
|
eventHub.on(EVENT_BUS_EVENT, (data) => {
|
|
728
728
|
if (isInstanceWorker()) {
|
|
729
729
|
return null;
|
package/lib/index.js
CHANGED
|
@@ -307,7 +307,7 @@ function ABTNode(options) {
|
|
|
307
307
|
});
|
|
308
308
|
blockletManager.setMaxListeners(0);
|
|
309
309
|
const securityAPI = new SecurityAPI({ teamManager, blockletManager });
|
|
310
|
-
const webhookAPI = new WebhookAPI({ states, teamManager,
|
|
310
|
+
const webhookAPI = new WebhookAPI({ states, teamManager, teamAPI });
|
|
311
311
|
|
|
312
312
|
const {
|
|
313
313
|
handleBlockletWafChange,
|
|
@@ -856,6 +856,7 @@ function ABTNode(options) {
|
|
|
856
856
|
updateOAuthClient: teamAPI.updateOAuthClient.bind(teamAPI),
|
|
857
857
|
|
|
858
858
|
updateBlockletSettings: blockletManager.updateBlockletSettings.bind(blockletManager),
|
|
859
|
+
createWebhookDisabledNotification: teamAPI.createWebhookDisabledNotification.bind(teamAPI),
|
|
859
860
|
|
|
860
861
|
// migrate audit log
|
|
861
862
|
migrateAuditLog: (dataDir) => {
|
|
@@ -886,7 +887,7 @@ function ABTNode(options) {
|
|
|
886
887
|
webhookManager: webhookAPI,
|
|
887
888
|
});
|
|
888
889
|
|
|
889
|
-
const webhook = WebHook({ events, dataDirs, instance
|
|
890
|
+
const webhook = WebHook({ events, dataDirs, instance });
|
|
890
891
|
|
|
891
892
|
const initCron = async () => {
|
|
892
893
|
if (isInstanceWorker()) {
|
|
@@ -980,6 +981,13 @@ function ABTNode(options) {
|
|
|
980
981
|
getWebHooks: states.webhook.list.bind(states.webhook),
|
|
981
982
|
createWebHook: webhook.create.bind(webhook),
|
|
982
983
|
deleteWebHook: webhook.delete.bind(webhook),
|
|
984
|
+
updateWebHookState: (params) => {
|
|
985
|
+
const { isService, webhook: webhookParams, ...rest } = params;
|
|
986
|
+
if (isService) {
|
|
987
|
+
return teamAPI.updateWebHookState.call(teamAPI, { webhook: webhookParams, ...rest });
|
|
988
|
+
}
|
|
989
|
+
return webhook.updateWebHookState.call(webhook, { id: webhookParams?.id, url: webhookParams?.url, ...rest });
|
|
990
|
+
},
|
|
983
991
|
getWebhookSenders: webhook.listSenders.bind(webhook),
|
|
984
992
|
getMessageSender: webhook.getMessageSender.bind(webhook),
|
|
985
993
|
sendTestMessage: webhook.sendTestMessage.bind(webhook),
|
package/lib/states/user.js
CHANGED
|
@@ -3,6 +3,7 @@ const pickBy = require('lodash/pickBy');
|
|
|
3
3
|
const get = require('lodash/get');
|
|
4
4
|
const pick = require('lodash/pick');
|
|
5
5
|
const uniq = require('lodash/uniq');
|
|
6
|
+
const isEqual = require('lodash/isEqual');
|
|
6
7
|
const { isValid, toAddress } = require('@arcblock/did');
|
|
7
8
|
const { PASSPORT_STATUS, USER_MAX_INVITE_DEPTH, ROLES } = require('@abtnode/constant');
|
|
8
9
|
const { BaseState } = require('@abtnode/models');
|
|
@@ -17,6 +18,7 @@ const { validateOwner } = require('../util');
|
|
|
17
18
|
const { loginSchema, disconnectAccountSchema } = require('../validators/user');
|
|
18
19
|
const ExtendBase = require('./base');
|
|
19
20
|
const { isInDashboard, isUserPrivacyEnabled, isAdminUser } = require('../util/verify-user-private');
|
|
21
|
+
const { updateWebhookFailureState } = require('../util/webhook');
|
|
20
22
|
|
|
21
23
|
const isNullOrUndefined = (x) => x === undefined || x === null;
|
|
22
24
|
|
|
@@ -1356,6 +1358,51 @@ SELECT did,inviter,generation FROM UserTree`.trim();
|
|
|
1356
1358
|
throw error;
|
|
1357
1359
|
}
|
|
1358
1360
|
}
|
|
1361
|
+
|
|
1362
|
+
async updateWebhook(userDid, webhook, createNotification = () => {}) {
|
|
1363
|
+
const user = await this.getUser(userDid);
|
|
1364
|
+
if (!user) {
|
|
1365
|
+
throw new CustomError(404, `User with did ${userDid} not found`);
|
|
1366
|
+
}
|
|
1367
|
+
if (!webhook || !webhook.url) {
|
|
1368
|
+
throw new CustomError(400, 'Invalid webhook');
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (
|
|
1372
|
+
webhook.consecutiveFailures !== undefined &&
|
|
1373
|
+
(!Number.isInteger(webhook.consecutiveFailures) || webhook.consecutiveFailures < 0)
|
|
1374
|
+
) {
|
|
1375
|
+
throw new CustomError(400, 'consecutiveFailures must be a non-negative integer.');
|
|
1376
|
+
}
|
|
1377
|
+
const existWebhooks = user.extra?.webhooks ?? [];
|
|
1378
|
+
|
|
1379
|
+
let hasChanged = false;
|
|
1380
|
+
const updatedWebhooks = existWebhooks.map((existing) => {
|
|
1381
|
+
if (existing.url === webhook.url) {
|
|
1382
|
+
const merged = { ...existing };
|
|
1383
|
+
// 使用抽离的核心逻辑更新失败状态
|
|
1384
|
+
updateWebhookFailureState(merged, webhook, createNotification);
|
|
1385
|
+
// Check if this specific webhook has changed
|
|
1386
|
+
if (!isEqual(existing, merged)) {
|
|
1387
|
+
hasChanged = true;
|
|
1388
|
+
return merged;
|
|
1389
|
+
}
|
|
1390
|
+
return existing;
|
|
1391
|
+
}
|
|
1392
|
+
return existing;
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
// Skip update if webhook data hasn't changed
|
|
1396
|
+
if (!hasChanged) {
|
|
1397
|
+
return user;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
const [updatedRows, [updatedUser]] = await this.update({ did: userDid }, { extra: { webhooks: updatedWebhooks } });
|
|
1401
|
+
if (updatedRows === 1) {
|
|
1402
|
+
return updatedUser;
|
|
1403
|
+
}
|
|
1404
|
+
return user;
|
|
1405
|
+
}
|
|
1359
1406
|
}
|
|
1360
1407
|
|
|
1361
1408
|
module.exports = User;
|
|
@@ -36,6 +36,7 @@ const updateWebhookEndpointSchema = Joi.object({
|
|
|
36
36
|
)
|
|
37
37
|
.optional(),
|
|
38
38
|
status: Joi.string().valid('enabled', 'disabled').optional(),
|
|
39
|
+
metadata: Joi.object().optional(),
|
|
39
40
|
});
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -64,6 +65,13 @@ class WebhookEndpointState extends BaseState {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
await updateWebhookEndpointSchema.validateAsync(updates, { stripUnknown: true });
|
|
68
|
+
if (updates.metadata && updates.status && updates.status !== 'enabled') {
|
|
69
|
+
updates.metadata = {
|
|
70
|
+
...(doc.metadata || {}),
|
|
71
|
+
...(updates.metadata || {}),
|
|
72
|
+
consecutiveFailures: 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
67
75
|
|
|
68
76
|
await this.update({ id }, { $set: updates });
|
|
69
77
|
return doc;
|
package/lib/states/webhook.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const logger = require('@abtnode/logger')('@abtnode/core:states:webhook');
|
|
2
|
-
|
|
2
|
+
const { CustomError } = require('@blocklet/error');
|
|
3
3
|
const BaseState = require('./base');
|
|
4
4
|
const { validateWebhook } = require('../validators/webhook');
|
|
5
|
+
const { updateWebhookFailureState } = require('../util/webhook');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @extends BaseState<import('@abtnode/models').WebHookState>
|
|
@@ -65,6 +66,69 @@ class WebhookState extends BaseState {
|
|
|
65
66
|
throw new Error(`webhook with id ${id} not exist`);
|
|
66
67
|
}
|
|
67
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 更新 webhook 状态(开启/关闭或连续失败次数)
|
|
72
|
+
* @param {string} id - webhook ID
|
|
73
|
+
* @param {object} options - 更新选项
|
|
74
|
+
* @param {string} [options.url] - 要更新的 URL,不传则更新所有 URL 类型的 param
|
|
75
|
+
* @param {boolean} [options.enabled] - 开启/关闭状态
|
|
76
|
+
* @param {number} [options.consecutiveFailures] - 连续失败次数,不传则默认+1,传入必须大于原有值或为0
|
|
77
|
+
* @throws {Error} 当同时提供 enabled 和 consecutiveFailures 时抛出错误
|
|
78
|
+
* @throws {Error} 当未提供任何更新参数时抛出错误
|
|
79
|
+
* @throws {Error} 当 consecutiveFailures 值无效时抛出错误
|
|
80
|
+
* @throws {Error} 当指定的 url 不存在时抛出错误
|
|
81
|
+
*/
|
|
82
|
+
async updateWebhook(id, { url, enabled, consecutiveFailures } = {}, createNotification = () => {}) {
|
|
83
|
+
// 验证不能同时设置开启/关闭和连续失败次数
|
|
84
|
+
if (enabled !== undefined && consecutiveFailures !== undefined) {
|
|
85
|
+
throw new CustomError(400, 'Cannot update enabled and consecutiveFailures at the same time.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 验证至少提供一个更新参数
|
|
89
|
+
if (enabled === undefined && consecutiveFailures === undefined) {
|
|
90
|
+
throw new CustomError(400, 'Must provide either enabled or consecutiveFailures to update.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 获取当前 webhook 状态
|
|
94
|
+
const currentWebhook = await this.findOne(id);
|
|
95
|
+
|
|
96
|
+
if (!currentWebhook) {
|
|
97
|
+
throw new CustomError(404, `webhook with id ${id} not exist`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 确定匹配函数
|
|
101
|
+
const matcher = url ? (item) => item.value === url : (item) => item.name === 'url' && item.value;
|
|
102
|
+
|
|
103
|
+
// 验证是否有匹配的项
|
|
104
|
+
const hasMatchingItems = currentWebhook.params.some(matcher);
|
|
105
|
+
if (!hasMatchingItems) {
|
|
106
|
+
if (url) {
|
|
107
|
+
throw new CustomError(404, `URL ${url} not found in webhook params.`);
|
|
108
|
+
}
|
|
109
|
+
throw new CustomError(400, 'No valid URL params found to update.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 遍历并更新匹配的项(只复制需要修改的项)
|
|
113
|
+
const updatedParams = currentWebhook.params.map((item) => {
|
|
114
|
+
if (matcher(item)) {
|
|
115
|
+
const cloned = { ...item };
|
|
116
|
+
// 使用抽离的核心逻辑更新失败状态(webhook.js 需要验证递增)
|
|
117
|
+
updateWebhookFailureState(cloned, { enabled, consecutiveFailures }, createNotification, {
|
|
118
|
+
validateIncremental: true,
|
|
119
|
+
});
|
|
120
|
+
return cloned;
|
|
121
|
+
}
|
|
122
|
+
return item;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// 更新整个 params 数组
|
|
126
|
+
const [updatedRows, [updatedWebhook]] = await this.update({ id }, { params: updatedParams });
|
|
127
|
+
if (updatedRows === 1) {
|
|
128
|
+
return updatedWebhook;
|
|
129
|
+
}
|
|
130
|
+
return currentWebhook;
|
|
131
|
+
}
|
|
68
132
|
}
|
|
69
133
|
|
|
70
134
|
module.exports = WebhookState;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const { CustomError } = require('@blocklet/error');
|
|
2
|
+
const { WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD } = require('@abtnode/constant');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 更新 webhook 项的失败状态(直接修改传入的对象)
|
|
6
|
+
* @param {Object} item - webhook 对象(会被直接修改)
|
|
7
|
+
* @param {Object} updates - 更新参数
|
|
8
|
+
* @param {boolean} [updates.enabled] - 新的启用状态
|
|
9
|
+
* @param {number} [updates.consecutiveFailures] - 新的连续失败次数(如果不传则自动 +1)
|
|
10
|
+
* @param {Function} [createNotification] - 达到阈值时的通知回调
|
|
11
|
+
* @param {Object} [options] - 额外选项
|
|
12
|
+
* @param {boolean} [options.validateIncremental] - 是否验证 consecutiveFailures 必须递增(默认 false)
|
|
13
|
+
*/
|
|
14
|
+
function updateWebhookFailureState(item, updates, createNotification, options = {}) {
|
|
15
|
+
const { enabled, consecutiveFailures } = updates;
|
|
16
|
+
const { validateIncremental = false } = options;
|
|
17
|
+
|
|
18
|
+
// 处理 enabled 更新
|
|
19
|
+
if (enabled !== undefined) {
|
|
20
|
+
item.enabled = enabled;
|
|
21
|
+
// 如果关闭 webhook,自动将连续失败次数重置为 0
|
|
22
|
+
if (enabled === false) {
|
|
23
|
+
item.consecutiveFailures = 0;
|
|
24
|
+
}
|
|
25
|
+
return item;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 处理 consecutiveFailures 更新
|
|
29
|
+
const currentFailures = item.consecutiveFailures || 0;
|
|
30
|
+
|
|
31
|
+
if (consecutiveFailures === undefined) {
|
|
32
|
+
// 未传入值,默认 +1
|
|
33
|
+
// 这么做的原因是由于,在队列中拿到的值不是最新的值,在外面进行 + 1 操作是不准确的
|
|
34
|
+
item.consecutiveFailures = currentFailures + 1;
|
|
35
|
+
} else {
|
|
36
|
+
// 验证必须是非负整数
|
|
37
|
+
if (!Number.isInteger(consecutiveFailures) || consecutiveFailures < 0) {
|
|
38
|
+
throw new CustomError(400, 'consecutiveFailures must be a non-negative integer.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 如果需要验证递增
|
|
42
|
+
if (validateIncremental && consecutiveFailures !== 0 && consecutiveFailures <= currentFailures) {
|
|
43
|
+
throw new CustomError(
|
|
44
|
+
400,
|
|
45
|
+
`consecutiveFailures must be greater than the current value (${currentFailures}) or equal to 0.`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
item.consecutiveFailures = consecutiveFailures;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 达到阈值则自动禁用并触发通知
|
|
53
|
+
if (item.consecutiveFailures >= WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD) {
|
|
54
|
+
item.enabled = false;
|
|
55
|
+
item.consecutiveFailures = 0;
|
|
56
|
+
if (createNotification) {
|
|
57
|
+
createNotification(item);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return item;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
updateWebhookFailureState,
|
|
65
|
+
};
|
package/lib/webhook/index.js
CHANGED
|
@@ -77,10 +77,56 @@ const getSlackUrlInfo = async ({ blockletUrl, path: actionPath = '/notifications
|
|
|
77
77
|
};
|
|
78
78
|
|
|
79
79
|
// eslint-disable-next-line no-unused-vars
|
|
80
|
-
module.exports = ({ events, dataDirs, instance
|
|
80
|
+
module.exports = ({ events, dataDirs, instance }) => {
|
|
81
81
|
const nodeState = states.node;
|
|
82
82
|
const webhookState = states.webhook;
|
|
83
83
|
|
|
84
|
+
const updateWebHookState = async ({ id, url, enabled, consecutiveFailures }) => {
|
|
85
|
+
try {
|
|
86
|
+
const updateRes = await webhookState.updateWebhook(
|
|
87
|
+
id,
|
|
88
|
+
{
|
|
89
|
+
url,
|
|
90
|
+
enabled,
|
|
91
|
+
consecutiveFailures,
|
|
92
|
+
},
|
|
93
|
+
(updated) => {
|
|
94
|
+
instance.createWebhookDisabledNotification({ webhook: updated });
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
return updateRes;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
logger.error('update webhook state error', { err });
|
|
100
|
+
throw new Error(err.message);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 安全地更新 webhook 状态,不影响主流程
|
|
106
|
+
* @param {object} item - webhook item
|
|
107
|
+
* @param {number} consecutiveFailures - 连续失败次数
|
|
108
|
+
*/
|
|
109
|
+
const safeUpdateWebhookState = (item, consecutiveFailures) => {
|
|
110
|
+
// 只处理需要跟踪状态的类型
|
|
111
|
+
if (item.type !== 'slack' && item.type !== 'api') {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const urlParam = item.params.find((x) => x.name === 'url');
|
|
116
|
+
if (!urlParam || !urlParam.value) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 异步更新,不等待结果,不影响主流程
|
|
121
|
+
updateWebHookState({
|
|
122
|
+
id: item.id,
|
|
123
|
+
url: urlParam.value,
|
|
124
|
+
consecutiveFailures,
|
|
125
|
+
}).catch((err) => {
|
|
126
|
+
logger.debug('safe update webhook state failed', { err, itemId: item.id, url: urlParam.value });
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
|
|
84
130
|
const sendMessage = async ({ extra, isService, ...message } = {}) => {
|
|
85
131
|
try {
|
|
86
132
|
const webhookList = !isService ? await webhookState.list() : [];
|
|
@@ -97,6 +143,20 @@ module.exports = ({ events, dataDirs, instance, teamManager }) => {
|
|
|
97
143
|
if (webhookList.length) {
|
|
98
144
|
for (let i = 0; i < webhookList.length; i++) {
|
|
99
145
|
const item = webhookList[i];
|
|
146
|
+
// 对于 slack 和 api 类型,检查是否启用
|
|
147
|
+
if (item.type === 'slack' || item.type === 'api') {
|
|
148
|
+
const urlParam = item.params.find((x) => x.name === 'url');
|
|
149
|
+
if (!urlParam || !urlParam.value) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
// 如果没有开启,则跳过发送
|
|
153
|
+
const isEnabled = urlParam.enabled ?? true;
|
|
154
|
+
if (!isEnabled) {
|
|
155
|
+
logger.debug('webhook is disabled, skip sending', { type: item.type, url: urlParam.value });
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
100
160
|
if (typeof senderFns[item.type] !== 'function') {
|
|
101
161
|
const senderInstance = WebHookSender.getMessageSender(item.type);
|
|
102
162
|
senderFns[item.type] = senderInstance.send.bind(senderInstance);
|
|
@@ -134,8 +194,13 @@ module.exports = ({ events, dataDirs, instance, teamManager }) => {
|
|
|
134
194
|
try {
|
|
135
195
|
// eslint-disable-next-line
|
|
136
196
|
await senderFns[item.type](item.params, options);
|
|
197
|
+
// 发送成功,重置失败次数
|
|
198
|
+
safeUpdateWebhookState(item, 0);
|
|
137
199
|
} catch (err) {
|
|
138
200
|
logger.error('webhook sender error', { error: err, item });
|
|
201
|
+
// 发送失败,增加失败次数
|
|
202
|
+
const currentFailures = item.consecutiveFailures || 0;
|
|
203
|
+
safeUpdateWebhookState(item, currentFailures + 1);
|
|
139
204
|
}
|
|
140
205
|
}
|
|
141
206
|
}
|
|
@@ -269,6 +334,7 @@ module.exports = ({ events, dataDirs, instance, teamManager }) => {
|
|
|
269
334
|
getMessageSender: WebHookSender.getMessageSender,
|
|
270
335
|
create: createWebhook,
|
|
271
336
|
delete: deleteWebhook,
|
|
337
|
+
updateWebHookState,
|
|
272
338
|
sendTestMessage,
|
|
273
339
|
};
|
|
274
340
|
};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.17.4-beta-20251202-
|
|
6
|
+
"version": "1.17.4-beta-20251202-122551-267b614d",
|
|
7
7
|
"description": "",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -17,21 +17,21 @@
|
|
|
17
17
|
"author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
|
|
18
18
|
"license": "Apache-2.0",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@abtnode/analytics": "1.17.4-beta-20251202-
|
|
21
|
-
"@abtnode/auth": "1.17.4-beta-20251202-
|
|
22
|
-
"@abtnode/certificate-manager": "1.17.4-beta-20251202-
|
|
23
|
-
"@abtnode/constant": "1.17.4-beta-20251202-
|
|
24
|
-
"@abtnode/cron": "1.17.4-beta-20251202-
|
|
25
|
-
"@abtnode/db-cache": "1.17.4-beta-20251202-
|
|
26
|
-
"@abtnode/docker-utils": "1.17.4-beta-20251202-
|
|
27
|
-
"@abtnode/logger": "1.17.4-beta-20251202-
|
|
28
|
-
"@abtnode/models": "1.17.4-beta-20251202-
|
|
29
|
-
"@abtnode/queue": "1.17.4-beta-20251202-
|
|
30
|
-
"@abtnode/rbac": "1.17.4-beta-20251202-
|
|
31
|
-
"@abtnode/router-provider": "1.17.4-beta-20251202-
|
|
32
|
-
"@abtnode/static-server": "1.17.4-beta-20251202-
|
|
33
|
-
"@abtnode/timemachine": "1.17.4-beta-20251202-
|
|
34
|
-
"@abtnode/util": "1.17.4-beta-20251202-
|
|
20
|
+
"@abtnode/analytics": "1.17.4-beta-20251202-122551-267b614d",
|
|
21
|
+
"@abtnode/auth": "1.17.4-beta-20251202-122551-267b614d",
|
|
22
|
+
"@abtnode/certificate-manager": "1.17.4-beta-20251202-122551-267b614d",
|
|
23
|
+
"@abtnode/constant": "1.17.4-beta-20251202-122551-267b614d",
|
|
24
|
+
"@abtnode/cron": "1.17.4-beta-20251202-122551-267b614d",
|
|
25
|
+
"@abtnode/db-cache": "1.17.4-beta-20251202-122551-267b614d",
|
|
26
|
+
"@abtnode/docker-utils": "1.17.4-beta-20251202-122551-267b614d",
|
|
27
|
+
"@abtnode/logger": "1.17.4-beta-20251202-122551-267b614d",
|
|
28
|
+
"@abtnode/models": "1.17.4-beta-20251202-122551-267b614d",
|
|
29
|
+
"@abtnode/queue": "1.17.4-beta-20251202-122551-267b614d",
|
|
30
|
+
"@abtnode/rbac": "1.17.4-beta-20251202-122551-267b614d",
|
|
31
|
+
"@abtnode/router-provider": "1.17.4-beta-20251202-122551-267b614d",
|
|
32
|
+
"@abtnode/static-server": "1.17.4-beta-20251202-122551-267b614d",
|
|
33
|
+
"@abtnode/timemachine": "1.17.4-beta-20251202-122551-267b614d",
|
|
34
|
+
"@abtnode/util": "1.17.4-beta-20251202-122551-267b614d",
|
|
35
35
|
"@aigne/aigne-hub": "^0.10.10",
|
|
36
36
|
"@arcblock/did": "^1.27.12",
|
|
37
37
|
"@arcblock/did-connect-js": "^1.27.12",
|
|
@@ -43,15 +43,15 @@
|
|
|
43
43
|
"@arcblock/pm2-events": "^0.0.5",
|
|
44
44
|
"@arcblock/validator": "^1.27.12",
|
|
45
45
|
"@arcblock/vc": "^1.27.12",
|
|
46
|
-
"@blocklet/constant": "1.17.4-beta-20251202-
|
|
46
|
+
"@blocklet/constant": "1.17.4-beta-20251202-122551-267b614d",
|
|
47
47
|
"@blocklet/did-space-js": "^1.2.6",
|
|
48
|
-
"@blocklet/env": "1.17.4-beta-20251202-
|
|
48
|
+
"@blocklet/env": "1.17.4-beta-20251202-122551-267b614d",
|
|
49
49
|
"@blocklet/error": "^0.3.3",
|
|
50
|
-
"@blocklet/meta": "1.17.4-beta-20251202-
|
|
51
|
-
"@blocklet/resolver": "1.17.4-beta-20251202-
|
|
52
|
-
"@blocklet/sdk": "1.17.4-beta-20251202-
|
|
53
|
-
"@blocklet/server-js": "1.17.4-beta-20251202-
|
|
54
|
-
"@blocklet/store": "1.17.4-beta-20251202-
|
|
50
|
+
"@blocklet/meta": "1.17.4-beta-20251202-122551-267b614d",
|
|
51
|
+
"@blocklet/resolver": "1.17.4-beta-20251202-122551-267b614d",
|
|
52
|
+
"@blocklet/sdk": "1.17.4-beta-20251202-122551-267b614d",
|
|
53
|
+
"@blocklet/server-js": "1.17.4-beta-20251202-122551-267b614d",
|
|
54
|
+
"@blocklet/store": "1.17.4-beta-20251202-122551-267b614d",
|
|
55
55
|
"@blocklet/theme": "^3.2.11",
|
|
56
56
|
"@fidm/x509": "^1.2.1",
|
|
57
57
|
"@ocap/mcrypto": "^1.27.12",
|
|
@@ -116,5 +116,5 @@
|
|
|
116
116
|
"express": "^4.18.2",
|
|
117
117
|
"unzipper": "^0.10.11"
|
|
118
118
|
},
|
|
119
|
-
"gitHead": "
|
|
119
|
+
"gitHead": "58a5f7d49ccce973fdb214337aee262435704ec5"
|
|
120
120
|
}
|