@abtnode/core 1.17.4-beta-20251201-225048-b1682a09 → 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);
@@ -2121,10 +2229,6 @@ class TeamAPI extends EventEmitter {
2121
2229
  return this.teamManager.updateStoreList(teamDid, storeList);
2122
2230
  }
2123
2231
 
2124
- getNotificationReceivers(payload) {
2125
- return this.teamManager.getNotificationReceivers(payload);
2126
- }
2127
-
2128
2232
  createNotification = (payload) => {
2129
2233
  return this.teamManager.createNotification(payload);
2130
2234
  };
@@ -5148,15 +5148,6 @@ class DiskBlockletManager extends BaseBlockletManager {
5148
5148
  await forEachBlocklet(blocklet, hookFn, { parallel: true, concurrencyLimit: 4 });
5149
5149
  }
5150
5150
 
5151
- async getReceiverDids(teamDid) {
5152
- const receivers = await this.teamAPI.getNotificationReceivers({
5153
- teamDid,
5154
- roles: [ROLES.ADMIN, ROLES.OWNER],
5155
- selection: { did: 1 },
5156
- });
5157
- return receivers.map((r) => r.did);
5158
- }
5159
-
5160
5151
  // 目前:Blocklet 的安装消息都会聚合在这里。
5161
5152
  async _createNotification(did, notification, { skipGetBlocklet = false } = {}) {
5162
5153
  if (isCLI()) {
@@ -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,
@@ -653,8 +653,6 @@ function ABTNode(options) {
653
653
  disconnectFromEndpoint: blockletManager.disconnectFromEndpoint.bind(blockletManager),
654
654
  publishToEndpoint: blockletManager.publishToEndpoint.bind(blockletManager),
655
655
 
656
- getNotificationReceivers: teamAPI.getNotificationReceivers.bind(teamAPI),
657
-
658
656
  // ServerBlocklet Notifications
659
657
  createNotification: teamAPI.createNotification.bind(teamAPI),
660
658
  getNotifications: teamAPI.getNotification.bind(teamAPI),
@@ -858,6 +856,7 @@ function ABTNode(options) {
858
856
  updateOAuthClient: teamAPI.updateOAuthClient.bind(teamAPI),
859
857
 
860
858
  updateBlockletSettings: blockletManager.updateBlockletSettings.bind(blockletManager),
859
+ createWebhookDisabledNotification: teamAPI.createWebhookDisabledNotification.bind(teamAPI),
861
860
 
862
861
  // migrate audit log
863
862
  migrateAuditLog: (dataDir) => {
@@ -888,7 +887,7 @@ function ABTNode(options) {
888
887
  webhookManager: webhookAPI,
889
888
  });
890
889
 
891
- const webhook = WebHook({ events, dataDirs, instance, teamManager });
890
+ const webhook = WebHook({ events, dataDirs, instance });
892
891
 
893
892
  const initCron = async () => {
894
893
  if (isInstanceWorker()) {
@@ -982,6 +981,13 @@ function ABTNode(options) {
982
981
  getWebHooks: states.webhook.list.bind(states.webhook),
983
982
  createWebHook: webhook.create.bind(webhook),
984
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
+ },
985
991
  getWebhookSenders: webhook.listSenders.bind(webhook),
986
992
  getMessageSender: webhook.getMessageSender.bind(webhook),
987
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
 
@@ -454,7 +456,7 @@ SELECT did,inviter,generation FROM UserTree`.trim();
454
456
  }
455
457
 
456
458
  // eslint-disable-next-line require-await
457
- async getUsersByDids({ dids, query }, context = {}) {
459
+ async getUsersByDids({ dids, query, paging }, context = {}) {
458
460
  const {
459
461
  approved,
460
462
  includeTags = false,
@@ -485,6 +487,20 @@ SELECT did,inviter,generation FROM UserTree`.trim();
485
487
  include.push(this.getConnectedInclude());
486
488
  }
487
489
 
490
+ // 如果提供了分页参数,使用分页查询
491
+ if (paging) {
492
+ const sorting = [['createdAt', 'DESC']];
493
+ selection.createdAt = 1;
494
+ const result = await this.paginate({ where: condition, include }, sorting, paging, selection);
495
+
496
+ if (includeFollowStatus && user?.did) {
497
+ result.list = await this._enrichWithFollowStatus(result.list, user.did, dids);
498
+ }
499
+
500
+ return result;
501
+ }
502
+
503
+ // 否则返回所有结果
488
504
  let result = await this.find({ where: condition, include }, selection);
489
505
 
490
506
  if (includeFollowStatus && user?.did) {
@@ -540,27 +556,39 @@ SELECT did,inviter,generation FROM UserTree`.trim();
540
556
  * get user by did
541
557
  * @param {string} did user's did
542
558
  */
543
- async getUser(did, { enableConnectedAccount = false, includeTags = false } = {}) {
544
- let user = await this.findOne({ where: { did }, include: includeTags ? [this.getTagInclude()] : [] });
545
- let connectedAccounts = [];
546
- let passports = [];
559
+ async getUser(
560
+ did,
561
+ {
562
+ enableConnectedAccount = false,
563
+ includeTags = false,
564
+ includePassports = true,
565
+ includeConnectedAccounts = true,
566
+ selection = {},
567
+ } = {}
568
+ ) {
569
+ let user = await this.findOne({ where: { did }, include: includeTags ? [this.getTagInclude()] : [] }, selection);
547
570
 
548
571
  // search in connected accounts
549
572
  if (!user && enableConnectedAccount) {
550
573
  const connectedAccount = await this.connectedAccount.findOne({ did });
551
574
  if (connectedAccount) {
552
- user = await this.findOne({ did: connectedAccount.userDid });
575
+ user = await this.findOne({ did: connectedAccount.userDid }, selection);
553
576
  }
554
577
  }
555
578
 
556
579
  if (user) {
557
- [connectedAccounts, passports] = await Promise.all([
558
- this.connectedAccount.find({ userDid: user.did }),
559
- this.passport.find({ userDid: user.did }),
560
- ]);
561
-
562
- user.connectedAccounts = connectedAccounts;
563
- user.passports = passports;
580
+ if (includeConnectedAccounts && includePassports) {
581
+ const [connectedAccounts, passports] = await Promise.all([
582
+ this.connectedAccount.find({ userDid: user.did }),
583
+ this.passport.find({ userDid: user.did }),
584
+ ]);
585
+ user.connectedAccounts = connectedAccounts;
586
+ user.passports = passports;
587
+ } else if (includeConnectedAccounts) {
588
+ user.connectedAccounts = await this.connectedAccount.find({ userDid: user.did });
589
+ } else if (includePassports) {
590
+ user.passports = await this.passport.find({ userDid: user.did });
591
+ }
564
592
  }
565
593
 
566
594
  return user;
@@ -1330,6 +1358,51 @@ SELECT did,inviter,generation FROM UserTree`.trim();
1330
1358
  throw error;
1331
1359
  }
1332
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
+ }
1333
1406
  }
1334
1407
 
1335
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;
@@ -224,16 +224,38 @@ class TeamManager extends EventEmitter {
224
224
  /**
225
225
  * 判断 passport 是否已经过期
226
226
  */
227
- isPassportExpired(passport) {
228
- return passport.status !== 'valid' || (passport.expirationDate && dayjs(passport.expirationDate).isBefore(dayjs()));
227
+ isPassportExpired(passport, now) {
228
+ return passport.status !== 'valid' || (passport.expirationDate && dayjs(passport.expirationDate).isBefore(now));
229
+ }
230
+
231
+ /**
232
+ * 验证用户是否有效(是否存在、passport是否过期)
233
+ * @private
234
+ */
235
+ _filterValidUsers(users) {
236
+ const now = dayjs();
237
+ return users.filter((user) => {
238
+ if (!user) {
239
+ return false;
240
+ }
241
+ const { passports = [] } = user;
242
+ // 如果用户没有 passport,或者 有一个 passport 没有过期,则返回 true
243
+ if (!passports?.length || passports.some((x) => !this.isPassportExpired(x, now))) {
244
+ return true;
245
+ }
246
+ logger.warn(`user's passports are all expired: ${user.did}`);
247
+ return false;
248
+ });
229
249
  }
230
250
 
231
251
  async getNotificationReceivers(payload) {
232
- const { teamDid, userDids = [], roles = [], selection = {}, includeConnectedAccounts = false } = payload;
252
+ const { teamDid, userDids = [], roles = [], selection = {}, includeConnectedAccounts = false, paging } = payload;
233
253
  // 会根据 teamDid 返回对应 state
234
254
  const userState = await this.getUserState(teamDid);
235
255
  if (!userDids.length && !roles.length) {
236
- return [];
256
+ return paging
257
+ ? { list: [], paging: { total: 0, pageSize: paging.size || 20, pageCount: 0, page: paging.page || 1 } }
258
+ : [];
237
259
  }
238
260
  try {
239
261
  // 这些用户信息已经是 approved 的用户,不需要再次确认
@@ -245,7 +267,7 @@ class TeamManager extends EventEmitter {
245
267
  if (userDids.length > 0) {
246
268
  let queryUsers = [];
247
269
  if (userDids.includes('*')) {
248
- queryUsers = await userState.getUsersByDids({
270
+ const result = await userState.getUsersByDids({
249
271
  dids: userDids,
250
272
  query: {
251
273
  approved: true,
@@ -253,45 +275,57 @@ class TeamManager extends EventEmitter {
253
275
  includeConnectedAccounts,
254
276
  includePassports: true,
255
277
  },
278
+ paging,
256
279
  });
280
+
281
+ // 如果是分页查询,处理分页结果
282
+ if (paging && result.list) {
283
+ const validUsers = this._filterValidUsers(result.list);
284
+ return {
285
+ list: users.concat(validUsers),
286
+ paging: result.paging,
287
+ };
288
+ }
289
+
290
+ queryUsers = result;
257
291
  } else {
258
292
  // 使用 getUser 查询的目的是为了避免传入的 receiver 不在user表中而存在于 connected_account 表中
259
293
  queryUsers = await Promise.all(
260
- userDids.map((did) => userState.getUser(did, { includePassports: true, enableConnectedAccount: true }))
294
+ userDids.map((did) =>
295
+ userState.getUser(did, {
296
+ enableConnectedAccount: true,
297
+ includeConnectedAccounts: false,
298
+ selection,
299
+ })
300
+ )
261
301
  );
262
302
  }
263
303
 
264
- const validUsers = queryUsers.filter((user, index) => {
265
- if (!user) {
266
- logger.warn('receiver is not exist: ', { userDid: userDids[index] });
267
- return false;
268
- }
269
- const { passports = [] } = user;
270
- // 如果用户没有 passport,或者 有一个 passport 没有过期,则返回 true
271
- if (!passports?.length || passports.some((x) => !this.isPassportExpired(x))) {
272
- return true;
273
- }
274
- logger.warn(`user's passports are all expired: ${user.did}`);
275
- return false;
276
- });
277
-
304
+ const validUsers = this._filterValidUsers(queryUsers);
278
305
  users = users.concat(validUsers);
279
306
  }
280
307
 
281
308
  return users;
282
309
  } catch (error) {
283
310
  logger.error('get receivers failed: ', error);
284
- return [];
311
+ return paging
312
+ ? { list: [], paging: { total: 0, pageSize: paging.size || 20, pageCount: 0, page: paging.page || 1 } }
313
+ : [];
285
314
  }
286
315
  }
287
316
 
288
317
  async getAdminReceivers({ teamDid }) {
289
- const receivers = await this.getNotificationReceivers({
290
- teamDid: teamDid ?? this.nodeDid,
291
- roles: [ROLES.ADMIN, ROLES.OWNER],
292
- selection: { did: 1 },
293
- });
294
- return receivers.map((r) => r.did);
318
+ try {
319
+ const receivers = await this.getNotificationReceivers({
320
+ teamDid: teamDid ?? this.nodeDid,
321
+ roles: [ROLES.ADMIN, ROLES.OWNER],
322
+ selection: { did: 1 },
323
+ });
324
+ return receivers.map((r) => r.did);
325
+ } catch (error) {
326
+ logger.error('get admin receivers failed', { error });
327
+ return [];
328
+ }
295
329
  }
296
330
 
297
331
  /**
@@ -351,30 +385,65 @@ class TeamManager extends EventEmitter {
351
385
  }
352
386
 
353
387
  /**
354
- * 获取接收者列表
388
+ * 获取接收者列表(支持分页)
355
389
  * @private
390
+ * @param {string} teamDid - 团队 DID
391
+ * @param {string|string[]} receiver - 接收者
392
+ * @param {boolean} isExist - 通知是否已存在
393
+ * @returns {AsyncGenerator<string[]>} 异步生成器,每次返回一页接收者的 DID 列表
356
394
  */
357
- async _getReceiverList(teamDid, receiver, isExist) {
395
+ async *_getReceiverList(teamDid, receiver, isExist) {
396
+ // 1. 如果通知已存在,直接返回规范化后的接收者
397
+ // 这种情况 receiver 一定存在。
358
398
  if (isExist) {
359
- return this._normalizeReceiver(receiver);
399
+ yield this._normalizeReceiver(receiver);
400
+ return;
360
401
  }
361
402
 
362
403
  const receivers = this._normalizeReceiver(receiver);
363
- if (receivers) {
404
+
405
+ // 2. 如果传入的是具体的 DID 数组(不包含 *),不需要分页查询,直接拿来用
406
+ if (receivers && !receivers.includes('*')) {
407
+ yield receivers;
408
+ return;
409
+ }
410
+
411
+ // 3. 如果传入 receiver 包含 * 需要分页查询,返回一页的数据,开始执行一页
412
+ if (receivers && receivers.includes('*')) {
364
413
  try {
365
- const users = await this.getNotificationReceivers({
366
- teamDid,
367
- userDids: receivers,
368
- includeConnectedAccounts: true,
369
- });
370
- return users.map((u) => u.did);
414
+ const pageSize = 100; // 每页查询 100 个用户
415
+ let currentPage = 1;
416
+ let hasMore = true;
417
+
418
+ // eslint-disable-next-line no-await-in-loop
419
+ while (hasMore) {
420
+ // eslint-disable-next-line no-await-in-loop
421
+ const result = await this.getNotificationReceivers({
422
+ teamDid,
423
+ userDids: receivers,
424
+ selection: { did: 1 },
425
+ paging: { page: currentPage, pageSize },
426
+ });
427
+
428
+ const userDids = result.list.map((u) => u.did);
429
+
430
+ if (userDids.length > 0) {
431
+ yield userDids;
432
+ }
433
+
434
+ hasMore = currentPage < result.paging.pageCount;
435
+ currentPage += 1;
436
+ }
371
437
  } catch (error) {
372
- logger.error('get receivers failed', { error });
373
- return [];
438
+ logger.error('get receivers failed with pagination', { error });
439
+ yield [];
374
440
  }
441
+ return;
375
442
  }
376
443
 
377
- return this.getAdminReceivers({ teamDid });
444
+ // 4. 如果没有传入 receiver,需要查询站点的 admin 相关的用户
445
+ const adminReceivers = await this.getAdminReceivers({ teamDid });
446
+ yield adminReceivers;
378
447
  }
379
448
 
380
449
  /**
@@ -432,11 +501,10 @@ class TeamManager extends EventEmitter {
432
501
  * 创建通知文档
433
502
  * @private
434
503
  */
435
- async _createNotificationDoc(notificationState, payload, receivers, source, isServices, teamDid) {
504
+ async _createNotificationDoc(notificationState, payload, source, isServices, teamDid) {
436
505
  try {
437
506
  return await notificationState.create({
438
507
  ...payload,
439
- receiver: receivers,
440
508
  source,
441
509
  ...(!isServices ? {} : { teamDid }),
442
510
  });
@@ -539,18 +607,16 @@ class TeamManager extends EventEmitter {
539
607
 
540
608
  // 检查通知是否已存在
541
609
  const isExist = await this._checkExistingNotification(notificationState, id || notification?.id);
542
- // 获取接收者列表
543
- const receivers = await this._getReceiverList(teamDid, receiver, isExist);
544
610
 
545
- if (!receivers?.length && process.env.NODE_ENV !== 'test') {
546
- logger.warn('No valid receivers', {
547
- teamDid,
548
- receiver,
549
- notification: JSON.stringify(notification || payload || {}),
550
- });
551
- return undefined;
611
+ let doc = null;
612
+ const source = this._determineSource(teamDid, payload);
613
+ const isServices = teamDid && !this.isNodeTeam(teamDid);
614
+ if (!isExist && !payload.pushOnly) {
615
+ doc = await this._createNotificationDoc(notificationState, payload, source, isServices, teamDid);
616
+ logger.info('notification created successfully', { teamDid, notificationId: doc.id });
552
617
  }
553
618
 
619
+ // 获取 actor 信息
554
620
  const notificationActor = notification?.activity?.actor || payload.activity?.actor;
555
621
  let actorInfo = null;
556
622
  if (notificationActor) {
@@ -559,32 +625,53 @@ class TeamManager extends EventEmitter {
559
625
  actorInfo = pick(user, ['did', 'fullName', 'avatar']);
560
626
  }
561
627
 
562
- if (payload.pushOnly || isExist) {
563
- this.emit(EVENTS.NOTIFICATION_CREATE_QUEUED, {
564
- channels: payload.channels || [NOTIFICATION_SEND_CHANNEL.WALLET],
565
- notification: {
566
- ...(notification || payload),
567
- },
568
- receivers: receivers ?? receiver,
569
- teamDid: teamDid || this.nodeDid,
570
- sender: payload.sender,
571
- pushOnly: true,
572
- isExist,
573
- ...(actorInfo ? { actorInfo } : {}),
574
- options: typeof payload.options === 'object' && payload.options !== null ? payload.options : {},
575
- });
576
- return undefined;
577
- }
628
+ // 获取接收者列表(使用异步生成器逐页处理)
629
+ const receiverGenerator = this._getReceiverList(teamDid, receiver, isExist);
630
+ let hasReceivers = false;
578
631
 
579
- const source = this._determineSource(teamDid, payload);
580
- const isServices = teamDid && !this.isNodeTeam(teamDid);
632
+ for await (const receiversPage of receiverGenerator) {
633
+ if (!receiversPage?.length) {
634
+ continue;
635
+ }
581
636
 
582
- const doc = await this._createNotificationDoc(notificationState, payload, receivers, source, isServices, teamDid);
637
+ hasReceivers = true;
583
638
 
584
- logger.info('notification created successfully', { teamDid, notificationId: doc.id });
639
+ if (payload.pushOnly || isExist) {
640
+ this.emit(EVENTS.NOTIFICATION_CREATE_QUEUED, {
641
+ channels: payload.channels || [NOTIFICATION_SEND_CHANNEL.WALLET],
642
+ notification: {
643
+ ...(notification || payload),
644
+ },
645
+ receivers: receiversPage,
646
+ teamDid: teamDid || this.nodeDid,
647
+ sender: payload.sender,
648
+ pushOnly: true,
649
+ isExist,
650
+ ...(actorInfo ? { actorInfo } : {}),
651
+ options: typeof payload.options === 'object' && payload.options !== null ? payload.options : {},
652
+ });
653
+ } else {
654
+ const defaultChannel = this._getDefaultChannels(isServices, source);
655
+ await this._emitNotificationEvents(
656
+ doc,
657
+ payload,
658
+ receiversPage,
659
+ teamDid,
660
+ isServices,
661
+ defaultChannel,
662
+ actorInfo
663
+ );
664
+ }
665
+ }
585
666
 
586
- const defaultChannel = this._getDefaultChannels(isServices, source);
587
- await this._emitNotificationEvents(doc, payload, receivers, teamDid, isServices, defaultChannel, actorInfo);
667
+ if (!hasReceivers && process.env.NODE_ENV !== 'test') {
668
+ logger.warn('No valid receivers', {
669
+ teamDid,
670
+ receiver,
671
+ notification: JSON.stringify(notification || payload || {}),
672
+ });
673
+ return undefined;
674
+ }
588
675
 
589
676
  return doc;
590
677
  } catch (error) {
@@ -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-20251201-225048-b1682a09",
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-20251201-225048-b1682a09",
21
- "@abtnode/auth": "1.17.4-beta-20251201-225048-b1682a09",
22
- "@abtnode/certificate-manager": "1.17.4-beta-20251201-225048-b1682a09",
23
- "@abtnode/constant": "1.17.4-beta-20251201-225048-b1682a09",
24
- "@abtnode/cron": "1.17.4-beta-20251201-225048-b1682a09",
25
- "@abtnode/db-cache": "1.17.4-beta-20251201-225048-b1682a09",
26
- "@abtnode/docker-utils": "1.17.4-beta-20251201-225048-b1682a09",
27
- "@abtnode/logger": "1.17.4-beta-20251201-225048-b1682a09",
28
- "@abtnode/models": "1.17.4-beta-20251201-225048-b1682a09",
29
- "@abtnode/queue": "1.17.4-beta-20251201-225048-b1682a09",
30
- "@abtnode/rbac": "1.17.4-beta-20251201-225048-b1682a09",
31
- "@abtnode/router-provider": "1.17.4-beta-20251201-225048-b1682a09",
32
- "@abtnode/static-server": "1.17.4-beta-20251201-225048-b1682a09",
33
- "@abtnode/timemachine": "1.17.4-beta-20251201-225048-b1682a09",
34
- "@abtnode/util": "1.17.4-beta-20251201-225048-b1682a09",
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-20251201-225048-b1682a09",
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-20251201-225048-b1682a09",
48
+ "@blocklet/env": "1.17.4-beta-20251202-122551-267b614d",
49
49
  "@blocklet/error": "^0.3.3",
50
- "@blocklet/meta": "1.17.4-beta-20251201-225048-b1682a09",
51
- "@blocklet/resolver": "1.17.4-beta-20251201-225048-b1682a09",
52
- "@blocklet/sdk": "1.17.4-beta-20251201-225048-b1682a09",
53
- "@blocklet/server-js": "1.17.4-beta-20251201-225048-b1682a09",
54
- "@blocklet/store": "1.17.4-beta-20251201-225048-b1682a09",
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": "150c52d552b8b2d8c3a5bae57818aabf90dfd115"
119
+ "gitHead": "58a5f7d49ccce973fdb214337aee262435704ec5"
120
120
  }