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
package/CHANGELOG.md CHANGED
@@ -1,6 +1,51 @@
1
- # История версий
1
+ # История версий
2
+
3
+
4
+ ### [1.27.1](https://github.com/blockmineJS/blockmine/compare/v1.27.0...v1.27.1) (2026-05-15)
5
+
6
+ ## [1.27.0](https://github.com/blockmineJS/blockmine/compare/v1.25.0...v1.27.0) (2026-05-12)
2
7
 
3
8
 
9
+ ### 🛠 Рефакторинг
10
+
11
+ * внутренний большой рефактор бэкенда ([eb27f26](https://github.com/blockmineJS/blockmine/commit/eb27f2600218f05a8b8707adfed3a5a2298531f6))
12
+ * полноценный внутренний рефактор нод ([28387a9](https://github.com/blockmineJS/blockmine/commit/28387a917bf89e33e9715cbd03483accc136b9af))
13
+
14
+
15
+ ### 🐛 Исправления
16
+
17
+ * нода "сообщение в чате" работает в локал мире ([17bbde8](https://github.com/blockmineJS/blockmine/commit/17bbde82115cecee646a10bc26f21ec23856e33e))
18
+ * пост инсталл скрипт теперь есть... ([c5248cb](https://github.com/blockmineJS/blockmine/commit/c5248cbb805057b52efaea67209bea325b2e0b31))
19
+ * причина кика теперь корректно пишется ([08de739](https://github.com/blockmineJS/blockmine/commit/08de739fc03ce35ae136b3bd5cb5b822145139cf))
20
+ * роут install local теперь может работать и через апи ключ ([03f1176](https://github.com/blockmineJS/blockmine/commit/03f117690cda3d2966a928be217ad78154d4ec87))
21
+ * address PR79 review feedback by САХАРОК ([5dceb17](https://github.com/blockmineJS/blockmine/commit/5dceb174ef3009f47cb5a94f48f5e90c4c790feb))
22
+ * address remaining PR79 review feedback by САХАРОК ([dde6905](https://github.com/blockmineJS/blockmine/commit/dde690593320344f181f9a13fc4dc4905872e2cf))
23
+ * close active connection on API key deletion by САХАРОК ([2f7db12](https://github.com/blockmineJS/blockmine/commit/2f7db123b0b46c3aedf806d7d543316d7aace1f5))
24
+ * close remaining plugin workflow review issues by САХАРОК ([9f2dd7f](https://github.com/blockmineJS/blockmine/commit/9f2dd7fa422b5b276df4da96f7b9302d489b5e9f))
25
+ * polish language selector modal by САХАРОК ([2cebe65](https://github.com/blockmineJS/blockmine/commit/2cebe6554b21191e7f6ce8a532e243832f2a2ded))
26
+ * polish plugin list alignment and text clarity by САХАРОК ([8c27d60](https://github.com/blockmineJS/blockmine/commit/8c27d6062625e5a55e76a193ed1d3500a88f8246))
27
+ * remove remaining emoji from english visual editor by САХАРОК ([5a4f356](https://github.com/blockmineJS/blockmine/commit/5a4f3563fad29a4054bc98b31b5bc79c0d0b8572))
28
+
29
+
30
+ ### ✨ Новые возможности
31
+
32
+ * большая переработка интерфейса и прочее ([d28e0fa](https://github.com/blockmineJS/blockmine/commit/d28e0fa4af1815476ad90ddd0fb5799db95707ea))
33
+ * в лайв дебаге появилась функция которая дает опробовать ноды без запуска бота ([bbd669f](https://github.com/blockmineJS/blockmine/commit/bbd669f463d62bffacb65b8d6066d5345a0e237f))
34
+ * новая нода - имя бота ([574eada](https://github.com/blockmineJS/blockmine/commit/574eada6af5e89b2b67159b82ca435a2f8446004))
35
+ * новая нода - прочитать/записать в стор ([eeb588e](https://github.com/blockmineJS/blockmine/commit/eeb588e1305d8bfb68cf7e388abf8b092a7865c2))
36
+ * новая нода - стоп бот ([0639ed4](https://github.com/blockmineJS/blockmine/commit/0639ed439d0263fda229f261f317d54df3876d8c))
37
+ * новая нода - таймер ([1d7899d](https://github.com/blockmineJS/blockmine/commit/1d7899d243232ae3062a84d6bc46f0f05897b40d))
38
+ * новая нода - шаффл. перемешать массив ([67e785a](https://github.com/blockmineJS/blockmine/commit/67e785aa7706eb19a489042fcf2aec20643a0b79))
39
+ * новые ноды - события ([ae4db8e](https://github.com/blockmineJS/blockmine/commit/ae4db8e8a11dcdf9c72536a850d65032fa528661))
40
+ * новые ноды. проверить право у юзера, добавить/убрать из группы ([3249483](https://github.com/blockmineJS/blockmine/commit/32494831636432013aa72978daa41bc4668a51af))
41
+ * обновлен сайдбар ([8d77a48](https://github.com/blockmineJS/blockmine/commit/8d77a4834bc60c76de6010a794b4b79926d3fed1))
42
+ * обновление mineflayer. 4.33 -> 4.37.1 . Поддерживает новые версии майнкрафта ([b7f2317](https://github.com/blockmineJS/blockmine/commit/b7f23175ba983fda2a2108d31e54d291d9d17212))
43
+ * improve plugin workflows and panel UX by САХАРОК ([09f111a](https://github.com/blockmineJS/blockmine/commit/09f111a9c47e7fd639b345be3d95de4c203213a9))
44
+ * polish management, viewer, and toast ux by САХАРОК ([d52e9b3](https://github.com/blockmineJS/blockmine/commit/d52e9b3d5eb56bc45c0417e10f397ad87441809e))
45
+ * polish panel ux, theming, and transitions by САХАРОК ([1f347b4](https://github.com/blockmineJS/blockmine/commit/1f347b442cef70f579cdc74c207554f95e68d15a))
46
+ * polish plugin UX and localize panel states by САХАРОК ([ef32fc0](https://github.com/blockmineJS/blockmine/commit/ef32fc08494c41e7a15312a2d0ed251255f3bd2b))
47
+ * refine visual editor and panel polish by САХАРОК ([0118ec6](https://github.com/blockmineJS/blockmine/commit/0118ec6de81078845551b443e5129b41e6e064f9))
48
+
4
49
  ## [1.25.0](https://github.com/blockmineJS/blockmine/compare/v1.24.0...v1.25.0) (2025-12-22)
5
50
 
6
51
 
package/backend/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  const fs = require('fs');
3
3
  const os = require('os');
4
4
  const path = require('path');
@@ -27,7 +27,7 @@
27
27
  "ts-jest": "^29.4.5"
28
28
  },
29
29
  "dependencies": {
30
- "@octokit/rest": "^22.0.1",
30
+ "@octokit/rest": "^20.1.1",
31
31
  "awilix": "^12.0.5",
32
32
  "date-fns": "^4.1.0",
33
33
  "diff": "^8.0.2",
@@ -35,7 +35,7 @@
35
35
  "express-validator": "^7.2.1",
36
36
  "google-ai-kit": "^1.1.3",
37
37
  "lru-cache": "^10.4.3",
38
- "mineflayer": "^4.33.0",
38
+ "mineflayer": "^4.37.1",
39
39
  "mineflayer-pathfinder": "^2.4.5",
40
40
  "openrouter-kit": "^0.1.81",
41
41
  "pino": "^9.7.0",
@@ -0,0 +1,2 @@
1
+ ALTER TABLE "InstalledPlugin" ADD COLUMN "sourceRefType" TEXT;
2
+ ALTER TABLE "InstalledPlugin" ADD COLUMN "sourceRef" TEXT;
@@ -1,3 +1,3 @@
1
- # Please do not edit this file manually
2
- # It should be added in your version-control system (i.e. Git)
1
+ # Please do not edit this file manually
2
+ # It should be added in your version-control system (i.e. Git)
3
3
  provider = "sqlite"
@@ -79,6 +79,8 @@ model InstalledPlugin {
79
79
  description String?
80
80
  sourceType String
81
81
  sourceUri String?
82
+ sourceRefType String?
83
+ sourceRef String?
82
84
  path String
83
85
  isEnabled Boolean @default(true)
84
86
 
@@ -163,6 +163,14 @@ router.delete('/:keyId', authorize('bot:update'), async (req, res) => {
163
163
  const botId = parseInt(req.params.botId, 10);
164
164
  const keyId = parseInt(req.params.keyId, 10);
165
165
 
166
+ const { getIO } = require('../../real-time/socketHandler');
167
+
168
+ const io = getIO();
169
+
170
+ io.of("/bot-api")
171
+ .in(`key_${keyId}`)
172
+ .disconnectSockets(true)
173
+
166
174
  const result = await prisma.botApiKey.deleteMany({
167
175
  where: { id: keyId, botId },
168
176
  });
@@ -17,6 +17,7 @@ const { deepMergeSettings } = require('../../core/utils/settingsMerger');
17
17
  const { checkBotAccess } = require('../middleware/botAccess');
18
18
  const { filterSecretSettings, prepareSettingsForSave, isGroupedSettings } = require('../../core/utils/secretsFilter');
19
19
  const PluginHooks = require('../../core/PluginHooks');
20
+ const rateLimit = require('express-rate-limit');
20
21
 
21
22
  const multer = require('multer');
22
23
  const archiver = require('archiver');
@@ -26,6 +27,160 @@ const os = require('os');
26
27
  const upload = multer({ storage: multer.memoryStorage() });
27
28
 
28
29
  const router = express.Router();
30
+ const GITHUB_REQUEST_TIMEOUT_MS = 10000;
31
+ const GITHUB_OWNER_REPO_PATTERN = /^[A-Za-z0-9_.-]+$/;
32
+
33
+ const githubPreviewLimiter = rateLimit({
34
+ windowMs: 60 * 1000,
35
+ max: 30,
36
+ standardHeaders: true,
37
+ legacyHeaders: false,
38
+ message: { message: 'Too many GitHub preview requests. Try again later.' },
39
+ });
40
+
41
+ const githubInstallLimiter = rateLimit({
42
+ windowMs: 60 * 1000,
43
+ max: 20,
44
+ standardHeaders: true,
45
+ legacyHeaders: false,
46
+ message: { message: 'Too many GitHub install requests. Try again later.' },
47
+ });
48
+
49
+ function getGithubHeaders(extra = {}) {
50
+ const headers = {
51
+ 'Accept': 'application/vnd.github+json',
52
+ 'User-Agent': 'BlockMine',
53
+ ...extra
54
+ };
55
+
56
+ if (process.env.GITHUB_TOKEN) {
57
+ headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
58
+ }
59
+
60
+ return headers;
61
+ }
62
+
63
+ function parseGithubRepoUrl(repoUrl) {
64
+ if (typeof repoUrl !== 'string') {
65
+ throw new Error('Repository URL is required.');
66
+ }
67
+
68
+ const trimmed = repoUrl.trim();
69
+ if (!trimmed) {
70
+ throw new Error('Repository URL is required.');
71
+ }
72
+
73
+ let parsedUrl;
74
+ try {
75
+ parsedUrl = new URL(trimmed);
76
+ } catch (error) {
77
+ throw new Error('Invalid GitHub repository URL.');
78
+ }
79
+
80
+ if (!['github.com', 'www.github.com'].includes(parsedUrl.hostname)) {
81
+ throw new Error('Only GitHub repository links are supported.');
82
+ }
83
+
84
+ const pathParts = parsedUrl.pathname.replace(/\/+$/, '').split('/').filter(Boolean);
85
+ if (pathParts.length < 2) {
86
+ throw new Error('GitHub repository URL must include owner and repository name.');
87
+ }
88
+
89
+ const owner = pathParts[0];
90
+ const repo = pathParts[1].replace(/\.git$/i, '');
91
+
92
+ if (!owner || !repo) {
93
+ throw new Error('GitHub repository URL must include owner and repository name.');
94
+ }
95
+ if (!GITHUB_OWNER_REPO_PATTERN.test(owner) || !GITHUB_OWNER_REPO_PATTERN.test(repo)) {
96
+ throw new Error('GitHub repository URL contains unsupported owner or repository characters.');
97
+ }
98
+
99
+ return {
100
+ owner,
101
+ repo,
102
+ normalizedUrl: `https://github.com/${owner}/${repo}`
103
+ };
104
+ }
105
+
106
+ function normalizeGithubRepoUrl(repoUrl) {
107
+ return parseGithubRepoUrl(repoUrl).normalizedUrl;
108
+ }
109
+
110
+ async function fetchGithubJson(url) {
111
+ const controller = new AbortController();
112
+ const timeoutId = setTimeout(() => controller.abort(), GITHUB_REQUEST_TIMEOUT_MS);
113
+ let response;
114
+ try {
115
+ response = await fetch(url, {
116
+ headers: getGithubHeaders(),
117
+ signal: controller.signal
118
+ });
119
+ } finally {
120
+ clearTimeout(timeoutId);
121
+ }
122
+
123
+ if (!response.ok) {
124
+ const error = new Error(`GitHub API request failed with status ${response.status}.`);
125
+ error.status = response.status;
126
+ throw error;
127
+ }
128
+
129
+ return response.json();
130
+ }
131
+
132
+ async function fetchGithubReadme(owner, repo) {
133
+ const controller = new AbortController();
134
+ const timeoutId = setTimeout(() => controller.abort(), GITHUB_REQUEST_TIMEOUT_MS);
135
+ let response;
136
+ try {
137
+ response = await fetch(`https://api.github.com/repos/${owner}/${repo}/readme`, {
138
+ headers: getGithubHeaders({ 'Accept': 'application/vnd.github.raw+json' }),
139
+ signal: controller.signal
140
+ });
141
+ } finally {
142
+ clearTimeout(timeoutId);
143
+ }
144
+
145
+ if (!response.ok) {
146
+ return null;
147
+ }
148
+
149
+ return response.text();
150
+ }
151
+
152
+ async function renderGithubMarkdown(markdown, owner, repo) {
153
+ if (!markdown) {
154
+ return null;
155
+ }
156
+
157
+ const controller = new AbortController();
158
+ const timeoutId = setTimeout(() => controller.abort(), GITHUB_REQUEST_TIMEOUT_MS);
159
+ let response;
160
+ try {
161
+ response = await fetch('https://api.github.com/markdown', {
162
+ method: 'POST',
163
+ headers: getGithubHeaders({
164
+ 'Accept': 'text/html',
165
+ 'Content-Type': 'application/json',
166
+ }),
167
+ signal: controller.signal,
168
+ body: JSON.stringify({
169
+ text: markdown,
170
+ mode: 'gfm',
171
+ context: `${owner}/${repo}`
172
+ })
173
+ });
174
+ } finally {
175
+ clearTimeout(timeoutId);
176
+ }
177
+
178
+ if (!response.ok) {
179
+ return null;
180
+ }
181
+
182
+ return response.text();
183
+ }
29
184
 
30
185
  const conditionalRestartAuth = (req, res, next) => {
31
186
  if (process.env.DEBUG === 'true' || process.env.NODE_ENV === 'development') {
@@ -352,6 +507,17 @@ router.get('/:id/logs', conditionalListAuth, authenticateUniversal, checkBotAcce
352
507
  }
353
508
  });
354
509
 
510
+ router.post('/:botId/plugins/install/local', authenticateUniversal, checkBotAccess, authorize('plugin:install'), async (req, res) => {
511
+ const { botId } = req.params;
512
+ const { path } = req.body;
513
+ try {
514
+ const newPlugin = await pluginManager.installFromLocalPath(parseInt(botId), path);
515
+ res.status(201).json(newPlugin);
516
+ } catch (error) {
517
+ res.status(500).json({ message: error.message });
518
+ }
519
+ });
520
+
355
521
  router.use(authenticate);
356
522
  router.use('/:botId/event-graphs', eventGraphsRouter);
357
523
  router.use('/:botId/plugins/ide', pluginIdeRouter);
@@ -622,25 +788,108 @@ router.get('/:botId/plugins', authenticateUniversal, checkBotAccess, authorize('
622
788
  } catch (error) { res.status(500).json({ error: 'Не удалось получить плагины бота' }); }
623
789
  });
624
790
 
625
- router.post('/:botId/plugins/install/github', authenticateUniversal, checkBotAccess, authorize('plugin:install'), async (req, res) => {
626
- const { botId } = req.params;
791
+ router.post('/:botId/plugins/install/github/preview', githubPreviewLimiter, authenticateUniversal, checkBotAccess, authorize('plugin:install'), async (req, res) => {
627
792
  const { repoUrl } = req.body;
793
+
628
794
  try {
629
- const newPlugin = await pluginManager.installFromGithub(parseInt(botId), repoUrl);
630
- res.status(201).json(newPlugin);
795
+ const { owner, repo, normalizedUrl } = parseGithubRepoUrl(repoUrl);
796
+ const repoInfo = await fetchGithubJson(`https://api.github.com/repos/${owner}/${repo}`);
797
+
798
+ let tags = [];
799
+ let latestRelease = null;
800
+ let readme = null;
801
+ let readmeHtml = null;
802
+
803
+ try {
804
+ const tagsData = await fetchGithubJson(`https://api.github.com/repos/${owner}/${repo}/tags?per_page=20`);
805
+ tags = Array.isArray(tagsData) ? tagsData.map(tag => ({
806
+ name: tag.name,
807
+ sha: tag.commit?.sha || null
808
+ })) : [];
809
+ } catch (error) {
810
+ console.warn(`[GitHub Preview] Failed to load tags for ${owner}/${repo}:`, error.message);
811
+ }
812
+
813
+ try {
814
+ latestRelease = await fetchGithubJson(`https://api.github.com/repos/${owner}/${repo}/releases/latest`);
815
+ } catch (error) {
816
+ console.warn(`[GitHub Preview] Failed to load latest release for ${owner}/${repo}:`, error.message);
817
+ }
818
+
819
+ try {
820
+ readme = await fetchGithubReadme(owner, repo);
821
+ } catch (error) {
822
+ console.warn(`[GitHub Preview] Failed to load README for ${owner}/${repo}:`, error.message);
823
+ }
824
+
825
+ try {
826
+ readmeHtml = await renderGithubMarkdown(readme, owner, repo);
827
+ } catch (error) {
828
+ console.warn(`[GitHub Preview] Failed to render README for ${owner}/${repo}:`, error.message);
829
+ }
830
+
831
+ res.json({
832
+ repo: {
833
+ name: repoInfo.name,
834
+ fullName: repoInfo.full_name,
835
+ description: repoInfo.description || '',
836
+ defaultBranch: repoInfo.default_branch,
837
+ stars: repoInfo.stargazers_count || 0,
838
+ htmlUrl: repoInfo.html_url || normalizedUrl,
839
+ visibility: repoInfo.private ? 'private' : 'public'
840
+ },
841
+ latestReleaseTag: latestRelease?.tag_name || null,
842
+ tags,
843
+ readme,
844
+ readmeHtml
845
+ });
631
846
  } catch (error) {
632
- res.status(500).json({ message: error.message });
847
+ if (error.name === 'AbortError') {
848
+ return res.status(504).json({ message: 'GitHub request timed out. Please try again.' });
849
+ }
850
+ if (error.status === 404) {
851
+ return res.status(404).json({ message: 'GitHub repository not found or it is private.' });
852
+ }
853
+ if (error.status === 403) {
854
+ return res.status(503).json({ message: 'GitHub API rate limit exceeded. Try again a bit later.' });
855
+ }
856
+ const status = error.status || (/required|invalid|only github|must include|unsupported/i.test(error.message) ? 400 : 500);
857
+ res.status(status).json({ message: error.message });
633
858
  }
634
859
  });
635
860
 
636
- router.post('/:botId/plugins/install/local', authenticateUniversal, checkBotAccess, authorize('plugin:install'), async (req, res) => {
861
+ router.post('/:botId/plugins/install/github', githubInstallLimiter, authenticateUniversal, checkBotAccess, authorize('plugin:install'), async (req, res) => {
637
862
  const { botId } = req.params;
638
- const { path } = req.body;
863
+ const { repoUrl, tag } = req.body;
864
+ let normalizedRepoUrl = repoUrl;
865
+ const normalizedTag = typeof tag === 'string' && tag.trim() ? tag.trim() : null;
639
866
  try {
640
- const newPlugin = await pluginManager.installFromLocalPath(parseInt(botId), path);
867
+ normalizedRepoUrl = normalizeGithubRepoUrl(repoUrl);
868
+ const newPlugin = await pluginManager.installFromGithub(parseInt(botId), normalizedRepoUrl, prisma, false, normalizedTag);
641
869
  res.status(201).json(newPlugin);
642
870
  } catch (error) {
643
- res.status(500).json({ message: error.message });
871
+ let status = /required|invalid|only github|must include|unsupported/i.test(error.message) ? 400 : 500;
872
+ let message = error.message;
873
+
874
+ if (/status:\s*404|статус:\s*404/i.test(message)) {
875
+ status = 404;
876
+ message = normalizedTag
877
+ ? `GitHub tag "${normalizedTag}" was not found in ${normalizedRepoUrl}.`
878
+ : `GitHub repository was not found or is private: ${normalizedRepoUrl}.`;
879
+ } else if (/status:\s*403|статус:\s*403/i.test(message)) {
880
+ status = 503;
881
+ message = 'GitHub temporarily refused the request or rate limit was exceeded. Try again later.';
882
+ } else if (/package\.json/i.test(message)) {
883
+ status = 400;
884
+ message = 'The GitHub repository does not contain a valid plugin package.json.';
885
+ } else if (/fetch/i.test(message)) {
886
+ status = 502;
887
+ message = normalizedTag
888
+ ? `Failed to connect to GitHub while downloading tag "${normalizedTag}".`
889
+ : 'Failed to connect to GitHub while downloading the repository.';
890
+ }
891
+
892
+ res.status(status).json({ message });
644
893
  }
645
894
  });
646
895
 
@@ -178,7 +178,11 @@ router.put('/:graphId',
178
178
  dataToUpdate.graphJson = graphJson;
179
179
 
180
180
  const parsedGraph = JSON.parse(graphJson);
181
- const eventNodes = parsedGraph.nodes.filter(node => node.type.startsWith('event:'));
181
+ const NON_TRIGGER_TYPES = ['custom_event', 'call_event'];
182
+ const eventNodes = parsedGraph.nodes.filter(node =>
183
+ node.type.startsWith('event:') &&
184
+ !NON_TRIGGER_TYPES.includes(node.type.split(':')[1])
185
+ );
182
186
  const eventTypes = [...new Set(eventNodes.map(node => node.type.split(':')[1]))];
183
187
 
184
188
  const existingGraph = await prisma.eventGraph.findUnique({
@@ -458,5 +462,151 @@ router.post('/:graphId/duplicate',
458
462
  }
459
463
  );
460
464
 
465
+ router.post('/test-run/:graphId',
466
+ authorize('management:edit'),
467
+ async (req, res) => {
468
+ const { botId, graphId } = req.params;
469
+ const { eventType = 'chat', eventArgs = {} } = req.body || {};
470
+
471
+ try {
472
+ const eventGraph = await prisma.eventGraph.findFirst({
473
+ where: { id: parseInt(graphId), botId: parseInt(botId) }
474
+ });
475
+ if (!eventGraph) {
476
+ return res.status(404).json({ error: 'Event graph not found' });
477
+ }
478
+ if (!eventGraph.graphJson) {
479
+ return res.status(400).json({ error: 'Graph has no data' });
480
+ }
481
+
482
+ const parsed = JSON.parse(eventGraph.graphJson);
483
+ const graph = {
484
+ id: eventGraph.id,
485
+ name: eventGraph.name,
486
+ nodes: parsed.nodes || [],
487
+ connections: parsed.connections || [],
488
+ variables: parsed.variables || []
489
+ };
490
+
491
+ const nodeRegistry = require('../../core/NodeRegistry');
492
+ const GraphExecutionEngine = require('../../core/GraphExecutionEngine');
493
+ const { getGlobalDebugManager } = require('../../core/services/DebugSessionManager');
494
+ const { buildTestContext } = require('../../core/services/TestModeContext');
495
+
496
+ const debugManager = getGlobalDebugManager();
497
+ const debugState = debugManager.getOrCreate(parseInt(botId), parseInt(graphId));
498
+ debugState.enableTestMode({ botId: parseInt(botId), graphId: parseInt(graphId), eventType, eventArgs });
499
+
500
+ const engine = new GraphExecutionEngine(nodeRegistry, null);
501
+
502
+ const context = buildTestContext({
503
+ botId: parseInt(botId),
504
+ graphId: parseInt(graphId),
505
+ eventArgs
506
+ });
507
+
508
+ engine.execute(graph, context, eventType).catch(err => {
509
+ if (!err) return;
510
+ if (err.name === 'BreakLoopSignal') return;
511
+ if (err.message === 'Execution stopped by debugger') return;
512
+ logger.error('[Test Run] Execution error:', err.message);
513
+ });
514
+
515
+ res.json({ success: true, message: 'Test run started in step mode' });
516
+ } catch (error) {
517
+ logger.error('[Test Run] Failed:', error);
518
+ res.status(500).json({ error: error.message || 'Test run failed' });
519
+ }
520
+ }
521
+ );
522
+
523
+ router.post('/run-node/:graphId/:nodeId',
524
+ authorize('management:edit'),
525
+ async (req, res) => {
526
+ const { botId, graphId, nodeId } = req.params;
527
+ const { inputs = {}, variables = {} } = req.body || {};
528
+
529
+ try {
530
+ const eventGraph = await prisma.eventGraph.findFirst({
531
+ where: { id: parseInt(graphId), botId: parseInt(botId) }
532
+ });
533
+ if (!eventGraph) {
534
+ return res.status(404).json({ error: 'Event graph not found' });
535
+ }
536
+ const parsed = JSON.parse(eventGraph.graphJson || '{}');
537
+ const node = (parsed.nodes || []).find(n => n.id === nodeId);
538
+ if (!node) {
539
+ return res.status(404).json({ error: 'Node not found in graph' });
540
+ }
541
+
542
+ const nodeRegistry = require('../../core/NodeRegistry');
543
+ const nodeConfig = nodeRegistry.getNodeConfig(node.type);
544
+ if (!nodeConfig) {
545
+ return res.status(400).json({ error: `Unknown node type: ${node.type}` });
546
+ }
547
+
548
+ const GraphExecutionEngine = require('../../core/GraphExecutionEngine');
549
+ const { buildTestContext } = require('../../core/services/TestModeContext');
550
+ const engine = new GraphExecutionEngine(nodeRegistry, null);
551
+
552
+ engine.activeGraph = {
553
+ id: parseInt(graphId),
554
+ nodes: parsed.nodes || [],
555
+ connections: parsed.connections || [],
556
+ variables: parsed.variables || []
557
+ };
558
+ engine.context = {
559
+ ...buildTestContext({
560
+ botId: parseInt(botId),
561
+ graphId: parseInt(graphId),
562
+ eventArgs: inputs
563
+ }),
564
+ variables
565
+ };
566
+
567
+ for (const [pin, value] of Object.entries(inputs)) {
568
+ engine.memo.set(`__forced:${node.id}:${pin}`, value);
569
+ }
570
+ const originalResolve = engine.resolvePinValue.bind(engine);
571
+ engine.resolvePinValue = async function(targetNode, pinName) {
572
+ if (targetNode.id === node.id && engine.memo.has(`__forced:${node.id}:${pinName}`)) {
573
+ return engine.memo.get(`__forced:${node.id}:${pinName}`);
574
+ }
575
+ return originalResolve(targetNode, pinName);
576
+ };
577
+
578
+ const startedAt = Date.now();
579
+ const helpers = {
580
+ resolvePinValue: engine.resolvePinValue.bind(engine),
581
+ traverse: async () => {},
582
+ memo: engine.memo,
583
+ clearLoopBodyMemo: () => {}
584
+ };
585
+
586
+ let outputs = {};
587
+ let errorMsg = null;
588
+ try {
589
+ await nodeConfig.executor.call(engine, node, engine.context, helpers);
590
+ outputs = await engine._captureNodeOutputs(node);
591
+ } catch (e) {
592
+ errorMsg = e.message;
593
+ }
594
+
595
+ res.json({
596
+ success: !errorMsg,
597
+ nodeId: node.id,
598
+ nodeType: node.type,
599
+ executionTime: Date.now() - startedAt,
600
+ outputs,
601
+ error: errorMsg,
602
+ variables: engine.context.variables
603
+ });
604
+ } catch (error) {
605
+ logger.error('[Run Node] Failed:', error);
606
+ res.status(500).json({ error: error.message || 'Run node failed' });
607
+ }
608
+ }
609
+ );
610
+
461
611
  module.exports = router;
462
612
 
@@ -0,0 +1,38 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+
4
+ router.get('/', async (req, res) => {
5
+ const startTime = Date.now();
6
+ const checks = {};
7
+ let overallHealthy = true;
8
+
9
+ try {
10
+ const prisma = require('../../lib/prisma');
11
+ await prisma.$queryRaw`SELECT 1`;
12
+ checks.database = { status: 'healthy' };
13
+ } catch (err) {
14
+ checks.database = { status: 'unhealthy', error: err.message };
15
+ overallHealthy = false;
16
+ }
17
+
18
+ try {
19
+ const botManager = req.app.get('botManager');
20
+ checks.botManager = { status: botManager ? 'healthy' : 'unhealthy' };
21
+ if (!botManager) overallHealthy = false;
22
+ } catch (err) {
23
+ checks.botManager = { status: 'unhealthy', error: err.message };
24
+ overallHealthy = false;
25
+ }
26
+
27
+ const duration = Date.now() - startTime;
28
+ const status = overallHealthy ? 200 : 503;
29
+
30
+ res.status(status).json({
31
+ status: overallHealthy ? 'healthy' : 'unhealthy',
32
+ checks,
33
+ duration,
34
+ timestamp: new Date().toISOString(),
35
+ });
36
+ });
37
+
38
+ module.exports = router;