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 +262 -0
- package/id_gen.js +138 -0
- package/package.json +5 -1
- package/registry.js +634 -0
- package/word.js +209 -0
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.
|
|
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 };
|