blockmine 1.25.0 → 1.27.1

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 (165) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/backend/cli.js +1 -1
  3. package/backend/package.json +2 -2
  4. package/backend/prisma/migrations/20260328173000_add_plugin_source_ref/migration.sql +2 -0
  5. package/backend/prisma/migrations/migration_lock.toml +2 -2
  6. package/backend/prisma/schema.prisma +2 -0
  7. package/backend/src/api/routes/apiKeys.js +8 -0
  8. package/backend/src/api/routes/bots.js +258 -9
  9. package/backend/src/api/routes/eventGraphs.js +151 -1
  10. package/backend/src/api/routes/health.js +38 -0
  11. package/backend/src/api/routes/nodeRegistry.js +63 -0
  12. package/backend/src/api/routes/plugins.js +254 -29
  13. package/backend/src/container.js +11 -8
  14. package/backend/src/core/BotCommandLoader.js +161 -0
  15. package/backend/src/core/BotConnection.js +125 -0
  16. package/backend/src/core/BotEventHandlers.js +234 -0
  17. package/backend/src/core/BotIPCHandler.js +445 -0
  18. package/backend/src/core/BotManager.js +15 -7
  19. package/backend/src/core/BotProcess.js +75 -142
  20. package/backend/src/core/EventGraphManager.js +7 -3
  21. package/backend/src/core/GraphDebugHandler.js +229 -0
  22. package/backend/src/core/GraphDebugIPC.js +117 -0
  23. package/backend/src/core/GraphExecutionEngine.js +545 -978
  24. package/backend/src/core/GraphTraversal.js +80 -0
  25. package/backend/src/core/GraphValidation.js +73 -0
  26. package/backend/src/core/NodeDefinition.js +138 -0
  27. package/backend/src/core/NodeRegistry.js +153 -141
  28. package/backend/src/core/PluginManager.js +272 -31
  29. package/backend/src/core/RewindSignal.js +9 -0
  30. package/backend/src/core/config/ConfigValidator.js +72 -0
  31. package/backend/src/core/config/FeatureFlags.js +52 -0
  32. package/backend/src/core/config/__tests__/ConfigValidator.test.js +232 -0
  33. package/backend/src/core/domain/entities/Bot.js +39 -0
  34. package/backend/src/core/domain/entities/Command.js +41 -0
  35. package/backend/src/core/domain/entities/EventGraph.js +39 -0
  36. package/backend/src/core/domain/entities/Plugin.js +45 -0
  37. package/backend/src/core/domain/entities/User.js +40 -0
  38. package/backend/src/core/domain/services/DependencyResolver.js +168 -0
  39. package/backend/src/core/domain/services/GraphValidator.js +117 -0
  40. package/backend/src/core/domain/services/PermissionChecker.js +34 -0
  41. package/backend/src/core/domain/services/__tests__/DependencyResolver.test.js +126 -0
  42. package/backend/src/core/domain/valueObjects/BotConfig.js +27 -0
  43. package/backend/src/core/domain/valueObjects/DependencyGraph.js +86 -0
  44. package/backend/src/core/domain/valueObjects/PluginManifest.js +36 -0
  45. package/backend/src/core/errors/BaseError.js +29 -0
  46. package/backend/src/core/errors/ErrorHandler.js +81 -0
  47. package/backend/src/core/errors/__tests__/ErrorHandler.test.js +188 -0
  48. package/backend/src/core/errors/index.js +68 -0
  49. package/backend/src/core/infrastructure/BatchingUtility.js +66 -0
  50. package/backend/src/core/infrastructure/CircuitBreaker.js +103 -0
  51. package/backend/src/core/infrastructure/ConnectionPool.js +81 -0
  52. package/backend/src/core/infrastructure/RateLimiter.js +64 -0
  53. package/backend/src/core/infrastructure/__tests__/BatchingUtility.test.js +86 -0
  54. package/backend/src/core/infrastructure/__tests__/CircuitBreaker.test.js +156 -0
  55. package/backend/src/core/infrastructure/__tests__/ConnectionPool.test.js +146 -0
  56. package/backend/src/core/infrastructure/__tests__/RateLimiter.test.js +171 -0
  57. package/backend/src/core/ipc/botApiFactory.js +72 -0
  58. package/backend/src/core/ipc/ipcMessageTypes.js +115 -0
  59. package/backend/src/core/logging/AuditLogger.js +61 -0
  60. package/backend/src/core/logging/StructuredLogger.js +80 -0
  61. package/backend/src/core/logging/__tests__/StructuredLogger.test.js +213 -0
  62. package/backend/src/core/logging/index.js +7 -0
  63. package/backend/src/core/metrics/MetricsCollector.js +104 -0
  64. package/backend/src/core/metrics/__tests__/MetricsCollector.test.js +131 -0
  65. package/backend/src/core/node-registries/actionsNodes.js +191 -0
  66. package/backend/src/core/node-registries/arraysNodes.js +152 -0
  67. package/backend/src/core/node-registries/botNodes.js +48 -0
  68. package/backend/src/core/node-registries/containerNodes.js +141 -0
  69. package/backend/src/core/node-registries/dataNodes.js +284 -0
  70. package/backend/src/core/node-registries/debugNodes.js +23 -0
  71. package/backend/src/core/node-registries/eventsNodes.js +223 -0
  72. package/backend/src/core/node-registries/flowNodes.js +151 -0
  73. package/backend/src/core/node-registries/furnaceNodes.js +123 -0
  74. package/backend/src/core/node-registries/index.js +108 -0
  75. package/backend/src/core/node-registries/inventory.js +102 -106
  76. package/backend/src/core/node-registries/logicNodes.js +54 -0
  77. package/backend/src/core/node-registries/mathNodes.js +38 -0
  78. package/backend/src/core/node-registries/navigationNodes.js +109 -0
  79. package/backend/src/core/node-registries/objectsNodes.js +90 -0
  80. package/backend/src/core/node-registries/stringsNodes.js +165 -0
  81. package/backend/src/core/node-registries/timeNodes.js +105 -0
  82. package/backend/src/core/node-registries/typeNodes.js +22 -0
  83. package/backend/src/core/node-registries/usersNodes.js +126 -0
  84. package/backend/src/core/nodes/arrays/shuffle.js +14 -0
  85. package/backend/src/core/nodes/bot/get_name.js +8 -0
  86. package/backend/src/core/nodes/bot/stop_bot.js +5 -0
  87. package/backend/src/core/nodes/container/open.js +101 -111
  88. package/backend/src/core/nodes/data/store_read.js +26 -0
  89. package/backend/src/core/nodes/data/store_write.js +23 -0
  90. package/backend/src/core/nodes/event/call_event.js +31 -0
  91. package/backend/src/core/nodes/event/custom_event.js +8 -0
  92. package/backend/src/core/nodes/flow/timer.js +35 -0
  93. package/backend/src/core/nodes/inventory/drop.js +73 -65
  94. package/backend/src/core/nodes/inventory/equip.js +54 -45
  95. package/backend/src/core/nodes/inventory/select_slot.js +48 -46
  96. package/backend/src/core/nodes/navigation/follow.js +54 -51
  97. package/backend/src/core/nodes/navigation/go_to.js +41 -53
  98. package/backend/src/core/nodes/navigation/go_to_entity.js +65 -69
  99. package/backend/src/core/nodes/navigation/go_to_player.js +65 -70
  100. package/backend/src/core/nodes/navigation/stop.js +17 -26
  101. package/backend/src/core/nodes/users/add_to_group.js +24 -0
  102. package/backend/src/core/nodes/users/check_permission.js +26 -0
  103. package/backend/src/core/nodes/users/remove_from_group.js +24 -0
  104. package/backend/src/core/services/BotIPCMessageRouter.js +337 -0
  105. package/backend/src/core/services/BotLifecycleService.js +41 -632
  106. package/backend/src/core/services/CacheManager.js +83 -23
  107. package/backend/src/core/services/CrashRestartManager.js +42 -0
  108. package/backend/src/core/services/DebugSessionManager.js +114 -12
  109. package/backend/src/core/services/EventGraphService.js +69 -0
  110. package/backend/src/core/services/MinecraftBotManager.js +9 -1
  111. package/backend/src/core/services/PluginManagementService.js +84 -0
  112. package/backend/src/core/services/TestModeContext.js +65 -0
  113. package/backend/src/core/services/__tests__/CacheManager.test.js +168 -0
  114. package/backend/src/core/services.js +1 -11
  115. package/backend/src/core/validation/InputValidator.js +167 -0
  116. package/backend/src/core/validation/__tests__/InputValidator.test.js +296 -0
  117. package/backend/src/real-time/botApi/index.js +1 -1
  118. package/backend/src/real-time/socketHandler.js +26 -0
  119. package/backend/src/server.js +10 -5
  120. package/frontend/dist/assets/{browser-ponyfill-DN7pwmHT.js → browser-ponyfill-D8y0Ty7C.js} +1 -1
  121. package/frontend/dist/assets/index-CFJLS0dk.css +32 -0
  122. package/frontend/dist/assets/{index-LSy71uwm.js → index-D91UGNMG.js} +1880 -1881
  123. package/frontend/dist/index.html +2 -2
  124. package/frontend/dist/locales/en/bots.json +4 -1
  125. package/frontend/dist/locales/en/common.json +7 -1
  126. package/frontend/dist/locales/en/login.json +2 -0
  127. package/frontend/dist/locales/en/management.json +79 -1
  128. package/frontend/dist/locales/en/nodes.json +59 -4
  129. package/frontend/dist/locales/en/plugin-detail.json +24 -4
  130. package/frontend/dist/locales/en/plugins.json +226 -7
  131. package/frontend/dist/locales/en/setup.json +2 -0
  132. package/frontend/dist/locales/en/sidebar.json +171 -3
  133. package/frontend/dist/locales/en/visual-editor.json +230 -31
  134. package/frontend/dist/locales/ru/bots.json +4 -1
  135. package/frontend/dist/locales/ru/login.json +2 -0
  136. package/frontend/dist/locales/ru/management.json +79 -1
  137. package/frontend/dist/locales/ru/minecraft-viewer.json +3 -0
  138. package/frontend/dist/locales/ru/nodes.json +105 -51
  139. package/frontend/dist/locales/ru/plugins.json +103 -4
  140. package/frontend/dist/locales/ru/setup.json +2 -0
  141. package/frontend/dist/locales/ru/sidebar.json +171 -3
  142. package/frontend/dist/locales/ru/visual-editor.json +232 -33
  143. package/frontend/package.json +2 -0
  144. package/nul +12 -0
  145. package/package.json +3 -3
  146. package/scripts/postinstall.js +38 -0
  147. package/backend/package-lock.json +0 -6801
  148. package/backend/src/core/node-registries/actions.js +0 -202
  149. package/backend/src/core/node-registries/arrays.js +0 -155
  150. package/backend/src/core/node-registries/bot.js +0 -23
  151. package/backend/src/core/node-registries/container.js +0 -162
  152. package/backend/src/core/node-registries/data.js +0 -290
  153. package/backend/src/core/node-registries/debug.js +0 -26
  154. package/backend/src/core/node-registries/events.js +0 -201
  155. package/backend/src/core/node-registries/flow.js +0 -139
  156. package/backend/src/core/node-registries/furnace.js +0 -143
  157. package/backend/src/core/node-registries/logic.js +0 -62
  158. package/backend/src/core/node-registries/math.js +0 -42
  159. package/backend/src/core/node-registries/navigation.js +0 -111
  160. package/backend/src/core/node-registries/objects.js +0 -98
  161. package/backend/src/core/node-registries/strings.js +0 -187
  162. package/backend/src/core/node-registries/time.js +0 -113
  163. package/backend/src/core/node-registries/type.js +0 -25
  164. package/backend/src/core/node-registries/users.js +0 -79
  165. package/frontend/dist/assets/index-SfhKxI4-.css +0 -32
@@ -13,7 +13,192 @@ const DATA_DIR = path.join(os.homedir(), '.blockmine');
13
13
  const PLUGINS_BASE_DIR = path.join(DATA_DIR, 'storage', 'plugins');
14
14
 
15
15
  const TELEMETRY_ENABLED = true;
16
- const STATS_SERVER_URL = 'http://185.65.200.184:3000';
16
+ const STATS_SERVER_URL = process.env.STATS_SERVER_URL || 'http://185.65.200.184:3000';
17
+
18
+ function normalizeGithubRepoUrl(repoUrl) {
19
+ if (!repoUrl || typeof repoUrl !== 'string') return null;
20
+
21
+ let trimmed = repoUrl.trim();
22
+ if (!trimmed) return null;
23
+
24
+ try {
25
+ trimmed = trimmed.replace(/^git\+/i, '');
26
+ if (/^github\.com\//i.test(trimmed)) {
27
+ trimmed = `https://${trimmed}`;
28
+ }
29
+
30
+ const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
31
+ if (sshMatch) {
32
+ const owner = sshMatch[1].toLowerCase();
33
+ const repo = sshMatch[2].toLowerCase();
34
+ return `https://github.com/${owner}/${repo}`;
35
+ }
36
+
37
+ const parsed = new URL(trimmed);
38
+ const hostname = parsed.hostname.toLowerCase();
39
+ if (hostname !== 'github.com' && hostname !== 'www.github.com') return null;
40
+
41
+ const pathParts = parsed.pathname.split('/').filter(Boolean);
42
+ if (pathParts.length < 2) return null;
43
+
44
+ const owner = pathParts[0].toLowerCase();
45
+ const repo = pathParts[1].replace(/\.git$/i, '').toLowerCase();
46
+ return `https://github.com/${owner}/${repo}`;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function parseGithubOwnerRepo(repoUrl) {
53
+ const normalized = normalizeGithubRepoUrl(repoUrl);
54
+ if (!normalized) return null;
55
+ try {
56
+ const parsed = new URL(normalized);
57
+ const parts = parsed.pathname.split('/').filter(Boolean);
58
+ if (parts.length < 2) return null;
59
+ return { owner: parts[0], repo: parts[1] };
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ function getGithubRepoName(repoUrl) {
66
+ const ownerRepo = parseGithubOwnerRepo(repoUrl);
67
+ return ownerRepo?.repo || null;
68
+ }
69
+
70
+ function getGithubHeaders(extra = {}) {
71
+ const headers = {
72
+ Accept: 'application/vnd.github+json',
73
+ 'User-Agent': 'BlockMine',
74
+ ...extra
75
+ };
76
+
77
+ if (process.env.GITHUB_TOKEN) {
78
+ headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
79
+ }
80
+
81
+ return headers;
82
+ }
83
+
84
+ async function fetchJsonWithTimeout(url, timeoutMs = 7000) {
85
+ const controller = new AbortController();
86
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
87
+ try {
88
+ const response = await fetch(url, { headers: getGithubHeaders(), signal: controller.signal });
89
+ if (!response.ok) {
90
+ return null;
91
+ }
92
+ return response.json();
93
+ } catch {
94
+ return null;
95
+ } finally {
96
+ clearTimeout(timeoutId);
97
+ }
98
+ }
99
+
100
+ async function fetchGithubResponseWithTimeout(url, options = {}, timeoutMs = 10000) {
101
+ const controller = new AbortController();
102
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
103
+
104
+ try {
105
+ const response = await fetch(url, {
106
+ ...options,
107
+ headers: getGithubHeaders(options.headers || {}),
108
+ signal: controller.signal
109
+ });
110
+
111
+ if (!response.ok) {
112
+ const error = new Error(`GitHub request failed with status ${response.status}.`);
113
+ error.status = response.status;
114
+ throw error;
115
+ }
116
+
117
+ return response;
118
+ } catch (error) {
119
+ if (error.name === 'AbortError') {
120
+ const timeoutError = new Error('GitHub request timed out.');
121
+ timeoutError.name = 'AbortError';
122
+ throw timeoutError;
123
+ }
124
+
125
+ throw error;
126
+ } finally {
127
+ clearTimeout(timeoutId);
128
+ }
129
+ }
130
+
131
+ async function fetchGithubRepoInfo(repoUrl) {
132
+ const ownerRepo = parseGithubOwnerRepo(repoUrl);
133
+ if (!ownerRepo) return null;
134
+
135
+ return fetchJsonWithTimeout(`https://api.github.com/repos/${ownerRepo.owner}/${ownerRepo.repo}`);
136
+ }
137
+
138
+ async function downloadGithubArchive(repoUrl, ref) {
139
+ const ownerRepo = parseGithubOwnerRepo(repoUrl);
140
+ if (!ownerRepo) {
141
+ throw new Error('Invalid GitHub repository URL.');
142
+ }
143
+
144
+ const response = await fetchGithubResponseWithTimeout(
145
+ `https://api.github.com/repos/${ownerRepo.owner}/${ownerRepo.repo}/zipball/${encodeURIComponent(ref)}`,
146
+ { headers: { Accept: 'application/vnd.github+json' } },
147
+ 15000
148
+ );
149
+
150
+ return response;
151
+ }
152
+
153
+ async function fetchGithubPackageVersion(repoUrl, ref = null) {
154
+ const ownerRepo = parseGithubOwnerRepo(repoUrl);
155
+ if (!ownerRepo) return null;
156
+
157
+ const resolvedRef = ref || (await fetchGithubRepoInfo(repoUrl))?.default_branch;
158
+ if (!resolvedRef) return null;
159
+
160
+ const packageJsonData = await fetchJsonWithTimeout(
161
+ `https://api.github.com/repos/${ownerRepo.owner}/${ownerRepo.repo}/contents/package.json?ref=${encodeURIComponent(resolvedRef)}`
162
+ );
163
+
164
+ if (!packageJsonData?.content) {
165
+ return null;
166
+ }
167
+
168
+ try {
169
+ const packageJsonRaw = Buffer.from(packageJsonData.content, packageJsonData.encoding || 'base64').toString('utf8');
170
+ const packageJson = JSON.parse(packageJsonRaw);
171
+ return typeof packageJson?.version === 'string' ? packageJson.version : null;
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+
177
+ async function fetchLatestGithubVersionTag(repoUrl) {
178
+ const ownerRepo = parseGithubOwnerRepo(repoUrl);
179
+ if (!ownerRepo) return null;
180
+
181
+ const releaseData = await fetchJsonWithTimeout(`https://api.github.com/repos/${ownerRepo.owner}/${ownerRepo.repo}/releases/latest`);
182
+ const releaseTag = typeof releaseData?.tag_name === 'string' ? releaseData.tag_name : null;
183
+ if (releaseTag) return releaseTag;
184
+
185
+ const tagsData = await fetchJsonWithTimeout(`https://api.github.com/repos/${ownerRepo.owner}/${ownerRepo.repo}/tags?per_page=20`);
186
+ if (!Array.isArray(tagsData) || tagsData.length === 0) return null;
187
+
188
+ const semverTags = tagsData
189
+ .map(item => item?.name)
190
+ .filter(Boolean)
191
+ .map(tag => ({ tag, normalized: semver.coerce(tag)?.version || null }))
192
+ .filter(item => item.normalized);
193
+
194
+ if (semverTags.length > 0) {
195
+ semverTags.sort((a, b) => semver.rcompare(a.normalized, b.normalized));
196
+ return semverTags[0].tag;
197
+ }
198
+
199
+ const firstTag = tagsData[0]?.name;
200
+ return typeof firstTag === 'string' ? firstTag : null;
201
+ }
17
202
 
18
203
  function reportPluginDownload(pluginName) {
19
204
  if (!TELEMETRY_ENABLED) return;
@@ -188,6 +373,8 @@ class PluginManager {
188
373
  async installFromGithub(botId, repoUrl, prismaClient = prisma, isUpdate = false, tag = null) {
189
374
  const botPluginsDir = path.join(PLUGINS_BASE_DIR, `bot_${botId}`);
190
375
  await fse.mkdir(botPluginsDir, { recursive: true });
376
+ let sourceRefType = tag ? 'tag' : 'branch';
377
+ let sourceRef = tag || 'main';
191
378
 
192
379
  if (!isUpdate) {
193
380
  const existing = await prismaClient.installedPlugin.findFirst({ where: { botId, sourceUri: repoUrl } });
@@ -197,32 +384,30 @@ class PluginManager {
197
384
  try {
198
385
  const url = new URL(repoUrl);
199
386
  const repoPath = url.pathname.replace(/^\/|\.git$/g, '');
387
+ const repoInfo = tag ? null : await fetchGithubRepoInfo(repoUrl);
388
+ if (repoInfo?.default_branch) {
389
+ sourceRef = repoInfo.default_branch;
390
+ }
200
391
 
201
392
  let response;
202
393
 
203
- // Если указан тег - скачиваем конкретный релиз
204
394
  if (tag) {
205
- const archiveUrlTag = `https://github.com/${repoPath}/archive/refs/tags/${encodeURIComponent(tag)}.zip`;
206
395
  console.log(`[PluginManager] Скачиваем релиз ${tag} из ${repoUrl}...`);
207
396
  try {
208
- response = await fetch(archiveUrlTag);
397
+ response = await downloadGithubArchive(repoUrl, tag);
209
398
  } catch (err) {
210
399
  throw new Error(`Ошибка сети при скачивании релиза ${tag}: ${err.message || err}`);
211
400
  }
212
- if (!response.ok) {
213
- throw new Error(`Не удалось скачать релиз ${tag}. Статус: ${response.status}. Возможно, тег не существует.`);
214
- }
215
401
  } else {
216
- // Если тег не указан - скачиваем последний коммит из main/master (старое поведение)
217
- const archiveUrlMain = `https://github.com/${repoPath}/archive/refs/heads/main.zip`;
218
- const archiveUrlMaster = `https://github.com/${repoPath}/archive/refs/heads/master.zip`;
219
-
220
- response = await fetch(archiveUrlMain);
221
- if (!response.ok) {
222
- console.log(`[PluginManager] Ветка 'main' не найдена для ${repoUrl}, пробую 'master'...`);
223
- response = await fetch(archiveUrlMaster);
224
- if (!response.ok) {
225
- throw new Error(`Не удалось скачать архив плагина. Статус: ${response.status}`);
402
+ try {
403
+ response = await downloadGithubArchive(repoUrl, sourceRef);
404
+ } catch (err) {
405
+ if (!repoInfo && sourceRef !== 'master') {
406
+ console.log(`[PluginManager] Ветка '${sourceRef}' не найдена для ${repoUrl}, пробую 'master'...`);
407
+ sourceRef = 'master';
408
+ response = await downloadGithubArchive(repoUrl, sourceRef);
409
+ } else {
410
+ throw new Error(`Не удалось скачать архив плагина: ${err.message || err}`);
226
411
  }
227
412
  }
228
413
  }
@@ -264,7 +449,10 @@ class PluginManager {
264
449
  depCheck.warnings.forEach(w => console.warn(`[PluginManager] ⚠️ ${w}`));
265
450
  }
266
451
 
267
- const newPlugin = await this.registerPlugin(botId, localPath, 'GITHUB', repoUrl, prismaClient);
452
+ const newPlugin = await this.registerPlugin(botId, localPath, 'GITHUB', repoUrl, prismaClient, {
453
+ sourceRefType,
454
+ sourceRef
455
+ });
268
456
 
269
457
  reportPluginDownload(packageJson.name);
270
458
 
@@ -285,7 +473,7 @@ class PluginManager {
285
473
  }
286
474
  }
287
475
 
288
- async registerPlugin(botId, directoryPath, sourceType, sourceUri, prismaClient = prisma) {
476
+ async registerPlugin(botId, directoryPath, sourceType, sourceUri, prismaClient = prisma, extraData = {}) {
289
477
  const packageJsonPath = path.join(directoryPath, 'package.json');
290
478
  let packageJson;
291
479
  try {
@@ -307,6 +495,7 @@ class PluginManager {
307
495
  sourceType,
308
496
  sourceUri: sourceUri || directoryPath,
309
497
  manifest: JSON.stringify(packageJson.botpanel || {}),
498
+ ...extraData,
310
499
  };
311
500
 
312
501
  return prismaClient.installedPlugin.upsert({
@@ -397,24 +586,76 @@ class PluginManager {
397
586
  where: { botId, sourceType: 'GITHUB' }
398
587
  });
399
588
  const updatesAvailable = [];
400
- const catalogMap = new Map(catalog.map(item => [item.repoUrl, item]));
589
+ const catalogMapByRepo = new Map();
590
+ const catalogMapByName = new Map();
591
+ const catalogMapByRepoName = new Map();
592
+ const latestTagCache = new Map();
593
+
594
+ for (const item of catalog) {
595
+ const normalizedRepoUrl = normalizeGithubRepoUrl(item.repoUrl);
596
+ if (normalizedRepoUrl) {
597
+ catalogMapByRepo.set(normalizedRepoUrl, item);
598
+ const repoName = getGithubRepoName(normalizedRepoUrl);
599
+ if (repoName && !catalogMapByRepoName.has(repoName)) {
600
+ catalogMapByRepoName.set(repoName, item);
601
+ }
602
+ }
603
+ if (item?.name) {
604
+ catalogMapByName.set(String(item.name).toLowerCase(), item);
605
+ }
606
+ }
401
607
 
402
608
  for (const plugin of githubPlugins) {
403
609
  try {
404
- const catalogInfo = catalogMap.get(plugin.sourceUri);
405
- if (!catalogInfo || !catalogInfo.latestTag) continue;
610
+ const normalizedSourceUri = normalizeGithubRepoUrl(plugin.sourceUri);
611
+ const repoName = getGithubRepoName(normalizedSourceUri || plugin.sourceUri);
612
+ const catalogInfo =
613
+ (normalizedSourceUri ? catalogMapByRepo.get(normalizedSourceUri) : null) ||
614
+ catalogMapByName.get(String(plugin.name).toLowerCase()) ||
615
+ (repoName ? catalogMapByRepoName.get(repoName) : null);
616
+
617
+ const targetRepoUrl = catalogInfo?.repoUrl || normalizedSourceUri || normalizeGithubRepoUrl(plugin.sourceUri) || plugin.sourceUri;
618
+ let latestTagRaw =
619
+ catalogInfo?.latestTag ||
620
+ catalogInfo?.recommendedVersion ||
621
+ catalogInfo?.version ||
622
+ catalogInfo?.latestVersion ||
623
+ catalogInfo?.tag;
624
+
625
+ if (!latestTagRaw) {
626
+ const cacheKey = targetRepoUrl || plugin.name;
627
+ if (latestTagCache.has(cacheKey)) {
628
+ latestTagRaw = latestTagCache.get(cacheKey);
629
+ } else {
630
+ const fetchedTag = targetRepoUrl ? await fetchLatestGithubVersionTag(targetRepoUrl) : null;
631
+ latestTagCache.set(cacheKey, fetchedTag);
632
+ latestTagRaw = fetchedTag;
633
+ }
634
+ }
635
+
636
+ let latestVersionRaw = latestTagRaw;
637
+ if (!latestVersionRaw && targetRepoUrl) {
638
+ latestVersionRaw = await fetchGithubPackageVersion(
639
+ targetRepoUrl,
640
+ plugin.sourceRefType === 'branch' ? plugin.sourceRef : null
641
+ );
642
+ }
643
+
644
+ if (!latestVersionRaw) continue;
406
645
 
407
- const localVersion = semver.coerce(plugin.version)?.version || plugin.version;
408
- const recommendedVersion = semver.coerce(catalogInfo.latestTag)?.version || catalogInfo.latestTag;
646
+ const localSemver = semver.coerce(plugin.version);
647
+ const remoteSemver = semver.coerce(latestVersionRaw);
648
+ if (!localSemver || !remoteSemver) continue;
409
649
 
410
- if (semver.gt(recommendedVersion, localVersion)) {
650
+ if (semver.gt(remoteSemver.version, localSemver.version)) {
411
651
  updatesAvailable.push({
412
652
  id: plugin.id,
413
653
  name: plugin.name,
414
654
  sourceUri: plugin.sourceUri,
415
- currentVersion: localVersion,
416
- recommendedVersion: recommendedVersion,
417
- latestTag: catalogInfo.latestTag, // 🏷️ Добавляем тег для использования при обновлении
655
+ currentVersion: localSemver.version,
656
+ recommendedVersion: remoteSemver.version,
657
+ latestTag: catalogInfo?.latestTag || latestTagRaw || null,
658
+ targetRepoUrl,
418
659
  });
419
660
  }
420
661
  } catch (error) {
@@ -424,7 +665,7 @@ class PluginManager {
424
665
  return updatesAvailable;
425
666
  }
426
667
 
427
- async updatePlugin(pluginId, targetTag = null) {
668
+ async updatePlugin(pluginId, targetTag = null, targetRepoUrl = null) {
428
669
  const plugin = await prisma.installedPlugin.findUnique({ where: { id: pluginId } });
429
670
  if (!plugin || plugin.sourceType !== 'GITHUB') {
430
671
  throw new Error('Плагин не найден или не является GitHub-плагином.');
@@ -432,7 +673,7 @@ class PluginManager {
432
673
 
433
674
  console.log(`[PluginManager] Начало обновления плагина ${plugin.name}${targetTag ? ` до версии ${targetTag}` : ''}...`);
434
675
 
435
- const repoUrl = plugin.sourceUri;
676
+ const repoUrl = targetRepoUrl || plugin.sourceUri;
436
677
  const botId = plugin.botId;
437
678
  const oldVersion = plugin.version;
438
679
 
@@ -654,4 +895,4 @@ class PluginManager {
654
895
  }
655
896
  }
656
897
 
657
- module.exports = PluginManager;
898
+ module.exports = PluginManager;
@@ -0,0 +1,9 @@
1
+ class RewindSignal extends Error {
2
+ constructor(target) {
3
+ super('Rewind signal');
4
+ this.name = 'RewindSignal';
5
+ this.target = target;
6
+ }
7
+ }
8
+
9
+ module.exports = RewindSignal;
@@ -0,0 +1,72 @@
1
+ const { ConfigurationError } = require('../errors');
2
+
3
+ const HEX_64_REGEX = /^[0-9a-fA-F]{64}$/;
4
+ const VALID_NODE_ENVS = ['development', 'production', 'test'];
5
+ const VALID_LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'fatal'];
6
+
7
+ class ConfigValidator {
8
+ validate(config) {
9
+ const errors = [];
10
+
11
+ if (!config || typeof config !== 'object') {
12
+ errors.push('config.errors.config_required');
13
+ return { valid: false, errors };
14
+ }
15
+
16
+ const server = config.server || {};
17
+ const security = config.security || {};
18
+
19
+ if (!server.host || typeof server.host !== 'string') {
20
+ errors.push('config.errors.server_host_required');
21
+ }
22
+
23
+ const port = server.port;
24
+ if (port === undefined || port === null) {
25
+ errors.push('config.errors.server_port_required');
26
+ } else if (!Number.isInteger(port) || port < 1 || port > 65535) {
27
+ errors.push('config.errors.server_port_invalid');
28
+ }
29
+
30
+ if (!security.jwtSecret || typeof security.jwtSecret !== 'string') {
31
+ errors.push('config.errors.jwt_secret_required');
32
+ } else if (security.jwtSecret.length < 32) {
33
+ errors.push('config.errors.jwt_secret_too_short');
34
+ }
35
+
36
+ if (!security.encryptionKey || typeof security.encryptionKey !== 'string') {
37
+ errors.push('config.errors.encryption_key_required');
38
+ } else if (!HEX_64_REGEX.test(security.encryptionKey)) {
39
+ errors.push('config.errors.encryption_key_invalid');
40
+ }
41
+
42
+ return { valid: errors.length === 0, errors };
43
+ }
44
+
45
+ validateEnv() {
46
+ const errors = [];
47
+ const warnings = [];
48
+
49
+ const nodeEnv = process.env.NODE_ENV;
50
+ if (nodeEnv !== undefined && !VALID_NODE_ENVS.includes(nodeEnv)) {
51
+ errors.push('config.errors.node_env_invalid');
52
+ }
53
+
54
+ const logLevel = process.env.LOG_LEVEL;
55
+ if (logLevel !== undefined && !VALID_LOG_LEVELS.includes(logLevel)) {
56
+ errors.push('config.errors.log_level_invalid');
57
+ }
58
+
59
+ return { valid: errors.length === 0, errors, warnings };
60
+ }
61
+
62
+ validateAndThrow(config) {
63
+ const result = this.validate(config);
64
+ if (!result.valid) {
65
+ throw new ConfigurationError('config.errors.invalid_configuration', {
66
+ context: { errors: result.errors },
67
+ });
68
+ }
69
+ }
70
+ }
71
+
72
+ module.exports = ConfigValidator;
@@ -0,0 +1,52 @@
1
+ const DEFAULTS = {
2
+ useNewDependencyResolver: true,
3
+ useStructuredLogger: true,
4
+ useErrorHandler: true,
5
+ useInputValidation: true,
6
+ useRateLimiter: false,
7
+ useCircuitBreaker: false,
8
+ useMetrics: false,
9
+ useAuditLog: false,
10
+ };
11
+
12
+ class FeatureFlags {
13
+ constructor(overrides = {}) {
14
+ this._flags = { ...DEFAULTS, ...this._loadFromEnv(), ...overrides };
15
+ }
16
+
17
+ _loadFromEnv() {
18
+ const flags = {};
19
+ for (const key of Object.keys(DEFAULTS)) {
20
+ const envKey = `FEATURE_${key.replace(/([A-Z])/g, '_$1').toUpperCase()}`;
21
+ if (process.env[envKey] !== undefined) {
22
+ flags[key] = process.env[envKey] === 'true';
23
+ }
24
+ }
25
+ return flags;
26
+ }
27
+
28
+ isEnabled(flag) {
29
+ return this._flags[flag] === true;
30
+ }
31
+
32
+ enable(flag) {
33
+ this._flags[flag] = true;
34
+ }
35
+
36
+ disable(flag) {
37
+ this._flags[flag] = false;
38
+ }
39
+
40
+ getAll() {
41
+ return { ...this._flags };
42
+ }
43
+ }
44
+
45
+ let instance = null;
46
+
47
+ function getFeatureFlags() {
48
+ if (!instance) instance = new FeatureFlags();
49
+ return instance;
50
+ }
51
+
52
+ module.exports = { FeatureFlags, getFeatureFlags };