foliko 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugins/core/default/bootstrap.js +23 -1
- package/plugins/core/default/config.js +3 -1
- package/plugins/messaging/email/index.js +1 -1
- package/plugins/messaging/feishu/index.js +1 -1
- package/plugins/messaging/qq/index.js +1 -1
- package/plugins/messaging/telegram/index.js +1 -1
- package/plugins/messaging/weixin/index.js +3 -3
- package/src/plugin/manager.js +271 -48
package/package.json
CHANGED
|
@@ -65,6 +65,9 @@ async function bootstrapDefaults(framework, config = {}) {
|
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// 启用 bootstrap 模式:load() 只 install 不 start,由 startAll() 统一启动
|
|
69
|
+
framework.pluginManager.setBootstrapping(true);
|
|
70
|
+
|
|
68
71
|
const shouldLoad = (plugin) => {
|
|
69
72
|
const name = typeof plugin === 'string' ? plugin : (plugin.name || plugin.prototype?.name);
|
|
70
73
|
if (framework.pluginManager.has(name)) { return false; }
|
|
@@ -187,11 +190,30 @@ async function bootstrapDefaults(framework, config = {}) {
|
|
|
187
190
|
}));
|
|
188
191
|
}
|
|
189
192
|
|
|
193
|
+
// 6.75 Messaging plugins (from plugins/messaging/ subdirectory)
|
|
194
|
+
const messagingPlugins = ['telegram', 'qq', 'weixin', 'feishu', 'email'];
|
|
195
|
+
for (const name of messagingPlugins) {
|
|
196
|
+
if (shouldLoad(name)) {
|
|
197
|
+
try {
|
|
198
|
+
const pluginDir = path.resolve(__dirname, '..', '..', '..', 'plugins', 'messaging', name);
|
|
199
|
+
if (!fs.existsSync(pluginDir)) continue;
|
|
200
|
+
const PluginClass = require(`../../messaging/${name}`);
|
|
201
|
+
const pluginConfig = agentConfig[name] || {};
|
|
202
|
+
await framework.loadPlugin(new PluginClass(pluginConfig));
|
|
203
|
+
} catch (err) {
|
|
204
|
+
log.warn(`Messaging plugin '${name}' failed to load:`, err.message);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
190
209
|
// 7. Custom plugins from directories
|
|
191
210
|
await loadCustomPlugins(framework, agentConfig);
|
|
192
211
|
|
|
193
|
-
//
|
|
212
|
+
// 统一启动所有插件
|
|
194
213
|
await framework.pluginManager.startAll();
|
|
214
|
+
|
|
215
|
+
// 退出 bootstrap 模式
|
|
216
|
+
framework.pluginManager.setBootstrapping(false);
|
|
195
217
|
}
|
|
196
218
|
|
|
197
219
|
module.exports = {
|
|
@@ -89,6 +89,8 @@ function loadAgentConfig(framework, agentDir = '.foliko') {
|
|
|
89
89
|
const pc = JSON.parse(fs.readFileSync(pluginsFile, 'utf-8'));
|
|
90
90
|
if (pc.telegram) config.telegram = pc.telegram;
|
|
91
91
|
if (pc.weixin) config.weixin = pc.weixin;
|
|
92
|
+
if (pc.qq) config.qq = pc.qq;
|
|
93
|
+
if (pc.feishu) config.feishu = pc.feishu;
|
|
92
94
|
if (pc.email) config.email = pc.email;
|
|
93
95
|
if (pc.pluginLinks) config.pluginLinks = pc.pluginLinks;
|
|
94
96
|
} catch (err) { log.error('Failed to load plugins.json:', err.message); }
|
|
@@ -181,7 +183,7 @@ function loadAgentConfig(framework, agentDir = '.foliko') {
|
|
|
181
183
|
|
|
182
184
|
const homePlugins = path.join(homeDir, 'plugins.json');
|
|
183
185
|
if (!fs.existsSync(path.join(resolvedDir, 'plugins.json')) && fs.existsSync(homePlugins)) {
|
|
184
|
-
try { const pc = JSON.parse(fs.readFileSync(homePlugins, 'utf-8')); if (pc.telegram && !config.telegram) config.telegram = pc.telegram; if (pc.weixin && !config.weixin) config.weixin = pc.weixin; if (pc.email && !config.email) config.email = pc.email; config.pluginLinks = { ...(pc.pluginLinks || {}), ...config.pluginLinks }; } catch {}
|
|
186
|
+
try { const pc = JSON.parse(fs.readFileSync(homePlugins, 'utf-8')); if (pc.telegram && !config.telegram) config.telegram = pc.telegram; if (pc.weixin && !config.weixin) config.weixin = pc.weixin; if (pc.qq && !config.qq) config.qq = pc.qq; if (pc.feishu && !config.feishu) config.feishu = pc.feishu; if (pc.email && !config.email) config.email = pc.email; config.pluginLinks = { ...(pc.pluginLinks || {}), ...config.pluginLinks }; } catch {}
|
|
185
187
|
}
|
|
186
188
|
|
|
187
189
|
const homeMcp = path.join(homeDir, 'mcp_config.json');
|
|
@@ -20,7 +20,7 @@ class FeishuPlugin extends Plugin {
|
|
|
20
20
|
this.version = '1.1.0'
|
|
21
21
|
this.description = '飞书对话插件,使用 WebSocket 长连接接收消息'
|
|
22
22
|
this.priority = 80
|
|
23
|
-
this.enabled =
|
|
23
|
+
this.enabled = config.enabled === true
|
|
24
24
|
this.systemPrompt = `你是一个飞书助手。回复内容不要使用markdown格式文本。
|
|
25
25
|
|
|
26
26
|
**重要:** 子Agent 匹配规则必须遵守:
|
|
@@ -27,7 +27,7 @@ class TelegramPlugin extends Plugin {
|
|
|
27
27
|
this.version = '2.1.0'
|
|
28
28
|
this.description = 'Telegram 对话插件,绑定主Agent进行持续对话'
|
|
29
29
|
this.priority = 80
|
|
30
|
-
this.enabled =
|
|
30
|
+
this.enabled = config.enabled === true
|
|
31
31
|
this.systemPrompt = `你是一个有帮助的AI助手。回复内容不要使用markdown格式文本。
|
|
32
32
|
|
|
33
33
|
**重要:** 子Agent 匹配规则必须遵守:
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* - forceLogin: 是否强制重新扫码登录
|
|
7
7
|
* - qrcodeTerminal: 是否在终端渲染二维码 (默认 true)
|
|
8
8
|
*/
|
|
9
|
-
const { CLEAR_LINE, CYAN, DIM, GREEN, RED, YELLOW, colored } = require('../../../cli/
|
|
10
|
-
const { renderLine } = require('../../../cli/
|
|
9
|
+
const { CLEAR_LINE, CYAN, DIM, GREEN, RED, YELLOW, colored } = require('../../../src/cli/utils/ansi');
|
|
10
|
+
const { renderLine } = require('../../../src/cli/utils/markdown');
|
|
11
11
|
const { debounce, throttle } = require('../../../src/utils');
|
|
12
12
|
const interval = (fn) => {
|
|
13
13
|
const handle = setInterval(fn, 3000);
|
|
@@ -32,7 +32,7 @@ class WeixinPlugin extends Plugin {
|
|
|
32
32
|
this.description = '微信对话插件,使用微信网页账号进行对话'
|
|
33
33
|
this.priority = 80
|
|
34
34
|
// 默认不启用,需要在 plugins.json 中设置 enabled: true
|
|
35
|
-
this.enabled =
|
|
35
|
+
this.enabled = config.enabled === true
|
|
36
36
|
this.path=`.foliko/data`
|
|
37
37
|
this.systemPrompt=`你是一个微信助手。
|
|
38
38
|
|
package/src/plugin/manager.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* PluginManager 插件管理器
|
|
5
|
-
*
|
|
5
|
+
* 负责插件的加载、卸载、重载、启用/禁用
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { Plugin } = require('./base');
|
|
@@ -18,6 +18,7 @@ class PluginManager {
|
|
|
18
18
|
this.framework = framework;
|
|
19
19
|
this._plugins = new Map();
|
|
20
20
|
this._loading = false;
|
|
21
|
+
this._bootstrapping = false;
|
|
21
22
|
this._stateFile = path.join(
|
|
22
23
|
framework?.getCwd?.() ?? process.cwd(),
|
|
23
24
|
'.foliko',
|
|
@@ -37,13 +38,41 @@ class PluginManager {
|
|
|
37
38
|
// placeholder - plugins can register event descriptions
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
/**
|
|
42
|
+
* 注册插件(不加载,只记录状态)
|
|
43
|
+
*/
|
|
44
|
+
register(plugin, options = {}) {
|
|
41
45
|
if (!(plugin instanceof Plugin)) {
|
|
42
46
|
throw new PluginError('Plugin must be an instance of Plugin');
|
|
43
47
|
}
|
|
44
|
-
|
|
48
|
+
|
|
49
|
+
const savedState = this._loadState();
|
|
50
|
+
const savedEnabled = savedState[plugin.name]?.enabled;
|
|
51
|
+
const savedConfig = savedState[plugin.name]?.config;
|
|
52
|
+
|
|
53
|
+
// 恢复保存的配置
|
|
54
|
+
if (savedConfig && plugin.config && plugin.name !== 'ai') {
|
|
55
|
+
plugin.config = { ...plugin.config, ...savedConfig };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 确定启用状态:系统插件强制启用 → state 文件记录 → 插件默认
|
|
59
|
+
let enabled;
|
|
60
|
+
if (plugin.system) {
|
|
61
|
+
enabled = true;
|
|
62
|
+
} else if (savedEnabled !== undefined) {
|
|
63
|
+
enabled = savedEnabled;
|
|
64
|
+
} else {
|
|
65
|
+
enabled = plugin.enabled !== false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this._plugins.set(plugin.name, {
|
|
69
|
+
instance: plugin,
|
|
70
|
+
status: 'registered',
|
|
71
|
+
enabled,
|
|
72
|
+
sourcePath: plugin.sourcePath || null,
|
|
73
|
+
});
|
|
45
74
|
this._knownPlugins.add(plugin.name);
|
|
46
|
-
this._log.debug(`Plugin registered: ${plugin.name}`);
|
|
75
|
+
this._log.debug(`Plugin registered: ${plugin.name} (enabled: ${enabled})`);
|
|
47
76
|
return this;
|
|
48
77
|
}
|
|
49
78
|
|
|
@@ -71,7 +100,6 @@ class PluginManager {
|
|
|
71
100
|
|
|
72
101
|
/**
|
|
73
102
|
* 获取所有已知插件信息(包括已加载和未加载的)
|
|
74
|
-
* @returns {Array<{name: string, status: string, enabled: boolean, version?: string, system?: boolean}>}
|
|
75
103
|
*/
|
|
76
104
|
getAllKnown() {
|
|
77
105
|
const result = [];
|
|
@@ -81,7 +109,7 @@ class PluginManager {
|
|
|
81
109
|
const inst = entry.instance;
|
|
82
110
|
result.push({
|
|
83
111
|
name,
|
|
84
|
-
status: '
|
|
112
|
+
status: entry.status || 'registered',
|
|
85
113
|
enabled: entry.enabled !== false,
|
|
86
114
|
version: inst.version,
|
|
87
115
|
system: inst.system,
|
|
@@ -90,6 +118,18 @@ class PluginManager {
|
|
|
90
118
|
result.push({ name, status: 'known', enabled: false });
|
|
91
119
|
}
|
|
92
120
|
}
|
|
121
|
+
// 加入已加载但不在 knownPlugins 中的
|
|
122
|
+
for (const [name, entry] of this._plugins) {
|
|
123
|
+
if (!result.find(p => p.name === name)) {
|
|
124
|
+
result.push({
|
|
125
|
+
name,
|
|
126
|
+
status: entry.status || 'registered',
|
|
127
|
+
enabled: entry.enabled !== false,
|
|
128
|
+
version: entry.instance?.version,
|
|
129
|
+
system: entry.instance?.system || false,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
93
133
|
return result;
|
|
94
134
|
}
|
|
95
135
|
|
|
@@ -97,31 +137,71 @@ class PluginManager {
|
|
|
97
137
|
return this._plugins.size;
|
|
98
138
|
}
|
|
99
139
|
|
|
100
|
-
|
|
101
|
-
|
|
140
|
+
/**
|
|
141
|
+
* 加载插件
|
|
142
|
+
* @param {Plugin|Object} plugin - 插件实例或类
|
|
143
|
+
* @param {Object} [options] - 加载选项
|
|
144
|
+
* @param {boolean} [options.forceEnabled] - 强制启用,忽略 state 文件的 disabled
|
|
145
|
+
*/
|
|
146
|
+
async load(plugin, options = {}) {
|
|
147
|
+
let pluginInstance = plugin;
|
|
148
|
+
|
|
149
|
+
// 如果是类(Plugin 子类),实例化
|
|
150
|
+
if (typeof plugin === 'function' && plugin.prototype instanceof Plugin) {
|
|
151
|
+
pluginInstance = new plugin();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const name = pluginInstance.name;
|
|
102
155
|
|
|
103
|
-
|
|
156
|
+
// 已存在且已加载 → 跳过
|
|
157
|
+
const existing = this._plugins.get(name);
|
|
158
|
+
if (existing && existing.status === 'loaded' && pluginInstance._started) {
|
|
104
159
|
this._log.debug(`Plugin '${name}' already loaded, skipping`);
|
|
105
160
|
return;
|
|
106
161
|
}
|
|
107
162
|
|
|
163
|
+
// 已注册但未加载 → 检查 enabled 状态
|
|
164
|
+
if (existing) {
|
|
165
|
+
if (!existing.enabled && !options.forceEnabled) {
|
|
166
|
+
this._log.info(`Plugin '${name}' is disabled, skipping load`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
pluginInstance = existing.instance;
|
|
170
|
+
} else {
|
|
171
|
+
// 未注册 → 先注册
|
|
172
|
+
this.register(pluginInstance, options);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const entry = this._plugins.get(name);
|
|
176
|
+
if (!entry.enabled && !options.forceEnabled) {
|
|
177
|
+
this._log.info(`Plugin '${name}' is disabled, skipping load`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 如果 forceEnabled,覆盖 enabled 状态
|
|
182
|
+
if (options.forceEnabled) {
|
|
183
|
+
entry.enabled = true;
|
|
184
|
+
pluginInstance.enabled = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
108
187
|
this._log.info(`Loading plugin: ${name}`);
|
|
109
188
|
|
|
110
|
-
if (typeof
|
|
111
|
-
|
|
189
|
+
if (typeof pluginInstance.install === 'function') {
|
|
190
|
+
pluginInstance.install(this.framework);
|
|
112
191
|
}
|
|
113
192
|
|
|
114
|
-
|
|
115
|
-
instance: plugin,
|
|
116
|
-
sourcePath: plugin.sourcePath || null,
|
|
117
|
-
enabled: plugin.enabled !== false,
|
|
118
|
-
});
|
|
119
|
-
this._knownPlugins.add(name);
|
|
193
|
+
entry.status = 'loaded';
|
|
120
194
|
|
|
121
|
-
|
|
122
|
-
|
|
195
|
+
// bootstrapping 模式下不 start,由 startAll() 统一启动
|
|
196
|
+
if (!this._bootstrapping) {
|
|
197
|
+
if (typeof pluginInstance.start === 'function') {
|
|
198
|
+
await pluginInstance.start(this.framework);
|
|
199
|
+
pluginInstance._started = true;
|
|
200
|
+
}
|
|
123
201
|
}
|
|
124
202
|
|
|
203
|
+
this.framework?.emit('plugin:loaded', pluginInstance);
|
|
204
|
+
this._saveState();
|
|
125
205
|
this._log.info(`Plugin loaded: ${name}`);
|
|
126
206
|
}
|
|
127
207
|
|
|
@@ -139,7 +219,7 @@ class PluginManager {
|
|
|
139
219
|
}
|
|
140
220
|
|
|
141
221
|
this._plugins.delete(name);
|
|
142
|
-
|
|
222
|
+
return true;
|
|
143
223
|
}
|
|
144
224
|
|
|
145
225
|
async unloadExcept(keepSet) {
|
|
@@ -169,7 +249,6 @@ class PluginManager {
|
|
|
169
249
|
if (typeof plugin.reload === 'function') {
|
|
170
250
|
await plugin.reload(this.framework);
|
|
171
251
|
} else {
|
|
172
|
-
// Default reload: uninstall + install + start
|
|
173
252
|
if (typeof plugin.uninstall === 'function') {
|
|
174
253
|
plugin.uninstall(this.framework);
|
|
175
254
|
}
|
|
@@ -181,50 +260,145 @@ class PluginManager {
|
|
|
181
260
|
}
|
|
182
261
|
}
|
|
183
262
|
|
|
263
|
+
this.framework?.emit('plugin:reloaded', plugin);
|
|
184
264
|
this._log.info(`Plugin reloaded: ${name}`);
|
|
185
265
|
}
|
|
186
266
|
|
|
267
|
+
/**
|
|
268
|
+
* 启动所有已加载、已启用、未启动的插件(按优先级排序)
|
|
269
|
+
*/
|
|
187
270
|
async startAll() {
|
|
188
|
-
|
|
271
|
+
const entries = Array.from(this._plugins.values())
|
|
272
|
+
.filter(e => e.status === 'loaded' && e.enabled)
|
|
273
|
+
.sort((a, b) => (a.instance.priority || 100) - (b.instance.priority || 100));
|
|
274
|
+
|
|
275
|
+
for (const entry of entries) {
|
|
189
276
|
const plugin = entry.instance;
|
|
277
|
+
if (plugin._started) continue;
|
|
190
278
|
if (typeof plugin.start === 'function') {
|
|
191
279
|
try {
|
|
192
280
|
await plugin.start(this.framework);
|
|
281
|
+
plugin._started = true;
|
|
193
282
|
} catch (err) {
|
|
194
|
-
this._log.warn(`startAll: ${name} failed: ${err.message}`);
|
|
283
|
+
this._log.warn(`startAll: ${plugin.name} failed: ${err.message}`);
|
|
195
284
|
}
|
|
196
285
|
}
|
|
197
286
|
}
|
|
198
287
|
}
|
|
199
288
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
289
|
+
/**
|
|
290
|
+
* 启用插件
|
|
291
|
+
*/
|
|
292
|
+
async enable(name) {
|
|
293
|
+
const entry = this._plugins.get(name);
|
|
294
|
+
if (!entry) {
|
|
295
|
+
throw new PluginNotFoundError(name);
|
|
296
|
+
}
|
|
297
|
+
if (entry.instance?.system) {
|
|
298
|
+
throw new PluginError(`Plugin '${name}' is a system plugin, cannot be disabled/enabled`);
|
|
299
|
+
}
|
|
300
|
+
if (entry.enabled) {
|
|
301
|
+
this._log.info(`Plugin '${name}' already enabled`);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
entry.enabled = true;
|
|
306
|
+
if (entry.instance) {
|
|
307
|
+
entry.instance.enabled = true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 如果已加载,重新启动
|
|
311
|
+
if (entry.status === 'loaded') {
|
|
203
312
|
try {
|
|
204
|
-
|
|
313
|
+
// 停止旧实例
|
|
314
|
+
if (entry.instance._started) {
|
|
315
|
+
if (typeof entry.instance.stopBot === 'function') {
|
|
316
|
+
await entry.instance.stopBot();
|
|
317
|
+
} else if (typeof entry.instance.stop === 'function') {
|
|
318
|
+
await entry.instance.stop();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 如果记录过 sourcePath,重新 require 最新代码
|
|
323
|
+
if (entry.sourcePath) {
|
|
324
|
+
delete require.cache[require.resolve(entry.sourcePath)];
|
|
325
|
+
const mod = require(entry.sourcePath);
|
|
326
|
+
let NewClass = mod.default || mod;
|
|
327
|
+
if (typeof NewClass === 'function' && NewClass.prototype instanceof Plugin) {
|
|
328
|
+
const newInstance = new NewClass();
|
|
329
|
+
newInstance._sourcePath = entry.sourcePath;
|
|
330
|
+
if (entry.instance.config) newInstance.config = { ...entry.instance.config };
|
|
331
|
+
entry.instance = newInstance;
|
|
332
|
+
await newInstance.install(this.framework);
|
|
333
|
+
await newInstance.start(this.framework);
|
|
334
|
+
newInstance._started = true;
|
|
335
|
+
this.framework?.emit('plugin:reloaded', newInstance);
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
// 没有 sourcePath,调用 reload
|
|
339
|
+
if (typeof entry.instance.reload === 'function') {
|
|
340
|
+
await entry.instance.reload(this.framework);
|
|
341
|
+
}
|
|
342
|
+
if (typeof entry.instance.start === 'function') {
|
|
343
|
+
await entry.instance.start(this.framework);
|
|
344
|
+
}
|
|
345
|
+
entry.instance._started = true;
|
|
346
|
+
}
|
|
205
347
|
} catch (err) {
|
|
206
|
-
this._log.
|
|
348
|
+
this._log.error(`Enable/start failed for '${name}':`, err.message);
|
|
349
|
+
}
|
|
350
|
+
} else if (entry.status === 'registered') {
|
|
351
|
+
// 只注册过但未加载,现在加载
|
|
352
|
+
try {
|
|
353
|
+
await this.load(entry.instance);
|
|
354
|
+
} catch (err) {
|
|
355
|
+
this._log.error(`Enable/load failed for '${name}':`, err.message);
|
|
207
356
|
}
|
|
208
357
|
}
|
|
209
|
-
}
|
|
210
358
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
entry.enabled = true;
|
|
215
|
-
this._log.info(`Plugin enabled: ${name}`);
|
|
216
|
-
}
|
|
359
|
+
this.framework?.emit('plugin:enabled', entry.instance);
|
|
360
|
+
this._saveState();
|
|
361
|
+
this._log.info(`Plugin '${name}' enabled`);
|
|
217
362
|
}
|
|
218
363
|
|
|
364
|
+
/**
|
|
365
|
+
* 禁用插件
|
|
366
|
+
*/
|
|
219
367
|
async disable(name) {
|
|
220
368
|
const entry = this._plugins.get(name);
|
|
221
|
-
if (entry) {
|
|
222
|
-
|
|
223
|
-
|
|
369
|
+
if (!entry) {
|
|
370
|
+
throw new PluginNotFoundError(name);
|
|
371
|
+
}
|
|
372
|
+
if (entry.instance?.system) {
|
|
373
|
+
throw new PluginError(`Plugin '${name}' is a system plugin, cannot be disabled`);
|
|
374
|
+
}
|
|
375
|
+
if (!entry.enabled) {
|
|
376
|
+
this._log.info(`Plugin '${name}' already disabled`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
entry.enabled = false;
|
|
381
|
+
if (entry.instance) {
|
|
382
|
+
entry.instance.enabled = false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 如果正在运行,停止
|
|
386
|
+
if (entry.instance._started) {
|
|
387
|
+
try {
|
|
388
|
+
if (typeof entry.instance.stopBot === 'function') {
|
|
389
|
+
await entry.instance.stopBot();
|
|
390
|
+
} else if (typeof entry.instance.stop === 'function') {
|
|
391
|
+
await entry.instance.stop();
|
|
392
|
+
}
|
|
393
|
+
entry.instance._started = false;
|
|
394
|
+
} catch (err) {
|
|
395
|
+
this._log.error(`Stop failed for '${name}':`, err.message);
|
|
224
396
|
}
|
|
225
|
-
entry.enabled = false;
|
|
226
|
-
this._log.info(`Plugin disabled: ${name}`);
|
|
227
397
|
}
|
|
398
|
+
|
|
399
|
+
this.framework?.emit('plugin:disabled', entry.instance);
|
|
400
|
+
this._saveState();
|
|
401
|
+
this._log.info(`Plugin '${name}' disabled`);
|
|
228
402
|
}
|
|
229
403
|
|
|
230
404
|
updatePluginConfig(name, config) {
|
|
@@ -235,26 +409,75 @@ class PluginManager {
|
|
|
235
409
|
if (entry.instance.config) {
|
|
236
410
|
Object.assign(entry.instance.config, config);
|
|
237
411
|
}
|
|
412
|
+
this._saveState();
|
|
238
413
|
}
|
|
239
414
|
|
|
240
|
-
|
|
241
|
-
this.
|
|
415
|
+
setBootstrapping(val) {
|
|
416
|
+
this._bootstrapping = val !== false;
|
|
242
417
|
}
|
|
243
418
|
|
|
244
|
-
|
|
245
|
-
|
|
419
|
+
// ========== 状态持久化 ==========
|
|
420
|
+
|
|
421
|
+
_getStateFile() {
|
|
422
|
+
const dir = path.dirname(this._stateFile);
|
|
423
|
+
if (!fs.existsSync(dir)) {
|
|
424
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
425
|
+
}
|
|
426
|
+
return this._stateFile;
|
|
246
427
|
}
|
|
247
428
|
|
|
248
|
-
|
|
249
|
-
|
|
429
|
+
_saveState() {
|
|
430
|
+
try {
|
|
431
|
+
const stateFile = this._getStateFile();
|
|
432
|
+
let state = safeJsonParse(
|
|
433
|
+
fs.existsSync(stateFile) ? fs.readFileSync(stateFile, 'utf-8') : '{}',
|
|
434
|
+
{}
|
|
435
|
+
);
|
|
436
|
+
for (const [name, entry] of this._plugins) {
|
|
437
|
+
state[name] = {
|
|
438
|
+
enabled: entry.enabled,
|
|
439
|
+
config: entry.instance?.config || {},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
443
|
+
this._stateCache = state;
|
|
444
|
+
} catch (err) {
|
|
445
|
+
this._log.error('Failed to save state:', err.message);
|
|
446
|
+
}
|
|
250
447
|
}
|
|
251
448
|
|
|
252
|
-
|
|
253
|
-
this.
|
|
449
|
+
_loadState() {
|
|
450
|
+
if (this._stateCache !== null) return this._stateCache;
|
|
451
|
+
try {
|
|
452
|
+
const stateFile = this._getStateFile();
|
|
453
|
+
if (fs.existsSync(stateFile)) {
|
|
454
|
+
this._stateCache = safeJsonParse(fs.readFileSync(stateFile, 'utf-8'), {});
|
|
455
|
+
return this._stateCache;
|
|
456
|
+
}
|
|
457
|
+
} catch (err) {
|
|
458
|
+
this._log.error('Failed to load state:', err.message);
|
|
459
|
+
}
|
|
460
|
+
this._stateCache = {};
|
|
461
|
+
return this._stateCache;
|
|
254
462
|
}
|
|
255
463
|
|
|
256
464
|
_setStateFile(cwd) {
|
|
257
465
|
this._stateFile = path.join(cwd, '.foliko', 'data', 'plugins-state.json');
|
|
466
|
+
this._stateCache = null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ========== 事件描述 ==========
|
|
470
|
+
|
|
471
|
+
registerEventDescription(eventType, description, schema = null) {
|
|
472
|
+
this._eventDescriptions.set(eventType, { description, schema });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
getEventDescriptions() {
|
|
476
|
+
return new Map(this._eventDescriptions);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
getEventDescription(eventType) {
|
|
480
|
+
return this._eventDescriptions.get(eventType) || null;
|
|
258
481
|
}
|
|
259
482
|
}
|
|
260
483
|
|