@abtnode/core 1.17.8-beta-20260108-224855-28496abb → 1.17.8-beta-20260111-112953-aed5ff39

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.
Files changed (66) hide show
  1. package/lib/api/team/access-key-manager.js +104 -0
  2. package/lib/api/team/invitation-manager.js +461 -0
  3. package/lib/api/team/notification-manager.js +189 -0
  4. package/lib/api/team/oauth-manager.js +60 -0
  5. package/lib/api/team/org-crud-manager.js +202 -0
  6. package/lib/api/team/org-manager.js +56 -0
  7. package/lib/api/team/org-member-manager.js +403 -0
  8. package/lib/api/team/org-query-manager.js +126 -0
  9. package/lib/api/team/org-resource-manager.js +186 -0
  10. package/lib/api/team/passport-manager.js +670 -0
  11. package/lib/api/team/rbac-manager.js +335 -0
  12. package/lib/api/team/session-manager.js +540 -0
  13. package/lib/api/team/store-manager.js +198 -0
  14. package/lib/api/team/tag-manager.js +230 -0
  15. package/lib/api/team/user-auth-manager.js +132 -0
  16. package/lib/api/team/user-manager.js +78 -0
  17. package/lib/api/team/user-query-manager.js +299 -0
  18. package/lib/api/team/user-social-manager.js +354 -0
  19. package/lib/api/team/user-update-manager.js +224 -0
  20. package/lib/api/team/verify-code-manager.js +161 -0
  21. package/lib/api/team.js +439 -3287
  22. package/lib/blocklet/manager/disk/auth-manager.js +68 -0
  23. package/lib/blocklet/manager/disk/backup-manager.js +288 -0
  24. package/lib/blocklet/manager/disk/cleanup-manager.js +157 -0
  25. package/lib/blocklet/manager/disk/component-manager.js +83 -0
  26. package/lib/blocklet/manager/disk/config-manager.js +191 -0
  27. package/lib/blocklet/manager/disk/controller-manager.js +64 -0
  28. package/lib/blocklet/manager/disk/delete-reset-manager.js +328 -0
  29. package/lib/blocklet/manager/disk/download-manager.js +96 -0
  30. package/lib/blocklet/manager/disk/env-config-manager.js +311 -0
  31. package/lib/blocklet/manager/disk/federated-manager.js +651 -0
  32. package/lib/blocklet/manager/disk/hook-manager.js +124 -0
  33. package/lib/blocklet/manager/disk/install-component-manager.js +95 -0
  34. package/lib/blocklet/manager/disk/install-core-manager.js +448 -0
  35. package/lib/blocklet/manager/disk/install-download-manager.js +313 -0
  36. package/lib/blocklet/manager/disk/install-manager.js +36 -0
  37. package/lib/blocklet/manager/disk/install-upgrade-manager.js +340 -0
  38. package/lib/blocklet/manager/disk/job-manager.js +467 -0
  39. package/lib/blocklet/manager/disk/lifecycle-manager.js +26 -0
  40. package/lib/blocklet/manager/disk/notification-manager.js +343 -0
  41. package/lib/blocklet/manager/disk/query-manager.js +562 -0
  42. package/lib/blocklet/manager/disk/settings-manager.js +507 -0
  43. package/lib/blocklet/manager/disk/start-manager.js +611 -0
  44. package/lib/blocklet/manager/disk/stop-restart-manager.js +292 -0
  45. package/lib/blocklet/manager/disk/update-manager.js +153 -0
  46. package/lib/blocklet/manager/disk.js +669 -5796
  47. package/lib/blocklet/manager/helper/blue-green-start-blocklet.js +5 -0
  48. package/lib/blocklet/manager/lock.js +18 -0
  49. package/lib/event/index.js +28 -24
  50. package/lib/router/helper.js +5 -1
  51. package/lib/util/blocklet/app-utils.js +192 -0
  52. package/lib/util/blocklet/blocklet-loader.js +258 -0
  53. package/lib/util/blocklet/config-manager.js +232 -0
  54. package/lib/util/blocklet/did-document.js +240 -0
  55. package/lib/util/blocklet/environment.js +555 -0
  56. package/lib/util/blocklet/health-check.js +449 -0
  57. package/lib/util/blocklet/install-utils.js +365 -0
  58. package/lib/util/blocklet/logo.js +57 -0
  59. package/lib/util/blocklet/meta-utils.js +269 -0
  60. package/lib/util/blocklet/port-manager.js +141 -0
  61. package/lib/util/blocklet/process-manager.js +504 -0
  62. package/lib/util/blocklet/runtime-info.js +105 -0
  63. package/lib/util/blocklet/validation.js +418 -0
  64. package/lib/util/blocklet.js +98 -3066
  65. package/lib/util/wallet-app-notification.js +40 -0
  66. package/package.json +22 -22
@@ -458,6 +458,11 @@ const blueGreenStartBlocklet = async (
458
458
  // 根据情况更新 route table, 会判断只有包含多 interfaces 的 DID 才会更新 route table
459
459
  // 如果是蓝绿更新发起的,则不更新 route table,因为蓝绿更新会自动更新 route table
460
460
  if (!['true', '1'].includes(process.env.ABT_NODE_DISABLE_BLUE_GREEN) && !ignoreBlockletStartedEvent) {
461
+ // 检查 blocklet 是否仍然存在(可能在异步执行期间被删除)
462
+ if (!(await manager.hasBlocklet({ did }))) {
463
+ logger.warn('skip blueOrGreenStarted event: blocklet no longer exists', { did });
464
+ return;
465
+ }
461
466
  const latestBlocklet = await manager.getBlocklet(did, { e2eMode });
462
467
  manager.emit(BlockletEvents.blueOrGreenStarted, {
463
468
  blocklet: latestBlocklet,
@@ -0,0 +1,18 @@
1
+ const { DBCache, getAbtNodeRedisAndSQLiteUrl } = require('@abtnode/db-cache');
2
+
3
+ const statusLock = new DBCache(() => ({
4
+ prefix: 'blocklet-status-lock',
5
+ ttl: 120_000,
6
+ ...getAbtNodeRedisAndSQLiteUrl(),
7
+ }));
8
+
9
+ const startLock = new DBCache(() => ({
10
+ prefix: 'blocklet-start-lock',
11
+ ttl: process.env.NODE_ENV === 'test' ? 1_000 : 120_000,
12
+ ...getAbtNodeRedisAndSQLiteUrl(),
13
+ }));
14
+
15
+ module.exports = {
16
+ statusLock,
17
+ startLock,
18
+ };
@@ -23,7 +23,7 @@ const {
23
23
  const { joinURL } = require('ufo');
24
24
  const { encode } = require('@abtnode/util/lib/base32');
25
25
  const dayjs = require('@abtnode/util/lib/dayjs');
26
-
26
+ const { deleteBlockletCache, clearBlockletInfoCache } = require('@abtnode/util/lib/blocklet-cache');
27
27
  const { isWorkerInstance } = require('@abtnode/util/lib/pm2/is-instance-worker');
28
28
  const { isInServerlessMode } = require('@abtnode/util/lib/serverless');
29
29
  const { NodeMonitSender } = require('../monitor/node-monit-sender');
@@ -37,12 +37,8 @@ const { getBackupEndpoint, getBackupFilesUrlFromEndpoint, getDIDSpacesUrlFromEnd
37
37
  const { autoBackupHandlerFactory, autoBackupHandler } = require('./auto-backup-handler');
38
38
 
39
39
  const eventBusHandler = require('../blocklet/webhook/event-bus');
40
- const {
41
- isDevelopmentMode,
42
- deleteBlockletCache,
43
- updateDidDocument,
44
- updateDidDocumentStateOnly,
45
- } = require('../util/blocklet');
40
+ const { isDevelopmentMode, updateDidDocument, updateDidDocumentStateOnly } = require('../util/blocklet');
41
+
46
42
  const { backupBlockletSites, cleanBlockletSitesBackup, rollbackBlockletSites } = require('./util');
47
43
  const { ensureBlockletHasMultipleInterfaces } = require('../router/helper');
48
44
  const { sendServerlessHeartbeat } = require('../util/launcher');
@@ -93,7 +89,32 @@ module.exports = ({
93
89
  }
94
90
  };
95
91
 
92
+ /**
93
+ * 缓存清除事件列表,所有进程(master + workers)都必须处理
94
+ * 触发场景:
95
+ * - blocklet.upgraded: configTheme, blocklet.componentRemoved, blocklet.componentInstalled, installed, removed, spaceConnected
96
+ * - blocklet.blueOrGreenStarted: started,单个和多个的启动
97
+ */
98
+ [
99
+ BlockletEvents.started,
100
+ BlockletEvents.updated,
101
+ BlockletEvents.upgraded,
102
+ BlockletEvents.blueOrGreenStarted,
103
+ BlockletEvents.stopped,
104
+ BlockletEvents.appDidChanged,
105
+ ].forEach((name) => {
106
+ eventHub.on(name, (data) => {
107
+ const did = get(data, 'meta.did');
108
+ if (did) {
109
+ logger.info(`delete blocklet cache on ${name}`, { did });
110
+ deleteBlockletCache(did);
111
+ clearBlockletInfoCache(did);
112
+ }
113
+ });
114
+ });
115
+
96
116
  // Listen events from eventHub and call eventHandler
117
+ // Only master/primary process handles full event processing to avoid redundant operations
97
118
  [...Object.values(BlockletEvents), ...Object.values(TeamEvents), ...Object.values(EVENTS)].forEach((name) => {
98
119
  if (isWorkerInstance()) {
99
120
  return;
@@ -111,23 +132,6 @@ module.exports = ({
111
132
  });
112
133
  }
113
134
 
114
- // clear blocklet cache
115
- if (
116
- [
117
- BlockletEvents.updated,
118
- BlockletEvents.started,
119
- BlockletEvents.removed,
120
- BlockletEvents.statusChange,
121
- BlockletEvents.installed,
122
- ].includes(name)
123
- ) {
124
- const did = get(data, 'meta.did');
125
- if (did) {
126
- logger.info(`delete blocklet cache on ${name}`, { did });
127
- deleteBlockletCache(did);
128
- }
129
- }
130
-
131
135
  if (typeof eventHandler === 'function') {
132
136
  eventHandler({ name, data });
133
137
  }
@@ -646,7 +646,11 @@ const getStaticRoot = (component) => {
646
646
  }
647
647
 
648
648
  const main = component.meta?.main;
649
- return main ? path.join(appDir, main) : appDir;
649
+ const root = main ? path.join(appDir, main) : appDir;
650
+ if (root?.endsWith('index.html') || root?.endsWith('index.htm')) {
651
+ return path.dirname(root);
652
+ }
653
+ return root;
650
654
  };
651
655
 
652
656
  /**
@@ -0,0 +1,192 @@
1
+ /**
2
+ * App Utils Module
3
+ *
4
+ * Functions for app-level utilities like archive creation, SK utilities, and domain handling
5
+ * Logo functions moved to ./logo.js
6
+ */
7
+
8
+ const fs = require('fs-extra');
9
+ const path = require('node:path');
10
+ const os = require('node:os');
11
+ const get = require('lodash/get');
12
+ const uniq = require('lodash/uniq');
13
+ const createArchive = require('archiver');
14
+
15
+ const { toAddress } = require('@ocap/util');
16
+ const { getDidDomainForBlocklet } = require('@abtnode/util/lib/get-domain-for-blocklet');
17
+ const { BLOCKLET_CONFIGURABLE_KEY, BLOCKLET_MODES } = require('@blocklet/constant');
18
+
19
+ const { getSlpDid, shouldEnableSlpDomain, getBlockletKnownAs } = require('./did-document');
20
+
21
+ // updateBlockletFallbackLogo, ensureAppLogo
22
+
23
+ /**
24
+ * Create a zip archive of a data directory
25
+ * @param {string} dataDir - Directory to archive
26
+ * @param {string} fileName - Output filename
27
+ * @returns {Promise<string>} Path to created archive
28
+ */
29
+ const createDataArchive = (dataDir, fileName) => {
30
+ const zipPath = path.join(os.tmpdir(), fileName);
31
+ if (fs.existsSync(zipPath)) {
32
+ fs.removeSync(zipPath);
33
+ }
34
+
35
+ const archive = createArchive('zip', { zlib: { level: 9 } });
36
+ const stream = fs.createWriteStream(zipPath);
37
+
38
+ return new Promise((resolve, reject) => {
39
+ archive
40
+ .directory(dataDir, false)
41
+ .on('error', (err) => reject(err))
42
+ .pipe(stream);
43
+
44
+ stream.on('close', () => resolve(zipPath));
45
+ archive.finalize();
46
+ });
47
+ };
48
+
49
+ /**
50
+ * Check if an app SK is currently in use
51
+ * @param {object} blocklet - Blocklet with environments and migratedFrom
52
+ * @param {string} appSk - App SK to check
53
+ * @returns {boolean}
54
+ */
55
+ const isBlockletAppSkUsed = ({ environments, migratedFrom = [] }, appSk) => {
56
+ const isUsedInEnv = environments.find((e) => e.key === BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK)?.value === appSk;
57
+ const isUsedInHistory = migratedFrom.some((x) => x.appSk === appSk);
58
+ return isUsedInEnv || isUsedInHistory;
59
+ };
60
+
61
+ /**
62
+ * Check if the app SK is being rotated
63
+ * @param {Array} newConfigs - New configuration array
64
+ * @param {Array} oldConfigs - Old configuration array
65
+ * @param {string} externalSk - External SK if any
66
+ * @returns {boolean}
67
+ */
68
+ const isRotatingAppSk = (newConfigs, oldConfigs, externalSk) => {
69
+ const newSk = newConfigs.find((x) => BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK === x.key);
70
+ if (!newSk) {
71
+ // If no newSk found, we are not rotating the appSk
72
+ return false;
73
+ }
74
+
75
+ const oldSk = oldConfigs.find((x) => BLOCKLET_CONFIGURABLE_KEY.BLOCKLET_APP_SK === x.key);
76
+ if (!oldSk) {
77
+ // If we have no oldSk, we are setting the initial appSk for external managed apps
78
+ // If we have no oldSk, but we are not external managed apps, we are rotating the appSk
79
+ return !externalSk;
80
+ }
81
+
82
+ // Otherwise, we must be rotating the appSk
83
+ // eslint-disable-next-line sonarjs/prefer-single-boolean-return
84
+ if (oldSk.value !== newSk.value) {
85
+ return true;
86
+ }
87
+
88
+ return false;
89
+ };
90
+
91
+ /**
92
+ * Get blocklet URL for launcher
93
+ * @param {object} options - Options
94
+ * @param {object} options.blocklet - Blocklet object
95
+ * @param {object} options.nodeInfo - Node info
96
+ * @returns {string} Blocklet URL
97
+ */
98
+ const getBlockletURLForLauncher = ({ blocklet, nodeInfo }) => {
99
+ const enableSlpDomain = shouldEnableSlpDomain(nodeInfo.mode);
100
+ let didDomain = '';
101
+ if (enableSlpDomain) {
102
+ didDomain = getDidDomainForBlocklet({
103
+ did: getSlpDid(nodeInfo.did, blocklet.appPid),
104
+ didDomain: nodeInfo.slpDomain,
105
+ });
106
+ } else {
107
+ didDomain = getDidDomainForBlocklet({
108
+ did: blocklet.appPid,
109
+ didDomain: nodeInfo.didDomain,
110
+ });
111
+ }
112
+
113
+ return `https://${didDomain}`;
114
+ };
115
+
116
+ /**
117
+ * Get list of DID domains for a blocklet
118
+ * @param {object} blocklet - Blocklet object
119
+ * @param {object} nodeInfo - Node info
120
+ * @returns {Array} Array of domain aliases
121
+ */
122
+ const getBlockletDidDomainList = (blocklet, nodeInfo) => {
123
+ const domainAliases = [];
124
+ const alsoKnownAs = getBlockletKnownAs(blocklet);
125
+
126
+ const dids = [blocklet.appPid, blocklet.appDid, ...alsoKnownAs].filter(Boolean).map((did) => toAddress(did));
127
+
128
+ uniq(dids).forEach((did) => {
129
+ const domain = getDidDomainForBlocklet({ did, didDomain: nodeInfo.didDomain });
130
+
131
+ domainAliases.push({ value: domain, isProtected: true });
132
+ });
133
+
134
+ const enableSlpDomain = shouldEnableSlpDomain(nodeInfo.mode);
135
+ if (enableSlpDomain) {
136
+ const slpDid = getSlpDid(nodeInfo.did, blocklet.appPid);
137
+ const domain = getDidDomainForBlocklet({ did: slpDid, didDomain: nodeInfo.slpDomain });
138
+
139
+ domainAliases.push({ value: domain, isProtected: true });
140
+ }
141
+
142
+ return domainAliases;
143
+ };
144
+
145
+ /**
146
+ * Get component names with version for display
147
+ * @param {object} app - App blocklet
148
+ * @param {Array} componentDids - Component DIDs
149
+ * @returns {string} Comma-separated component names with versions
150
+ */
151
+ const getComponentNamesWithVersion = (app = {}, componentDids = []) => {
152
+ const str = uniq(componentDids)
153
+ .map((x) => {
154
+ const component = (app.children || []).find((y) => y.meta.did === x);
155
+ return `${component.meta.title}@${component.meta.version}`;
156
+ })
157
+ .join(', ');
158
+ return str;
159
+ };
160
+
161
+ /**
162
+ * Check if blocklet is in development mode
163
+ * @param {import('@blocklet/server-js').BlockletState} blocklet
164
+ * @returns {boolean}
165
+ */
166
+ const isDevelopmentMode = (blocklet) => blocklet?.mode === BLOCKLET_MODES.DEVELOPMENT;
167
+
168
+ /**
169
+ * Get hook arguments for a blocklet
170
+ * @param {object} blocklet - Blocklet object
171
+ * @returns {object} Hook arguments
172
+ */
173
+ const getHookArgs = (blocklet) => ({
174
+ output: blocklet.mode === BLOCKLET_MODES.DEVELOPMENT ? '' : path.join(blocklet.env.logsDir, 'output.log'),
175
+ error: blocklet.mode === BLOCKLET_MODES.DEVELOPMENT ? '' : path.join(blocklet.env.logsDir, 'error.log'),
176
+ timeout:
177
+ Math.max(
178
+ get(blocklet, 'meta.timeout.script', 120),
179
+ ...(blocklet?.children || []).map((child) => child.meta?.timeout?.script || 0)
180
+ ) * 1000,
181
+ });
182
+
183
+ module.exports = {
184
+ createDataArchive,
185
+ isBlockletAppSkUsed,
186
+ isRotatingAppSk,
187
+ getBlockletURLForLauncher,
188
+ getBlockletDidDomainList,
189
+ getComponentNamesWithVersion,
190
+ isDevelopmentMode,
191
+ getHookArgs,
192
+ };
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Blocklet Loader Module
3
+ *
4
+ * Functions for loading blocklet state from database and cache
5
+ * Handles blocklet retrieval, caching, and bundle expansion
6
+ */
7
+
8
+ const fs = require('fs-extra');
9
+ const path = require('node:path');
10
+ const get = require('lodash/get');
11
+ const pick = require('lodash/pick');
12
+
13
+ const { toAddress } = require('@ocap/util');
14
+ const { isValid: isValidDid } = require('@arcblock/did');
15
+ const logger = require('@abtnode/logger')('@abtnode/core:util:blocklet:blocklet-loader');
16
+ const { DBCache, getAbtNodeRedisAndSQLiteUrl } = require('@abtnode/db-cache');
17
+ const { BLOCKLET_CACHE_TTL } = require('@abtnode/constant');
18
+ const { BLOCKLET_BUNDLE_FILE } = require('@blocklet/constant');
19
+ const { parseOptionalComponents } = require('@blocklet/resolver');
20
+ const { forEachBlockletSync, getComponentName } = require('@blocklet/meta/lib/util');
21
+ const { getComponentProcessId } = require('@blocklet/meta/lib/get-component-process-id');
22
+
23
+ const { expandBundle } = require('../index');
24
+ const { getComponentDirs, fillBlockletConfigs } = require('./environment');
25
+ const { formatBlockletTheme } = require('./meta-utils');
26
+
27
+ // Blocklet state cache
28
+ const blockletCache = new DBCache(() => ({
29
+ prefix: 'blocklet-state',
30
+ ttl: BLOCKLET_CACHE_TTL,
31
+ ...getAbtNodeRedisAndSQLiteUrl(),
32
+ }));
33
+
34
+ /**
35
+ * Delete blocklet from cache
36
+ * @param {string} did - Blocklet DID
37
+ */
38
+ const deleteBlockletCache = async (did) => {
39
+ await blockletCache.del(did);
40
+ };
41
+
42
+ /**
43
+ * Ensure blocklet bundle is expanded
44
+ * @param {object} _meta - Blocklet meta (unused)
45
+ * @param {string} appDir - App directory path
46
+ */
47
+ const ensureBlockletExpanded = async (_meta, appDir) => {
48
+ const bundlePath = path.join(appDir, BLOCKLET_BUNDLE_FILE);
49
+ if (fs.existsSync(bundlePath)) {
50
+ try {
51
+ const nodeModulesPath = path.join(appDir, 'node_modules');
52
+ if (fs.existsSync(nodeModulesPath)) {
53
+ await fs.remove(nodeModulesPath);
54
+ }
55
+ await expandBundle(bundlePath, appDir);
56
+ await fs.remove(bundlePath);
57
+ } catch (err) {
58
+ throw new Error(`Failed to expand blocklet bundle: ${err.message}`);
59
+ }
60
+ }
61
+ };
62
+
63
+ /**
64
+ * Internal function to get blocklet with full state
65
+ * @param {object} options - Options
66
+ * @returns {Promise<object>} Blocklet object
67
+ */
68
+ const _getBlocklet = async ({
69
+ did,
70
+ dataDirs,
71
+ states,
72
+ e2eMode = false,
73
+ throwOnNotExist = true,
74
+ ensureIntegrity = false,
75
+ getOptionalComponents = false,
76
+ } = {}) => {
77
+ if (!did) {
78
+ throw new Error('Blocklet did does not exist');
79
+ }
80
+ if (!isValidDid(did)) {
81
+ logger.error('Blocklet did is invalid', { did });
82
+ throw new Error('Blocklet did is invalid');
83
+ }
84
+
85
+ if (!dataDirs) {
86
+ throw new Error('dataDirs does not exist');
87
+ }
88
+
89
+ if (!states) {
90
+ throw new Error('states does not exist');
91
+ }
92
+
93
+ const blocklet = await states.blocklet.getBlocklet(did);
94
+ if (!blocklet) {
95
+ if (throwOnNotExist || ensureIntegrity) {
96
+ logger.error('can not find blocklet in database by did', { did });
97
+ throw new Error('can not find blocklet in database by did');
98
+ }
99
+ return null;
100
+ }
101
+
102
+ // 优化:并行查询独立数据(只查询一次 extraDoc,然后从内存中同步提取)
103
+ const [extraDoc, nodeInfo, site] = await Promise.all([
104
+ states.blockletExtras.getExtraByDid(blocklet.meta.did),
105
+ states.node.read(),
106
+ states.site.findOneByBlocklet(blocklet.meta.did),
107
+ ]);
108
+
109
+ // 从 extraDoc 中同步提取 settings(不需要再次查询数据库)
110
+ const extrasMeta = extraDoc ? pick(extraDoc, ['did', 'meta', 'controller']) : null;
111
+ const settings = states.blockletExtras.getFromDoc({ doc: extraDoc, dids: [blocklet.meta.did], name: 'settings' });
112
+
113
+ // app settings
114
+ // FIXME: @zhanghan 在 server 开发模式下,使用 `node /workspace/arcblock/blocklet-server/core/cli/tools/dev.js` 运行的 blocklet,blocklet.meta.did 和 blocklet.appPid 是不一致的
115
+ blocklet.trustedPassports = get(settings, 'trustedPassports') || [];
116
+ blocklet.trustedFactories = (get(settings, 'trustedFactories') || []).map((x) => {
117
+ if (!x.passport.ttlPolicy) {
118
+ x.passport.ttlPolicy = 'never';
119
+ x.passport.ttl = 0;
120
+ }
121
+ if (x.factoryAddress) {
122
+ x.factoryAddress = toAddress(x.factoryAddress);
123
+ }
124
+ return x;
125
+ });
126
+ blocklet.enablePassportIssuance = get(settings, 'enablePassportIssuance', true);
127
+ blocklet.settings = settings || {};
128
+
129
+ if (extrasMeta) {
130
+ blocklet.controller = extrasMeta.controller;
131
+ }
132
+
133
+ blocklet.settings.storeList = blocklet.settings.storeList || [];
134
+ blocklet.settings.theme = formatBlockletTheme(blocklet.settings.theme);
135
+ blocklet.settings.languages = blocklet.settings.languages || [];
136
+
137
+ // 移除第一个版本中 from 为 tmpl 的导航
138
+ if (blocklet?.settings?.navigations && Array.isArray(blocklet.settings.navigations)) {
139
+ blocklet.settings.navigations = (blocklet.settings.navigations || []).filter(
140
+ (item) => !(item?.parent === '/team' && ['tmpl'].includes(item.from))
141
+ );
142
+ }
143
+
144
+ (nodeInfo?.blockletRegistryList || []).forEach((store) => {
145
+ if (!blocklet.settings.storeList.find((x) => x.url === store.url)) {
146
+ blocklet.settings.storeList.push({
147
+ ...store,
148
+ protected: true,
149
+ });
150
+ }
151
+ });
152
+
153
+ blocklet.site = site;
154
+ blocklet.enableDocker = nodeInfo.enableDocker;
155
+ blocklet.enableDockerNetwork = nodeInfo.enableDockerNetwork;
156
+
157
+ // 第一次 forEachBlockletSync:收集所有组件的 dids
158
+ const componentConfigRequests = [];
159
+ forEachBlockletSync(blocklet, (component, { ancestors }) => {
160
+ const dids = [...ancestors.map((x) => x.meta.did), component.meta.did];
161
+ componentConfigRequests.push({
162
+ componentDid: component.meta.did,
163
+ dids,
164
+ });
165
+ });
166
+
167
+ // 基于缓存文档,为每个组件提取 configs(同步操作,不需要再次查询数据库)
168
+ const configsMap = new Map();
169
+ componentConfigRequests.forEach(({ componentDid, dids }) => {
170
+ const configs = states.blockletExtras.getFromDoc({ doc: extraDoc, dids, name: 'configs' });
171
+ configsMap.set(componentDid, configs);
172
+ });
173
+
174
+ // 第二次 forEachBlockletSync:填充组件
175
+ forEachBlockletSync(blocklet, (component, { id, level, ancestors }) => {
176
+ // component env
177
+ try {
178
+ // Validate component has required meta fields for getComponentDirs
179
+ if (!component.meta) {
180
+ throw new Error(`Component missing meta field: ${component.meta?.did || id}`);
181
+ }
182
+ if (!component.meta.name && !component.meta.bundleName) {
183
+ throw new Error(
184
+ `Component missing meta.name and meta.bundleName: ${component.meta.did || id}. ` +
185
+ 'This may indicate a migration issue with blocklet_children table.'
186
+ );
187
+ }
188
+
189
+ component.env = {
190
+ id,
191
+ name: getComponentName(component, ancestors),
192
+ processId: getComponentProcessId(component, ancestors),
193
+ ...getComponentDirs(component, {
194
+ dataDirs,
195
+ ensure: ensureIntegrity,
196
+ ancestors,
197
+ e2eMode: level === 0 ? e2eMode : false,
198
+ }),
199
+ };
200
+ } catch (error) {
201
+ logger.error('Failed to set component env in _getBlocklet', {
202
+ componentDid: component.meta?.did,
203
+ componentName: component.meta?.name,
204
+ componentBundleName: component.meta?.bundleName,
205
+ error: error.message,
206
+ stack: error.stack,
207
+ });
208
+ throw error;
209
+ }
210
+
211
+ // component config - 从预取的 configsMap 中获取
212
+ const configs = configsMap.get(component.meta.did) || [];
213
+ const rootBlocklet = ancestors.length > 0 ? ancestors[0] : blocklet;
214
+ fillBlockletConfigs(component, configs, { rootBlocklet, nodeInfo, dataDirs });
215
+ });
216
+
217
+ if (getOptionalComponents) {
218
+ const optionalComponents = await parseOptionalComponents(blocklet);
219
+ blocklet.optionalComponents = optionalComponents;
220
+ } else {
221
+ blocklet.optionalComponents = [];
222
+ }
223
+
224
+ return blocklet;
225
+ };
226
+
227
+ /**
228
+ * Get blocklet with optional caching
229
+ * @param {object} options - Options
230
+ * @returns {Promise<object>} Blocklet object
231
+ */
232
+ const getBlocklet = ({
233
+ did,
234
+ ensureIntegrity = false,
235
+ getOptionalComponents = false,
236
+ useCache = false,
237
+ ...rest
238
+ } = {}) => {
239
+ let cacheKey = '';
240
+
241
+ if (useCache) {
242
+ cacheKey = JSON.stringify({
243
+ ensureIntegrity,
244
+ getOptionalComponents,
245
+ });
246
+ }
247
+
248
+ return blockletCache.autoCacheGroup(did, cacheKey, () => {
249
+ return _getBlocklet({ did, ensureIntegrity, getOptionalComponents, ...rest });
250
+ });
251
+ };
252
+
253
+ module.exports = {
254
+ blockletCache,
255
+ deleteBlockletCache,
256
+ ensureBlockletExpanded,
257
+ getBlocklet,
258
+ };