mm_machine 2.9.4 → 2.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dir_scanner.js ADDED
@@ -0,0 +1,262 @@
1
+ var fs = require('fs');
2
+ var path = require('path');
3
+
4
+ /**
5
+ * DirScanner 目录检索器
6
+ * 独立工具类,负责扫描目录查找 JSON 配置文件
7
+ * 不依赖 Manager 或 Registry,可独立使用
8
+ */
9
+ class DirScanner {
10
+ /**
11
+ * 构造函数
12
+ * @param {object} [cfg] 配置参数
13
+ * @param {number} [cfg.cache_ttl=30000] 缓存有效期(毫秒),默认 30 秒
14
+ */
15
+ constructor(cfg) {
16
+ var opts = cfg || {};
17
+ this._cache = new Map();
18
+ this._cache_ttl = opts.cache_ttl || 30000;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * 规范化扫描参数
24
+ * 处理参数重载:options 可选,force 可以是第三个参数
25
+ * @param {object|boolean} opts_in 配置对象或 force 标志
26
+ * @param {boolean} force_in force 标志
27
+ * @returns {{options: object, force: boolean}} 规范化后的参数对象
28
+ * @private
29
+ */
30
+ DirScanner.prototype._normScanOpts = function (opts_in, force_in) {
31
+ var opts = opts_in;
32
+ var force = force_in;
33
+ if (typeof opts === 'boolean') {
34
+ force = opts;
35
+ opts = null;
36
+ }
37
+ return { options: opts || {}, force: force || false };
38
+ };
39
+
40
+ /**
41
+ * 获取缓存结果
42
+ * @param {string} key 缓存键
43
+ * @returns {Array|null} 缓存结果,不存在或已过期返回 null
44
+ * @private
45
+ */
46
+ DirScanner.prototype._getCache = function (key) {
47
+ var entry = this._cache.get(key);
48
+ if (!entry) return null;
49
+ if (Date.now() - entry.time > this._cache_ttl) {
50
+ this._cache.delete(key);
51
+ return null;
52
+ }
53
+ return entry.results;
54
+ };
55
+
56
+ /**
57
+ * 设置缓存
58
+ * @param {string} key 缓存键
59
+ * @param {Array} results 扫描结果
60
+ * @private
61
+ */
62
+ DirScanner.prototype._setCache = function (key, results) {
63
+ this._cache.set(key, { results: results, time: Date.now() });
64
+ };
65
+
66
+ /**
67
+ * 递归扫描指定目录,查找匹配的 JSON 配置文件
68
+ * @param {string} dir 要扫描的目录路径
69
+ * @param {string} pattern 文件匹配模式(如 '*.json')
70
+ * @param {object} [opts_in] 可选项
71
+ * @param {string[]} [opts_in.exclude=['node_modules', '.git']] 排除的目录名列表
72
+ * @param {Function} [opts_in.filter] 自定义过滤器,接收 {file, name} 对象,返回 true 表示保留
73
+ * @param {boolean} [force_in] 是否强制重新扫描(忽略缓存)
74
+ * @returns {Array<{file: string, name: string}>} 扫描结果数组
75
+ */
76
+ DirScanner.prototype.scanDir = function (dir, pattern, opts_in, force_in) {
77
+ if (typeof dir !== 'string' || !dir) {
78
+ throw new TypeError('目录路径必须是字符串');
79
+ }
80
+ if (typeof pattern !== 'string' || !pattern) {
81
+ throw new TypeError('文件匹配模式必须是字符串');
82
+ }
83
+
84
+ var opts = this._normScanOpts(opts_in, force_in);
85
+ var force = opts.force;
86
+ var options = opts.options;
87
+
88
+ // 缓存检查
89
+ var cache_key = dir + '::' + pattern;
90
+ if (!force) {
91
+ var cached = this._getCache(cache_key);
92
+ if (cached) return cached;
93
+ }
94
+
95
+ var exclude = options.exclude || ['node_modules', '.git'];
96
+ var filter = options.filter || null;
97
+ var results = [];
98
+ var abs_dir = path.resolve(dir);
99
+
100
+ // 检查目录是否存在
101
+ if (!fs.existsSync(abs_dir)) {
102
+ return results;
103
+ }
104
+
105
+ this._scanDirRecu(abs_dir, pattern, exclude, filter, results);
106
+
107
+ // 设置缓存
108
+ this._setCache(cache_key, results);
109
+ return results;
110
+ };
111
+
112
+ /**
113
+ * 递归扫描目录内部方法
114
+ * @param {string} dir 当前目录
115
+ * @param {string} pattern 匹配模式
116
+ * @param {string[]} exclude 排除目录
117
+ * @param {Function|null} filter 自定义过滤器
118
+ * @param {Array} results 结果数组
119
+ * @private
120
+ */
121
+ DirScanner.prototype._scanDirRecu = function (dir, pattern, exclude, filter, results) {
122
+ var items;
123
+ try {
124
+ // eslint-disable-next-line mm_eslint/variable-name
125
+ items = fs.readdirSync(dir, { withFileTypes: true });
126
+ } catch {
127
+ return;
128
+ }
129
+
130
+ for (var i = 0; i < items.length; i++) {
131
+ var item = items[i];
132
+ var cur_path = path.join(dir, item.name);
133
+
134
+ if (item.isDirectory()) {
135
+ // 跳过排除目录
136
+ if (exclude.indexOf(item.name) !== -1) {
137
+ continue;
138
+ }
139
+ this._scanDirRecu(cur_path, pattern, exclude, filter, results);
140
+ } else if (item.isFile()) {
141
+ // 检查文件是否匹配模式
142
+ if (!this._matchPattern(item.name, pattern)) {
143
+ continue;
144
+ }
145
+
146
+ var name = this._extractName(cur_path);
147
+ var entry = { file: cur_path, name: name };
148
+
149
+ // 应用自定义过滤器
150
+ if (filter) {
151
+ if (!filter(entry)) {
152
+ continue;
153
+ }
154
+ }
155
+
156
+ results.push(entry);
157
+ }
158
+ }
159
+ };
160
+
161
+ /**
162
+ * 检查文件名是否匹配模式
163
+ * @param {string} filename 文件名
164
+ * @param {string} pattern 匹配模式(如 *.json)
165
+ * @returns {boolean} 是否匹配
166
+ * @private
167
+ */
168
+ DirScanner.prototype._matchPattern = function (filename, pattern) {
169
+ // 简单通配符匹配
170
+ if (pattern === '*') return true;
171
+ if (pattern === '*' + path.extname(filename)) return true;
172
+ if (pattern === filename) return true;
173
+
174
+ // 将 * 转换为正则
175
+ var regex_str = '^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$';
176
+ return new RegExp(regex_str).test(filename);
177
+ };
178
+
179
+ /**
180
+ * 从 JSON 文件提取 name
181
+ * 优先读取 JSON 文件中的 name 字段,失败则从文件名去扩展名
182
+ * @param {string} file_path 文件路径
183
+ * @returns {string} 模块名称
184
+ * @private
185
+ */
186
+ DirScanner.prototype._extractName = function (file_path) {
187
+ try {
188
+ var content = fs.readFileSync(file_path, 'utf8');
189
+ var config = JSON.parse(content);
190
+ if (config && config.name) {
191
+ return config.name;
192
+ }
193
+ } catch {
194
+ // JSON 解析失败,忽略
195
+ }
196
+ // 从文件名去扩展名
197
+ return path.basename(file_path, path.extname(file_path));
198
+ };
199
+
200
+ /**
201
+ * 扫描并解析 JSON 配置文件
202
+ * @param {string} dir 要扫描的目录路径
203
+ * @param {string} pattern 文件匹配模式
204
+ * @param {object} [options] 可选项(同 scanDir)
205
+ * @param {boolean} [force] 是否强制重新扫描
206
+ * @returns {Array<{file: string, config: object}>} 扫描结果数组,JSON 解析失败时跳过该文件
207
+ */
208
+ DirScanner.prototype.scanAndParse = function (dir, pattern, options, force) {
209
+ var files = this.scanDir(dir, pattern, options, force);
210
+ var results = [];
211
+
212
+ for (var i = 0; i < files.length; i++) {
213
+ var entry = files[i];
214
+ try {
215
+ var content = fs.readFileSync(entry.file, 'utf8');
216
+ var config = JSON.parse(content);
217
+ results.push({ file: entry.file, config: config });
218
+ } catch {
219
+ // JSON 解析失败时跳过该文件
220
+ continue;
221
+ }
222
+ }
223
+
224
+ return results;
225
+ };
226
+
227
+ /**
228
+ * 扫描目录并一键注册到 Manager
229
+ * @param {string} dir 要扫描的目录路径
230
+ * @param {object} manager Manager 实例
231
+ * @param {boolean} [force] 是否强制重新扫描
232
+ * @returns {number} 成功注册的模块数量
233
+ */
234
+ DirScanner.prototype.scanAndRegister = function (dir, manager, force) {
235
+ if (!manager || typeof manager.registerMod !== 'function') {
236
+ throw new TypeError('manager 必须包含 registerMod 方法');
237
+ }
238
+ var entries = this.scanAndParse(dir, '*.json', null, force);
239
+ var count = 0;
240
+
241
+ for (var i = 0; i < entries.length; i++) {
242
+ var entry = entries[i];
243
+ try {
244
+ manager.registerMod(entry.file, entry.config);
245
+ count++;
246
+ } catch {
247
+ // 单个模块注册失败不影响其他模块
248
+ continue;
249
+ }
250
+ }
251
+
252
+ return count;
253
+ };
254
+
255
+ /**
256
+ * 清除所有扫描缓存
257
+ */
258
+ DirScanner.prototype.clearCache = function () {
259
+ this._cache.clear();
260
+ };
261
+
262
+ module.exports = { DirScanner };
package/id_gen.js ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * IdGen 雪花算法 ID 生成器
3
+ * 基于雪花算法(Snowflake)的分布式唯一 ID 生成器,生成 64 位整数 ID
4
+ */
5
+ class IdGen {
6
+ /**
7
+ * 构造函数
8
+ * @param {object} config 配置参数
9
+ * @param {number} config.data_center_no 数据中心编号 (0-31)
10
+ * @param {number} config.machine_no 机器编号 (0-31)
11
+ * @param {number} [config.epoch=1700000000000] 自定义起始时间戳(毫秒),默认 2023-11-15 00:00:00 UTC
12
+ * @param {number} [config.sequence=0] 初始序列号
13
+ */
14
+ constructor(config = {}) {
15
+ this.data_center_no = config.data_center_no || config.dc_id || 0;
16
+ this.machine_no = config.machine_no || config.worker_id || 0;
17
+ this.sequence = config.sequence || 0;
18
+ this.epoch = config.epoch || 1700000000000;
19
+ this.last_timestamp = -1;
20
+
21
+ this._initBits();
22
+ }
23
+ }
24
+
25
+ /**
26
+ * 获取当前时间戳
27
+ * @returns {number} 当前时间戳
28
+ */
29
+ IdGen.prototype._timeGen = function () {
30
+ return Date.now();
31
+ };
32
+
33
+ /**
34
+ * 等待下一毫秒
35
+ * @param {number} last_timestamp 上次时间戳
36
+ * @returns {number} 新的时间戳
37
+ */
38
+ IdGen.prototype._tilNextMillis = function (last_timestamp) {
39
+ var timestamp = this._timeGen();
40
+ while (timestamp <= last_timestamp) {
41
+ timestamp = this._timeGen();
42
+ }
43
+ return timestamp;
44
+ };
45
+
46
+ /**
47
+ * 初始化位运算参数
48
+ * @private
49
+ */
50
+ IdGen.prototype._initBits = function () {
51
+ this.dc_id_bits = 5;
52
+ this.worker_id_bits = 5;
53
+ this.sequence_bits = 12;
54
+
55
+ this.max_dc_id = -1 ^ (-1 << this.dc_id_bits);
56
+ this.max_worker_id = -1 ^ (-1 << this.worker_id_bits);
57
+ this.sequence_mask = -1 ^ (-1 << this.sequence_bits);
58
+
59
+ this.worker_id_shift = this.sequence_bits;
60
+ this.dc_id_shift = this.sequence_bits + this.worker_id_bits;
61
+ this.timestamp_left_shift = this.sequence_bits + this.worker_id_bits + this.dc_id_bits;
62
+
63
+ if (this.data_center_no > this.max_dc_id || this.data_center_no < 0) {
64
+ throw new TypeError('数据中心编号必须在0-' + this.max_dc_id + '之间');
65
+ }
66
+ if (this.machine_no > this.max_worker_id || this.machine_no < 0) {
67
+ throw new TypeError('机器编号必须在0-' + this.max_worker_id + '之间');
68
+ }
69
+ };
70
+
71
+ /**
72
+ * 生成下一个ID
73
+ * @returns {string} 唯一ID
74
+ */
75
+ IdGen.prototype.nextId = function () {
76
+ var timestamp = this._timeGen();
77
+
78
+ if (timestamp < this.last_timestamp) {
79
+ throw new Error('时钟回拨,拒绝生成ID');
80
+ }
81
+
82
+ if (this.last_timestamp === timestamp) {
83
+ this.sequence = (this.sequence + 1) & this.sequence_mask;
84
+ if (this.sequence === 0) {
85
+ timestamp = this._tilNextMillis(this.last_timestamp);
86
+ }
87
+ } else {
88
+ this.sequence = 0;
89
+ }
90
+
91
+ this.last_timestamp = timestamp;
92
+
93
+ var id =
94
+ (BigInt(timestamp - this.epoch) << BigInt(this.timestamp_left_shift)) |
95
+ (BigInt(this.data_center_no) << BigInt(this.dc_id_shift)) |
96
+ (BigInt(this.machine_no) << BigInt(this.worker_id_shift)) |
97
+ BigInt(this.sequence);
98
+
99
+ return id.toString();
100
+ };
101
+
102
+ /**
103
+ * 生成带前缀的ID
104
+ * @param {string} prefix 前缀
105
+ * @returns {string} 带前缀的唯一ID
106
+ */
107
+ IdGen.prototype.generateId = function (prefix) {
108
+ if (typeof prefix !== 'string') {
109
+ throw new TypeError('前缀必须是字符串');
110
+ }
111
+ return prefix + '_' + this.nextId();
112
+ };
113
+
114
+ /**
115
+ * 解析ID信息
116
+ * @param {string} id ID字符串
117
+ * @returns {object} ID信息对象
118
+ */
119
+ IdGen.prototype.parseId = function (id) {
120
+ if (typeof id !== 'string') {
121
+ throw new TypeError('ID必须是字符串');
122
+ }
123
+ var num = BigInt(id.replace(/^[a-zA-Z_]+_/, ''));
124
+ var timestamp = Number((num >> 22n) + BigInt(this.epoch));
125
+ var dc_id = Number((num >> 17n) & 31n);
126
+ var worker_id = Number((num >> 12n) & 31n);
127
+ var sequence = Number(num & 4095n);
128
+
129
+ return {
130
+ timestamp: timestamp,
131
+ datetime: new Date(timestamp).toISOString(),
132
+ data_center_no: dc_id,
133
+ machine_no: worker_id,
134
+ sequence: sequence
135
+ };
136
+ };
137
+
138
+ module.exports = { IdGen };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mm_machine",
3
- "version": "2.9.4",
3
+ "version": "2.9.5",
4
4
  "description": "A flexible Node.js plugin mechanism system for dynamic loading, management and execution of modules. Supports hot reload, lifecycle management, and modern JavaScript features.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -30,6 +30,10 @@
30
30
  "mod.js",
31
31
  "index.js",
32
32
  "drive.js",
33
+ "dir_scanner.js",
34
+ "id_gen.js",
35
+ "registry.js",
36
+ "word.js",
33
37
  "README.md"
34
38
  ],
35
39
  "dependencies": {
package/registry.js ADDED
@@ -0,0 +1,634 @@
1
+ /**
2
+ * Registry 注册表类
3
+ * 持久化存储所有模块的元数据,启动时一次 IO 读取即可恢复全部模块信息
4
+ * @class Registry
5
+ * @augments Drive
6
+ */
7
+ var fs = require('fs');
8
+ var path = require('path');
9
+ var { Drive } = require('./drive');
10
+
11
+ /**
12
+ * 新增条目自动补全的默认值
13
+ */
14
+ var defaultEntry = {
15
+ title: '',
16
+ description: '',
17
+ type: 'plugin',
18
+ version: '1.0.0',
19
+ sort: 100,
20
+ main: '',
21
+ enabled: true,
22
+ auto_start: false,
23
+ lazy: 'auto',
24
+ state: 'stopped',
25
+ // eslint-disable-next-line mm_eslint/variable-name
26
+ registered_at: '',
27
+ updated_at: ''
28
+ };
29
+
30
+ class Registry extends Drive {
31
+ /**
32
+ * 构造函数
33
+ * @param {object} manager Manager 实例(必需),用于注册模块
34
+ * @param {object} [cfg] 配置参数
35
+ * @param {string} [cfg.file] 注册表文件绝对路径,默认从 Manager 配置拼接
36
+ */
37
+ constructor(manager, cfg) {
38
+ var config = cfg || {};
39
+ super(config);
40
+ this._manager = manager;
41
+ this._data = { version: '1.0', updated_at: '', modules: [] };
42
+ this._file = config.file || Registry._resolveFile(manager);
43
+ this._save_timer = null;
44
+ this._dirty = false;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * 解析注册表文件路径
50
+ * @param {object} manager Manager 实例
51
+ * @returns {string} 注册表文件绝对路径
52
+ * @private
53
+ * @static
54
+ */
55
+ Registry._resolveFile = function (manager) {
56
+ // 有显式配置时直接使用
57
+ if (manager.config.registry_file) {
58
+ return path.resolve(manager.dir || process.cwd(), manager.config.registry_file);
59
+ }
60
+ // 默认:data/register/{构造函数名}_{config.name}.json
61
+ var base_dir = path.resolve(manager.dir || process.cwd(), 'data/register');
62
+ var class_name = manager.constructor.name || 'Manager';
63
+ var mod_name = manager.config.name || 'default';
64
+ return path.join(base_dir, class_name + '_' + mod_name + '.json');
65
+ };
66
+
67
+ // ==================== 数据加载/保存 ====================
68
+
69
+ /**
70
+ * 从文件加载注册表数据
71
+ * 使用 require() 加载(支持缓存),文件不存在返回空数据结构
72
+ * @returns {object} 注册表数据 { version, modules[] }
73
+ */
74
+ Registry.prototype.load = function () {
75
+ try {
76
+ if (fs.existsSync(this._file)) {
77
+ var data = require(this._file);
78
+ if (data && data.modules) {
79
+ this._data = data;
80
+ return this._data;
81
+ }
82
+ }
83
+ } catch {
84
+ // 文件不存在或解析失败,使用空数据
85
+ }
86
+ this._data = { version: '1.0', updated_at: '', modules: [] };
87
+ return this._data;
88
+ };
89
+
90
+ /**
91
+ * 保存注册表数据到文件
92
+ * 防抖保存(500ms 内多次调用只写一次),写完后清除 require.cache
93
+ */
94
+ Registry.prototype.save = function () {
95
+ this._dirty = true;
96
+
97
+ if (this._save_timer) {
98
+ return;
99
+ }
100
+
101
+ this._save_timer = setTimeout(() => {
102
+ this._save_timer = null;
103
+ this._flushSave();
104
+ }, 500);
105
+ };
106
+
107
+ /**
108
+ * 立即写入文件
109
+ * @private
110
+ */
111
+ Registry.prototype._flushSave = function () {
112
+ if (!this._dirty) {
113
+ return;
114
+ }
115
+
116
+ this._data.updated_at = new Date().toISOString();
117
+
118
+ try {
119
+ var dir = path.dirname(this._file);
120
+ if (!fs.existsSync(dir)) {
121
+ fs.mkdirSync(dir, { recursive: true });
122
+ }
123
+ fs.writeFileSync(this._file, JSON.stringify(this._data, null, 2), 'utf8');
124
+
125
+ // 清除 require.cache 确保下次 load 读取最新数据
126
+ try {
127
+ delete require.cache[require.resolve(this._file)];
128
+ } catch {
129
+ // 忽略
130
+ }
131
+
132
+ this._dirty = false;
133
+ } catch {
134
+ // 保存失败,记录错误
135
+ }
136
+ };
137
+
138
+ // ==================== 查询方法 ====================
139
+
140
+ /**
141
+ * 获取所有注册表项的浅拷贝数组
142
+ * @returns {Array<object>} 注册表项数组
143
+ */
144
+ Registry.prototype.getAllEntries = function () {
145
+ return this._data.modules.slice();
146
+ };
147
+
148
+ /**
149
+ * 按名称获取单个注册表项
150
+ * @param {string} name 模块名称
151
+ * @returns {object|null} 注册表项,不存在返回 null
152
+ */
153
+ Registry.prototype.getEntry = function (name) {
154
+ var modules = this._data.modules;
155
+ for (var i = 0; i < modules.length; i++) {
156
+ if (modules[i].name === name) {
157
+ return modules[i];
158
+ }
159
+ }
160
+ return null;
161
+ };
162
+
163
+ /**
164
+ * 获取注册表项数量
165
+ * @returns {number} 数量
166
+ */
167
+ Registry.prototype.getEntryCount = function () {
168
+ return this._data.modules.length;
169
+ };
170
+
171
+ /**
172
+ * 检查注册表项是否存在
173
+ * @param {string} name 模块名称
174
+ * @returns {boolean} 是否存在
175
+ */
176
+ Registry.prototype.hasEntry = function (name) {
177
+ return this.getEntry(name) !== null;
178
+ };
179
+
180
+ // ==================== 增删改方法 ====================
181
+
182
+ /**
183
+ * 添加注册表项
184
+ * 自动补全缺失字段,然后保存
185
+ * @param {object} data 注册表项数据
186
+ */
187
+ Registry.prototype.addEntry = function (data) {
188
+ if (!data || !data.name) {
189
+ throw new TypeError('注册表项必须包含 name 字段');
190
+ }
191
+
192
+ if (this.hasEntry(data.name)) {
193
+ return;
194
+ }
195
+
196
+ var now = new Date().toISOString();
197
+ var entry = {
198
+ ...defaultEntry,
199
+ ...data,
200
+ // eslint-disable-next-line mm_eslint/variable-name
201
+ registered_at: data.registered_at || now,
202
+ updated_at: now
203
+ };
204
+
205
+ this._data.modules.push(entry);
206
+ this.save();
207
+ };
208
+
209
+ /**
210
+ * 更新注册表项
211
+ * 部分合并,只覆盖 data 中存在的字段
212
+ * @param {string} name 模块名称
213
+ * @param {object} data 要更新的字段
214
+ */
215
+ Registry.prototype.updateEntry = function (name, data) {
216
+ if (!data) {
217
+ return;
218
+ }
219
+
220
+ var modules = this._data.modules;
221
+ for (var i = 0; i < modules.length; i++) {
222
+ if (modules[i].name === name) {
223
+ for (var k in data) {
224
+ modules[i][k] = data[k];
225
+ }
226
+ modules[i].updated_at = new Date().toISOString();
227
+ this.save();
228
+ return;
229
+ }
230
+ }
231
+ };
232
+
233
+ /**
234
+ * 移除注册表项并保存
235
+ * @param {string} name 模块名称
236
+ */
237
+ Registry.prototype.removeEntry = function (name) {
238
+ var modules = this._data.modules;
239
+ for (var i = 0; i < modules.length; i++) {
240
+ if (modules[i].name === name) {
241
+ modules.splice(i, 1);
242
+ this.save();
243
+ return;
244
+ }
245
+ }
246
+ };
247
+
248
+ // ==================== 自启动管理 ====================
249
+
250
+ /**
251
+ * 设置模块是否自启动
252
+ * @param {string} name 模块名称
253
+ * @param {boolean} flag 是否自启动
254
+ */
255
+ Registry.prototype.setAutoStart = function (name, flag) {
256
+ this.updateEntry(name, { auto_start: !!flag });
257
+ };
258
+
259
+ /**
260
+ * 获取所有自启动模块列表
261
+ * @returns {Array<object>} 自启动模块列表
262
+ */
263
+ Registry.prototype.getAutoStartList = function () {
264
+ var modules = this._data.modules;
265
+ var list = [];
266
+ for (var i = 0; i < modules.length; i++) {
267
+ if (modules[i].auto_start) {
268
+ list.push(modules[i]);
269
+ }
270
+ }
271
+ return list;
272
+ };
273
+
274
+ // ==================== 核心操作 ====================
275
+
276
+ /**
277
+ * 同步注册表数据到 Manager
278
+ * 遍历 _data.modules,从注册表数据直接创建模块实例(0 IO)
279
+ * 已存在的模块跳过(不重复创建)
280
+ */
281
+ Registry.prototype.syncToManager = function () {
282
+ var modules = this._data.modules;
283
+ for (var i = 0; i < modules.length; i++) {
284
+ var entry = modules[i];
285
+
286
+ // 跳过 config_file 不存在的模块
287
+ if (entry.config_file && !fs.existsSync(entry.config_file)) {
288
+ continue;
289
+ }
290
+
291
+ if (this._manager && typeof this._manager.registerModEx === 'function') {
292
+ this._manager.registerModEx(entry);
293
+ }
294
+ }
295
+ };
296
+
297
+ /**
298
+ * 启动自启动模块
299
+ * 遍历 getAutoStartList(),调用 mod.load() 加载,
300
+ * 根据 lazy 策略决定是否继续 init/start
301
+ * @returns {Promise<void>}
302
+ */
303
+ Registry.prototype.startAutoStart = async function () {
304
+ var list = this.getAutoStartList();
305
+
306
+ for (var i = 0; i < list.length; i++) {
307
+ var entry = list[i];
308
+ var mod = this._manager ? this._manager.getMod(entry.name) : null;
309
+ if (!mod) {
310
+ continue;
311
+ }
312
+
313
+ try {
314
+ // 加载模块
315
+ await mod.load();
316
+
317
+ // 根据 lazy 策略决定是否继续
318
+ if (this._shouldContinue(entry)) {
319
+ await mod.init();
320
+ await mod.start();
321
+ }
322
+ } catch {
323
+ // 单个模块启动失败不影响其他模块
324
+ }
325
+ }
326
+ };
327
+
328
+ /**
329
+ * 判断是否继续推进生命周期(init + start)
330
+ * @param {object} entry 注册表项
331
+ * @returns {boolean} 是否继续
332
+ * @private
333
+ */
334
+ Registry.prototype._shouldContinue = function (entry) {
335
+ var lazy = entry.lazy !== undefined ? entry.lazy : 'auto';
336
+
337
+ if (lazy === false) {
338
+ return true;
339
+ }
340
+ if (lazy === 'auto') {
341
+ var mgr_lazy = this._manager ? this._manager.config.lazy_load : false;
342
+ return mgr_lazy === false || mgr_lazy === 'auto';
343
+ }
344
+ return false;
345
+ };
346
+
347
+ /**
348
+ * 启动引导
349
+ * load() → syncToManager() → 注册表为空时扫描 → startAutoStart() → save()
350
+ * @returns {Promise<void>}
351
+ */
352
+ Registry.prototype.boot = async function () {
353
+ // 1. 加载注册表文件
354
+ this.load();
355
+
356
+ // 2. 注册模块到 Manager
357
+ this.syncToManager();
358
+
359
+ // 3. 注册表为空时扫描文件系统(首次启动)
360
+ // 实际扫描由 Manager.boot() 在调用 registry.boot() 后处理
361
+
362
+ // 4. 启动自启动模块
363
+ await this.startAutoStart();
364
+
365
+ // 5. 保存注册表
366
+ this.save();
367
+ };
368
+
369
+ /**
370
+ * 一键注册多个模块
371
+ * @param {Array<string|object>} files 文件路径数组或 {file, config} 对象数组
372
+ * @returns {number} 成功注册的模块数量
373
+ */
374
+ Registry.prototype.registerAll = function (files) {
375
+ if (!files || !Array.isArray(files)) {
376
+ return 0;
377
+ }
378
+
379
+ var count = 0;
380
+
381
+ for (var i = 0; i < files.length; i++) {
382
+ if (this._registerOne(files[i])) {
383
+ count++;
384
+ }
385
+ }
386
+
387
+ return count;
388
+ };
389
+
390
+ /**
391
+ * 注册单个文件条目
392
+ * @param {string|object} item 文件路径或 {file, config} 对象
393
+ * @returns {boolean} 是否注册成功
394
+ * @private
395
+ */
396
+ // eslint-disable-next-line complexity, max-lines-per-function
397
+ Registry.prototype._registerOne = function (item) {
398
+ var file_path = '';
399
+ var cfg = null;
400
+
401
+ if (typeof item === 'string') {
402
+ file_path = item;
403
+ } else if (item && item.file) {
404
+ file_path = item.file;
405
+ cfg = item.config || null;
406
+ } else {
407
+ return false;
408
+ }
409
+
410
+ try {
411
+ // 未提供配置时从文件读取
412
+ if (!cfg) {
413
+ var content = fs.readFileSync(file_path, 'utf8');
414
+ cfg = JSON.parse(content);
415
+ }
416
+
417
+ if (!cfg || !cfg.name) {
418
+ return false;
419
+ }
420
+
421
+ var entry = {
422
+ name: cfg.name,
423
+ title: cfg.title || '',
424
+ description: cfg.description || '',
425
+ type: cfg.type || 'plugin',
426
+ version: cfg.version || '1.0.0',
427
+ sort: cfg.sort || 100,
428
+ config_file: file_path,
429
+ main: cfg.main || '',
430
+ enabled: cfg.enabled !== false,
431
+ auto_start: !!cfg.auto_start,
432
+ lazy: cfg.lazy !== undefined ? cfg.lazy : 'auto'
433
+ };
434
+
435
+ this.addEntry(entry);
436
+
437
+ if (this._manager && typeof this._manager.registerModEx === 'function') {
438
+ this._manager.registerModEx(entry);
439
+ }
440
+
441
+ return true;
442
+ } catch {
443
+ // 单个模块注册失败不影响其他模块
444
+ return false;
445
+ }
446
+ };
447
+
448
+ /**
449
+ * 获取全量模块目录
450
+ * 三步交叉比对:扫描文件系统 → 交叉比对注册表 → 补充运行时状态
451
+ * @param {object} scanner DirScanner 实例
452
+ * @param {string[]} dirs 要扫描的目录列表
453
+ * @param {boolean} [force] 是否强制重新扫描文件系统
454
+ * @returns {Array<object>} 全量目录列表
455
+ */
456
+ Registry.prototype.getCatalog = function (scanner, dirs, force) {
457
+ if (!scanner || typeof scanner.scanAndParse !== 'function') {
458
+ throw new TypeError('scanner 必须包含 scanAndParse 方法');
459
+ }
460
+ if (!dirs || !Array.isArray(dirs)) {
461
+ return [];
462
+ }
463
+
464
+ // 1. 扫描文件系统
465
+ var file_dict = this._scanFileSystem(scanner, dirs, force);
466
+
467
+ // 2. 与注册表交叉比对
468
+ var registry_dict = this._buildRegistryMap();
469
+
470
+ // 3. 构建全量目录
471
+ var result = this._buildCatalog(file_dict, registry_dict);
472
+
473
+ // 4. 补充注册表中有但文件系统中不存在的模块
474
+ this._appendMissingModules(result.catalog, result.processed, registry_dict);
475
+
476
+ return result.catalog;
477
+ };
478
+
479
+ /**
480
+ * 扫描文件系统,构建文件映射
481
+ * @param {object} scanner DirScanner 实例
482
+ * @param {string[]} dirs 目录列表
483
+ * @param {boolean} force 是否强制扫描
484
+ * @returns {object} 文件路径到配置的映射
485
+ * @private
486
+ */
487
+ Registry.prototype._scanFileSystem = function (scanner, dirs, force) {
488
+ var file_dict = {};
489
+ for (var i = 0; i < dirs.length; i++) {
490
+ var dir = dirs[i];
491
+ var entries = scanner.scanAndParse(dir, '*.json', null, !!force);
492
+ for (var j = 0; j < entries.length; j++) {
493
+ var entry = entries[j];
494
+ file_dict[entry.file] = entry.config;
495
+ }
496
+ }
497
+ return file_dict;
498
+ };
499
+
500
+ /**
501
+ * 构建注册表名称映射
502
+ * @returns {object} 名称到注册表项的映射
503
+ * @private
504
+ */
505
+ Registry.prototype._buildRegistryMap = function () {
506
+ var registry_dict = {};
507
+ var reg_modules = this._data.modules;
508
+ for (var k = 0; k < reg_modules.length; k++) {
509
+ var reg = reg_modules[k];
510
+ registry_dict[reg.name] = reg;
511
+ }
512
+ return registry_dict;
513
+ };
514
+
515
+ /**
516
+ * 从文件系统扫描结果构建目录条目
517
+ * @param {object} file_dict 文件映射
518
+ * @param {object} registry_dict 注册表映射
519
+ * @returns {Array} 目录条目列表
520
+ * @private
521
+ */
522
+ Registry.prototype._buildCatalog = function (file_dict, registry_dict) {
523
+ var catalog = [];
524
+ var processed = {};
525
+
526
+ for (var file_path in file_dict) {
527
+ var cfg = file_dict[file_path];
528
+ var name = cfg.name || path.basename(file_path, path.extname(file_path));
529
+ var reg_ref = registry_dict[name] || null;
530
+
531
+ var item = {
532
+ name: name,
533
+ title: cfg.title || '',
534
+ type: cfg.type || 'plugin',
535
+ sort: cfg.sort || 100,
536
+ config_file: file_path,
537
+ main: cfg.main || '',
538
+ registered: !!reg_ref,
539
+ auto_start: reg_ref ? !!reg_ref.auto_start : false,
540
+ lazy: reg_ref ? reg_ref.lazy : 'auto',
541
+ // eslint-disable-next-line mm_eslint/variable-name
542
+ lifecycle_state: null,
543
+ file_exists: true,
544
+ status: 'unregistered'
545
+ };
546
+
547
+ this._addRuntimeState(item, reg_ref, name);
548
+
549
+ catalog.push(item);
550
+ processed[name] = true;
551
+ }
552
+
553
+ return { catalog: catalog, processed: processed };
554
+ };
555
+
556
+ /**
557
+ * 补充运行时状态
558
+ * @param {object} item 目录条目
559
+ * @param {object|null} reg_ref 注册表项
560
+ * @param {string} name 模块名称
561
+ * @private
562
+ */
563
+ Registry.prototype._addRuntimeState = function (item, reg_ref, name) {
564
+ if (!reg_ref || !this._manager) {
565
+ return;
566
+ }
567
+
568
+ var mod = this._manager.getMod(name);
569
+ if (mod) {
570
+ // eslint-disable-next-line no-param-reassign
571
+ item.lifecycle_state = mod.lifecycle_state;
572
+ // eslint-disable-next-line no-param-reassign
573
+ item.status = Registry._calcStatus(mod.lifecycle_state);
574
+ } else {
575
+ // eslint-disable-next-line no-param-reassign
576
+ item.status = 'stopped';
577
+ }
578
+ };
579
+
580
+ /**
581
+ * 追加注册表中有但文件系统中不存在的模块
582
+ * @param {Array} catalog 目录列表
583
+ * @param {object} processed 已处理模块名称集合
584
+ * @param {object} registry_dict 注册表映射
585
+ * @private
586
+ */
587
+ // eslint-disable-next-line complexity
588
+ Registry.prototype._appendMissingModules = function (catalog, processed, registry_dict) {
589
+ var reg_modules = this._data.modules;
590
+
591
+ for (var n = 0; n < reg_modules.length; n++) {
592
+ var reg = reg_modules[n];
593
+ if (processed[reg.name]) {
594
+ continue;
595
+ }
596
+
597
+ var mod = this._manager ? this._manager.getMod(reg.name) : null;
598
+ catalog.push({
599
+ name: reg.name,
600
+ title: reg.title || '',
601
+ type: reg.type || 'plugin',
602
+ sort: reg.sort || 100,
603
+ config_file: reg.config_file || '',
604
+ main: reg.main || '',
605
+ registered: true,
606
+ auto_start: !!reg.auto_start,
607
+ lazy: reg.lazy || 'auto',
608
+ // eslint-disable-next-line mm_eslint/variable-name
609
+ lifecycle_state: mod ? mod.lifecycle_state : null,
610
+ file_exists: false,
611
+ status: 'file_missing'
612
+ });
613
+ }
614
+ };
615
+
616
+ /**
617
+ * 根据 lifecycle_state 计算状态
618
+ * @param {string} state 生命周期状态
619
+ * @returns {string} 状态值
620
+ * @private
621
+ * @static
622
+ */
623
+ Registry._calcStatus = function (state) {
624
+ switch (state) {
625
+ case 'started':
626
+ return 'running';
627
+ case 'paused':
628
+ return 'paused';
629
+ default:
630
+ return 'stopped';
631
+ }
632
+ };
633
+
634
+ module.exports = { Registry };
package/word.js ADDED
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Word 命名格式转换工具类
3
+ * 所有方法均为静态方法,无需实例化
4
+ */
5
+ class Word {
6
+ /**
7
+ * 构造函数
8
+ */
9
+ constructor() {}
10
+ }
11
+
12
+ /**
13
+ * 分割单词
14
+ * @param {string} name 名称
15
+ * @returns {Array} 单词数组
16
+ */
17
+ Word.splitWords = function (name) {
18
+ if (!name || name.length === 0) {
19
+ return [];
20
+ }
21
+
22
+ var words = [];
23
+ var current_word = '';
24
+ var last_char_type = null;
25
+
26
+ for (var i = 0; i < name.length; i++) {
27
+ var char = name[i];
28
+ var char_info = Word._getCharInfo(char);
29
+
30
+ // 处理下划线和数字
31
+ if (char_info.is_under || char_info.is_digit) {
32
+ last_char_type = Word._handleSepa(words, current_word, char, char_info);
33
+ current_word = '';
34
+ continue;
35
+ }
36
+
37
+ // 处理第一个字符
38
+ if (current_word.length === 0) {
39
+ current_word = char;
40
+ last_char_type = char_info.type;
41
+ continue;
42
+ }
43
+
44
+ // 处理字符转换
45
+ var result = Word._handleCharTran(words, current_word, char, last_char_type, char_info.type);
46
+ words = result.words;
47
+ current_word = result.current_word;
48
+ last_char_type = char_info.type;
49
+ }
50
+
51
+ // 添加最后一个单词
52
+ if (current_word.length > 0) {
53
+ words.push(current_word);
54
+ }
55
+
56
+ return words;
57
+ };
58
+
59
+ /**
60
+ * 获取字符信息
61
+ * @param {string} char 字符
62
+ * @returns {object} 字符信息
63
+ * @private
64
+ */
65
+ Word._getCharInfo = function (char) {
66
+ var is_upper = char >= 'A' && char <= 'Z';
67
+ var is_lower = char >= 'a' && char <= 'z';
68
+ var is_digit = char >= '0' && char <= '9';
69
+ var type = is_upper ? 'upper' : is_lower ? 'lower' : is_digit ? 'digit' : 'other';
70
+ return { is_upper, is_lower, is_digit, is_under: char === '_', type };
71
+ };
72
+
73
+ /**
74
+ * 处理分隔符
75
+ * @param {Array} words 单词数组
76
+ * @param {string} current_word 当前单词
77
+ * @param {string} char 字符
78
+ * @param {object} char_info 字符信息
79
+ * @returns {string} 新的字符类型
80
+ * @private
81
+ */
82
+ Word._handleSepa = function (words, current_word, char, char_info) {
83
+ if (current_word.length > 0) {
84
+ words.push(current_word);
85
+ }
86
+ if (char_info.is_digit) {
87
+ words.push(char);
88
+ return 'digit';
89
+ }
90
+ return 'underscore';
91
+ };
92
+
93
+ /**
94
+ * 处理字符转换
95
+ * @param {Array} words 单词数组
96
+ * @param {string} current_word 当前单词
97
+ * @param {string} char 字符
98
+ * @param {string} last_type 上一个字符类型
99
+ * @param {string} current_type 当前字符类型
100
+ * @returns {object} 处理结果
101
+ * @private
102
+ */
103
+ Word._handleCharTran = function (words, current_word, char, last_type, current_type) {
104
+ var is_last_letter = last_type === 'upper' || last_type === 'lower';
105
+ var is_current_letter = current_type === 'upper' || current_type === 'lower';
106
+
107
+ if (!is_last_letter || !is_current_letter) {
108
+ return { words, current_word: current_word + char };
109
+ }
110
+
111
+ if (last_type === 'upper' && current_type === 'lower') {
112
+ // 大写转小写
113
+ if (current_word.length > 1) {
114
+ var last_char = current_word[current_word.length - 1];
115
+ words.push(current_word.substring(0, current_word.length - 1));
116
+ return { words, current_word: last_char + char };
117
+ }
118
+ return { words, current_word: current_word + char };
119
+ }
120
+
121
+ if (last_type === 'lower' && current_type === 'upper') {
122
+ // 小写转大写
123
+ words.push(current_word);
124
+ return { words, current_word: char };
125
+ }
126
+
127
+ // 相同类型
128
+ return { words, current_word: current_word + char };
129
+ };
130
+
131
+ /**
132
+ * 驼峰转蛇形
133
+ * @param {string} name 名称
134
+ * @returns {string} 小写蛇形命名
135
+ */
136
+ Word.camelToSnake = function (name) {
137
+ var words = Word.splitWords(name);
138
+ for (var i = 0; i < words.length; i++) {
139
+ words[i] = words[i].toLowerCase();
140
+ }
141
+ return words.join('_');
142
+ };
143
+
144
+ /**
145
+ * 蛇形转驼峰
146
+ * @param {string} name 名称
147
+ * @returns {string} 小驼峰命名
148
+ */
149
+ Word.snakeToCamel = function (name) {
150
+ var words = Word.splitWords(name);
151
+ for (var i = 0; i < words.length; i++) {
152
+ words[i] = words[i].toLowerCase();
153
+ if (i > 0) {
154
+ words[i] = words[i].charAt(0).toUpperCase() + words[i].substring(1);
155
+ }
156
+ }
157
+ return words.join('');
158
+ };
159
+
160
+ /**
161
+ * 大驼峰转蛇形
162
+ * @param {string} name 名称
163
+ * @returns {string} 小写蛇形命名
164
+ */
165
+ Word.pascalToSnake = function (name) {
166
+ var words = Word.splitWords(name);
167
+ for (var i = 0; i < words.length; i++) {
168
+ words[i] = words[i].toLowerCase();
169
+ }
170
+ return words.join('_');
171
+ };
172
+
173
+ /**
174
+ * 蛇形转大驼峰
175
+ * @param {string} name 名称
176
+ * @returns {string} 大驼峰命名
177
+ */
178
+ Word.snakeToPascal = function (name) {
179
+ var words = Word.splitWords(name);
180
+ for (var i = 0; i < words.length; i++) {
181
+ words[i] = words[i].toLowerCase();
182
+ words[i] = words[i].charAt(0).toUpperCase() + words[i].substring(1);
183
+ }
184
+ return words.join('');
185
+ };
186
+
187
+ /**
188
+ * 驼峰转短横线
189
+ * @param {string} name 名称
190
+ * @returns {string} 短横线命名
191
+ */
192
+ Word.camelToKebab = function (name) {
193
+ var words = Word.splitWords(name);
194
+ for (var i = 0; i < words.length; i++) {
195
+ words[i] = words[i].toLowerCase();
196
+ }
197
+ return words.join('-');
198
+ };
199
+
200
+ /**
201
+ * 蛇形转短横线
202
+ * @param {string} name 名称
203
+ * @returns {string} 短横线命名
204
+ */
205
+ Word.snakeToKebab = function (name) {
206
+ return name.replace(/_/g, '-');
207
+ };
208
+
209
+ module.exports = { Word };