foliko 1.0.1 → 1.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.
@@ -0,0 +1,478 @@
1
+ /**
2
+ * PythonPluginLoader - Python 插件加载器
3
+ * 通过 python_execute 工具执行 Python 插件
4
+ */
5
+
6
+ const fs = require('fs')
7
+ const path = require('path')
8
+ const { Plugin } = require('../src/core/plugin-base')
9
+ const { z } = require('zod')
10
+
11
+ // 将 JSON Schema 转换为 Zod Schema
12
+ function jsonSchemaToZod(jsonSchema) {
13
+ if (!jsonSchema || jsonSchema.type !== 'object') {
14
+ return z.object({})
15
+ }
16
+
17
+ const properties = jsonSchema.properties || {}
18
+ const required = jsonSchema.required || []
19
+
20
+ const shape = {}
21
+ for (const [key, prop] of Object.entries(properties)) {
22
+ let zodType
23
+ switch (prop.type) {
24
+ case 'string':
25
+ zodType = z.string()
26
+ break
27
+ case 'number':
28
+ zodType = z.number()
29
+ break
30
+ case 'boolean':
31
+ zodType = z.boolean()
32
+ break
33
+ case 'array':
34
+ zodType = z.array(z.any())
35
+ break
36
+ case 'object':
37
+ zodType = jsonSchemaToZod(prop)
38
+ break
39
+ default:
40
+ zodType = z.any()
41
+ }
42
+ // 如果不是 required 字段,则标记为 optional
43
+ if (!required.includes(key)) {
44
+ zodType = zodType.optional()
45
+ }
46
+ shape[key] = zodType
47
+ }
48
+
49
+ return z.object(shape)
50
+ }
51
+
52
+ class PythonPluginLoader extends Plugin {
53
+ constructor(config = {}) {
54
+ super()
55
+ this.name = 'python-plugin-loader'
56
+ this.version = '1.0.0'
57
+ this.description = 'Python 插件加载器'
58
+
59
+ this._agentDir = config.agentDir || '.agent'
60
+ this._pythonPlugins = new Map()
61
+ this._framework = null
62
+ }
63
+
64
+ install(framework) {
65
+ this._framework = framework
66
+ return this
67
+ }
68
+
69
+ start(framework) {
70
+ // 先设置 framework 引用(供 _loadPythonPlugins 使用)
71
+ this._framework = framework
72
+
73
+ // 加载所有 Python 插件(会注册工具)
74
+ this._loadPythonPlugins()
75
+
76
+ // 注册 python_plugin 工具
77
+ framework.registerTool({
78
+ name: 'python_plugin',
79
+ description: '执行 Python 插件工具(当工具未注册时使用)',
80
+ inputSchema: z.object({
81
+ plugin: z.string().describe('插件名称(不含 .py)'),
82
+ tool: z.string().describe('工具名称'),
83
+ params: z.record(z.any()).describe('工具参数')
84
+ }),
85
+ execute: async (args) => {
86
+ const { plugin, tool, params } = args
87
+ return this._executePythonTool(plugin, tool, params)
88
+ }
89
+ })
90
+
91
+ // 监听 agent 创建事件,附加 Python 插件信息到系统提示词
92
+ framework.on('agent:created', (agent) => {
93
+ this._refreshAgentPythonPluginsPrompt(agent)
94
+ })
95
+
96
+ // 等待框架就绪后,刷新所有已有 agent 的 Python 插件提示词
97
+ if (framework._ready) {
98
+ this._refreshAllAgentsPythonPluginsPrompt(framework)
99
+ } else {
100
+ framework.once('framework:ready', () => {
101
+ this._refreshAllAgentsPythonPluginsPrompt(framework)
102
+ })
103
+ }
104
+
105
+ return this
106
+ }
107
+
108
+ /**
109
+ * 构建 Python 插件描述
110
+ */
111
+ _buildPythonPluginsDescription() {
112
+ if (this._pythonPlugins.size === 0) {
113
+ return ''
114
+ }
115
+
116
+ let desc = '【Python 插件工具】\n'
117
+ desc += '可以直接调用以下 Python 插件工具:\n\n'
118
+
119
+ for (const [name, plugin] of this._pythonPlugins) {
120
+ desc += `[${plugin.info.name}] ${plugin.info.description || ''}\n`
121
+ if (plugin.info.tools && Array.isArray(plugin.info.tools)) {
122
+ for (const tool of plugin.info.tools) {
123
+ const paramsStr = Object.keys(tool.params || {}).join(', ') || '无参数'
124
+ desc += ` - ${tool.name}(${paramsStr}): ${tool.description || '无描述'}\n`
125
+ }
126
+ }
127
+ desc += '\n'
128
+ }
129
+
130
+ desc += '调用格式:python_plugin({ plugin: "插件名", tool: "工具名", params: {...} })\n'
131
+ return desc.trim()
132
+ }
133
+
134
+ /**
135
+ * 刷新单个 agent 的 Python 插件提示词
136
+ */
137
+ _refreshAgentPythonPluginsPrompt(agent) {
138
+ const existingPrompt = agent._originalPrompt || ''
139
+ if (existingPrompt.includes('[Python 插件工具]')) {
140
+ return
141
+ }
142
+
143
+ const pyDesc = this._buildPythonPluginsDescription()
144
+ if (!pyDesc) return
145
+
146
+ // 将 Python 插件描述追加到系统提示词
147
+ agent.setSystemPrompt(existingPrompt + '\n\n' + pyDesc)
148
+ }
149
+
150
+ /**
151
+ * 刷新所有 agent 的 Python 插件提示词
152
+ */
153
+ _refreshAllAgentsPythonPluginsPrompt(framework) {
154
+ const visited = new Set()
155
+
156
+ const traverse = (agent) => {
157
+ if (!agent || visited.has(agent)) return
158
+ visited.add(agent)
159
+ this._refreshAgentPythonPluginsPrompt(agent)
160
+
161
+ // 递归处理子 agent
162
+ const subAgents = agent.getSubAgents?.() || agent._subAgents || new Map()
163
+ for (const [name, subAgentInfo] of subAgents) {
164
+ traverse(subAgentInfo.agent)
165
+ }
166
+ }
167
+
168
+ const agents = framework._agents || []
169
+ for (const agent of agents) {
170
+ traverse(agent)
171
+ }
172
+ }
173
+
174
+ /**
175
+ * 加载所有 Python 插件
176
+ */
177
+ _loadPythonPlugins() {
178
+ const pluginsDir = path.join(this._agentDir, 'plugins')
179
+ if (!fs.existsSync(pluginsDir)) {
180
+ return
181
+ }
182
+
183
+ const files = fs.readdirSync(pluginsDir).filter(f => f.endsWith('.py'))
184
+
185
+ for (const file of files) {
186
+ try {
187
+ const pluginPath = path.join(pluginsDir, file)
188
+ const pluginName = file.replace('.py', '')
189
+
190
+ const pluginInfo = this._loadPythonPluginMeta(pluginPath)
191
+ if (pluginInfo) {
192
+ this._pythonPlugins.set(pluginName, {
193
+ name: pluginName,
194
+ path: pluginPath,
195
+ info: pluginInfo,
196
+ code: fs.readFileSync(pluginPath, 'utf-8')
197
+ })
198
+ console.log(`[PythonPluginLoader] Loaded: ${pluginName}`)
199
+
200
+ // 注册插件的每个工具
201
+ if (pluginInfo.tools && Array.isArray(pluginInfo.tools)) {
202
+ for (const tool of pluginInfo.tools) {
203
+ this._registerPythonTool(pluginName, tool)
204
+ }
205
+ }
206
+ }
207
+ } catch (err) {
208
+ console.error(`[PythonPluginLoader] Failed to load ${file}:`, err.message)
209
+ }
210
+ }
211
+
212
+ console.log(`[PythonPluginLoader] Total Python plugins: ${this._pythonPlugins.size}`)
213
+ }
214
+
215
+ /**
216
+ * 注册 Python 插件的工具到框架
217
+ */
218
+ _registerPythonTool(pluginName, tool) {
219
+ if (!tool.name) return
220
+
221
+ try {
222
+ this._framework.registerTool({
223
+ name: tool.name,
224
+ description: tool.description || `${tool.name} (from ${pluginName})`,
225
+ inputSchema: this._parseToolParams(tool.params || {}),
226
+ execute: async (args) => {
227
+ return this._executePythonTool(pluginName, tool.name, args)
228
+ }
229
+ })
230
+ console.log(`[PythonPluginLoader] Registered tool: ${tool.name}`)
231
+ } catch (err) {
232
+ console.error(`[PythonPluginLoader] Failed to register tool ${tool.name}:`, err.message)
233
+ }
234
+ }
235
+
236
+ /**
237
+ * 解析工具参数 schema
238
+ */
239
+ _parseToolParams(params) {
240
+ // 构建 JSON Schema 格式
241
+ const properties = {}
242
+ const required = []
243
+
244
+ for (const [key, value] of Object.entries(params)) {
245
+ let type = 'string'
246
+ if (typeof value === 'boolean') type = 'boolean'
247
+ else if (typeof value === 'number') type = 'number'
248
+ else if (Array.isArray(value)) type = 'array'
249
+ else if (typeof value === 'object') type = 'object'
250
+
251
+ properties[key] = {
252
+ type,
253
+ description: ''
254
+ }
255
+ required.push(key)
256
+ }
257
+
258
+ const jsonSchema = {
259
+ type: 'object',
260
+ properties,
261
+ required
262
+ }
263
+
264
+ // 转换为 Zod Schema 以兼容 AI SDK
265
+ return jsonSchemaToZod(jsonSchema)
266
+ }
267
+
268
+ /**
269
+ * 加载单个 Python 插件的元信息
270
+ */
271
+ _loadPythonPluginMeta(pluginPath) {
272
+ const code = fs.readFileSync(pluginPath, 'utf-8')
273
+
274
+ // 找到 plugin_info = {
275
+ const startIdx = code.indexOf('plugin_info')
276
+ if (startIdx === -1) {
277
+ console.warn(`[PythonPluginLoader] ${path.basename(pluginPath)}: no plugin_info found`)
278
+ return null
279
+ }
280
+
281
+ // 从 plugin_info 后开始,找到第一个 { 的位置
282
+ const braceStart = code.indexOf('{', startIdx)
283
+ if (braceStart === -1) return null
284
+
285
+ // 使用栈匹配找到对应的 }
286
+ let braceCount = 0
287
+ let endIdx = -1
288
+ for (let i = braceStart; i < code.length; i++) {
289
+ if (code[i] === '{') braceCount++
290
+ else if (code[i] === '}') {
291
+ braceCount--
292
+ if (braceCount === 0) {
293
+ endIdx = i
294
+ break
295
+ }
296
+ }
297
+ }
298
+
299
+ if (endIdx === -1) {
300
+ console.warn(`[PythonPluginLoader] ${path.basename(pluginPath)}: unclosed brace`)
301
+ return null
302
+ }
303
+
304
+ try {
305
+ // 提取并清理 plugin_info 内容
306
+ let infoStr = code.substring(braceStart, endIdx + 1)
307
+
308
+ // 移除 Python 注释
309
+ infoStr = infoStr.replace(/#.*$/gm, '')
310
+
311
+ // 转换为 JS 兼容的格式
312
+ infoStr = infoStr
313
+ .replace(/True/g, 'true')
314
+ .replace(/False/g, 'false')
315
+ .replace(/None/g, 'null')
316
+ .replace(/'/g, '"')
317
+
318
+ const info = JSON.parse(infoStr)
319
+ return info
320
+ } catch (err) {
321
+ console.warn(`[PythonPluginLoader] Failed to parse ${path.basename(pluginPath)}:`, err.message)
322
+ return null
323
+ }
324
+ }
325
+
326
+ /**
327
+ * 解析单个值
328
+ */
329
+ _parseValue(str) {
330
+ str = str.trim()
331
+ if (!str) return null
332
+
333
+ // 字典
334
+ if (str.startsWith('{')) {
335
+ return this._parseDict(str)
336
+ }
337
+
338
+ // 字符串
339
+ if (str.startsWith('"') || str.startsWith("'")) {
340
+ return str.slice(1, -1)
341
+ }
342
+
343
+ // 布尔值
344
+ if (str === 'True') return true
345
+ if (str === 'False') return false
346
+ if (str === 'None') return null
347
+
348
+ // 数字
349
+ if (!isNaN(str)) return Number(str)
350
+
351
+ return str
352
+ }
353
+
354
+ /**
355
+ * 简单解析 Python 字典为 JS 对象
356
+ */
357
+ _parseDict(str) {
358
+ // 移除注释并清理
359
+ str = str.replace(/#.*$/gm, '').trim()
360
+ if (!str) return {}
361
+
362
+ const result = {}
363
+ let i = 0
364
+ let currentKey = null
365
+ let currentValue = ''
366
+ let inString = false
367
+ let stringChar = null
368
+
369
+ while (i < str.length) {
370
+ const char = str[i]
371
+
372
+ // 处理字符串
373
+ if ((char === '"' || char === "'") && str[i-1] !== '\\') {
374
+ if (!inString) {
375
+ inString = true
376
+ stringChar = char
377
+ } else if (char === stringChar) {
378
+ inString = false
379
+ stringChar = null
380
+ }
381
+ }
382
+
383
+ // 如果不在字符串内
384
+ if (!inString) {
385
+ // 找到键
386
+ if (currentKey === null && char === ':') {
387
+ currentKey = currentValue.trim().replace(/^["']|["']$/g, '')
388
+ currentValue = ''
389
+ }
390
+ // 找到值的结尾(逗号或右括号)
391
+ else if (char === ',' || char === '}') {
392
+ if (currentKey !== null) {
393
+ const value = currentValue.trim().replace(/,$/, '')
394
+ result[currentKey] = this._parseValue(value)
395
+ }
396
+ currentKey = null
397
+ currentValue = ''
398
+ } else {
399
+ currentValue += char
400
+ }
401
+ } else {
402
+ currentValue += char
403
+ }
404
+
405
+ i++
406
+ }
407
+
408
+ return result
409
+ }
410
+
411
+ /**
412
+ * 执行 Python 插件工具
413
+ */
414
+ async _executePythonTool(pluginName, toolName, params) {
415
+ const plugin = this._pythonPlugins.get(pluginName)
416
+ if (!plugin) {
417
+ return { success: false, error: `Plugin '${pluginName}' not found` }
418
+ }
419
+
420
+ // 构建执行代码 - 使用 base64 避免转义问题
421
+ const pluginDir = path.dirname(plugin.path)
422
+ const codeBase64 = Buffer.from(plugin.code, 'utf-8').toString('base64')
423
+
424
+ const execCode = `
425
+ import sys
426
+ import base64
427
+ sys.path.insert(0, r'${pluginDir}')
428
+
429
+ # 加载插件代码
430
+ plugin_code = base64.b64decode('${codeBase64}').decode('utf-8')
431
+ exec(compile(plugin_code, '${pluginName}.py', 'exec'))
432
+
433
+ # 执行工具
434
+ result = execute_tool('${toolName}', ${JSON.stringify(params)})
435
+ print(result)
436
+ `
437
+
438
+ try {
439
+ // 通过 python-execute 工具执行
440
+ const pythonExe = this._framework.toolRegistry._tools.get('python-execute')
441
+ if (!pythonExe) {
442
+ return { success: false, error: 'python-execute tool not found. Please install python-executor-plugin.' }
443
+ }
444
+
445
+ const pythonResult = await pythonExe.execute({ code: execCode })
446
+
447
+ if (pythonResult.success) {
448
+ try {
449
+ // 尝试解析 JSON 结果
450
+ const parsed = JSON.parse(pythonResult.output)
451
+ return parsed
452
+ } catch {
453
+ return { success: true, output: pythonResult.output }
454
+ }
455
+ } else {
456
+ return { success: false, error: pythonResult.error }
457
+ }
458
+ } catch (err) {
459
+ return { success: false, error: err.message }
460
+ }
461
+ }
462
+
463
+ /**
464
+ * 获取所有已加载的 Python 插件
465
+ */
466
+ getPythonPlugins() {
467
+ return Array.from(this._pythonPlugins.values())
468
+ }
469
+
470
+ /**
471
+ * 获取 Python 插件信息
472
+ */
473
+ getPythonPlugin(name) {
474
+ return this._pythonPlugins.get(name)
475
+ }
476
+ }
477
+
478
+ module.exports = { PythonPluginLoader }