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.
- package/.claude/settings.local.json +3 -1
- package/cli/src/ui/chat-ui.js +27 -2
- package/install.ps1 +179 -0
- package/package.json +2 -2
- package/plugins/default-plugins.js +9 -0
- package/plugins/python-executor-plugin.js +3 -3
- package/plugins/python-plugin-loader.js +478 -0
- package/skills/python-plugin-dev/SKILL.md +265 -0
- package/website/docs/api.html +159 -0
- package/website/docs/configuration.html +119 -0
- package/website/docs/plugin-development.html +155 -0
- package/website/docs/project-structure.html +142 -0
- package/website/docs/skill-development.html +85 -0
- package/website/index.html +197 -0
- package/website/script.js +77 -0
- package/website/styles.css +306 -0
|
@@ -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 }
|