@abtnode/core 1.17.5-beta-20251211-104355-426d7eb6 → 1.17.5-beta-20251214-231110-497f8d27

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
@@ -10,6 +10,7 @@ const { joinURL, withoutTrailingSlash } = require('ufo');
10
10
  const { Op } = require('sequelize');
11
11
  const dayjs = require('@abtnode/util/lib/dayjs');
12
12
  const logger = require('@abtnode/logger')('@abtnode/core:api:team');
13
+ const pAll = require('p-all');
13
14
  const {
14
15
  ROLES,
15
16
  genPermissionName,
@@ -287,6 +288,69 @@ class TeamAPI extends EventEmitter {
287
288
  const backup = Array.isArray(backups) && backups.length > 0 ? backups[0] : null;
288
289
  const appRuntimeInfo = await this.runtimeMonitor.getBlockletRuntimeInfo(teamDid);
289
290
 
291
+ const [traffic, integrations, studio] = await pAll([
292
+ // 查询最近 30 天的 traffic insights 聚合数据
293
+ async () => {
294
+ const endDate = dayjs().format('YYYY-MM-DD');
295
+ const startDate = dayjs().subtract(30, 'day').format('YYYY-MM-DD');
296
+ const [totalRequests, failedRequests] = await Promise.all([
297
+ this.states.trafficInsight.sum('totalRequests', {
298
+ did: teamDid,
299
+ date: { [Op.gte]: startDate, [Op.lte]: endDate },
300
+ }),
301
+ this.states.trafficInsight.sum('failedRequests', {
302
+ did: teamDid,
303
+ date: { [Op.gte]: startDate, [Op.lte]: endDate },
304
+ }),
305
+ ]);
306
+ return {
307
+ totalRequests: Number(totalRequests || 0),
308
+ failedRequests: Number(failedRequests || 0),
309
+ };
310
+ },
311
+ // 查询集成资源的数量
312
+ async () => {
313
+ const [webhookCount, accessKeyCount, oauthAppCount] = await pAll([
314
+ async () => {
315
+ const { webhookEndpointState } = await this.teamManager.getWebhookState(teamDid);
316
+ return webhookEndpointState.count({});
317
+ },
318
+ async () => {
319
+ const accessKeyState = await this.getAccessKeyState(teamDid);
320
+ return accessKeyState.count({});
321
+ },
322
+ async () => {
323
+ const { oauthClientState } = await this.teamManager.getOAuthState(teamDid);
324
+ return oauthClientState.count({});
325
+ },
326
+ ]);
327
+
328
+ return {
329
+ webhooks: webhookCount,
330
+ accessKeys: accessKeyCount,
331
+ oauthApps: oauthAppCount,
332
+ };
333
+ },
334
+ // 查询 Studio 创建的 Blocklet 数量
335
+ async () => {
336
+ try {
337
+ const { projectState, releaseState } = await this.teamManager.getProjectState(teamDid);
338
+ const projects = await projectState.getProjects({});
339
+ const projectIds = projects.map((p) => p.id);
340
+ const releasesCount =
341
+ projectIds.length > 0 ? await releaseState.count({ projectId: { [Op.in]: projectIds } }) : 0;
342
+ return {
343
+ blocklets: projects.length,
344
+ releases: releasesCount,
345
+ };
346
+ } catch (error) {
347
+ // 如果项目状态不存在或出错,忽略错误,使用默认值
348
+ logger.debug('Failed to get studio stats', { error, teamDid });
349
+ return { blocklets: 0, releases: 0 };
350
+ }
351
+ },
352
+ ]);
353
+
290
354
  return {
291
355
  user: {
292
356
  users,
@@ -298,6 +362,9 @@ class TeamAPI extends EventEmitter {
298
362
  },
299
363
  backup,
300
364
  appRuntimeInfo,
365
+ traffic,
366
+ integrations,
367
+ studio,
301
368
  };
302
369
  }
303
370
 
@@ -3526,10 +3593,11 @@ class TeamAPI extends EventEmitter {
3526
3593
  if (since && typeof since === 'string') {
3527
3594
  const sinceMatch = since.match(/^(\d+)h$/);
3528
3595
  if (sinceMatch) {
3529
- const hours = parseInt(sinceMatch[1], 10);
3530
- if (hours >= 1 && hours <= 24) {
3531
- startTime = dayjs().subtract(hours, 'hours').toDate();
3596
+ let hours = parseInt(sinceMatch[1], 10);
3597
+ if (hours < 1 || hours > 24) {
3598
+ hours = Math.min(Math.max(hours, 1), 24);
3532
3599
  }
3600
+ startTime = dayjs().subtract(hours, 'hours').toDate();
3533
3601
  }
3534
3602
  }
3535
3603
 
@@ -186,7 +186,6 @@ const {
186
186
  const states = require('../../states');
187
187
  const BaseBlockletManager = require('./base');
188
188
  const { get: getEngine } = require('./engine');
189
- const blockletPm2Events = require('./pm2-events');
190
189
  const runBlockletMigrationScripts = require('../migration');
191
190
  const hooks = require('../hooks');
192
191
  const { BlockletRuntimeMonitor } = require('../../monitor/blocklet-runtime-monitor');
@@ -269,11 +268,6 @@ const startLock = new DBCache(() => ({
269
268
  ...getAbtNodeRedisAndSQLiteUrl(),
270
269
  }));
271
270
 
272
- const pm2StatusMap = {
273
- online: BlockletStatus.running,
274
- stop: BlockletStatus.stopped,
275
- };
276
-
277
271
  const getWalletAppNotification = async (blocklet, tempBlockletInfo) => {
278
272
  let blockletInfo = tempBlockletInfo;
279
273
  if (!blockletInfo) {
@@ -334,7 +328,6 @@ class DiskBlockletManager extends BaseBlockletManager {
334
328
  checkUpdateQueue,
335
329
  reportComponentUsageQueue,
336
330
  resendNotificationQueue,
337
- daemon = false,
338
331
  teamManager,
339
332
  nodeAPI,
340
333
  teamAPI,
@@ -399,11 +392,6 @@ class DiskBlockletManager extends BaseBlockletManager {
399
392
 
400
393
  this.configSynchronizer = new ConfigSynchronizer({ manager: this, states });
401
394
 
402
- if (daemon) {
403
- blockletPm2Events.on('online', (data) => this._syncPm2Status('online', data.blockletDid, data.componentDid));
404
- blockletPm2Events.on('stop', (data) => this._syncPm2Status('stop', data.blockletDid, data.componentDid));
405
- }
406
-
407
395
  if (isFunction(this.checkUpdateQueue?.on)) {
408
396
  /**
409
397
  *
@@ -914,7 +902,8 @@ class DiskBlockletManager extends BaseBlockletManager {
914
902
  }
915
903
 
916
904
  try {
917
- await states.blockletChild.updateChildStatus(parentBlockletId, componentDid, status, false, {
905
+ await states.blockletChild.updateChildStatus(parentBlockletId, componentDid, {
906
+ status,
918
907
  operator,
919
908
  });
920
909
 
@@ -3449,7 +3438,7 @@ class DiskBlockletManager extends BaseBlockletManager {
3449
3438
  return x;
3450
3439
  });
3451
3440
 
3452
- const blueGreenComponentIds = await blueGreenGetComponentIds(oldBlocklet || blocklet, componentDids);
3441
+ const blueGreenComponentIds = blueGreenGetComponentIds(oldBlocklet || blocklet, componentDids);
3453
3442
 
3454
3443
  try {
3455
3444
  if (process.env.ABT_NODE_DISABLE_BLUE_GREEN === 'true' || process.env.ABT_NODE_DISABLE_BLUE_GREEN === '1') {
@@ -4962,20 +4951,6 @@ class DiskBlockletManager extends BaseBlockletManager {
4962
4951
  });
4963
4952
  }
4964
4953
 
4965
- async _syncPm2Status(pm2Status, did, componentDid) {
4966
- try {
4967
- const blocklet = await states.blocklet.getBlocklet(did);
4968
- const component = findComponentByIdV2(blocklet, componentDid);
4969
- if (component && util.shouldUpdateBlockletStatus(component.status)) {
4970
- const newStatus = pm2StatusMap[pm2Status];
4971
- await states.blocklet.setBlockletStatus(did, newStatus, { componentDids: [componentDid] });
4972
- logger.info('sync pm2 status to blocklet', { did, componentDid, pm2Status, newStatus });
4973
- }
4974
- } catch (error) {
4975
- logger.error('sync pm2 status to blocklet failed', { did, componentDid, pm2Status, error });
4976
- }
4977
- }
4978
-
4979
4954
  /**
4980
4955
  * @param {string} action install, upgrade, installComponent, upgradeComponent
4981
4956
  * @param {string} did
@@ -1,7 +1,6 @@
1
1
  const { BlockletStatus } = require('../../../states/blocklet');
2
- const { forEachBlocklet } = require('../../../util/blocklet');
3
2
 
4
- const blueGreenGetComponentIds = async (blocklet, componentDids) => {
3
+ const blueGreenGetComponentIds = (blocklet, componentDids) => {
5
4
  if (!blocklet) {
6
5
  return {
7
6
  componentDids: componentDids || [],
@@ -29,20 +28,16 @@ const blueGreenGetComponentIds = async (blocklet, componentDids) => {
29
28
  ];
30
29
  }
31
30
 
32
- await forEachBlocklet(
33
- blocklet,
34
- (b) => {
35
- if (!componentDidsSet.has(b.meta.did)) {
36
- return;
37
- }
38
- if (runningStatuses.has(b.greenStatus)) {
39
- greenComponentIds.push(b.meta.did);
40
- } else {
41
- blueComponentIds.push(b.meta.did);
42
- }
43
- },
44
- { parallel: true, concurrencyLimit: 10 }
45
- );
31
+ for (const b of children) {
32
+ if (!componentDidsSet.has(b.meta.did)) {
33
+ continue;
34
+ }
35
+ if (runningStatuses.has(b.greenStatus)) {
36
+ greenComponentIds.push(b.meta.did);
37
+ } else {
38
+ blueComponentIds.push(b.meta.did);
39
+ }
40
+ }
46
41
 
47
42
  return [
48
43
  {
@@ -73,6 +73,8 @@ const blueGreenStartBlocklet = async (
73
73
  throw new Error('No runnable component found');
74
74
  }
75
75
 
76
+ blocklet1.children = await states.blocklet.loadChildren(blocklet1.id);
77
+
76
78
  const customerDockerUseVolumeComponentIds = [];
77
79
  const otherComponentIds = [];
78
80
  await forEachBlockletSync(blocklet1, (b) => {
@@ -86,14 +88,6 @@ const blueGreenStartBlocklet = async (
86
88
  }
87
89
  });
88
90
 
89
- if (customerDockerUseVolumeComponentIds.length) {
90
- await manager.stop(
91
- { did, componentDids: customerDockerUseVolumeComponentIds, updateStatus: false, operator },
92
- context
93
- );
94
- await manager.start({ did, componentDids: customerDockerUseVolumeComponentIds, operator }, context);
95
- }
96
-
97
91
  // 分类组件 ID
98
92
  const entryComponentIds = [];
99
93
  const nonEntryComponentIds = [];
@@ -169,7 +163,7 @@ const blueGreenStartBlocklet = async (
169
163
  return;
170
164
  }
171
165
 
172
- const blueGreenComponentIds = await blueGreenGetComponentIds(blocklet1, entryComponentIds);
166
+ const blueGreenComponentIds = blueGreenGetComponentIds(blocklet1, entryComponentIds);
173
167
 
174
168
  const startedBlockletDids = [];
175
169
  const errorBlockletDids = [];
@@ -216,16 +210,9 @@ const blueGreenStartBlocklet = async (
216
210
  };
217
211
 
218
212
  if (status === BlockletStatus.running) {
219
- updates.startedAt = new Date();
220
- updates.stoppedAt = null;
221
- } else if (status === BlockletStatus.error || status === BlockletStatus.stopped) {
222
- updates.stoppedAt = new Date();
223
- }
224
-
225
- await states.blockletChild.updateChildStatus(appId, componentDid, status, isGreen, updates);
226
-
227
- if (status === BlockletStatus.running) {
228
- await states.blockletChild.updateChildStatus(appId, componentDid, BlockletStatus.stopped, !isGreen, updates);
213
+ await states.blockletChild.updateChildStatusRunning(appId, componentDid, isGreen, updates);
214
+ } else {
215
+ await states.blockletChild.updateChildStatusError(appId, componentDid, isGreen, updates);
229
216
  }
230
217
 
231
218
  // Get latest children from blocklet_children table and emit events immediately
@@ -285,6 +272,14 @@ const blueGreenStartBlocklet = async (
285
272
  });
286
273
  }
287
274
 
275
+ if (customerDockerUseVolumeComponentIds.length) {
276
+ await manager.stop(
277
+ { did, componentDids: customerDockerUseVolumeComponentIds, updateStatus: false, operator },
278
+ context
279
+ );
280
+ await manager.start({ did, componentDids: customerDockerUseVolumeComponentIds, operator }, context);
281
+ }
282
+
288
283
  const nextBlocklet = await manager.ensureBlocklet(did, { e2eMode });
289
284
 
290
285
  manager.emit(BlockletEvents.statusChange, nextBlocklet);
@@ -368,7 +363,12 @@ const blueGreenStartBlocklet = async (
368
363
  // Update status immediately
369
364
  await updateChildStatusImmediately(subDid, BlockletStatus.running, item.changeToGreen);
370
365
 
371
- await manager.deleteProcess({ did, componentDids: [subDid], isGreen: !item.changeToGreen });
366
+ await manager.deleteProcess({
367
+ did,
368
+ componentDids: [subDid],
369
+ isGreen: !item.changeToGreen,
370
+ shouldUpdateBlockletStatus: false,
371
+ });
372
372
 
373
373
  logger.info('Green environment started successfully', {
374
374
  did,
@@ -385,7 +385,12 @@ const blueGreenStartBlocklet = async (
385
385
  await updateChildStatusImmediately(subDid, BlockletStatus.error, item.changeToGreen);
386
386
 
387
387
  try {
388
- await manager.deleteProcess({ did, componentDids: [subDid], isGreen: item.changeToGreen });
388
+ await manager.deleteProcess({
389
+ did,
390
+ componentDids: [subDid],
391
+ isGreen: item.changeToGreen,
392
+ shouldUpdateBlockletStatus: false,
393
+ });
389
394
  } catch (cleanupError) {
390
395
  logger.error('Failed to cleanup green environment', { cleanupError });
391
396
  }
@@ -396,7 +401,7 @@ const blueGreenStartBlocklet = async (
396
401
 
397
402
  await pAll(tasks, { concurrency: 6 });
398
403
 
399
- const lastBlocklet = await manager.ensureBlocklet(did, { e2eMode });
404
+ const lastBlocklet = await manager.getBlocklet(did, { e2eMode });
400
405
  let errorDescription = '';
401
406
 
402
407
  // 处理启动失败的组件
@@ -441,8 +446,9 @@ const blueGreenStartBlocklet = async (
441
446
 
442
447
  // 根据情况更新 route table, 会判断只有包含多 interfaces 的 DID 才会更新 route table
443
448
  if (!['true', '1'].includes(process.env.ABT_NODE_DISABLE_BLUE_GREEN)) {
449
+ const latestBlocklet = await manager.getBlocklet(did, { e2eMode });
444
450
  manager.emit(BlockletEvents.blurOrGreenStarted, {
445
- blocklet: lastBlocklet,
451
+ blocklet: latestBlocklet,
446
452
  componentDids,
447
453
  context,
448
454
  });
@@ -39044,7 +39044,7 @@ module.exports = require("zlib");
39044
39044
  /***/ ((module) => {
39045
39045
 
39046
39046
  "use strict";
39047
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@abtnode/core","publishConfig":{"access":"public"},"version":"1.17.4","description":"","main":"lib/index.js","files":["lib"],"scripts":{"lint":"eslint tests lib --ignore-pattern \'tests/assets/*\'","lint:fix":"eslint --fix tests lib"},"keywords":[],"author":"wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)","license":"Apache-2.0","dependencies":{"@abtnode/analytics":"1.17.4","@abtnode/auth":"1.17.4","@abtnode/certificate-manager":"1.17.4","@abtnode/constant":"1.17.4","@abtnode/cron":"1.17.4","@abtnode/db-cache":"1.17.4","@abtnode/docker-utils":"1.17.4","@abtnode/logger":"1.17.4","@abtnode/models":"1.17.4","@abtnode/queue":"1.17.4","@abtnode/rbac":"1.17.4","@abtnode/router-provider":"1.17.4","@abtnode/static-server":"1.17.4","@abtnode/timemachine":"1.17.4","@abtnode/util":"1.17.4","@aigne/aigne-hub":"^0.10.10","@arcblock/did":"^1.27.12","@arcblock/did-connect-js":"^1.27.12","@arcblock/did-ext":"^1.27.12","@arcblock/did-motif":"^1.1.14","@arcblock/did-util":"^1.27.12","@arcblock/event-hub":"^1.27.12","@arcblock/jwt":"^1.27.12","@arcblock/pm2-events":"^0.0.5","@arcblock/validator":"^1.27.12","@arcblock/vc":"^1.27.12","@blocklet/constant":"1.17.4","@blocklet/did-space-js":"^1.2.6","@blocklet/env":"1.17.4","@blocklet/error":"^0.3.3","@blocklet/meta":"1.17.4","@blocklet/resolver":"1.17.4","@blocklet/sdk":"1.17.4","@blocklet/server-js":"1.17.4","@blocklet/store":"1.17.4","@blocklet/theme":"^3.2.11","@fidm/x509":"^1.2.1","@ocap/mcrypto":"^1.27.12","@ocap/util":"^1.27.12","@ocap/wallet":"^1.27.12","@slack/webhook":"^7.0.6","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":{"axios-mock-adapter":"^2.1.0","expand-tilde":"^2.0.2","express":"^4.18.2","unzipper":"^0.10.11"},"gitHead":"e5764f753181ed6a7c615cd4fc6682aacf0cb7cd"}');
39047
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@abtnode/core","publishConfig":{"access":"public"},"version":"1.17.4","description":"","main":"lib/index.js","files":["lib"],"scripts":{"lint":"eslint tests lib --ignore-pattern \'tests/assets/*\'","lint:fix":"eslint --fix tests lib"},"keywords":[],"author":"wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)","license":"Apache-2.0","dependencies":{"@abtnode/analytics":"1.17.4","@abtnode/auth":"1.17.4","@abtnode/certificate-manager":"1.17.4","@abtnode/constant":"1.17.4","@abtnode/cron":"1.17.4","@abtnode/db-cache":"1.17.4","@abtnode/docker-utils":"1.17.4","@abtnode/logger":"1.17.4","@abtnode/models":"1.17.4","@abtnode/queue":"1.17.4","@abtnode/rbac":"1.17.4","@abtnode/router-provider":"1.17.4","@abtnode/static-server":"1.17.4","@abtnode/timemachine":"1.17.4","@abtnode/util":"1.17.4","@aigne/aigne-hub":"^0.10.14","@arcblock/did":"^1.27.14","@arcblock/did-connect-js":"^1.27.14","@arcblock/did-ext":"^1.27.14","@arcblock/did-motif":"^1.1.14","@arcblock/did-util":"^1.27.14","@arcblock/event-hub":"^1.27.14","@arcblock/jwt":"^1.27.14","@arcblock/pm2-events":"^0.0.5","@arcblock/validator":"^1.27.14","@arcblock/vc":"^1.27.14","@blocklet/constant":"1.17.4","@blocklet/did-space-js":"^1.2.9","@blocklet/env":"1.17.4","@blocklet/error":"^0.3.4","@blocklet/meta":"1.17.4","@blocklet/resolver":"1.17.4","@blocklet/sdk":"1.17.4","@blocklet/server-js":"1.17.4","@blocklet/store":"1.17.4","@blocklet/theme":"^3.2.13","@fidm/x509":"^1.2.1","@ocap/mcrypto":"^1.27.14","@ocap/util":"^1.27.14","@ocap/wallet":"^1.27.14","@slack/webhook":"^7.0.6","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":{"axios-mock-adapter":"^2.1.0","expand-tilde":"^2.0.2","express":"^4.18.2","unzipper":"^0.10.11"},"gitHead":"e5764f753181ed6a7c615cd4fc6682aacf0cb7cd"}');
39048
39048
 
39049
39049
  /***/ }),
39050
39050
 
@@ -11,6 +11,7 @@ const isString = require('lodash/isString');
11
11
  const mapValues = require('lodash/mapValues');
12
12
  const map = require('lodash/map');
13
13
  const omit = require('lodash/omit');
14
+ const { Joi } = require('@arcblock/validator');
14
15
  const { joinURL } = require('ufo');
15
16
  const { Op } = require('sequelize');
16
17
  const { getDisplayName } = require('@blocklet/meta/lib/util');
@@ -22,6 +23,8 @@ const BaseState = require('./base');
22
23
  const { parse } = require('../util/ua');
23
24
  const { getScope } = require('../util/audit-log');
24
25
 
26
+ const emailSchema = Joi.string().email().required();
27
+
25
28
  const getServerInfo = (info) =>
26
29
  `[${info.name}](${joinURL(process.env.NODE_ENV === 'production' ? info.routing.adminPath : '', '/settings/about')})`;
27
30
  /**
@@ -232,15 +235,26 @@ const getTaggingInfo = async (args, node, info) => {
232
235
  /**
233
236
  * 隐藏私密信息,主要字段有
234
237
  * 1. email
238
+ * 2. password
235
239
  */
236
- const hidePrivateInfo = (result) => {
237
- const emailRegex =
238
- /^[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])?)*$/;
240
+ const hidePrivateInfo = (result, fields = []) => {
241
+ // 默认需要隐藏的字段(不区分大小写匹配)
242
+ const defaultFields = ['email', 'password'];
243
+ // 合并默认字段和自定义字段
244
+ const sensitiveFields = [...defaultFields, ...fields].map((f) => f.toLowerCase());
245
+
246
+ // 检查字段名是否需要隐藏
247
+ const isSensitiveField = (key) => {
248
+ if (!key) return false;
249
+ const lowerKey = key.toLowerCase();
250
+ return sensitiveFields.some((field) => lowerKey.includes(field));
251
+ };
239
252
 
240
253
  const processValue = (value, key) => {
241
- // 如果字段名包含 email 或值是邮箱格式,则隐藏
242
- if ((key && /email/i.test(key)) || (isString(value) && emailRegex.test(value))) {
243
- return '***';
254
+ // 如果字段名匹配敏感字段,则隐藏
255
+ // 使用反引号包裹,以代码样式显示星号,避免被 Markdown 渲染为加粗/斜体
256
+ if (isSensitiveField(key) || (isString(value) && !emailSchema.validate(value).error)) {
257
+ return '`***`';
244
258
  }
245
259
 
246
260
  // 递归处理对象
@@ -351,8 +365,19 @@ const getLogContent = async (action, args, context, result, info, node) => {
351
365
  return `blocklet ${getBlockletInfo(result, info)} audit federated login member ${args.memberPid} with status: ${
352
366
  args.status
353
367
  }`;
354
- case 'configNotification':
355
- return `updated following notification setting: ${args.notification}`;
368
+ case 'configNotification': {
369
+ const { notification } = args;
370
+ const notificationObject = JSON.parse(notification || '{}');
371
+ const resultObj = {};
372
+ if (notificationObject.email) {
373
+ resultObj.email = hidePrivateInfo(notificationObject.email, ['from', 'host', 'password']);
374
+ }
375
+ if (notificationObject.push) {
376
+ resultObj.push = hidePrivateInfo(notificationObject.push);
377
+ }
378
+
379
+ return `updated following notification setting: ${JSON.stringify(resultObj)}`;
380
+ }
356
381
  case 'updateComponentTitle':
357
382
  return `update component title to **${args.title}**`;
358
383
  case 'updateComponentMountPoint':
@@ -1031,7 +1056,7 @@ class AuditLogState extends BaseState {
1031
1056
  componentDid: context?.user?.componentDid || null,
1032
1057
  });
1033
1058
 
1034
- logger.info('create', data);
1059
+ logger.info('create', { action, userDid: actor.did, componentDid: context?.user?.componentDid || null });
1035
1060
  return resolve(data);
1036
1061
  } catch (err) {
1037
1062
  logger.error('create error', { error: err, action, args, context });
@@ -35,30 +35,109 @@ class BlockletChildState extends BaseState {
35
35
  return this.remove({ parentBlockletId });
36
36
  }
37
37
 
38
+ async updateChildStatusRunning(parentBlockletId, childDid, isGreen, additionalUpdates = {}) {
39
+ const now = new Date();
40
+ const baseUpdates = {
41
+ ...additionalUpdates,
42
+ updatedAt: now,
43
+ startedAt: now,
44
+ stoppedAt: null,
45
+ };
46
+
47
+ if (isGreen) {
48
+ // 绿环境启动成功 -> 绿 running,蓝 stopped
49
+ await this.update(
50
+ { parentBlockletId, childDid },
51
+ {
52
+ $set: {
53
+ ...baseUpdates,
54
+ greenStatus: BlockletStatus.running,
55
+ status: BlockletStatus.stopped,
56
+ },
57
+ }
58
+ );
59
+ } else {
60
+ // 蓝环境启动成功 -> 蓝 running,绿 stopped
61
+ await this.update(
62
+ { parentBlockletId, childDid },
63
+ {
64
+ $set: {
65
+ ...baseUpdates,
66
+ greenStatus: BlockletStatus.stopped,
67
+ status: BlockletStatus.running,
68
+ },
69
+ }
70
+ );
71
+ }
72
+ }
73
+
74
+ async updateChildStatusError(parentBlockletId, childDid, isGreen, additionalUpdates = {}) {
75
+ const now = new Date();
76
+ const baseUpdates = {
77
+ ...additionalUpdates,
78
+ updatedAt: now,
79
+ };
80
+
81
+ if (isGreen) {
82
+ // 绿环境启动失败
83
+ await this.update(
84
+ { parentBlockletId, childDid },
85
+ {
86
+ $set: {
87
+ ...baseUpdates,
88
+ greenStatus: BlockletStatus.error,
89
+ },
90
+ }
91
+ );
92
+ } else {
93
+ // 蓝环境启动失败
94
+ await this.update(
95
+ { parentBlockletId, childDid },
96
+ {
97
+ $set: {
98
+ ...baseUpdates,
99
+ status: BlockletStatus.error,
100
+ },
101
+ }
102
+ );
103
+ }
104
+ }
105
+
38
106
  /**
39
- * Update child status by parent blocklet ID and child DID
107
+ * Update child status only (without overwriting other fields)
108
+ * Used by setBlockletStatus to avoid race conditions in multi-process environments
40
109
  * @param {string} parentBlockletId - The parent blocklet ID
41
110
  * @param {string} childDid - The child DID
42
- * @param {number} status - The status to set
43
- * @param {Object} additionalFields - Additional fields to update
44
- * @param {boolean} isGreen - Whether the child is green
111
+ * @param {Object} options - Update options
112
+ * @param {number} options.status - The blue status to set
113
+ * @param {number} options.greenStatus - The green status to set
114
+ * @param {boolean} options.isGreen - Whether to update green status
115
+ * @param {boolean} options.isGreenAndBlue - Whether to update both statuses
116
+ * @param {string} options.operator - The operator
45
117
  * @returns {Promise<Object>} - Updated child
46
118
  */
47
- async updateChildStatus(parentBlockletId, childDid, status, isGreen = false, additionalFields = {}) {
48
- if (!parentBlockletId) {
49
- return null;
50
- }
51
- const children = await this.getChildrenByParentId(parentBlockletId);
52
- const child = children.find((c) => c.childDid === childDid);
53
-
54
- if (!child) {
119
+ async updateChildStatus(
120
+ parentBlockletId,
121
+ childDid,
122
+ { status, isGreen = false, isGreenAndBlue = false, operator } = {}
123
+ ) {
124
+ if (!parentBlockletId || !childDid) {
55
125
  return null;
56
126
  }
57
127
 
58
128
  const updates = {
59
- ...additionalFields,
129
+ updatedAt: new Date(),
130
+ inProgressStart: Date.now(),
60
131
  };
61
- if (isGreen) {
132
+
133
+ if (operator) {
134
+ updates.operator = operator;
135
+ }
136
+
137
+ if (isGreenAndBlue) {
138
+ updates.status = status;
139
+ updates.greenStatus = status;
140
+ } else if (isGreen) {
62
141
  updates.greenStatus = status;
63
142
  } else {
64
143
  updates.status = status;
@@ -72,47 +151,42 @@ class BlockletChildState extends BaseState {
72
151
  updates.stoppedAt = new Date();
73
152
  }
74
153
 
75
- const [, [updated]] = await this.update({ id: child.id }, { $set: updates });
154
+ const [, [updated]] = await this.update({ parentBlockletId, childDid }, { $set: updates });
76
155
  return updated;
77
156
  }
78
157
 
79
158
  /**
80
- * Batch update children status
159
+ * Update child ports only (without affecting status fields)
160
+ * Used by refreshBlockletPorts to avoid overwriting status during concurrent operations
81
161
  * @param {string} parentBlockletId - The parent blocklet ID
82
- * @param {Array<string>} childDids - Array of child DIDs to update
83
- * @param {number} status - The status to set
84
- * @param {Object} additionalFields - Additional fields to update
85
- * @returns {Promise<Array>} - Updated children
162
+ * @param {string} childDid - The child DID
163
+ * @param {Object} ports - The ports to set (for blue environment)
164
+ * @param {Object} greenPorts - The green ports to set (for green environment)
165
+ * @returns {Promise<Object>} - Updated child
86
166
  */
87
- async batchUpdateChildrenStatus(parentBlockletId, childDids, status, additionalFields = {}) {
88
- if (!parentBlockletId) {
89
- return [];
167
+ async updateChildPorts(parentBlockletId, childDid, { ports, greenPorts } = {}) {
168
+ if (!parentBlockletId || !childDid) {
169
+ return null;
90
170
  }
91
- const children = await this.getChildrenByParentId(parentBlockletId);
92
- const updates = [];
93
-
94
- for (const child of children) {
95
- if (childDids.includes(child.childDid)) {
96
- const childUpdates = {
97
- status,
98
- ...additionalFields,
99
- };
100
-
101
- if (status === BlockletStatus.running) {
102
- childUpdates.startedAt = new Date();
103
- childUpdates.stoppedAt = null;
104
- } else if (status === BlockletStatus.stopped) {
105
- childUpdates.startedAt = null;
106
- childUpdates.stoppedAt = new Date();
107
- }
108
171
 
109
- // eslint-disable-next-line no-await-in-loop
110
- await this.update({ id: child.id }, { $set: childUpdates });
111
- updates.push({ ...child, ...childUpdates });
112
- }
172
+ const updates = {
173
+ updatedAt: new Date(),
174
+ };
175
+
176
+ if (ports !== undefined) {
177
+ updates.ports = ports;
178
+ }
179
+ if (greenPorts !== undefined) {
180
+ updates.greenPorts = greenPorts;
113
181
  }
114
182
 
115
- return updates;
183
+ // Only update if there's something to update
184
+ if (Object.keys(updates).length <= 1) {
185
+ return null;
186
+ }
187
+
188
+ const [, [updated]] = await this.update({ parentBlockletId, childDid }, { $set: updates });
189
+ return updated;
116
190
  }
117
191
  }
118
192
 
@@ -5,12 +5,20 @@ const logger = require('@abtnode/logger')('@abtnode/core:states:blocklet-extras'
5
5
  const camelCase = require('lodash/camelCase');
6
6
  const get = require('lodash/get');
7
7
  const { CustomError } = require('@blocklet/error');
8
+ const security = require('@abtnode/util/lib/security');
9
+ const cloneDeep = require('@abtnode/util/lib/deep-clone');
8
10
 
9
11
  const BaseState = require('./base');
10
12
 
11
13
  const { mergeConfigs, parseConfigs, encryptConfigs } = require('../blocklet/extras');
12
14
  const { validateAddMeta } = require('../validators/blocklet-extra');
13
15
 
16
+ // settings 中需要加密的字段路径
17
+ const SETTINGS_SECURE_FIELDS = ['notification.email.password'];
18
+
19
+ // 加密数据的前缀标记,用于识别数据是否已加密
20
+ const ENCRYPTED_PREFIX = 'ENC:';
21
+
14
22
  const noop = (k) => (v) => v[k];
15
23
 
16
24
  /**
@@ -31,15 +39,69 @@ class BlockletExtrasState extends BaseState {
31
39
  // setting
32
40
  {
33
41
  name: 'settings',
34
- beforeSet: ({ old, cur }) => {
42
+ beforeSet: ({ old, cur, did, dek }) => {
35
43
  const merged = { ...old, ...cur };
36
44
  Object.keys(merged).forEach((key) => {
37
45
  if (merged[key] === undefined || merged[key] === null) {
38
46
  delete merged[key];
39
47
  }
40
48
  });
49
+
50
+ // 对敏感字段进行加密
51
+ const enableSecurity = dek && did;
52
+ if (enableSecurity) {
53
+ SETTINGS_SECURE_FIELDS.forEach((fieldPath) => {
54
+ const value = get(merged, fieldPath);
55
+ // 只加密 cur 中传入的新值,避免重复加密已存储的旧值
56
+ const newValue = get(cur, fieldPath);
57
+ if (newValue !== undefined && value) {
58
+ const keys = fieldPath.split('.');
59
+ let target = merged;
60
+ for (let i = 0; i < keys.length - 1; i++) {
61
+ target = target[keys[i]];
62
+ }
63
+ // 添加前缀标记,用于识别已加密的数据
64
+ const encrypted = ENCRYPTED_PREFIX + security.encrypt(String(value), did, dek);
65
+ target[keys[keys.length - 1]] = encrypted;
66
+ }
67
+ });
68
+ }
69
+
41
70
  return merged;
42
71
  },
72
+ afterGet: ({ data, did, dek }) => {
73
+ if (!data) {
74
+ return data;
75
+ }
76
+
77
+ // 对敏感字段进行解密
78
+ const enableSecurity = dek && did;
79
+ if (enableSecurity) {
80
+ const result = cloneDeep(data);
81
+ SETTINGS_SECURE_FIELDS.forEach((fieldPath) => {
82
+ const value = get(result, fieldPath);
83
+ // 只有带有加密前缀的数据才需要解密,未加密的历史数据保持原值
84
+ if (value && typeof value === 'string' && value.startsWith(ENCRYPTED_PREFIX)) {
85
+ try {
86
+ const keys = fieldPath.split('.');
87
+ let target = result;
88
+ for (let i = 0; i < keys.length - 1; i++) {
89
+ target = target[keys[i]];
90
+ }
91
+ // 去掉前缀后解密
92
+ const encryptedValue = value.slice(ENCRYPTED_PREFIX.length);
93
+ target[keys[keys.length - 1]] = security.decrypt(encryptedValue, did, dek);
94
+ } catch {
95
+ // 解密失败,保持原值(去掉前缀)
96
+ logger.warn('Failed to decrypt settings field', { fieldPath });
97
+ }
98
+ }
99
+ });
100
+ return result;
101
+ }
102
+
103
+ return data;
104
+ },
43
105
  },
44
106
  ];
45
107
 
@@ -300,6 +300,7 @@ class BlockletState extends BaseState {
300
300
  mode: child.mode || 'production',
301
301
  status: child.status || 0,
302
302
  ports: child.ports || {},
303
+ environments: child.environments || [],
303
304
  children: child.children || [],
304
305
  migratedFrom: child.migratedFrom || [],
305
306
  installedAt: child.installedAt,
@@ -384,12 +385,11 @@ class BlockletState extends BaseState {
384
385
  if (child.deployedFrom !== undefined) updates.deployedFrom = child.deployedFrom;
385
386
  if (child.mode !== undefined) updates.mode = child.mode;
386
387
  if (child.ports !== undefined) updates.ports = child.ports;
387
- if (child.children !== undefined) updates.children = child.children || [];
388
- if (child.migratedFrom !== undefined) updates.migratedFrom = child.migratedFrom;
388
+ if (child.environments !== undefined) updates.environments = child.environments;
389
389
 
390
390
  // Only update status-related fields if explicitly provided
391
391
  if (child.status !== undefined) updates.status = child.status;
392
- if (child.installedAt !== undefined) updates.installedAt = child.installedAt;
392
+ // Note: installedAt should only be set on first install, never updated
393
393
  if (child.startedAt !== undefined) updates.startedAt = child.startedAt;
394
394
  if (child.stoppedAt !== undefined) updates.stoppedAt = child.stoppedAt;
395
395
  if (child.pausedAt !== undefined) updates.pausedAt = child.pausedAt;
@@ -418,9 +418,8 @@ class BlockletState extends BaseState {
418
418
  mode: child.mode || 'production',
419
419
  status: child.status || 0,
420
420
  ports: child.ports || {},
421
- children: child.children || [],
422
- migratedFrom: child.migratedFrom || [],
423
- installedAt: child.installedAt,
421
+ environments: child.environments || [],
422
+ installedAt: new Date(),
424
423
  startedAt: child.startedAt,
425
424
  stoppedAt: child.stoppedAt,
426
425
  pausedAt: child.pausedAt,
@@ -865,8 +864,17 @@ class BlockletState extends BaseState {
865
864
  if (actuallyRefreshedDids.length > 0) {
866
865
  await this.updateBlocklet(did, {});
867
866
 
868
- // Save updated children to BlockletChild table
869
- await this.saveChildren(blocklet.id, blocklet.meta.did, blocklet.children);
867
+ // Only update ports/greenPorts to avoid overwriting status during concurrent operations
868
+ if (this.BlockletChildState) {
869
+ for (const component of blocklet.children) {
870
+ if (actuallyRefreshedDids.includes(component.meta?.did)) {
871
+ await this.BlockletChildState.updateChildPorts(blocklet.id, component.meta.did, {
872
+ ports: component.ports,
873
+ greenPorts: component.greenPorts,
874
+ });
875
+ }
876
+ }
877
+ }
870
878
  }
871
879
 
872
880
  return {
@@ -986,6 +994,8 @@ class BlockletState extends BaseState {
986
994
  return res;
987
995
  }
988
996
 
997
+ // Collect components to update
998
+ const componentsToUpdate = [];
989
999
  for (const component of doc.children || []) {
990
1000
  if (component.meta.group === BlockletGroup.gateway) {
991
1001
  continue;
@@ -995,6 +1005,9 @@ class BlockletState extends BaseState {
995
1005
  continue;
996
1006
  }
997
1007
 
1008
+ componentsToUpdate.push(component.meta.did);
1009
+
1010
+ // Update in-memory for return value
998
1011
  component[isGreen ? 'greenStatus' : 'status'] = status;
999
1012
  if (isGreenAndBlue) {
1000
1013
  component.greenStatus = status;
@@ -1020,11 +1033,26 @@ class BlockletState extends BaseState {
1020
1033
  updateData.status = status;
1021
1034
  }
1022
1035
 
1023
- updateData.children = doc.children;
1024
-
1036
+ // Update blocklet without children to avoid overwriting status during concurrent operations
1025
1037
  const res = await this.updateBlocklet(did, updateData);
1026
1038
 
1027
- res.children = doc.children;
1039
+ // Update each component's status individually using BlockletChildState
1040
+ if (this.BlockletChildState && componentsToUpdate.length > 0) {
1041
+ for (const componentDid of componentsToUpdate) {
1042
+ await this.BlockletChildState.updateChildStatus(doc.id, componentDid, {
1043
+ status,
1044
+ isGreen,
1045
+ isGreenAndBlue,
1046
+ operator,
1047
+ });
1048
+ }
1049
+ }
1050
+
1051
+ const children = await this.loadChildren(doc.id);
1052
+
1053
+ res.children = children;
1054
+ // Recalculate status after children are loaded with updated status
1055
+ res.status = getBlockletStatus(res);
1028
1056
  return res;
1029
1057
  } finally {
1030
1058
  await lock.releaseLock(lockName);
@@ -965,11 +965,13 @@ class NotificationState extends BaseState {
965
965
  throw new Error('Invalid since format. Expected format: "1h", "2h", "24h", etc.');
966
966
  }
967
967
 
968
- const hours = parseInt(sinceMatch[1], 10);
968
+ let hours = parseInt(sinceMatch[1], 10);
969
969
 
970
970
  // 验证范围:最小 1h,最大 24h
971
971
  if (hours < 1 || hours > 24) {
972
- throw new Error('The since parameter must be between 1h and 24h.');
972
+ logger.warn('The since parameter must be between 1h and 24h.');
973
+ // 限制 hours 在 1-24 范围内
974
+ hours = Math.min(Math.max(hours, 1), 24);
973
975
  }
974
976
 
975
977
  // 计算时间范围
@@ -1892,6 +1892,7 @@ const formatBlockletTheme = (rawTheme) => {
1892
1892
  themeConfig = {
1893
1893
  ...concept.themeConfig,
1894
1894
  prefer: concept.prefer,
1895
+ name: concept.name,
1895
1896
  };
1896
1897
  } else {
1897
1898
  // 兼容旧数据
@@ -1900,6 +1901,7 @@ const formatBlockletTheme = (rawTheme) => {
1900
1901
  dark: rawTheme.dark || {},
1901
1902
  common: rawTheme.common || {},
1902
1903
  prefer: rawTheme.prefer || 'system',
1904
+ name: rawTheme.name || 'Default',
1903
1905
  };
1904
1906
  }
1905
1907
  }
@@ -14,6 +14,105 @@ const notCheckPrimaryKeyTableNames = new Set(['tagging']);
14
14
 
15
15
  const needBreakErrors = [];
16
16
 
17
+ /**
18
+ * Generate a unique ID for blocklet_children records
19
+ */
20
+ function generateChildId() {
21
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
22
+ }
23
+
24
+ /**
25
+ * 因为删除了表字段,所以需要单独处理,不然 migrate 有旧的 children 数据会失败
26
+ * @param {object} params - Parameters
27
+ * @param {Sequelize} params.pgDb - PostgreSQL database connection
28
+ * @param {string} params.blockletId - Parent blocklet ID
29
+ * @param {string} params.parentBlockletDid - Parent blocklet DID
30
+ * @param {Array} params.children - Children array to migrate
31
+ */
32
+ async function migrateBlockletChildrenToTable({ pgDb, blockletId, parentBlockletDid, children }) {
33
+ if (!Array.isArray(children) || children.length === 0) {
34
+ return;
35
+ }
36
+
37
+ for (const child of children) {
38
+ const childMeta = child?.meta || {};
39
+ const childDid = childMeta?.did;
40
+
41
+ if (!childDid) {
42
+ console.warn(` ⚠️ Child in blocklet ${blockletId} has no meta.did, skipping`);
43
+ continue;
44
+ }
45
+
46
+ try {
47
+ // Check if child already exists
48
+ const [existing] = await pgDb.query(
49
+ 'SELECT id FROM blocklet_children WHERE "parentBlockletId" = $1 AND "childDid" = $2 LIMIT 1',
50
+ { bind: [blockletId, childDid], type: QueryTypes.SELECT }
51
+ );
52
+
53
+ if (existing) {
54
+ console.log(` ℹ️ Child ${childDid} already exists for blocklet ${blockletId}, skipping`);
55
+ continue;
56
+ }
57
+
58
+ // Insert child record
59
+ const insertSQL = `
60
+ INSERT INTO blocklet_children (
61
+ id, "parentBlockletId", "parentBlockletDid", "childDid", "mountPoint",
62
+ meta, "bundleSource", source, "deployedFrom", mode, status,
63
+ ports, environments, "installedAt", "startedAt",
64
+ "stoppedAt", "pausedAt", operator, "inProgressStart", "greenStatus",
65
+ "greenPorts", "createdAt", "updatedAt"
66
+ ) VALUES (
67
+ $1, $2, $3, $4, $5,
68
+ $6::jsonb, $7::jsonb, $8, $9, $10, $11,
69
+ $12::jsonb, $13::jsonb, $14, $15,
70
+ $16, $17, $18, $19, $20,
71
+ $21::jsonb, $22, $23
72
+ )
73
+ ON CONFLICT DO NOTHING
74
+ `;
75
+
76
+ const now = new Date();
77
+ const bindValues = [
78
+ generateChildId(), // id
79
+ blockletId, // parentBlockletId
80
+ parentBlockletDid, // parentBlockletDid
81
+ childDid, // childDid
82
+ child.mountPoint || null, // mountPoint
83
+ JSON.stringify(child.meta || {}), // meta
84
+ JSON.stringify(child.bundleSource || {}), // bundleSource
85
+ child.source || 0, // source
86
+ child.deployedFrom || '', // deployedFrom
87
+ child.mode || 'production', // mode
88
+ child.status || 0, // status
89
+ JSON.stringify(child.ports || {}), // ports
90
+ JSON.stringify(child.environments || []), // environments
91
+ child.installedAt || null, // installedAt
92
+ child.startedAt || null, // startedAt
93
+ child.stoppedAt || null, // stoppedAt
94
+ child.pausedAt || null, // pausedAt
95
+ child.operator || null, // operator
96
+ child.inProgressStart || null, // inProgressStart
97
+ child.greenStatus || null, // greenStatus
98
+ child.greenPorts ? JSON.stringify(child.greenPorts) : null, // greenPorts
99
+ now, // createdAt
100
+ now, // updatedAt
101
+ ];
102
+
103
+ await pgDb.query(insertSQL, { bind: bindValues });
104
+ console.log(` ✅ Migrated child ${childDid} to blocklet_children table`);
105
+ } catch (err) {
106
+ // Ignore unique constraint errors
107
+ if (err.name === 'SequelizeUniqueConstraintError' || err.message?.includes('UNIQUE constraint')) {
108
+ console.log(` ℹ️ Child ${childDid} already exists (unique constraint), skipping`);
109
+ continue;
110
+ }
111
+ console.error(` ❌ Failed to migrate child ${childDid}:`, err.message);
112
+ }
113
+ }
114
+ }
115
+
17
116
  function sortTableNames(tableNames, sort) {
18
117
  return [...tableNames].sort((a, b) => {
19
118
  const indexA = sort.indexOf(a);
@@ -50,7 +149,8 @@ async function migrateAllTablesNoModels(dbPath) {
50
149
  .filter((name) => !/^(sqlite|sequelize)/.test(name.toLowerCase()) && name !== 'runtime_insights');
51
150
 
52
151
  // 把 tableNames 排序, 把被依赖的表放前面
53
- tableNames = sortTableNames(tableNames, ['users', 'notification_receivers']);
152
+ // blocklet_children 需要在 blocklets 之前处理,因为 blocklets 的 children 字段需要迁移到 blocklet_children 表
153
+ tableNames = sortTableNames(tableNames, ['users', 'notification_receivers', 'blocklet_children', 'blocklets']);
54
154
 
55
155
  for (const tableName of tableNames) {
56
156
  console.log(`\n➡️ Starting migration for table: ${dbPath} ${tableName}`);
@@ -71,6 +171,14 @@ async function migrateAllTablesNoModels(dbPath) {
71
171
  if (dbPath.includes('server.db') && tableName === 'blocklets') {
72
172
  allCols = allCols.filter((c) => c !== 'controller');
73
173
  }
174
+
175
+ // 删除 blocklets 表中的 children 列, 因为 children 已经拆分到 blocklet_children 表
176
+ // children 数据会在迁移过程中单独处理
177
+ const hasChildrenColumn = tableName === 'blocklets' && sqliteSchema.children;
178
+ if (hasChildrenColumn) {
179
+ allCols = allCols.filter((c) => c !== 'children');
180
+ console.log(' ℹ️ Detected children column in blocklets table, will migrate to blocklet_children table');
181
+ }
74
182
  let pkCols = allCols.filter((c) => sqliteSchema[c].primaryKey);
75
183
  if (!pkCols.length) {
76
184
  pkCols = [allCols[0]];
@@ -167,6 +275,59 @@ async function migrateAllTablesNoModels(dbPath) {
167
275
  console.log(` Migrating rows ${offset + 1}-${offset + rows.length}`);
168
276
 
169
277
  for (const row of rows) {
278
+ // Handle children migration for blocklets table
279
+ if (hasChildrenColumn && row.children) {
280
+ try {
281
+ let children = row.children;
282
+ if (typeof children === 'string') {
283
+ try {
284
+ children = JSON.parse(children);
285
+ } catch {
286
+ children = null;
287
+ }
288
+ } else if (Buffer.isBuffer(children)) {
289
+ try {
290
+ children = JSON.parse(children.toString('utf8'));
291
+ } catch {
292
+ children = null;
293
+ }
294
+ }
295
+
296
+ if (Array.isArray(children) && children.length > 0) {
297
+ // Get parent blocklet DID from meta
298
+ let meta = row.meta;
299
+ if (typeof meta === 'string') {
300
+ try {
301
+ meta = JSON.parse(meta);
302
+ } catch {
303
+ meta = {};
304
+ }
305
+ } else if (Buffer.isBuffer(meta)) {
306
+ try {
307
+ meta = JSON.parse(meta.toString('utf8'));
308
+ } catch {
309
+ meta = {};
310
+ }
311
+ }
312
+
313
+ const parentBlockletDid = meta?.did;
314
+ if (parentBlockletDid) {
315
+ console.log(` 🔄 Migrating ${children.length} children for blocklet ${row.id}`);
316
+ await migrateBlockletChildrenToTable({
317
+ pgDb,
318
+ blockletId: row.id,
319
+ parentBlockletDid,
320
+ children,
321
+ });
322
+ } else {
323
+ console.warn(` ⚠️ Blocklet ${row.id} has no meta.did, skipping children migration`);
324
+ }
325
+ }
326
+ } catch (err) {
327
+ console.error(` ❌ Failed to migrate children for blocklet ${row.id}:`, err.message);
328
+ }
329
+ }
330
+
170
331
  // Fix invalid date values for all DATE/TIMESTAMP columns
171
332
  for (const dateCol of dateCols) {
172
333
  if (row[dateCol] != null) {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.17.5-beta-20251211-104355-426d7eb6",
6
+ "version": "1.17.5-beta-20251214-231110-497f8d27",
7
7
  "description": "",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -17,46 +17,46 @@
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.5-beta-20251211-104355-426d7eb6",
21
- "@abtnode/auth": "1.17.5-beta-20251211-104355-426d7eb6",
22
- "@abtnode/certificate-manager": "1.17.5-beta-20251211-104355-426d7eb6",
23
- "@abtnode/constant": "1.17.5-beta-20251211-104355-426d7eb6",
24
- "@abtnode/cron": "1.17.5-beta-20251211-104355-426d7eb6",
25
- "@abtnode/db-cache": "1.17.5-beta-20251211-104355-426d7eb6",
26
- "@abtnode/docker-utils": "1.17.5-beta-20251211-104355-426d7eb6",
27
- "@abtnode/logger": "1.17.5-beta-20251211-104355-426d7eb6",
28
- "@abtnode/models": "1.17.5-beta-20251211-104355-426d7eb6",
29
- "@abtnode/queue": "1.17.5-beta-20251211-104355-426d7eb6",
30
- "@abtnode/rbac": "1.17.5-beta-20251211-104355-426d7eb6",
31
- "@abtnode/router-provider": "1.17.5-beta-20251211-104355-426d7eb6",
32
- "@abtnode/static-server": "1.17.5-beta-20251211-104355-426d7eb6",
33
- "@abtnode/timemachine": "1.17.5-beta-20251211-104355-426d7eb6",
34
- "@abtnode/util": "1.17.5-beta-20251211-104355-426d7eb6",
35
- "@aigne/aigne-hub": "^0.10.10",
36
- "@arcblock/did": "^1.27.12",
37
- "@arcblock/did-connect-js": "^1.27.12",
38
- "@arcblock/did-ext": "^1.27.12",
20
+ "@abtnode/analytics": "1.17.5-beta-20251214-231110-497f8d27",
21
+ "@abtnode/auth": "1.17.5-beta-20251214-231110-497f8d27",
22
+ "@abtnode/certificate-manager": "1.17.5-beta-20251214-231110-497f8d27",
23
+ "@abtnode/constant": "1.17.5-beta-20251214-231110-497f8d27",
24
+ "@abtnode/cron": "1.17.5-beta-20251214-231110-497f8d27",
25
+ "@abtnode/db-cache": "1.17.5-beta-20251214-231110-497f8d27",
26
+ "@abtnode/docker-utils": "1.17.5-beta-20251214-231110-497f8d27",
27
+ "@abtnode/logger": "1.17.5-beta-20251214-231110-497f8d27",
28
+ "@abtnode/models": "1.17.5-beta-20251214-231110-497f8d27",
29
+ "@abtnode/queue": "1.17.5-beta-20251214-231110-497f8d27",
30
+ "@abtnode/rbac": "1.17.5-beta-20251214-231110-497f8d27",
31
+ "@abtnode/router-provider": "1.17.5-beta-20251214-231110-497f8d27",
32
+ "@abtnode/static-server": "1.17.5-beta-20251214-231110-497f8d27",
33
+ "@abtnode/timemachine": "1.17.5-beta-20251214-231110-497f8d27",
34
+ "@abtnode/util": "1.17.5-beta-20251214-231110-497f8d27",
35
+ "@aigne/aigne-hub": "^0.10.14",
36
+ "@arcblock/did": "^1.27.14",
37
+ "@arcblock/did-connect-js": "^1.27.14",
38
+ "@arcblock/did-ext": "^1.27.14",
39
39
  "@arcblock/did-motif": "^1.1.14",
40
- "@arcblock/did-util": "^1.27.12",
41
- "@arcblock/event-hub": "^1.27.12",
42
- "@arcblock/jwt": "^1.27.12",
40
+ "@arcblock/did-util": "^1.27.14",
41
+ "@arcblock/event-hub": "^1.27.14",
42
+ "@arcblock/jwt": "^1.27.14",
43
43
  "@arcblock/pm2-events": "^0.0.5",
44
- "@arcblock/validator": "^1.27.12",
45
- "@arcblock/vc": "^1.27.12",
46
- "@blocklet/constant": "1.17.5-beta-20251211-104355-426d7eb6",
47
- "@blocklet/did-space-js": "^1.2.6",
48
- "@blocklet/env": "1.17.5-beta-20251211-104355-426d7eb6",
49
- "@blocklet/error": "^0.3.3",
50
- "@blocklet/meta": "1.17.5-beta-20251211-104355-426d7eb6",
51
- "@blocklet/resolver": "1.17.5-beta-20251211-104355-426d7eb6",
52
- "@blocklet/sdk": "1.17.5-beta-20251211-104355-426d7eb6",
53
- "@blocklet/server-js": "1.17.5-beta-20251211-104355-426d7eb6",
54
- "@blocklet/store": "1.17.5-beta-20251211-104355-426d7eb6",
55
- "@blocklet/theme": "^3.2.11",
44
+ "@arcblock/validator": "^1.27.14",
45
+ "@arcblock/vc": "^1.27.14",
46
+ "@blocklet/constant": "1.17.5-beta-20251214-231110-497f8d27",
47
+ "@blocklet/did-space-js": "^1.2.9",
48
+ "@blocklet/env": "1.17.5-beta-20251214-231110-497f8d27",
49
+ "@blocklet/error": "^0.3.4",
50
+ "@blocklet/meta": "1.17.5-beta-20251214-231110-497f8d27",
51
+ "@blocklet/resolver": "1.17.5-beta-20251214-231110-497f8d27",
52
+ "@blocklet/sdk": "1.17.5-beta-20251214-231110-497f8d27",
53
+ "@blocklet/server-js": "1.17.5-beta-20251214-231110-497f8d27",
54
+ "@blocklet/store": "1.17.5-beta-20251214-231110-497f8d27",
55
+ "@blocklet/theme": "^3.2.13",
56
56
  "@fidm/x509": "^1.2.1",
57
- "@ocap/mcrypto": "^1.27.12",
58
- "@ocap/util": "^1.27.12",
59
- "@ocap/wallet": "^1.27.12",
57
+ "@ocap/mcrypto": "^1.27.14",
58
+ "@ocap/util": "^1.27.14",
59
+ "@ocap/wallet": "^1.27.14",
60
60
  "@slack/webhook": "^7.0.6",
61
61
  "archiver": "^7.0.1",
62
62
  "axios": "^1.7.9",
@@ -116,5 +116,5 @@
116
116
  "express": "^4.18.2",
117
117
  "unzipper": "^0.10.11"
118
118
  },
119
- "gitHead": "9ea2a8077a8edbf5021281e823719758eb9fe02c"
119
+ "gitHead": "aa782962d97ed5d9a994268621e9a652c995c783"
120
120
  }
@@ -1,18 +0,0 @@
1
- const blueGreenUpdateBlockletStatus = async ({ states, did, status, blueGreenComponentIds }) => {
2
- const outputBlocklet = {};
3
- await Promise.all(
4
- blueGreenComponentIds.map(async (item) => {
5
- if (!item.componentDids.length) {
6
- return;
7
- }
8
- const res = await states.blocklet.setBlockletStatus(did, status, {
9
- componentDids: item.componentDids,
10
- isGreen: item.changeToGreen,
11
- });
12
- Object.assign(outputBlocklet, res);
13
- })
14
- );
15
- return outputBlocklet;
16
- };
17
-
18
- module.exports = { blueGreenUpdateBlockletStatus };