@abtnode/core 1.16.52-beta-20250908-085420-224a58fa → 1.16.52-beta-20250911-023851-d988be85

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
@@ -699,6 +699,30 @@ class TeamAPI extends EventEmitter {
699
699
  return state.isFollowing(followerDid, userDids);
700
700
  }
701
701
 
702
+ async getUserInvites({ teamDid, userDid, paging, sort, options = {} }, context = {}) {
703
+ try {
704
+ if (paging?.pageSize > MAX_USER_PAGE_SIZE) {
705
+ throw new CustomError(400, `Length of users should not exceed ${MAX_USER_PAGE_SIZE} per page`);
706
+ }
707
+ const state = await this.getUserState(teamDid);
708
+ const { users, paging: resultPaging } = await state.getInvitees(userDid, { paging, sort, ...options }, context);
709
+ // 处理头像
710
+ const blocklet = await getBlocklet({ did: teamDid, states: this.states, dataDirs: this.dataDirs });
711
+ users.forEach((user) => {
712
+ if (user && user?.avatar && user?.avatar?.startsWith(USER_AVATAR_URL_PREFIX)) {
713
+ user.avatar = `${getFederatedUserAvatarUrl(user.avatar, blocklet)}?imageFilter=resize&w=48&h=48`;
714
+ }
715
+ });
716
+ return {
717
+ users,
718
+ paging: resultPaging,
719
+ };
720
+ } catch (error) {
721
+ logger.error('Failed to get user invites', { teamDid, userDid, error });
722
+ throw error;
723
+ }
724
+ }
725
+
702
726
  async updateUser({ teamDid, user }) {
703
727
  const state = await this.getUserState(teamDid);
704
728
  const exist = await state.getUserByDid(user.did);
@@ -38946,7 +38946,7 @@ module.exports = require("zlib");
38946
38946
  /***/ ((module) => {
38947
38947
 
38948
38948
  "use strict";
38949
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@abtnode/core","publishConfig":{"access":"public"},"version":"1.16.51","description":"","main":"lib/index.js","files":["lib"],"scripts":{"lint":"eslint tests lib --ignore-pattern \'tests/assets/*\'","lint:fix":"eslint --fix tests lib","test":"node tools/jest.js","coverage":"npm run test -- --coverage"},"keywords":[],"author":"wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)","license":"Apache-2.0","dependencies":{"@abtnode/analytics":"1.16.51","@abtnode/auth":"1.16.51","@abtnode/certificate-manager":"1.16.51","@abtnode/constant":"1.16.51","@abtnode/cron":"1.16.51","@abtnode/db-cache":"1.16.51","@abtnode/docker-utils":"1.16.51","@abtnode/logger":"1.16.51","@abtnode/models":"1.16.51","@abtnode/queue":"1.16.51","@abtnode/rbac":"1.16.51","@abtnode/router-provider":"1.16.51","@abtnode/static-server":"1.16.51","@abtnode/timemachine":"1.16.51","@abtnode/util":"1.16.51","@aigne/aigne-hub":"^0.8.9","@arcblock/did":"1.24.0","@arcblock/did-connect-js":"1.24.0","@arcblock/did-ext":"1.24.0","@arcblock/did-motif":"^1.1.14","@arcblock/did-util":"1.24.0","@arcblock/event-hub":"1.24.0","@arcblock/jwt":"1.24.0","@arcblock/pm2-events":"^0.0.5","@arcblock/validator":"1.24.0","@arcblock/vc":"1.24.0","@blocklet/constant":"1.16.51","@blocklet/did-space-js":"^1.1.23","@blocklet/env":"1.16.51","@blocklet/error":"^0.2.5","@blocklet/meta":"1.16.51","@blocklet/resolver":"1.16.51","@blocklet/sdk":"1.16.51","@blocklet/server-js":"1.16.51","@blocklet/store":"1.16.51","@blocklet/theme":"^3.1.36","@fidm/x509":"^1.2.1","@ocap/mcrypto":"1.24.0","@ocap/util":"1.24.0","@ocap/wallet":"1.24.0","@slack/webhook":"^5.0.4","archiver":"^7.0.1","axios":"^1.7.9","axon":"^2.0.3","chalk":"^4.1.2","cross-spawn":"^7.0.3","dayjs":"^1.11.13","deep-diff":"^1.0.2","detect-port":"^1.5.1","envfile":"^7.1.0","escape-string-regexp":"^4.0.0","fast-glob":"^3.3.2","filesize":"^10.1.1","flat":"^5.0.2","fs-extra":"^11.2.0","get-port":"^5.1.1","hasha":"^5.2.2","is-base64":"^1.1.0","is-cidr":"4","is-ip":"3","is-url":"^1.2.4","joi":"17.12.2","joi-extension-semver":"^5.0.0","js-yaml":"^4.1.0","kill-port":"^2.0.1","lodash":"^4.17.21","node-stream-zip":"^1.15.0","p-all":"^3.0.0","p-limit":"^3.1.0","p-map":"^4.0.0","p-retry":"^4.6.2","p-wait-for":"^3.2.0","private-ip":"^2.3.4","rate-limiter-flexible":"^5.0.5","read-last-lines":"^1.8.0","semver":"^7.6.3","sequelize":"^6.35.0","shelljs":"^0.8.5","slugify":"^1.6.6","ssri":"^8.0.1","stream-throttle":"^0.1.3","stream-to-promise":"^3.0.0","systeminformation":"^5.23.3","tail":"^2.2.4","tar":"^6.1.11","transliteration":"^2.3.5","ua-parser-js":"^1.0.2","ufo":"^1.5.3","uuid":"^11.1.0","valid-url":"^1.0.9","which":"^2.0.2","xbytes":"^1.8.0"},"devDependencies":{"expand-tilde":"^2.0.2","express":"^4.18.2","jest":"^29.7.0","unzipper":"^0.10.11"},"gitHead":"e5764f753181ed6a7c615cd4fc6682aacf0cb7cd"}');
38949
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@abtnode/core","publishConfig":{"access":"public"},"version":"1.16.51","description":"","main":"lib/index.js","files":["lib"],"scripts":{"lint":"eslint tests lib --ignore-pattern \'tests/assets/*\'","lint:fix":"eslint --fix tests lib","test":"node tools/jest.js","coverage":"npm run test -- --coverage"},"keywords":[],"author":"wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)","license":"Apache-2.0","dependencies":{"@abtnode/analytics":"1.16.51","@abtnode/auth":"1.16.51","@abtnode/certificate-manager":"1.16.51","@abtnode/constant":"1.16.51","@abtnode/cron":"1.16.51","@abtnode/db-cache":"1.16.51","@abtnode/docker-utils":"1.16.51","@abtnode/logger":"1.16.51","@abtnode/models":"1.16.51","@abtnode/queue":"1.16.51","@abtnode/rbac":"1.16.51","@abtnode/router-provider":"1.16.51","@abtnode/static-server":"1.16.51","@abtnode/timemachine":"1.16.51","@abtnode/util":"1.16.51","@aigne/aigne-hub":"^0.8.10","@arcblock/did":"1.24.0","@arcblock/did-connect-js":"1.24.0","@arcblock/did-ext":"1.24.0","@arcblock/did-motif":"^1.1.14","@arcblock/did-util":"1.24.0","@arcblock/event-hub":"1.24.0","@arcblock/jwt":"1.24.0","@arcblock/pm2-events":"^0.0.5","@arcblock/validator":"1.24.0","@arcblock/vc":"1.24.0","@blocklet/constant":"1.16.51","@blocklet/did-space-js":"^1.1.23","@blocklet/env":"1.16.51","@blocklet/error":"^0.2.5","@blocklet/meta":"1.16.51","@blocklet/resolver":"1.16.51","@blocklet/sdk":"1.16.51","@blocklet/server-js":"1.16.51","@blocklet/store":"1.16.51","@blocklet/theme":"^3.1.37","@fidm/x509":"^1.2.1","@ocap/mcrypto":"1.24.0","@ocap/util":"1.24.0","@ocap/wallet":"1.24.0","@slack/webhook":"^5.0.4","archiver":"^7.0.1","axios":"^1.7.9","axon":"^2.0.3","chalk":"^4.1.2","cross-spawn":"^7.0.3","dayjs":"^1.11.13","deep-diff":"^1.0.2","detect-port":"^1.5.1","envfile":"^7.1.0","escape-string-regexp":"^4.0.0","fast-glob":"^3.3.2","filesize":"^10.1.1","flat":"^5.0.2","fs-extra":"^11.2.0","get-port":"^5.1.1","hasha":"^5.2.2","is-base64":"^1.1.0","is-cidr":"4","is-ip":"3","is-url":"^1.2.4","joi":"17.12.2","joi-extension-semver":"^5.0.0","js-yaml":"^4.1.0","kill-port":"^2.0.1","lodash":"^4.17.21","node-stream-zip":"^1.15.0","p-all":"^3.0.0","p-limit":"^3.1.0","p-map":"^4.0.0","p-retry":"^4.6.2","p-wait-for":"^3.2.0","private-ip":"^2.3.4","rate-limiter-flexible":"^5.0.5","read-last-lines":"^1.8.0","semver":"^7.6.3","sequelize":"^6.35.0","shelljs":"^0.8.5","slugify":"^1.6.6","ssri":"^8.0.1","stream-throttle":"^0.1.3","stream-to-promise":"^3.0.0","systeminformation":"^5.23.3","tail":"^2.2.4","tar":"^6.1.11","transliteration":"^2.3.5","ua-parser-js":"^1.0.2","ufo":"^1.5.3","uuid":"^11.1.0","valid-url":"^1.0.9","which":"^2.0.2","xbytes":"^1.8.0"},"devDependencies":{"expand-tilde":"^2.0.2","express":"^4.18.2","jest":"^29.7.0","unzipper":"^0.10.11"},"gitHead":"e5764f753181ed6a7c615cd4fc6682aacf0cb7cd"}');
38950
38950
 
38951
38951
  /***/ }),
38952
38952
 
package/lib/index.js CHANGED
@@ -542,6 +542,9 @@ function ABTNode(options) {
542
542
  getUserFollowStats: teamAPI.getUserFollowStats.bind(teamAPI),
543
543
  checkFollowing: teamAPI.checkFollowing.bind(teamAPI),
544
544
 
545
+ // invite
546
+ getUserInvites: teamAPI.getUserInvites.bind(teamAPI),
547
+
545
548
  // Tagging
546
549
  createTag: teamAPI.createTag.bind(teamAPI),
547
550
  updateTag: teamAPI.updateTag.bind(teamAPI),
@@ -5,6 +5,11 @@ const get = require('lodash/get');
5
5
  const uniq = require('lodash/uniq');
6
6
  const isEqual = require('lodash/isEqual');
7
7
  const isNil = require('lodash/isNil');
8
+ const isObject = require('lodash/isObject');
9
+ const isArray = require('lodash/isArray');
10
+ const isString = require('lodash/isString');
11
+ const mapValues = require('lodash/mapValues');
12
+ const map = require('lodash/map');
8
13
  const { joinURL } = require('ufo');
9
14
  const { Op } = require('sequelize');
10
15
  const { getDisplayName } = require('@blocklet/meta/lib/util');
@@ -197,6 +202,36 @@ const getResponseHeaderPolicyInfo = (policy) => {
197
202
  .join(', ');
198
203
  };
199
204
 
205
+ /**
206
+ * 隐藏私密信息,主要字段有
207
+ * 1. email
208
+ */
209
+ const hidePrivateInfo = (result) => {
210
+ const emailRegex =
211
+ /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
212
+
213
+ const processValue = (value, key) => {
214
+ // 如果字段名包含 email 或值是邮箱格式,则隐藏
215
+ if ((key && /email/i.test(key)) || (isString(value) && emailRegex.test(value))) {
216
+ return '***';
217
+ }
218
+
219
+ // 递归处理对象
220
+ if (isObject(value) && !isArray(value)) {
221
+ return mapValues(value, processValue);
222
+ }
223
+
224
+ // 递归处理数组
225
+ if (isArray(value)) {
226
+ return map(value, (item) => processValue(item));
227
+ }
228
+
229
+ return value;
230
+ };
231
+
232
+ return processValue(result);
233
+ };
234
+
200
235
  /**
201
236
  * Create log content in markdown format
202
237
  *
@@ -211,7 +246,13 @@ const getResponseHeaderPolicyInfo = (policy) => {
211
246
  const getLogContent = async (action, args, context, result, info, node) => {
212
247
  const [site, [user, passport]] = await Promise.all([
213
248
  expandSite(args.id, info, node),
214
- expandUser(args.teamDid, args.userDid || get(args, 'user.did') || args.ownerDid, args.passportId, info, node),
249
+ expandUser(
250
+ args.teamDid,
251
+ args.userDid || get(args, 'user.did') || args.ownerDid || args.did,
252
+ args.passportId,
253
+ info,
254
+ node
255
+ ),
215
256
  ]);
216
257
 
217
258
  switch (action) {
@@ -369,6 +410,55 @@ const getLogContent = async (action, args, context, result, info, node) => {
369
410
  return `${args.user.approved ? 'enabled' : 'disabled'} user ${user}`;
370
411
  case 'updateUserTags':
371
412
  return `set tags to ${args.tags} for user ${user}`;
413
+ case 'updateUserExtra':
414
+ let type = args.type || '';
415
+ const extraResult = result.extra || result;
416
+ let resultStr = '\n';
417
+ let isFormat = false;
418
+ if (type === 'privacy' || Object.prototype.hasOwnProperty.call(extraResult, 'privacy')) {
419
+ resultStr += Object.keys(extraResult.privacy)
420
+ .map((x) => {
421
+ const value = extraResult.privacy[x];
422
+ let v = value;
423
+ if (value === true || value === 'all') {
424
+ v = 'public';
425
+ } else if (value === false || value === 'private') {
426
+ v = 'private';
427
+ } else if (value === 'follower-only') {
428
+ v = 'follower-only';
429
+ } else {
430
+ v = value;
431
+ }
432
+ return `- ${x}: ${v}`;
433
+ })
434
+ .join('\n');
435
+ isFormat = true;
436
+ type = 'privacy';
437
+ }
438
+ if (type === 'notifications' || Object.prototype.hasOwnProperty.call(extraResult, 'notifications')) {
439
+ resultStr += Object.keys(extraResult?.notifications || {})
440
+ .map((x) => `- ${x}: ${extraResult.notifications[x]}`)
441
+ .join('\n');
442
+ isFormat = true;
443
+ type = 'notifications';
444
+ }
445
+ if (type === 'webhooks' || Object.prototype.hasOwnProperty.call(extraResult, 'webhooks')) {
446
+ resultStr += (extraResult?.webhooks || []).length
447
+ ? extraResult.webhooks.map((x) => `- ${x.type}: ${x.url}`).join('\n')
448
+ : 'removed all webhooks';
449
+ isFormat = true;
450
+ type = 'webhooks';
451
+ }
452
+ if (!isFormat) {
453
+ resultStr = JSON.stringify(hidePrivateInfo(extraResult));
454
+ type = 'extra';
455
+ }
456
+ return `updated user ${type} for user ${user}: \n${resultStr}`;
457
+ case 'updateUserAddress':
458
+ const addressResult = Object.prototype.hasOwnProperty.call(result, 'address') ? result.address : result;
459
+ return `updated user address for user ${user}: \n${JSON.stringify(hidePrivateInfo(addressResult))}`;
460
+ case 'updateUserInfo':
461
+ return `update user info for user ${user}: \n\n${JSON.stringify(hidePrivateInfo(result))}`;
372
462
  case 'deletePassportIssuance':
373
463
  return `removed passport issuance ${args.sessionId}`;
374
464
  case 'createMemberInvitation':
@@ -551,6 +641,19 @@ const getLogContent = async (action, args, context, result, info, node) => {
551
641
  return `Update Blocklet Extras Settings:(${args.did} to ${changes.join(', ')})`;
552
642
  }
553
643
 
644
+ // user follow
645
+ case 'followUser':
646
+ case 'unfollowUser':
647
+ const followerUser = await node.getUser({ teamDid: args.teamDid, user: { did: args.followerDid } });
648
+ const prefix = process.env.NODE_ENV === 'production' ? info.routing.adminPath : '';
649
+ return `[${followerUser.fullName}](${joinURL(prefix, '/team/members')}) ${action === 'followUser' ? 'followed' : 'unfollowed'} user ${user}`;
650
+
651
+ // connect to aigne
652
+ case 'connectToAigne':
653
+ return `Connect to Aigne(${args.baseUrl})`;
654
+ case 'disconnectToAigne':
655
+ return `Disconnect to Aigne(${args.url})`;
656
+
554
657
  default:
555
658
  return action;
556
659
  }
@@ -599,6 +702,8 @@ const getLogCategory = (action) => {
599
702
  case 'createOAuthClient':
600
703
  case 'updateOAuthClient':
601
704
  case 'updateBlockletSettings':
705
+ case 'connectToAigne':
706
+ case 'disconnectToAigne':
602
707
  return 'blocklet';
603
708
 
604
709
  case 'addUploadEndpoint':
@@ -648,6 +753,11 @@ const getLogCategory = (action) => {
648
753
  case 'loginFederatedMaster':
649
754
  case 'migrateFederatedAccount':
650
755
  case 'destroySelf':
756
+ case 'updateUserExtra':
757
+ case 'updateUserAddress':
758
+ case 'updateUserInfo':
759
+ case 'followUser':
760
+ case 'unfollowUser':
651
761
  return 'team';
652
762
 
653
763
  // accessKeys
@@ -1094,7 +1094,7 @@ SELECT did,inviter,generation FROM UserTree`.trim();
1094
1094
  return userDids.reduce(
1095
1095
  (acc, userDid) => ({
1096
1096
  ...acc,
1097
- [userDid]: { followers: 0, following: 0 },
1097
+ [userDid]: { followers: 0, following: 0, invitees: 0 },
1098
1098
  }),
1099
1099
  {}
1100
1100
  );
@@ -1130,14 +1130,23 @@ SELECT did,inviter,generation FROM UserTree`.trim();
1130
1130
  GROUP BY "followerDid"
1131
1131
  `;
1132
1132
 
1133
- const [followersResults, followingResults] = await Promise.all([
1133
+ const inviteesQuery = `
1134
+ SELECT "inviter", COUNT(*) as count
1135
+ FROM users
1136
+ WHERE "inviter" IN (:userDids) AND "inviter" IS NOT NULL
1137
+ GROUP BY "inviter"
1138
+ `;
1139
+
1140
+ const [followersResults, followingResults, inviteesResults] = await Promise.all([
1134
1141
  this.model.sequelize.query(followersQuery, { replacements: { userDids }, type: Sequelize.QueryTypes.SELECT }),
1135
1142
  this.model.sequelize.query(followingQuery, { replacements: { userDids }, type: Sequelize.QueryTypes.SELECT }),
1143
+ this.model.sequelize.query(inviteesQuery, { replacements: { userDids }, type: Sequelize.QueryTypes.SELECT }),
1136
1144
  ]);
1137
1145
 
1138
1146
  // 使用 Map 构造函数和数组映射优化数据结构构建
1139
1147
  const followersMap = new Map(followersResults.map((row) => [row.userDid, Number(row.count)]));
1140
1148
  const followingMap = new Map(followingResults.map((row) => [row.followerDid, Number(row.count)]));
1149
+ const inviteesMap = new Map(inviteesResults.map((row) => [row.inviter, Number(row.count)]));
1141
1150
 
1142
1151
  // 使用 Object.fromEntries 优化结果对象构建,并进行隐私判断
1143
1152
  const result = Object.fromEntries(
@@ -1147,7 +1156,7 @@ SELECT did,inviter,generation FROM UserTree`.trim();
1147
1156
 
1148
1157
  // 如果用户设置了隐私且查看者不是用户本人,返回 0
1149
1158
  if (isPrivate && !isOwner) {
1150
- return [userDid, { followers: 0, following: 0 }];
1159
+ return [userDid, { followers: 0, following: 0, invitees: 0 }];
1151
1160
  }
1152
1161
 
1153
1162
  return [
@@ -1155,6 +1164,7 @@ SELECT did,inviter,generation FROM UserTree`.trim();
1155
1164
  {
1156
1165
  followers: followersMap.get(userDid) || 0,
1157
1166
  following: followingMap.get(userDid) || 0,
1167
+ invitees: inviteesMap.get(userDid) || 0,
1158
1168
  },
1159
1169
  ];
1160
1170
  })
@@ -1167,12 +1177,76 @@ SELECT did,inviter,generation FROM UserTree`.trim();
1167
1177
  return userDids.reduce(
1168
1178
  (acc, userDid) => ({
1169
1179
  ...acc,
1170
- [userDid]: { followers: 0, following: 0 },
1180
+ [userDid]: { followers: 0, following: 0, invitees: 0 },
1171
1181
  }),
1172
1182
  {}
1173
1183
  );
1174
1184
  }
1175
1185
  }
1186
+
1187
+ /**
1188
+ * 获取用户的受邀者列表
1189
+ * @param {string} userDid - 用户的 DID
1190
+ * @param {object} options - 选项
1191
+ * @param {object} options.paging - 分页参数
1192
+ * @param {object} options.sort - 排序参数
1193
+ * @param {boolean} options.includeFollowStatus - 是否包含当前用户的关注状态,默认 true
1194
+ * @param {object} context - 上下文,包含当前用户信息
1195
+ * @private
1196
+ */
1197
+ async getInvitees(userDid, options = {}, context = {}) {
1198
+ try {
1199
+ const { paging, sort, includeFollowStatus = true } = options;
1200
+ const { user: contextUser } = context;
1201
+
1202
+ const contextUserDid = contextUser?.userInfo?.did || contextUser?.did;
1203
+
1204
+ // 验证邀请者用户是否存在
1205
+ await this._validateUsersExist([userDid]);
1206
+
1207
+ // 查询被邀请的用户列表
1208
+ const where = { inviter: userDid };
1209
+
1210
+ // 设置排序
1211
+ const order = sort?.createdAt === -1 ? 'DESC' : 'ASC';
1212
+ const sorting = [['createdAt', sort?.createdAt ? order : 'DESC']];
1213
+
1214
+ // 分页查询
1215
+ const result = await this.paginate(
1216
+ { where, attributes: ['did', 'fullName', 'avatar', 'createdAt', 'inviter'] },
1217
+ sorting,
1218
+ paging
1219
+ );
1220
+
1221
+ // 准备关注状态映射
1222
+ let followingStatusMap = {};
1223
+ if (includeFollowStatus && contextUserDid && this.userFollowers) {
1224
+ const inviteeDids = result.list.map((user) => user.did);
1225
+ if (inviteeDids.length > 0) {
1226
+ followingStatusMap = await this.isFollowing(contextUserDid, inviteeDids);
1227
+ }
1228
+ }
1229
+
1230
+ // 构建增强的列表
1231
+ const enrichedList = result.list.map((user) => {
1232
+ const enrichedUser = { ...user };
1233
+
1234
+ // 添加关注状态
1235
+ if (includeFollowStatus && contextUserDid && contextUserDid !== user.did) {
1236
+ enrichedUser.isFollowing = followingStatusMap[user.did] || false;
1237
+ }
1238
+
1239
+ return enrichedUser;
1240
+ });
1241
+
1242
+ result.users = enrichedList;
1243
+
1244
+ return result;
1245
+ } catch (error) {
1246
+ logger.error('Failed to get invitees:', error);
1247
+ throw error;
1248
+ }
1249
+ }
1176
1250
  }
1177
1251
 
1178
1252
  module.exports = User;
@@ -0,0 +1,53 @@
1
+ const generateClusterNodeScript = (scriptPath, instances) => `
2
+ // Auto-generated cluster script
3
+ const cluster = require('cluster');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const numCPUs = os.cpus().length;
8
+ const workerCount = Math.min(${instances}, numCPUs);
9
+
10
+ // Restart protection: maximum restarts & time window, if 5 restarts in 60s, exit the entire program
11
+ const MAX_RESTARTS = 5;
12
+ const RESTART_WINDOW_MS = 60_000;
13
+ const restartTimestamps = [];
14
+
15
+ if (cluster.isMaster) {
16
+ console.log(\`[Master] PID=\${process.pid}, starting \${workerCount} workers...\`);
17
+
18
+ for (let i = 0; i < workerCount; i++) {
19
+ cluster.fork();
20
+ }
21
+
22
+ cluster.on('exit', (worker, code, signal) => {
23
+ console.error(\`[Master] Worker \${worker.process.pid} died (code=\${code}, signal=\${signal})\`);
24
+
25
+ const now = Date.now();
26
+ restartTimestamps.push(now);
27
+
28
+ // Clean up old records that exceed the window period
29
+ while (restartTimestamps.length > 0 && restartTimestamps[0] < now - RESTART_WINDOW_MS) {
30
+ restartTimestamps.shift();
31
+ }
32
+
33
+ if (restartTimestamps.length > MAX_RESTARTS) {
34
+ console.error('[Master] Too many worker restarts in short time, shutting down the cluster...');
35
+ // ⚠️ Exit the entire program, including master
36
+ process.exit(1);
37
+ return;
38
+ }
39
+
40
+ console.log('[Master] Restarting a new worker...');
41
+ setTimeout(() => cluster.fork(), 1000);
42
+ });
43
+
44
+ cluster.on('online', (worker) => {
45
+ console.log(\`[Master] Worker \${worker.process.pid} is online\`);
46
+ });
47
+ } else {
48
+ console.log(\`[Worker] PID=\${process.pid} running: ${scriptPath}\`);
49
+ require(path.resolve('./', '${scriptPath}'));
50
+ }
51
+ `;
52
+
53
+ module.exports = generateClusterNodeScript;
@@ -15,11 +15,13 @@ const { dockerCmdValidator } = require('@abtnode/docker-utils');
15
15
  const { getBlockletEngine } = require('@blocklet/meta/lib/engine');
16
16
  const { isExternalBlocklet } = require('@blocklet/meta/lib/util');
17
17
 
18
+ const { isNumber } = require('lodash');
18
19
  const parseDockerName = require('./parse-docker-name');
19
20
  const { createDockerImage } = require('./create-docker-image');
20
21
  const checkNeedRunDocker = require('./check-need-run-docker');
21
22
  const replaceEnvValue = require('./replace-env-value');
22
23
  const parseDockerCpVolume = require('./parse-docker-cp-volume');
24
+ const generateClusterNodeScript = require('./generate-cluster-node-script');
23
25
 
24
26
  const getSystemResources = (() => {
25
27
  let cachedResources = null;
@@ -205,9 +207,20 @@ async function parseDockerOptionsFromPm2({
205
207
  await fsp.writeFile(dockerEnvFile, envVars);
206
208
 
207
209
  let runScript = dockerInfo.script || '';
210
+
208
211
  if (!runScript && dockerInfo.installNodeModules) {
212
+ if (!overrideScript && nextOptions.script && isNumber(nextOptions.instances) && nextOptions.instances > 1) {
213
+ createClusterScripts({
214
+ appDir: nextOptions.env.BLOCKLET_APP_DIR,
215
+ scriptPath: nextOptions.script.replace(/(npm|yarn|pnpm|bun)/, '').trim(),
216
+ instances: nextOptions.instances,
217
+ });
218
+ nextOptions.script = 'index-cluster.js';
219
+ }
220
+
209
221
  const maxOldSpaceSize = Math.floor(Number(dockerEnv.BLOCKLET_DOCKER_MEMORY.replace('g', '')) * 0.85 * 1024);
210
222
  const nodeInterpreter = `node ${process.env.ABT_NODE_BLOCKLET_MODE === ABT_NODE_KERNEL_OR_BLOCKLET_MODE.PERFORMANT ? '' : '--optimize_for_size'} --max-old-space-size=${maxOldSpaceSize} --max-http-header-size=16384 ${nextOptions.script} -- BLOCKLET_NAME=${options.name}`;
223
+
211
224
  const bunInterpreter = `bun run ${nextOptions.script} -- BLOCKLET_NAME=${options.name}`;
212
225
  const engine = getBlockletEngine(meta);
213
226
  const runCommand = engine.interpreter === 'bun' ? bunInterpreter : nodeInterpreter;
@@ -386,6 +399,8 @@ async function parseDockerOptionsFromPm2({
386
399
  await promiseSpawn(`docker pull ${meta.docker.image}`, {}, { timeout: 120 * 1000, retry: 0 });
387
400
  }
388
401
 
402
+ delete nextOptions.instances;
403
+
389
404
  return nextOptions;
390
405
  }
391
406
 
@@ -421,4 +436,11 @@ ${userCommands.filter(Boolean).join('\n')}
421
436
  writeFileSync(userScriptPath, userScriptContent, { mode: 0o755 });
422
437
  }
423
438
 
439
+ function createClusterScripts({ appDir, scriptPath, instances }) {
440
+ const clusterScriptPath = path.join(appDir, 'index-cluster.js');
441
+ const rootScriptContent = generateClusterNodeScript(scriptPath, instances);
442
+ writeFileSync(clusterScriptPath, rootScriptContent, { mode: 0o755 });
443
+ return clusterScriptPath;
444
+ }
445
+
424
446
  module.exports = parseDockerOptionsFromPm2;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.16.52-beta-20250908-085420-224a58fa",
6
+ "version": "1.16.52-beta-20250911-023851-d988be85",
7
7
  "description": "",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -19,22 +19,22 @@
19
19
  "author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
20
20
  "license": "Apache-2.0",
21
21
  "dependencies": {
22
- "@abtnode/analytics": "1.16.52-beta-20250908-085420-224a58fa",
23
- "@abtnode/auth": "1.16.52-beta-20250908-085420-224a58fa",
24
- "@abtnode/certificate-manager": "1.16.52-beta-20250908-085420-224a58fa",
25
- "@abtnode/constant": "1.16.52-beta-20250908-085420-224a58fa",
26
- "@abtnode/cron": "1.16.52-beta-20250908-085420-224a58fa",
27
- "@abtnode/db-cache": "1.16.52-beta-20250908-085420-224a58fa",
28
- "@abtnode/docker-utils": "1.16.52-beta-20250908-085420-224a58fa",
29
- "@abtnode/logger": "1.16.52-beta-20250908-085420-224a58fa",
30
- "@abtnode/models": "1.16.52-beta-20250908-085420-224a58fa",
31
- "@abtnode/queue": "1.16.52-beta-20250908-085420-224a58fa",
32
- "@abtnode/rbac": "1.16.52-beta-20250908-085420-224a58fa",
33
- "@abtnode/router-provider": "1.16.52-beta-20250908-085420-224a58fa",
34
- "@abtnode/static-server": "1.16.52-beta-20250908-085420-224a58fa",
35
- "@abtnode/timemachine": "1.16.52-beta-20250908-085420-224a58fa",
36
- "@abtnode/util": "1.16.52-beta-20250908-085420-224a58fa",
37
- "@aigne/aigne-hub": "^0.8.9",
22
+ "@abtnode/analytics": "1.16.52-beta-20250911-023851-d988be85",
23
+ "@abtnode/auth": "1.16.52-beta-20250911-023851-d988be85",
24
+ "@abtnode/certificate-manager": "1.16.52-beta-20250911-023851-d988be85",
25
+ "@abtnode/constant": "1.16.52-beta-20250911-023851-d988be85",
26
+ "@abtnode/cron": "1.16.52-beta-20250911-023851-d988be85",
27
+ "@abtnode/db-cache": "1.16.52-beta-20250911-023851-d988be85",
28
+ "@abtnode/docker-utils": "1.16.52-beta-20250911-023851-d988be85",
29
+ "@abtnode/logger": "1.16.52-beta-20250911-023851-d988be85",
30
+ "@abtnode/models": "1.16.52-beta-20250911-023851-d988be85",
31
+ "@abtnode/queue": "1.16.52-beta-20250911-023851-d988be85",
32
+ "@abtnode/rbac": "1.16.52-beta-20250911-023851-d988be85",
33
+ "@abtnode/router-provider": "1.16.52-beta-20250911-023851-d988be85",
34
+ "@abtnode/static-server": "1.16.52-beta-20250911-023851-d988be85",
35
+ "@abtnode/timemachine": "1.16.52-beta-20250911-023851-d988be85",
36
+ "@abtnode/util": "1.16.52-beta-20250911-023851-d988be85",
37
+ "@aigne/aigne-hub": "^0.8.10",
38
38
  "@arcblock/did": "1.24.0",
39
39
  "@arcblock/did-connect-js": "1.24.0",
40
40
  "@arcblock/did-ext": "1.24.0",
@@ -45,16 +45,16 @@
45
45
  "@arcblock/pm2-events": "^0.0.5",
46
46
  "@arcblock/validator": "1.24.0",
47
47
  "@arcblock/vc": "1.24.0",
48
- "@blocklet/constant": "1.16.52-beta-20250908-085420-224a58fa",
48
+ "@blocklet/constant": "1.16.52-beta-20250911-023851-d988be85",
49
49
  "@blocklet/did-space-js": "^1.1.23",
50
- "@blocklet/env": "1.16.52-beta-20250908-085420-224a58fa",
50
+ "@blocklet/env": "1.16.52-beta-20250911-023851-d988be85",
51
51
  "@blocklet/error": "^0.2.5",
52
- "@blocklet/meta": "1.16.52-beta-20250908-085420-224a58fa",
53
- "@blocklet/resolver": "1.16.52-beta-20250908-085420-224a58fa",
54
- "@blocklet/sdk": "1.16.52-beta-20250908-085420-224a58fa",
55
- "@blocklet/server-js": "1.16.52-beta-20250908-085420-224a58fa",
56
- "@blocklet/store": "1.16.52-beta-20250908-085420-224a58fa",
57
- "@blocklet/theme": "^3.1.36",
52
+ "@blocklet/meta": "1.16.52-beta-20250911-023851-d988be85",
53
+ "@blocklet/resolver": "1.16.52-beta-20250911-023851-d988be85",
54
+ "@blocklet/sdk": "1.16.52-beta-20250911-023851-d988be85",
55
+ "@blocklet/server-js": "1.16.52-beta-20250911-023851-d988be85",
56
+ "@blocklet/store": "1.16.52-beta-20250911-023851-d988be85",
57
+ "@blocklet/theme": "^3.1.37",
58
58
  "@fidm/x509": "^1.2.1",
59
59
  "@ocap/mcrypto": "1.24.0",
60
60
  "@ocap/util": "1.24.0",
@@ -118,5 +118,5 @@
118
118
  "jest": "^29.7.0",
119
119
  "unzipper": "^0.10.11"
120
120
  },
121
- "gitHead": "400eda55fc1058c961fd622907237a649b5c4f86"
121
+ "gitHead": "90c5cf74138797733868312beb5e509c9db3a8f8"
122
122
  }