mm_os 4.3.7 → 5.0.0

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 (232) hide show
  1. package/README.md +101 -504
  2. package/index.js +26 -19
  3. package/lib/loader.js +787 -0
  4. package/lib/os.js +369 -0
  5. package/package.json +55 -84
  6. package/LICENSE +0 -21
  7. package/README_EN.md +0 -505
  8. package/adapter/adapter.js +0 -484
  9. package/adapter/custom_persistence.js +0 -661
  10. package/adapter/mqtt.js +0 -254
  11. package/adapter/socket.js +0 -244
  12. package/adapter/web.js +0 -92
  13. package/adapter/websocket.js +0 -43
  14. package/ai.js +0 -130
  15. package/com/api/README.md +0 -3
  16. package/com/api/com.json +0 -5
  17. package/com/api/config.tpl.json +0 -41
  18. package/com/api/drive.js +0 -732
  19. package/com/api/index.js +0 -201
  20. package/com/api/oauth.js +0 -200
  21. package/com/api/script.tpl.js +0 -32
  22. package/com/cmd/README.md +0 -11
  23. package/com/cmd/com.json +0 -5
  24. package/com/cmd/config.tpl.json +0 -48
  25. package/com/cmd/drive.js +0 -176
  26. package/com/cmd/index.js +0 -287
  27. package/com/cmd/job/config.json +0 -66
  28. package/com/cmd/job/index.js +0 -241
  29. package/com/cmd/job/test.js +0 -188
  30. package/com/cmd/msg.json +0 -48
  31. package/com/cmd/script.tpl.js +0 -12
  32. package/com/db/README.md +0 -3
  33. package/com/db/com.json +0 -5
  34. package/com/db/drive.js +0 -2206
  35. package/com/db/index.js +0 -241
  36. package/com/event/README.md +0 -4
  37. package/com/event/com.json +0 -5
  38. package/com/event/config.tpl.json +0 -20
  39. package/com/event/drive.js +0 -61
  40. package/com/event/index.js +0 -438
  41. package/com/event/script.tpl.js +0 -23
  42. package/com/mqtt/README.md +0 -47
  43. package/com/mqtt/com.json +0 -5
  44. package/com/mqtt/config.tpl.json +0 -42
  45. package/com/mqtt/drive.js +0 -704
  46. package/com/mqtt/index.js +0 -1052
  47. package/com/mqtt/index.test.js +0 -503
  48. package/com/mqtt/script.tpl.js +0 -719
  49. package/com/nav/README.md +0 -2
  50. package/com/nav/com.json +0 -5
  51. package/com/nav/config.tpl.json +0 -84
  52. package/com/nav/drive.js +0 -702
  53. package/com/nav/index.js +0 -228
  54. package/com/nav/tpl/admin_pc/page_channel.vue +0 -305
  55. package/com/nav/tpl/admin_pc/page_config.vue +0 -280
  56. package/com/nav/tpl/admin_pc/page_config_form.vue +0 -194
  57. package/com/nav/tpl/admin_pc/page_default.vue +0 -247
  58. package/com/nav/tpl/admin_pc/page_form.vue +0 -180
  59. package/com/nav/tpl/admin_pc/page_lang.vue +0 -264
  60. package/com/nav/tpl/admin_pc/page_nav.vue +0 -321
  61. package/com/nav/tpl/admin_pc/page_table.vue +0 -296
  62. package/com/nav/tpl/admin_pc/page_type.vue +0 -321
  63. package/com/nav/tpl/admin_pc/page_view.vue +0 -124
  64. package/com/nav/tpl/dev_pc/page_channel.vue +0 -253
  65. package/com/nav/tpl/dev_pc/page_config.vue +0 -253
  66. package/com/nav/tpl/dev_pc/page_default.vue +0 -247
  67. package/com/nav/tpl/dev_pc/page_form.vue +0 -162
  68. package/com/nav/tpl/dev_pc/page_nav.vue +0 -253
  69. package/com/nav/tpl/dev_pc/page_table.vue +0 -247
  70. package/com/nav/tpl/dev_pc/page_type.vue +0 -313
  71. package/com/nav/tpl/home_pc/page_channel.vue +0 -234
  72. package/com/nav/tpl/home_pc/page_default.vue +0 -234
  73. package/com/nav/tpl/home_pc/page_form.vue +0 -137
  74. package/com/nav/tpl/home_pc/page_list.vue +0 -234
  75. package/com/nav/tpl/home_pc/page_nav.vue +0 -221
  76. package/com/nav/tpl/home_pc/page_type.vue +0 -234
  77. package/com/nav/tpl/home_pc/page_view.vue +0 -125
  78. package/com/nav/tpl/home_phone/page_channel.vue +0 -234
  79. package/com/nav/tpl/home_phone/page_default.vue +0 -234
  80. package/com/nav/tpl/home_phone/page_form.vue +0 -137
  81. package/com/nav/tpl/home_phone/page_list.vue +0 -234
  82. package/com/nav/tpl/home_phone/page_nav.vue +0 -237
  83. package/com/nav/tpl/home_phone/page_type.vue +0 -234
  84. package/com/nav/tpl/home_phone/page_view.vue +0 -125
  85. package/com/nav/viewmodel.js +0 -446
  86. package/com/param/README.md +0 -2
  87. package/com/param/com.json +0 -5
  88. package/com/param/config.tpl.json +0 -136
  89. package/com/param/drive.js +0 -502
  90. package/com/param/index.js +0 -158
  91. package/com/param/script.tpl.js +0 -12
  92. package/com/pendant/README.md +0 -3
  93. package/com/pendant/com.json +0 -5
  94. package/com/pendant/config.tpl.json +0 -32
  95. package/com/pendant/drive.js +0 -204
  96. package/com/pendant/index.js +0 -467
  97. package/com/pendant/pendant.html +0 -16
  98. package/com/pendant/script.tpl.js +0 -18
  99. package/com/socket/README.md +0 -47
  100. package/com/socket/com.json +0 -5
  101. package/com/socket/config.tpl.json +0 -12
  102. package/com/socket/drive.js +0 -679
  103. package/com/socket/index.js +0 -402
  104. package/com/socket/script.tpl.js +0 -41
  105. package/com/sql/README.md +0 -3
  106. package/com/sql/com.json +0 -5
  107. package/com/sql/config.tpl.json +0 -74
  108. package/com/sql/drive.js +0 -1328
  109. package/com/sql/index.js +0 -152
  110. package/com/sql/script.tpl.js +0 -47
  111. package/com/static/README.md +0 -2
  112. package/com/static/com.json +0 -5
  113. package/com/static/config.tpl.json +0 -24
  114. package/com/static/drive.js +0 -194
  115. package/com/static/index.js +0 -237
  116. package/com/static/script.tpl.js +0 -28
  117. package/com/task/README.md +0 -3
  118. package/com/task/com.json +0 -5
  119. package/com/task/config.tpl.json +0 -24
  120. package/com/task/drive.js +0 -428
  121. package/com/task/index.js +0 -159
  122. package/com/task/script.tpl.js +0 -40
  123. package/com/template/com.json +0 -5
  124. package/com/template/config.tpl.json +0 -16
  125. package/com/template/drive.js +0 -81
  126. package/com/template/index.js +0 -141
  127. package/com.js +0 -237
  128. package/common/README.md +0 -2
  129. package/common/middleware/web_after/index.js +0 -26
  130. package/common/middleware/web_after/middleware.json +0 -9
  131. package/common/middleware/web_base/index.js +0 -113
  132. package/common/middleware/web_base/middleware.json +0 -19
  133. package/common/middleware/web_before/index.js +0 -33
  134. package/common/middleware/web_before/middleware.json +0 -9
  135. package/common/middleware/web_cors/index.js +0 -87
  136. package/common/middleware/web_cors/middleware.json +0 -24
  137. package/common/middleware/web_error/index.js +0 -131
  138. package/common/middleware/web_error/middleware.json +0 -19
  139. package/common/middleware/web_ip/index.js +0 -15
  140. package/common/middleware/web_ip/middleware.json +0 -14
  141. package/common/middleware/web_logger/index.js +0 -156
  142. package/common/middleware/web_logger/middleware.json +0 -14
  143. package/common/middleware/web_main/index.js +0 -26
  144. package/common/middleware/web_main/middleware.json +0 -9
  145. package/common/middleware/web_static/index.js +0 -73
  146. package/common/middleware/web_static/middleware.json +0 -54
  147. package/common/middleware/web_waf/index.js +0 -395
  148. package/common/middleware/web_waf/middleware.json +0 -13
  149. package/core/app/config.tpl.json +0 -67
  150. package/core/app/index.js +0 -711
  151. package/core/app/script.tpl.js +0 -52
  152. package/core/channel/index.js +0 -899
  153. package/core/channel/matcher.js +0 -585
  154. package/core/com/config.tpl.json +0 -16
  155. package/core/com/index.js +0 -77
  156. package/core/com/script.tpl.js +0 -5
  157. package/core/component/component.js +0 -42
  158. package/core/component/config.tpl.json +0 -63
  159. package/core/component/index.js +0 -273
  160. package/core/component/script.tpl.js +0 -19
  161. package/core/controller/config.tpl.json +0 -14
  162. package/core/controller/index.js +0 -371
  163. package/core/controller/script.tpl.js +0 -27
  164. package/core/factory/config.tpl.json +0 -14
  165. package/core/factory/entity.js +0 -275
  166. package/core/factory/index.js +0 -241
  167. package/core/factory/script.tpl.js +0 -16
  168. package/core/game/bat/index.js +0 -137
  169. package/core/game/bat/world.js +0 -622
  170. package/core/game/config.tpl.json +0 -16
  171. package/core/game/entity_admin.js +0 -230
  172. package/core/game/index.js +0 -186
  173. package/core/handler/config.tpl.json +0 -22
  174. package/core/handler/index.js +0 -181
  175. package/core/handler/script.tpl.js +0 -23
  176. package/core/logic/config.tpl.json +0 -14
  177. package/core/logic/index.js +0 -59
  178. package/core/logic/script.tpl.js +0 -19
  179. package/core/middleware/config.tpl.json +0 -16
  180. package/core/middleware/index.js +0 -117
  181. package/core/middleware/script.tpl.js +0 -37
  182. package/core/mod/config.tpl.json +0 -22
  183. package/core/mod/index.js +0 -136
  184. package/core/mod/script.tpl.js +0 -34
  185. package/core/model/config.tpl.json +0 -219
  186. package/core/model/index.js +0 -272
  187. package/core/model/model.js +0 -27
  188. package/core/model/script.tpl.js +0 -20
  189. package/core/notifier/config.tpl.json +0 -14
  190. package/core/notifier/index.js +0 -77
  191. package/core/notifier/script.tpl.js +0 -20
  192. package/core/plugin/config.tpl.json +0 -36
  193. package/core/plugin/index.js +0 -267
  194. package/core/plugin/script.tpl.js +0 -51
  195. package/core/pusher/config.tpl.json +0 -14
  196. package/core/pusher/index.js +0 -158
  197. package/core/pusher/script.tpl.js +0 -20
  198. package/core/room/bat/index.js +0 -170
  199. package/core/room/bat/room.js +0 -524
  200. package/core/room/config.tpl.json +0 -20
  201. package/core/room/index.js +0 -249
  202. package/core/room/room.js +0 -61
  203. package/core/scene/config.tpl.json +0 -14
  204. package/core/scene/index.js +0 -467
  205. package/core/scene/loop.js +0 -1306
  206. package/core/scene/map.js +0 -28
  207. package/core/scene/script.tpl.js +0 -22
  208. package/core/sender/config.tpl.json +0 -14
  209. package/core/sender/index.js +0 -79
  210. package/core/sender/script.tpl.js +0 -20
  211. package/core/service/config.tpl.json +0 -14
  212. package/core/service/index.js +0 -100
  213. package/core/service/script.tpl.js +0 -25
  214. package/core/store/config.tpl.json +0 -26
  215. package/core/store/index.js +0 -1794
  216. package/core/store/script.tpl.js +0 -22
  217. package/core/system/config.tpl.json +0 -18
  218. package/core/system/index.js +0 -312
  219. package/core/system/script.tpl.js +0 -77
  220. package/core/view/config.tpl.json +0 -14
  221. package/core/view/index.js +0 -91
  222. package/core/view/script.tpl.js +0 -20
  223. package/core/zone/bat/index.js +0 -744
  224. package/core/zone/config.tpl.json +0 -54
  225. package/core/zone/index.js +0 -655
  226. package/core/zone/script.tpl.js +0 -10
  227. package/core/zone/zone_bat.js +0 -136
  228. package/core//345/237/272/347/261/273/346/250/241/345/235/227/346/270/205/345/215/225.md +0 -24
  229. package/os.js +0 -53
  230. package/server.js +0 -602
  231. package/ulits/id_gen.js +0 -134
  232. package/ulits/word.js +0 -206
package/lib/loader.js ADDED
@@ -0,0 +1,787 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const vm = require('vm');
6
+ const { Base } = require('mm_corejs');
7
+ const { appAdmin } = require('mm_app');
8
+
9
+ /**
10
+ * 应用加载器类
11
+ *
12
+ * 扫描应用根目录下的各个应用,读取每个应用的配置文件(app.json),
13
+ * 通过 mm_app 的应用管理器(AppManager / App 驱动)加载应用并驱动其生命周期,
14
+ * 同时把应用下的资源清单(event.json / socket.json / mqtt.json / pendant.json /
15
+ * static.json)注册到 OS 对应的资源管理器。
16
+ *
17
+ * 应用与插件均由驱动对象承载:
18
+ * App 驱动 由 this.apps(AppManager)管理,暴露 config / dir / file /
19
+ * methods / resources,并通过 this.plugins(PluginManager)承载插件
20
+ * Plugin 驱动 由 App 的 this.plugins 管理,结构与应用驱动类似
21
+ *
22
+ * 插件位于应用目录下的 plugin 目录中,结构为:
23
+ * plugin/{插件名}/plugin.json 插件配置(可选,缺失时以目录名作为插件名)
24
+ * plugin/{插件名}/index.js 插件入口(可选)
25
+ * plugin/{插件名}/api_*\/... 插件级 api.json
26
+ * plugin/{插件名}/event_api/... 插件级 event.json
27
+ * plugin/{插件名}/static/... 插件级 static.json
28
+ */
29
+ class Loader extends Base {
30
+ /**
31
+ * 加载器默认配置
32
+ * @type {object}
33
+ */
34
+ static config = {
35
+ // 名称
36
+ name: 'loader',
37
+ // 标题
38
+ title: '应用加载器',
39
+ // 描述
40
+ description: '扫描应用目录,加载应用配置、生命周期与资源清单',
41
+ // 应用根目录,为空时取 mm_os/example/app
42
+ dir: '',
43
+ // 应用与资源管理器作用域
44
+ scope: 'sys',
45
+ // 应用入口文件名
46
+ main: 'index.js',
47
+ // 应用配置文件名
48
+ config_file: 'app.json',
49
+ // 资源目录名 -> 管理器名称 / 模块名 / 清单文件名
50
+ resources: {
51
+ event_api: { admin: 'event', module: 'mm_event', file: 'event.json' },
52
+ socket: { admin: 'socket', module: 'mm_socket', file: 'socket.json' },
53
+ mqtt: { admin: 'mqtt', module: 'mm_mqtt', file: 'mqtt.json' },
54
+ pendant: { admin: 'pendant', module: 'mm_pendant', file: 'pendant.json' },
55
+ static: { admin: 'statics', module: 'mm_statics', file: 'static.json' }
56
+ },
57
+ // 生命周期阶段 -> 应用方法名(应用未自定义时的回落值)
58
+ lifecycle: {
59
+ load: ['install', 'init'],
60
+ start: ['start'],
61
+ stop: ['stop', 'uninstall']
62
+ },
63
+ // 插件配置
64
+ plugin: {
65
+ // 插件目录名
66
+ dir: 'plugin',
67
+ // 插件配置文件名,可缺失
68
+ config_file: 'plugin.json',
69
+ // 插件入口文件名,可缺失
70
+ main: 'index.js',
71
+ // 资源目录名 -> 管理器名称 / 模块名 / 清单文件名 / 子目录名(空串表示以插件目录为根)
72
+ resources: {
73
+ api: { admin: 'api', module: 'mm_api', file: 'api.json', sub_dir: '' },
74
+ event_api: { admin: 'event', module: 'mm_event', file: 'event.json' },
75
+ static: { admin: 'statics', module: 'mm_statics', file: 'static.json' }
76
+ },
77
+ // 生命周期阶段 -> 插件方法名(插件未自定义时的回落值)
78
+ lifecycle: {
79
+ load: ['install', '_init'],
80
+ start: ['start'],
81
+ stop: ['stop', 'uninstall']
82
+ }
83
+ }
84
+ };
85
+
86
+ /**
87
+ * 构造函数
88
+ * @param {object} config 配置参数
89
+ * @param {object} server 框架实例
90
+ */
91
+ constructor(config, server) {
92
+ super(config, server);
93
+
94
+ /**
95
+ * 应用管理器,按作用域创建并托管应用驱动
96
+ * @type {object}
97
+ */
98
+ this.apps = appAdmin(this.config.scope);
99
+ this.apps.setServer(this.getServer());
100
+
101
+ /**
102
+ * 资源管理器缓存,键为资源目录名
103
+ * @type {Map}
104
+ */
105
+ this._managers = new Map();
106
+
107
+ /**
108
+ * 已输出过警告的资源目录名集合
109
+ * @type {Set}
110
+ */
111
+ this._warned = new Set();
112
+ }
113
+
114
+ // ---------- 生命周期钩子 ----------
115
+ /**
116
+ * 初始化钩子:加载全部应用
117
+ * @returns {Promise<void>} 无返回值
118
+ */
119
+ async onInit() {
120
+ await this.load();
121
+ }
122
+
123
+ /**
124
+ * 启动钩子:启动全部应用
125
+ * @returns {Promise<void>} 无返回值
126
+ */
127
+ async onStart() {
128
+ await this.startAll();
129
+ }
130
+
131
+ /**
132
+ * 停止钩子:停止全部应用
133
+ * @returns {Promise<void>} 无返回值
134
+ */
135
+ async onStop() {
136
+ await this.stopAll();
137
+ }
138
+
139
+ /**
140
+ * 释放钩子:清空应用与资源管理器缓存
141
+ * @returns {Promise<void>} 无返回值
142
+ */
143
+ async onDispose() {
144
+ await this.clear();
145
+ }
146
+
147
+ // ---------- 公开方法 ----------
148
+ /**
149
+ * 加载应用根目录下的全部应用
150
+ * @returns {Promise<void>} 无返回值
151
+ */
152
+ async load() {
153
+ if (this.getLogger()) {
154
+ this.apps.setLogger(this.getLogger());
155
+ }
156
+ const app_dir = this._getAppDir();
157
+ const dirs = this._listAppDirs(app_dir);
158
+ await this._loadApps(dirs);
159
+ this.log('info', `应用加载完成,共 ${this.apps.size()} 个`);
160
+ }
161
+
162
+ /**
163
+ * 启动全部应用
164
+ * @returns {Promise<void>} 无返回值
165
+ */
166
+ async startAll() {
167
+ for (const app of this.apps.values()) {
168
+ await this._startApp(app);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * 停止全部应用
174
+ * @returns {Promise<void>} 无返回值
175
+ */
176
+ async stopAll() {
177
+ for (const app of this.apps.values()) {
178
+ await this._stopApp(app);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * 获取应用
184
+ * @param {string} name 应用名称
185
+ * @returns {object} 应用驱动,不存在时返回 undefined
186
+ */
187
+ get(name) {
188
+ return this.apps.get(name);
189
+ }
190
+
191
+ /**
192
+ * 判断应用是否存在
193
+ * @param {string} name 应用名称
194
+ * @returns {boolean} 是否存在
195
+ */
196
+ has(name) {
197
+ return this.apps.has(name);
198
+ }
199
+
200
+ /**
201
+ * 获取应用列表
202
+ * @returns {Array} 应用驱动列表
203
+ */
204
+ list() {
205
+ return this.apps.values();
206
+ }
207
+
208
+ /**
209
+ * 重启应用:删除后重新加载,曾运行则再次启动
210
+ * @param {string} name 应用名称
211
+ * @returns {Promise<boolean>} 是否执行成功
212
+ */
213
+ async reload(name) {
214
+ const app = this.get(name);
215
+ if (!app) {
216
+ this.log('warn', `应用 ${name} 不存在,无法重启`);
217
+ return false;
218
+ }
219
+ const app_dir = app.dir;
220
+ const was_running = app.isRunning();
221
+ await this.apps.del(name);
222
+ const next = await this._loadApp(app_dir);
223
+ if (!next) return false;
224
+ if (was_running) {
225
+ await this._startApp(next);
226
+ }
227
+ return true;
228
+ }
229
+
230
+ /**
231
+ * 删除应用:停止并释放后从应用管理器移除
232
+ * @param {string} name 应用名称
233
+ * @returns {Promise<boolean>} 是否删除成功
234
+ */
235
+ async del(name) {
236
+ if (!this.has(name)) return false;
237
+ await this.apps.del(name);
238
+ return true;
239
+ }
240
+
241
+ /**
242
+ * 清空应用列表与资源管理器缓存
243
+ * @returns {Promise<void>} 无返回值
244
+ */
245
+ async clear() {
246
+ await this.apps.clear();
247
+ this._managers.clear();
248
+ this._warned.clear();
249
+ }
250
+
251
+ // ---------- 应用加载 ----------
252
+ /**
253
+ * 获取应用根目录,未配置时回落到 mm_os/example/app
254
+ * @returns {string} 应用根目录绝对路径
255
+ */
256
+ _getAppDir() {
257
+ if (this.config.dir) {
258
+ return path.resolve(this.config.dir);
259
+ }
260
+ return path.join(__dirname, '..', 'example', 'app');
261
+ }
262
+
263
+ /**
264
+ * 列出目录下的全部子目录,目录不存在时静默返回空列表
265
+ * @param {string} dir 目录绝对路径
266
+ * @returns {Array<string>} 子目录绝对路径列表
267
+ */
268
+ _listSubDirs(dir) {
269
+ if (!fs.existsSync(dir)) return [];
270
+ const items = fs.readdirSync(dir, { withFileTypes: true });
271
+ return items.filter(item => item.isDirectory()).map(item => path.join(dir, item.name));
272
+ }
273
+
274
+ /**
275
+ * 列出应用根目录下的全部应用目录
276
+ * @param {string} dir 应用根目录绝对路径
277
+ * @returns {Array<string>} 应用目录绝对路径列表
278
+ */
279
+ _listAppDirs(dir) {
280
+ if (!fs.existsSync(dir)) {
281
+ this.log('warn', `应用根目录不存在: ${dir}`);
282
+ return [];
283
+ }
284
+ return this._listSubDirs(dir);
285
+ }
286
+
287
+ /**
288
+ * 批量加载应用目录
289
+ * @param {Array<string>} dirs 应用目录列表
290
+ * @returns {Promise<void>} 无返回值
291
+ */
292
+ async _loadApps(dirs) {
293
+ for (const app_dir of dirs) {
294
+ await this._loadApp(app_dir);
295
+ }
296
+ }
297
+
298
+ /**
299
+ * 加载单个应用:创建驱动、注册资源、加载插件并初始化
300
+ * @param {string} app_dir 应用目录绝对路径
301
+ * @returns {Promise<object>} 应用驱动,跳过或失败时返回 null
302
+ */
303
+ async _loadApp(app_dir) {
304
+ const config_file = path.join(app_dir, this.config.config_file);
305
+ if (!fs.existsSync(config_file)) return null;
306
+
307
+ const app_config = this._resolveFileTemplates(this._readJson(config_file), config_file);
308
+ if (!app_config) return null;
309
+
310
+ const app_name = app_config.name || path.basename(app_dir);
311
+ if (this.has(app_name)) {
312
+ this.log('warn', `应用 ${app_name} 已存在,跳过: ${app_dir}`);
313
+ return null;
314
+ }
315
+
316
+ const app = this.apps.add(this._buildAppConfig(app_dir, app_config, app_name));
317
+ if (this.getLogger()) {
318
+ app.setLogger(this.getLogger());
319
+ }
320
+ this._registerResources(app, app.dir, this.config.resources);
321
+ this._loadPlugins(app, app_dir);
322
+
323
+ try {
324
+ await app.init();
325
+ } catch (err) {
326
+ this.log('error', `应用 ${app_name} 初始化失败: ${err.message}`);
327
+ }
328
+ return app;
329
+ }
330
+
331
+ /**
332
+ * 构建应用驱动配置:补齐名称、目录、入口与生命周期
333
+ * @param {string} app_dir 应用目录绝对路径
334
+ * @param {object} app_config 应用配置(app.json 内容)
335
+ * @param {string} app_name 应用名称
336
+ * @returns {object} 应用驱动配置
337
+ */
338
+ _buildAppConfig(app_dir, app_config, app_name) {
339
+ return {
340
+ ...app_config,
341
+ name: app_name,
342
+ dir: app_dir,
343
+ main: app_config.main || this.config.main,
344
+ lifecycle: app_config.lifecycle || this.config.lifecycle
345
+ };
346
+ }
347
+
348
+ /**
349
+ * 启动单个应用,失败时记录日志
350
+ * @param {object} app 应用驱动
351
+ * @returns {Promise<void>} 无返回值
352
+ */
353
+ async _startApp(app) {
354
+ try {
355
+ await app.start();
356
+ } catch (err) {
357
+ this.log('error', `启动应用 ${app.config.name} 失败: ${err.message}`);
358
+ }
359
+ }
360
+
361
+ /**
362
+ * 停止单个应用,失败时记录日志
363
+ * @param {object} app 应用驱动
364
+ * @returns {Promise<void>} 无返回值
365
+ */
366
+ async _stopApp(app) {
367
+ try {
368
+ await app.stop();
369
+ } catch (err) {
370
+ this.log('error', `停止应用 ${app.config.name} 失败: ${err.message}`);
371
+ }
372
+ }
373
+
374
+ // ---------- 插件加载 ----------
375
+ /**
376
+ * 加载应用下的全部插件
377
+ * @param {object} app 应用驱动
378
+ * @param {string} app_dir 应用目录绝对路径
379
+ */
380
+ _loadPlugins(app, app_dir) {
381
+ const plugin_config = this.config.plugin;
382
+ if (!plugin_config) return;
383
+
384
+ const plugin_root = path.join(app_dir, plugin_config.dir);
385
+ const dirs = this._listSubDirs(plugin_root);
386
+ for (const plugin_dir of dirs) {
387
+ this._loadPlugin(app, plugin_dir);
388
+ }
389
+ }
390
+
391
+ /**
392
+ * 加载单个插件:创建驱动并注册资源
393
+ * @param {object} app 应用驱动
394
+ * @param {string} plugin_dir 插件目录绝对路径
395
+ * @returns {object} 插件驱动,跳过时返回 null
396
+ */
397
+ _loadPlugin(app, plugin_dir) {
398
+ const plugin_config = this.config.plugin;
399
+ const config_file = path.join(plugin_dir, plugin_config.config_file);
400
+ const raw = fs.existsSync(config_file) ? this._readJson(config_file) : null;
401
+ const config = raw ? this._resolveFileTemplates(raw, config_file) : null;
402
+
403
+ const plugin_name = config && config.name ? config.name : path.basename(plugin_dir);
404
+ if (config && config.state === 0) {
405
+ this.log('info', `插件 ${app.config.name}.${plugin_name} 已禁用,跳过`);
406
+ return null;
407
+ }
408
+ if (app.plugins.has(plugin_name)) {
409
+ this.log('warn', `插件 ${app.config.name}.${plugin_name} 已存在,跳过`);
410
+ return null;
411
+ }
412
+
413
+ const plugin = app.plugins.add(this._buildPluginConfig(app, plugin_dir, config, plugin_name));
414
+ if (this.getLogger()) {
415
+ plugin.setLogger(this.getLogger());
416
+ }
417
+ this._registerResources(plugin, plugin_dir, plugin_config.resources);
418
+ return plugin;
419
+ }
420
+
421
+ /**
422
+ * 构建插件驱动配置:补齐名称、所属应用、目录、入口与生命周期
423
+ * @param {object} app 应用驱动
424
+ * @param {string} plugin_dir 插件目录绝对路径
425
+ * @param {object} config 插件配置,可为空
426
+ * @param {string} plugin_name 插件名称
427
+ * @returns {object} 插件驱动配置
428
+ */
429
+ _buildPluginConfig(app, plugin_dir, config, plugin_name) {
430
+ const cfg = config || {};
431
+ return {
432
+ ...cfg,
433
+ name: plugin_name,
434
+ app: cfg.app || app.config.name,
435
+ dir: plugin_dir,
436
+ main: cfg.main || this.config.plugin.main,
437
+ lifecycle: cfg.lifecycle || this.config.plugin.lifecycle
438
+ };
439
+ }
440
+
441
+ // ---------- 资源注册 ----------
442
+ /**
443
+ * 注册归属对象下的全部资源
444
+ * @param {object} owner 归属对象(应用或插件驱动)
445
+ * @param {string} base_dir 基础目录绝对路径
446
+ * @param {object} resources 资源目录名 -> 资源配置
447
+ */
448
+ _registerResources(owner, base_dir, resources) {
449
+ const map = resources || {};
450
+ for (const res_name of Object.keys(map)) {
451
+ const res = map[res_name];
452
+ // 未声明子目录时以资源目录名为子目录,空串时以基础目录为根
453
+ const sub_dir = res.sub_dir === undefined ? res_name : res.sub_dir;
454
+ const root = sub_dir ? path.join(base_dir, sub_dir) : base_dir;
455
+ this._registerResource(owner, res_name, res, root);
456
+ }
457
+ }
458
+
459
+ /**
460
+ * 注册某一类资源
461
+ * @param {object} owner 归属对象(应用或插件驱动)
462
+ * @param {string} res_name 资源目录名
463
+ * @param {object} res 资源配置
464
+ * @param {string} root 资源搜索根目录绝对路径
465
+ */
466
+ _registerResource(owner, res_name, res, root) {
467
+ if (!fs.existsSync(root)) return;
468
+
469
+ const manager = this._getManager(owner, res_name, res);
470
+ if (!manager) return;
471
+
472
+ const files = this._listConfigFiles(root, res.file);
473
+ for (const file of files) {
474
+ this._addResource(owner, res_name, manager, file);
475
+ }
476
+ }
477
+
478
+ /**
479
+ * 注册单个资源清单
480
+ * @param {object} owner 归属对象(应用或插件驱动)
481
+ * @param {string} res_name 资源目录名
482
+ * @param {object} manager 资源管理器
483
+ * @param {string} file 资源清单绝对路径
484
+ */
485
+ _addResource(owner, res_name, manager, file) {
486
+ const config = this._resolveFileTemplates(this._readJson(file), file);
487
+ if (!config) return;
488
+ // 驱动依赖 dir 解析 app 名与入口脚本
489
+ config.dir = path.dirname(file);
490
+ const name = config.name;
491
+ if (name && typeof manager.has === 'function' && manager.has(name)) {
492
+ this.log('warn', `资源 ${res_name} 已存在,跳过 ${file}: ${name}`);
493
+ return;
494
+ }
495
+ try {
496
+ const drive = manager.add(config);
497
+ owner.resources.push({ type: res_name, name: drive.config.name, file, drive });
498
+ } catch (err) {
499
+ this.log('error', `注册资源 ${res_name} 失败 ${file}: ${err.message}`);
500
+ }
501
+ }
502
+
503
+ /**
504
+ * 递归列出目录下指定名称的清单文件
505
+ * @param {string} dir 目录绝对路径
506
+ * @param {string} filename 清单文件名
507
+ * @returns {Array<string>} 清单文件绝对路径列表
508
+ */
509
+ _listConfigFiles(dir, filename) {
510
+ const files = [];
511
+ const items = fs.readdirSync(dir, { withFileTypes: true });
512
+ for (const item of items) {
513
+ const full = path.join(dir, item.name);
514
+ if (item.isDirectory()) {
515
+ files.push(...this._listConfigFiles(full, filename));
516
+ } else if (item.name === filename) {
517
+ files.push(full);
518
+ }
519
+ }
520
+ return files;
521
+ }
522
+
523
+ /**
524
+ * 获取归属对象所在作用域下的资源管理器:按 owner 作用域惰性加载
525
+ * @param {object} owner 归属对象(应用或插件驱动)
526
+ * @param {string} res_name 资源目录名
527
+ * @param {object} res 资源配置
528
+ * @returns {object} 资源管理器,不可用时返回 null
529
+ */
530
+ _getManager(owner, res_name, res) {
531
+ const scope = this._getOwnerScope(owner);
532
+ const key = `${scope}:${res_name}`;
533
+ if (this._managers.has(key)) {
534
+ return this._managers.get(key);
535
+ }
536
+
537
+ const manager = this._getModuleManager(res, scope);
538
+ if (!manager) {
539
+ this._warnOnce(key, `未找到资源管理器 ${res.admin}(${res.module}),跳过 ${res_name} 资源`);
540
+ return null;
541
+ }
542
+ if (typeof manager.add !== 'function') {
543
+ this._warnOnce(key, `资源管理器 ${res.admin} 不支持 add 方法,跳过 ${res_name} 资源`);
544
+ return null;
545
+ }
546
+
547
+ this._managers.set(key, manager);
548
+ return manager;
549
+ }
550
+
551
+ /**
552
+ * 获取归属对象的作用域名:插件取所属应用名,应用取自身名称
553
+ * @param {object} owner 归属对象(应用或插件驱动)
554
+ * @returns {string} 作用域名
555
+ */
556
+ _getOwnerScope(owner) {
557
+ const config = owner && owner.config ? owner.config : {};
558
+ return config.app || config.name || this.config.scope;
559
+ }
560
+
561
+ /**
562
+ * 按模块名惰性加载,并在指定作用域创建资源管理器
563
+ * @param {object} res 资源配置
564
+ * @param {string} scope 作用域名
565
+ * @returns {object} 资源管理器,加载失败时返回 null
566
+ */
567
+ _getModuleManager(res, scope) {
568
+ if (!res.module) return null;
569
+
570
+ let mod;
571
+ try {
572
+ const server = this.getServer();
573
+ mod = server && typeof server.require === 'function'
574
+ ? server.require(res.module)
575
+ : require(res.module);
576
+ } catch (err) {
577
+ return null;
578
+ }
579
+
580
+ const admin = mod && mod.admin;
581
+ if (!admin || typeof admin.get !== 'function') return null;
582
+
583
+ if (typeof admin.setServer === 'function') {
584
+ try {
585
+ admin.setServer(this.getServer());
586
+ } catch (err) {
587
+ // 服务器实例已设置,忽略
588
+ }
589
+ }
590
+ return admin.get(scope, { name: scope });
591
+ }
592
+
593
+ /**
594
+ * 输出一次性警告
595
+ * @param {string} key 去重键
596
+ * @param {string} message 警告内容
597
+ */
598
+ _warnOnce(key, message) {
599
+ if (this._warned.has(key)) return;
600
+ this._warned.add(key);
601
+ this.log('warn', message);
602
+ }
603
+
604
+ // ---------- 配置读取 ----------
605
+ /**
606
+ * 对配置中的模板字段求值(如 `{name || 'api'}`),以上下文求值后返回新配置
607
+ * @param {object} config 原始配置对象
608
+ * @param {string} file 配置文件绝对路径
609
+ * @returns {object} 求值后的配置对象
610
+ */
611
+ _resolveFileTemplates(config, file) {
612
+ if (!config) return config;
613
+ const ctx = this._buildTemplateContext(path.dirname(file));
614
+ return this._resolveTemplates(config, ctx);
615
+ }
616
+
617
+ /**
618
+ * 由资源目录推导模板上下文(名称、应用名、插件名、接口路径)
619
+ * @param {string} dir 资源目录绝对路径
620
+ * @returns {object} 模板上下文
621
+ */
622
+ _buildTemplateContext(dir) {
623
+ const name = path.basename(dir);
624
+ const app_name = this._extractPathSegment(dir, 'app');
625
+ const plugin_name = this._extractPathSegment(dir, 'plugin');
626
+ return {
627
+ name,
628
+ app: app_name,
629
+ plugin: plugin_name,
630
+ path: app_name ? `/${app_name}/${name}` : `/${name}`
631
+ };
632
+ }
633
+
634
+ /**
635
+ * 提取路径中指定目录名之后的段落值
636
+ * @param {string} dir 目录绝对路径
637
+ * @param {string} key 段落名(如 app / plugin)
638
+ * @returns {string} 段落值,未找到时返回空串
639
+ */
640
+ _extractPathSegment(dir, key) {
641
+ const parts = dir.split(path.sep);
642
+ const index = parts.indexOf(key);
643
+ if (index !== -1 && index + 1 < parts.length) {
644
+ return parts[index + 1];
645
+ }
646
+ return '';
647
+ }
648
+
649
+ /**
650
+ * 递归求值配置对象中的模板字段
651
+ * @param {*} config 配置对象、数组或基础值
652
+ * @param {object} ctx 模板上下文
653
+ * @returns {*} 求值后的配置
654
+ */
655
+ _resolveTemplates(config, ctx) {
656
+ if (Array.isArray(config)) {
657
+ return config.map(item => this._resolveTemplates(item, ctx));
658
+ }
659
+ if (config && typeof config === 'object') {
660
+ const result = {};
661
+ for (const key of Object.keys(config)) {
662
+ result[key] = this._resolveTemplates(config[key], ctx);
663
+ }
664
+ return result;
665
+ }
666
+ return this._renderTemplateValue(config, ctx);
667
+ }
668
+
669
+ /**
670
+ * 求值单个字符串模板,非模板或纯数字占位(如 `{0}`)时原样返回
671
+ * @param {*} value 待求值内容
672
+ * @param {object} ctx 模板上下文
673
+ * @returns {*} 求值结果
674
+ */
675
+ _renderTemplateValue(value, ctx) {
676
+ if (typeof value !== 'string') return value;
677
+ const matched = value.match(/^\{\s*([^{}]+?)\s*\}$/);
678
+ if (!matched) return value;
679
+ const expr = matched[1];
680
+ if (/^[+-]?\d+(\.\d+)?$/.test(expr)) return value;
681
+ try {
682
+ return vm.runInNewContext(`(${expr})`, this._buildSandbox(ctx));
683
+ } catch (err) {
684
+ this.log('warn', `模板字段求值失败 ${value}: ${err.message}`);
685
+ return value;
686
+ }
687
+ }
688
+
689
+ /**
690
+ * 构建模板求值沙箱,未定义的标识符解析为 undefined,便于 `||` 取默认值
691
+ * @param {object} ctx 模板上下文
692
+ * @returns {Proxy} 沙箱对象
693
+ */
694
+ _buildSandbox(ctx) {
695
+ return new Proxy(ctx, {
696
+ has: () => true,
697
+ get: (target, key) => (key in target ? target[key] : undefined)
698
+ });
699
+ }
700
+
701
+ /**
702
+ * 读取并解析 JSON 配置文件,失败时记录日志
703
+ * @param {string} file 配置文件绝对路径
704
+ * @returns {object} 解析结果,失败时返回 null
705
+ */
706
+ _readJson(file) {
707
+ try {
708
+ const text = fs.readFileSync(file, 'utf8');
709
+ return JSON.parse(this._stripComments(this._stripBom(text)));
710
+ } catch (err) {
711
+ this.log('error', `解析配置失败 ${file}: ${err.message}`);
712
+ return null;
713
+ }
714
+ }
715
+
716
+ /**
717
+ * 去除文本开头的字节顺序标记(BOM)
718
+ * @param {string} text 原始文本
719
+ * @returns {string} 去除 BOM 后的文本
720
+ */
721
+ _stripBom(text) {
722
+ return text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
723
+ }
724
+
725
+ /**
726
+ * 去除 JSON 文本中的注释(支持 // 与 /* *\/,不处理字符串内部)
727
+ * @param {string} text 原始文本
728
+ * @returns {string} 去除注释后的文本
729
+ */
730
+ _stripComments(text) {
731
+ let out = '';
732
+ let in_string = false;
733
+ let in_line = false;
734
+ let in_block = false;
735
+ let quote = '';
736
+
737
+ for (let i = 0; i < text.length; i++) {
738
+ const char = text[i];
739
+ const next = text[i + 1];
740
+
741
+ if (in_line) {
742
+ if (char === '\n') {
743
+ in_line = false;
744
+ out += char;
745
+ }
746
+ continue;
747
+ }
748
+ if (in_block) {
749
+ if (char === '*' && next === '/') {
750
+ in_block = false;
751
+ i++;
752
+ }
753
+ continue;
754
+ }
755
+ if (in_string) {
756
+ out += char;
757
+ if (char === '\\') {
758
+ out += next || '';
759
+ i++;
760
+ } else if (char === quote) {
761
+ in_string = false;
762
+ }
763
+ continue;
764
+ }
765
+ if (char === '"' || char === "'") {
766
+ in_string = true;
767
+ quote = char;
768
+ out += char;
769
+ continue;
770
+ }
771
+ if (char === '/' && next === '/') {
772
+ in_line = true;
773
+ i++;
774
+ continue;
775
+ }
776
+ if (char === '/' && next === '*') {
777
+ in_block = true;
778
+ i++;
779
+ continue;
780
+ }
781
+ out += char;
782
+ }
783
+ return out;
784
+ }
785
+ }
786
+
787
+ module.exports = Loader;