@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 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);
@@ -944,6 +944,7 @@ module.exports = Object.freeze({
944
944
  'status',
945
945
  ],
946
946
  USER_SESSION_STATUS,
947
+ WEBHOOK_CONSECUTIVE_FAILURES_THRESHOLD: 50,
947
948
  });
948
949
 
949
950
 
@@ -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
- const response = await axios.post(webhook.url, event.request, { timeout: AXIOS_TIMEOUT });
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 };
@@ -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, blockletManager });
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, teamManager });
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),
@@ -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;
@@ -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
+ };
@@ -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, teamManager }) => {
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
  };
@@ -63,6 +63,8 @@ APISender.describe = () => ({
63
63
  validate: (x) => !!x,
64
64
  value: '',
65
65
  type: 'url',
66
+ enabled: true,
67
+ consecutiveFailures: 0,
66
68
  schema: Joi.string()
67
69
  .required()
68
70
  .uri({ scheme: [/https?/] }),
@@ -187,6 +187,8 @@ SlackSender.describe = () => ({
187
187
  defaultValue: '',
188
188
  value: '',
189
189
  type: 'slack',
190
+ enabled: true,
191
+ consecutiveFailures: 0,
190
192
  schema: Joi.string()
191
193
  .required()
192
194
  .custom((v, helper) => {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.17.4-beta-20251202-034514-637cd8e2",
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-034514-637cd8e2",
21
- "@abtnode/auth": "1.17.4-beta-20251202-034514-637cd8e2",
22
- "@abtnode/certificate-manager": "1.17.4-beta-20251202-034514-637cd8e2",
23
- "@abtnode/constant": "1.17.4-beta-20251202-034514-637cd8e2",
24
- "@abtnode/cron": "1.17.4-beta-20251202-034514-637cd8e2",
25
- "@abtnode/db-cache": "1.17.4-beta-20251202-034514-637cd8e2",
26
- "@abtnode/docker-utils": "1.17.4-beta-20251202-034514-637cd8e2",
27
- "@abtnode/logger": "1.17.4-beta-20251202-034514-637cd8e2",
28
- "@abtnode/models": "1.17.4-beta-20251202-034514-637cd8e2",
29
- "@abtnode/queue": "1.17.4-beta-20251202-034514-637cd8e2",
30
- "@abtnode/rbac": "1.17.4-beta-20251202-034514-637cd8e2",
31
- "@abtnode/router-provider": "1.17.4-beta-20251202-034514-637cd8e2",
32
- "@abtnode/static-server": "1.17.4-beta-20251202-034514-637cd8e2",
33
- "@abtnode/timemachine": "1.17.4-beta-20251202-034514-637cd8e2",
34
- "@abtnode/util": "1.17.4-beta-20251202-034514-637cd8e2",
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-034514-637cd8e2",
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-034514-637cd8e2",
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-034514-637cd8e2",
51
- "@blocklet/resolver": "1.17.4-beta-20251202-034514-637cd8e2",
52
- "@blocklet/sdk": "1.17.4-beta-20251202-034514-637cd8e2",
53
- "@blocklet/server-js": "1.17.4-beta-20251202-034514-637cd8e2",
54
- "@blocklet/store": "1.17.4-beta-20251202-034514-637cd8e2",
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": "d0e748c5cae1a08c63cfc5bb59a6d471c3120e7a"
119
+ "gitHead": "58a5f7d49ccce973fdb214337aee262435704ec5"
120
120
  }