foliko 2.0.0 → 2.0.2
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/ambient/index.js +4 -0
- package/plugins/core/coordinator/index.js +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 +5 -1
- package/plugins/messaging/weixin/index.js +3 -3
- package/src/plugin/manager.js +280 -48
package/package.json
CHANGED
package/plugins/ambient/index.js
CHANGED
|
@@ -44,6 +44,7 @@ class AmbientAgentPlugin extends Plugin {
|
|
|
44
44
|
this._explorerLoop = null
|
|
45
45
|
this._memories = []
|
|
46
46
|
this._thinkAgent = null
|
|
47
|
+
this._initialized = false
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
// ============================================================================
|
|
@@ -88,6 +89,9 @@ class AmbientAgentPlugin extends Plugin {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
start(framework) {
|
|
92
|
+
if (this._initialized) return this;
|
|
93
|
+
this._initialized = true;
|
|
94
|
+
|
|
91
95
|
if (!this.config.enabled) {
|
|
92
96
|
// log.info('插件已禁用,跳过启动')
|
|
93
97
|
return this
|
|
@@ -20,7 +20,7 @@ class CoordinatorPlugin extends Plugin {
|
|
|
20
20
|
this.version = '1.0.0';
|
|
21
21
|
this.description = 'Coordinator模式插件,支持多Worker协作';
|
|
22
22
|
this.priority = 50;
|
|
23
|
-
this.enabled =
|
|
23
|
+
this.enabled = config.enabled === true
|
|
24
24
|
this._framework = null;
|
|
25
25
|
this._coordinatorAgent = null;
|
|
26
26
|
}
|
|
@@ -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 匹配规则必须遵守:
|
|
@@ -56,6 +56,7 @@ class TelegramPlugin extends Plugin {
|
|
|
56
56
|
this._sessionScopes = new Map()
|
|
57
57
|
this._pendingResponses = new Map()
|
|
58
58
|
this._thinkingMessages = new Map()
|
|
59
|
+
this._initialized = false
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
install(framework) {
|
|
@@ -64,6 +65,9 @@ class TelegramPlugin extends Plugin {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
start(framework) {
|
|
68
|
+
if (this._initialized) return this;
|
|
69
|
+
this._initialized = true;
|
|
70
|
+
|
|
67
71
|
if (!this.config.botToken) {
|
|
68
72
|
log.warn(' No bot token. Set TELEGRAM_BOT_TOKEN env var.')
|
|
69
73
|
return this
|
|
@@ -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,154 @@ 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,用现有实例重启
|
|
339
|
+
if (typeof entry.instance.reload === 'function') {
|
|
340
|
+
// 有自定义 reload,由它处理完整重启(内部应调 start)
|
|
341
|
+
await entry.instance.reload(this.framework);
|
|
342
|
+
} else {
|
|
343
|
+
// 没有 reload,走完整 uninstall + install + start
|
|
344
|
+
if (typeof entry.instance.uninstall === 'function') {
|
|
345
|
+
entry.instance.uninstall(this.framework);
|
|
346
|
+
}
|
|
347
|
+
if (typeof entry.instance.install === 'function') {
|
|
348
|
+
entry.instance.install(this.framework);
|
|
349
|
+
}
|
|
350
|
+
if (typeof entry.instance.start === 'function') {
|
|
351
|
+
await entry.instance.start(this.framework);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
entry.instance._started = true;
|
|
355
|
+
}
|
|
205
356
|
} catch (err) {
|
|
206
|
-
this._log.
|
|
357
|
+
this._log.error(`Enable/start failed for '${name}':`, err.message);
|
|
358
|
+
}
|
|
359
|
+
} else if (entry.status === 'registered') {
|
|
360
|
+
// 只注册过但未加载,现在加载
|
|
361
|
+
try {
|
|
362
|
+
await this.load(entry.instance);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
this._log.error(`Enable/load failed for '${name}':`, err.message);
|
|
207
365
|
}
|
|
208
366
|
}
|
|
209
|
-
}
|
|
210
367
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
entry.enabled = true;
|
|
215
|
-
this._log.info(`Plugin enabled: ${name}`);
|
|
216
|
-
}
|
|
368
|
+
this.framework?.emit('plugin:enabled', entry.instance);
|
|
369
|
+
this._saveState();
|
|
370
|
+
this._log.info(`Plugin '${name}' enabled`);
|
|
217
371
|
}
|
|
218
372
|
|
|
373
|
+
/**
|
|
374
|
+
* 禁用插件
|
|
375
|
+
*/
|
|
219
376
|
async disable(name) {
|
|
220
377
|
const entry = this._plugins.get(name);
|
|
221
|
-
if (entry) {
|
|
222
|
-
|
|
223
|
-
|
|
378
|
+
if (!entry) {
|
|
379
|
+
throw new PluginNotFoundError(name);
|
|
380
|
+
}
|
|
381
|
+
if (entry.instance?.system) {
|
|
382
|
+
throw new PluginError(`Plugin '${name}' is a system plugin, cannot be disabled`);
|
|
383
|
+
}
|
|
384
|
+
if (!entry.enabled) {
|
|
385
|
+
this._log.info(`Plugin '${name}' already disabled`);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
entry.enabled = false;
|
|
390
|
+
if (entry.instance) {
|
|
391
|
+
entry.instance.enabled = false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// 如果正在运行,停止
|
|
395
|
+
if (entry.instance._started) {
|
|
396
|
+
try {
|
|
397
|
+
if (typeof entry.instance.stopBot === 'function') {
|
|
398
|
+
await entry.instance.stopBot();
|
|
399
|
+
} else if (typeof entry.instance.stop === 'function') {
|
|
400
|
+
await entry.instance.stop();
|
|
401
|
+
}
|
|
402
|
+
entry.instance._started = false;
|
|
403
|
+
} catch (err) {
|
|
404
|
+
this._log.error(`Stop failed for '${name}':`, err.message);
|
|
224
405
|
}
|
|
225
|
-
entry.enabled = false;
|
|
226
|
-
this._log.info(`Plugin disabled: ${name}`);
|
|
227
406
|
}
|
|
407
|
+
|
|
408
|
+
this.framework?.emit('plugin:disabled', entry.instance);
|
|
409
|
+
this._saveState();
|
|
410
|
+
this._log.info(`Plugin '${name}' disabled`);
|
|
228
411
|
}
|
|
229
412
|
|
|
230
413
|
updatePluginConfig(name, config) {
|
|
@@ -235,26 +418,75 @@ class PluginManager {
|
|
|
235
418
|
if (entry.instance.config) {
|
|
236
419
|
Object.assign(entry.instance.config, config);
|
|
237
420
|
}
|
|
421
|
+
this._saveState();
|
|
238
422
|
}
|
|
239
423
|
|
|
240
|
-
|
|
241
|
-
this.
|
|
424
|
+
setBootstrapping(val) {
|
|
425
|
+
this._bootstrapping = val !== false;
|
|
242
426
|
}
|
|
243
427
|
|
|
244
|
-
|
|
245
|
-
|
|
428
|
+
// ========== 状态持久化 ==========
|
|
429
|
+
|
|
430
|
+
_getStateFile() {
|
|
431
|
+
const dir = path.dirname(this._stateFile);
|
|
432
|
+
if (!fs.existsSync(dir)) {
|
|
433
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
434
|
+
}
|
|
435
|
+
return this._stateFile;
|
|
246
436
|
}
|
|
247
437
|
|
|
248
|
-
|
|
249
|
-
|
|
438
|
+
_saveState() {
|
|
439
|
+
try {
|
|
440
|
+
const stateFile = this._getStateFile();
|
|
441
|
+
let state = safeJsonParse(
|
|
442
|
+
fs.existsSync(stateFile) ? fs.readFileSync(stateFile, 'utf-8') : '{}',
|
|
443
|
+
{}
|
|
444
|
+
);
|
|
445
|
+
for (const [name, entry] of this._plugins) {
|
|
446
|
+
state[name] = {
|
|
447
|
+
enabled: entry.enabled,
|
|
448
|
+
config: entry.instance?.config || {},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
452
|
+
this._stateCache = state;
|
|
453
|
+
} catch (err) {
|
|
454
|
+
this._log.error('Failed to save state:', err.message);
|
|
455
|
+
}
|
|
250
456
|
}
|
|
251
457
|
|
|
252
|
-
|
|
253
|
-
this.
|
|
458
|
+
_loadState() {
|
|
459
|
+
if (this._stateCache !== null) return this._stateCache;
|
|
460
|
+
try {
|
|
461
|
+
const stateFile = this._getStateFile();
|
|
462
|
+
if (fs.existsSync(stateFile)) {
|
|
463
|
+
this._stateCache = safeJsonParse(fs.readFileSync(stateFile, 'utf-8'), {});
|
|
464
|
+
return this._stateCache;
|
|
465
|
+
}
|
|
466
|
+
} catch (err) {
|
|
467
|
+
this._log.error('Failed to load state:', err.message);
|
|
468
|
+
}
|
|
469
|
+
this._stateCache = {};
|
|
470
|
+
return this._stateCache;
|
|
254
471
|
}
|
|
255
472
|
|
|
256
473
|
_setStateFile(cwd) {
|
|
257
474
|
this._stateFile = path.join(cwd, '.foliko', 'data', 'plugins-state.json');
|
|
475
|
+
this._stateCache = null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ========== 事件描述 ==========
|
|
479
|
+
|
|
480
|
+
registerEventDescription(eventType, description, schema = null) {
|
|
481
|
+
this._eventDescriptions.set(eventType, { description, schema });
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
getEventDescriptions() {
|
|
485
|
+
return new Map(this._eventDescriptions);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
getEventDescription(eventType) {
|
|
489
|
+
return this._eventDescriptions.get(eventType) || null;
|
|
258
490
|
}
|
|
259
491
|
}
|
|
260
492
|
|