@abtnode/core 1.17.3-beta-20251118-061144-335cd35d → 1.17.3-beta-20251119-034511-f26047c0

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
@@ -68,6 +68,7 @@ const { passportDisplaySchema } = require('../validators/util');
68
68
  const { validateUserRolePassport } = require('../util/validate-user-role-passport');
69
69
  const { getOrgInviteLink, createOrgValidators, isOrgOwner, isAdmingPath } = require('../util/org');
70
70
  const { createOrgInputSchema, updateOrgInputSchema } = require('../validators/org');
71
+ const { checkPushChannelAvailable, getNotificationPushState } = require('../util/notification');
71
72
 
72
73
  const sanitizeUrl = (url) => {
73
74
  if (!url) {
@@ -3410,6 +3411,54 @@ class TeamAPI extends EventEmitter {
3410
3411
  throw err;
3411
3412
  }
3412
3413
  }
3414
+
3415
+ async getNotificationStats({ teamDid, since = '1h' }) {
3416
+ let startTime = dayjs().subtract(1, 'hours').toDate();
3417
+
3418
+ if (since && typeof since === 'string') {
3419
+ const sinceMatch = since.match(/^(\d+)h$/);
3420
+ if (sinceMatch) {
3421
+ const hours = parseInt(sinceMatch[1], 10);
3422
+ if (hours >= 1 && hours <= 24) {
3423
+ startTime = dayjs().subtract(hours, 'hours').toDate();
3424
+ }
3425
+ }
3426
+ }
3427
+
3428
+ try {
3429
+ const state = await this.getNotificationState(teamDid);
3430
+ const isServer = this.teamManager.isNodeTeam(teamDid);
3431
+ const blocklet = isServer
3432
+ ? {}
3433
+ : await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
3434
+ const channelsAvailable = checkPushChannelAvailable(blocklet, isServer);
3435
+ const results = await state.getNotificationsBySince({ since });
3436
+
3437
+ if (results.length === 0) {
3438
+ return {
3439
+ healthy: true,
3440
+ message: `There have been no push records since ${startTime}. Please choose another time range`,
3441
+ since: startTime,
3442
+ channels: channelsAvailable,
3443
+ };
3444
+ }
3445
+
3446
+ const pushState = getNotificationPushState(results, channelsAvailable, isServer);
3447
+
3448
+ return {
3449
+ healthy: true,
3450
+ since: startTime,
3451
+ channels: pushState,
3452
+ };
3453
+ } catch (err) {
3454
+ logger.error('Get notification service health failed', err, { teamDid });
3455
+ return {
3456
+ healthy: false,
3457
+ error: err.message,
3458
+ since: startTime,
3459
+ };
3460
+ }
3461
+ }
3413
3462
  }
3414
3463
 
3415
3464
  module.exports = TeamAPI;
package/lib/index.js CHANGED
@@ -669,6 +669,7 @@ function ABTNode(options) {
669
669
  getNotificationComponents: teamAPI.getNotificationComponents.bind(teamAPI),
670
670
  resendNotification: blockletManager.resendNotification.bind(blockletManager),
671
671
  getReceivers: teamAPI.getReceivers.bind(teamAPI),
672
+ getNotificationStats: teamAPI.getNotificationStats.bind(teamAPI),
672
673
 
673
674
  // AuditLog
674
675
  createAuditLog: async (params) => {
@@ -4,6 +4,7 @@ const { Sequelize, Op } = require('sequelize');
4
4
  const { isValid } = require('@arcblock/did');
5
5
  const { Joi } = require('@arcblock/validator');
6
6
  const { ROLES, SERVER_ROLES, NOTIFICATION_SEND_CHANNEL, NOTIFICATION_SEND_STATUS } = require('@abtnode/constant');
7
+ const dayjs = require('@abtnode/util/lib/dayjs');
7
8
  const BaseState = require('./base');
8
9
  const { getReceiversStatistics } = require('../util/notification');
9
10
 
@@ -954,6 +955,52 @@ class NotificationState extends BaseState {
954
955
  });
955
956
  return Number(countResult.total);
956
957
  }
958
+
959
+ async getNotificationsBySince({ since = '1h' }) {
960
+ // 解析 since 参数,格式为 "数字h",例如 "1h", "24h"
961
+ const sinceMatch = since.match(/^(\d+)h$/);
962
+ if (!sinceMatch) {
963
+ throw new Error('Invalid since format. Expected format: "1h", "2h", "24h", etc.');
964
+ }
965
+
966
+ const hours = parseInt(sinceMatch[1], 10);
967
+
968
+ // 验证范围:最小 1h,最大 24h
969
+ if (hours < 1 || hours > 24) {
970
+ throw new Error('The since parameter must be between 1h and 24h.');
971
+ }
972
+
973
+ // 计算时间范围
974
+ const startTime = dayjs().subtract(hours, 'hours').toDate();
975
+
976
+ // 第一步:在 notifications 表中查询符合时间范围的通知 ID
977
+ const notifications = await this.model.findAll({
978
+ where: {
979
+ createdAt: {
980
+ [Op.gte]: startTime,
981
+ },
982
+ },
983
+ attributes: ['id'],
984
+ });
985
+
986
+ // 如果没有找到符合条件的通知,直接返回空数组
987
+ if (notifications.length === 0) {
988
+ return [];
989
+ }
990
+
991
+ // 提取通知 ID 列表
992
+ const notificationIds = notifications.map((n) => n.id);
993
+
994
+ // 第二步:根据通知 ID 在 notification_receivers 表中查询数据
995
+ return this.notificationReceivers.model.findAll({
996
+ where: {
997
+ notificationId: {
998
+ [Op.in]: notificationIds,
999
+ },
1000
+ },
1001
+ order: [['createdAt', 'DESC']],
1002
+ });
1003
+ }
957
1004
  }
958
1005
 
959
1006
  module.exports = NotificationState;
@@ -2,7 +2,11 @@ const { joinURL } = require('ufo');
2
2
  const isUrl = require('is-url');
3
3
  const omit = require('lodash/omit');
4
4
  const groupBy = require('lodash/groupBy');
5
- const { NOTIFICATION_SEND_STATUS } = require('@abtnode/constant');
5
+ const {
6
+ NOTIFICATION_SEND_STATUS,
7
+ NOTIFICATION_SEND_CHANNEL,
8
+ NOTIFICATION_SEND_FAILED_REASON,
9
+ } = require('@abtnode/constant');
6
10
 
7
11
  const REMOVE_FIELDS = [
8
12
  'description',
@@ -131,9 +135,189 @@ function getReceiversStatistics(receivers) {
131
135
  };
132
136
  }
133
137
 
138
+ /**
139
+ * 检测消息推送渠道是否开启
140
+ */
141
+ function checkPushChannelAvailable(blocklet = {}, isServer = false) {
142
+ const config = blocklet.settings?.notification || {};
143
+
144
+ const pushKitEnabled = config.pushKit?.enabled && config.pushKit?.endpoint;
145
+ const emailEnabled = config.email?.enabled;
146
+
147
+ return {
148
+ [NOTIFICATION_SEND_CHANNEL.WALLET]: {
149
+ enabled: true,
150
+ },
151
+ ...(isServer
152
+ ? {}
153
+ : {
154
+ [NOTIFICATION_SEND_CHANNEL.PUSH]: {
155
+ enabled: !!pushKitEnabled,
156
+ },
157
+ [NOTIFICATION_SEND_CHANNEL.EMAIL]: {
158
+ enabled: emailEnabled || false,
159
+ },
160
+ }),
161
+ [NOTIFICATION_SEND_CHANNEL.WEBHOOK]: {
162
+ enabled: true,
163
+ },
164
+ };
165
+ }
166
+
167
+ function getColumnField(channel, suffix) {
168
+ switch (channel) {
169
+ case NOTIFICATION_SEND_CHANNEL.WALLET:
170
+ return `wallet${suffix}`;
171
+ case NOTIFICATION_SEND_CHANNEL.PUSH:
172
+ return `pushKit${suffix}`;
173
+ case NOTIFICATION_SEND_CHANNEL.EMAIL:
174
+ return `email${suffix}`;
175
+ default:
176
+ return '';
177
+ }
178
+ }
179
+
180
+ const SEND_STATUS_MAP = {
181
+ [NOTIFICATION_SEND_STATUS.PENDING]: 'pending',
182
+ [NOTIFICATION_SEND_STATUS.SENT]: 'success',
183
+ [NOTIFICATION_SEND_STATUS.FAILED]: 'failed',
184
+ };
185
+
186
+ function getFailedReasonMessage(reason, channel) {
187
+ if (!reason || !channel) {
188
+ return reason;
189
+ }
190
+ switch (reason) {
191
+ case NOTIFICATION_SEND_FAILED_REASON.USER_DISABLED:
192
+ return `The user has disabled the notification for the "${channel}" channel.`;
193
+ case NOTIFICATION_SEND_FAILED_REASON.CHANNEL_UNAVAILABLE:
194
+ return `The "${channel}" channel is not available.`;
195
+ case NOTIFICATION_SEND_FAILED_REASON.CHANNEL_DISABLED:
196
+ return `The "${channel}" channel was not selected for this notification.`;
197
+ case NOTIFICATION_SEND_FAILED_REASON.NOT_ONLINE:
198
+ return 'The user is not online.';
199
+ default:
200
+ return reason;
201
+ }
202
+ }
203
+
204
+ function getStatisticsState(results, channel = NOTIFICATION_SEND_CHANNEL.WALLET) {
205
+ const last = results[0];
206
+
207
+ const getIgnoredAndFailedCount = (data) => {
208
+ const notSuccessCount = data.filter(
209
+ (item) => item[getColumnField(channel, 'SendStatus')] !== NOTIFICATION_SEND_STATUS.SENT
210
+ ).length;
211
+ const ignoredCount = data.filter(
212
+ (item) =>
213
+ item[getColumnField(channel, 'SendStatus')] !== NOTIFICATION_SEND_STATUS.SENT &&
214
+ (item[getColumnField(channel, 'SendStatus')] === NOTIFICATION_SEND_STATUS.PENDING ||
215
+ [
216
+ ...Object.values(NOTIFICATION_SEND_FAILED_REASON),
217
+ 'Email Service is not available.',
218
+ 'Push Kit Service is not Enabled.',
219
+ ].includes(item[getColumnField(channel, 'SendFailedReason')]))
220
+ ).length;
221
+
222
+ return {
223
+ ignored: ignoredCount,
224
+ failed: notSuccessCount - ignoredCount,
225
+ };
226
+ };
227
+
228
+ return {
229
+ last: {
230
+ sendAt: last[getColumnField(channel, 'SendAt')],
231
+ sendStatus: SEND_STATUS_MAP[last[getColumnField(channel, 'SendStatus')]],
232
+ reason: getFailedReasonMessage(last[getColumnField(channel, 'SendFailedReason')], channel),
233
+ },
234
+ state: {
235
+ total: results.length,
236
+ success: results.filter((item) => item[getColumnField(channel, 'SendStatus')] === NOTIFICATION_SEND_STATUS.SENT)
237
+ .length,
238
+ ...getIgnoredAndFailedCount(results),
239
+ },
240
+ };
241
+ }
242
+
243
+ function getWebhookStatisticsState(results) {
244
+ // 收集所有 URL 的最新记录
245
+ const latestRecords = [];
246
+
247
+ const state = results.reduce(
248
+ (acc, item) => {
249
+ const webhook = item.webhook ?? {};
250
+ Object.values(webhook).forEach((records) => {
251
+ // 对每个 URL 的记录数组按 sendAt 倒序排列,取最新的
252
+ const latestRecord = records.sort((a, b) => new Date(b.sendAt) - new Date(a.sendAt))[0];
253
+ if (latestRecord) {
254
+ latestRecords.push(latestRecord);
255
+ acc.total++;
256
+ if (latestRecord.status === NOTIFICATION_SEND_STATUS.SENT) {
257
+ acc.success++;
258
+ } else if (latestRecord.status === NOTIFICATION_SEND_STATUS.FAILED) {
259
+ acc.failed++;
260
+ } else if (latestRecord.status === NOTIFICATION_SEND_STATUS.PENDING) {
261
+ acc.ignored++;
262
+ }
263
+ }
264
+ });
265
+ return acc;
266
+ },
267
+ {
268
+ total: 0,
269
+ success: 0,
270
+ failed: 0,
271
+ ignored: 0,
272
+ }
273
+ );
274
+
275
+ // 从所有 URL 的最新记录中,找到最新的那个
276
+ const lastWebhook = latestRecords.sort((a, b) => new Date(b.sendAt) - new Date(a.sendAt))[0];
277
+
278
+ return {
279
+ last: lastWebhook
280
+ ? {
281
+ sendAt: lastWebhook.sendAt,
282
+ type: lastWebhook.type,
283
+ sendStatus: SEND_STATUS_MAP[lastWebhook.status],
284
+ reason: lastWebhook.failedReason,
285
+ }
286
+ : null,
287
+ state,
288
+ };
289
+ }
290
+
291
+ function getNotificationPushState(results, channelsAvailable, isServer = false) {
292
+ return {
293
+ wallet: {
294
+ ...channelsAvailable[NOTIFICATION_SEND_CHANNEL.WALLET],
295
+ ...getStatisticsState(results, NOTIFICATION_SEND_CHANNEL.WALLET),
296
+ },
297
+ ...(isServer
298
+ ? {}
299
+ : {
300
+ pushKit: {
301
+ ...channelsAvailable[NOTIFICATION_SEND_CHANNEL.PUSH],
302
+ ...getStatisticsState(results, NOTIFICATION_SEND_CHANNEL.PUSH),
303
+ },
304
+ email: {
305
+ ...channelsAvailable[NOTIFICATION_SEND_CHANNEL.EMAIL],
306
+ ...getStatisticsState(results, NOTIFICATION_SEND_CHANNEL.EMAIL),
307
+ },
308
+ }),
309
+ webhook: {
310
+ ...channelsAvailable[NOTIFICATION_SEND_CHANNEL.WEBHOOK],
311
+ ...getWebhookStatisticsState(results),
312
+ },
313
+ };
314
+ }
315
+
134
316
  module.exports = {
135
317
  transformNotification,
136
318
  getStatusCounts,
137
319
  getWebhookStatusCounts,
138
320
  getReceiversStatistics,
321
+ checkPushChannelAvailable,
322
+ getNotificationPushState,
139
323
  };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.17.3-beta-20251118-061144-335cd35d",
6
+ "version": "1.17.3-beta-20251119-034511-f26047c0",
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.3-beta-20251118-061144-335cd35d",
21
- "@abtnode/auth": "1.17.3-beta-20251118-061144-335cd35d",
22
- "@abtnode/certificate-manager": "1.17.3-beta-20251118-061144-335cd35d",
23
- "@abtnode/constant": "1.17.3-beta-20251118-061144-335cd35d",
24
- "@abtnode/cron": "1.17.3-beta-20251118-061144-335cd35d",
25
- "@abtnode/db-cache": "1.17.3-beta-20251118-061144-335cd35d",
26
- "@abtnode/docker-utils": "1.17.3-beta-20251118-061144-335cd35d",
27
- "@abtnode/logger": "1.17.3-beta-20251118-061144-335cd35d",
28
- "@abtnode/models": "1.17.3-beta-20251118-061144-335cd35d",
29
- "@abtnode/queue": "1.17.3-beta-20251118-061144-335cd35d",
30
- "@abtnode/rbac": "1.17.3-beta-20251118-061144-335cd35d",
31
- "@abtnode/router-provider": "1.17.3-beta-20251118-061144-335cd35d",
32
- "@abtnode/static-server": "1.17.3-beta-20251118-061144-335cd35d",
33
- "@abtnode/timemachine": "1.17.3-beta-20251118-061144-335cd35d",
34
- "@abtnode/util": "1.17.3-beta-20251118-061144-335cd35d",
20
+ "@abtnode/analytics": "1.17.3-beta-20251119-034511-f26047c0",
21
+ "@abtnode/auth": "1.17.3-beta-20251119-034511-f26047c0",
22
+ "@abtnode/certificate-manager": "1.17.3-beta-20251119-034511-f26047c0",
23
+ "@abtnode/constant": "1.17.3-beta-20251119-034511-f26047c0",
24
+ "@abtnode/cron": "1.17.3-beta-20251119-034511-f26047c0",
25
+ "@abtnode/db-cache": "1.17.3-beta-20251119-034511-f26047c0",
26
+ "@abtnode/docker-utils": "1.17.3-beta-20251119-034511-f26047c0",
27
+ "@abtnode/logger": "1.17.3-beta-20251119-034511-f26047c0",
28
+ "@abtnode/models": "1.17.3-beta-20251119-034511-f26047c0",
29
+ "@abtnode/queue": "1.17.3-beta-20251119-034511-f26047c0",
30
+ "@abtnode/rbac": "1.17.3-beta-20251119-034511-f26047c0",
31
+ "@abtnode/router-provider": "1.17.3-beta-20251119-034511-f26047c0",
32
+ "@abtnode/static-server": "1.17.3-beta-20251119-034511-f26047c0",
33
+ "@abtnode/timemachine": "1.17.3-beta-20251119-034511-f26047c0",
34
+ "@abtnode/util": "1.17.3-beta-20251119-034511-f26047c0",
35
35
  "@aigne/aigne-hub": "^0.10.9",
36
36
  "@arcblock/did": "^1.27.7",
37
37
  "@arcblock/did-connect-js": "^1.27.7",
@@ -43,15 +43,15 @@
43
43
  "@arcblock/pm2-events": "^0.0.5",
44
44
  "@arcblock/validator": "^1.27.7",
45
45
  "@arcblock/vc": "^1.27.7",
46
- "@blocklet/constant": "1.17.3-beta-20251118-061144-335cd35d",
46
+ "@blocklet/constant": "1.17.3-beta-20251119-034511-f26047c0",
47
47
  "@blocklet/did-space-js": "^1.2.4",
48
- "@blocklet/env": "1.17.3-beta-20251118-061144-335cd35d",
48
+ "@blocklet/env": "1.17.3-beta-20251119-034511-f26047c0",
49
49
  "@blocklet/error": "^0.3.3",
50
- "@blocklet/meta": "1.17.3-beta-20251118-061144-335cd35d",
51
- "@blocklet/resolver": "1.17.3-beta-20251118-061144-335cd35d",
52
- "@blocklet/sdk": "1.17.3-beta-20251118-061144-335cd35d",
53
- "@blocklet/server-js": "1.17.3-beta-20251118-061144-335cd35d",
54
- "@blocklet/store": "1.17.3-beta-20251118-061144-335cd35d",
50
+ "@blocklet/meta": "1.17.3-beta-20251119-034511-f26047c0",
51
+ "@blocklet/resolver": "1.17.3-beta-20251119-034511-f26047c0",
52
+ "@blocklet/sdk": "1.17.3-beta-20251119-034511-f26047c0",
53
+ "@blocklet/server-js": "1.17.3-beta-20251119-034511-f26047c0",
54
+ "@blocklet/store": "1.17.3-beta-20251119-034511-f26047c0",
55
55
  "@blocklet/theme": "^3.2.6",
56
56
  "@fidm/x509": "^1.2.1",
57
57
  "@ocap/mcrypto": "^1.27.7",
@@ -116,5 +116,5 @@
116
116
  "express": "^4.18.2",
117
117
  "unzipper": "^0.10.11"
118
118
  },
119
- "gitHead": "8e2245b336c9b4cdfd8e53012ab44a11b68c5665"
119
+ "gitHead": "7ab331f3b29e171a1e02aca80e73f35b6a161b86"
120
120
  }