foliko 1.0.23 → 1.0.26
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 +15 -8
- package/README.md +114 -29
- package/cli/src/commands/chat.js +10 -2
- package/cli/src/commands/list.js +90 -0
- package/cli/src/index.js +8 -0
- package/install.ps1 +24 -4
- package/package.json +2 -2
- package/plugins/ai-plugin.js +1 -0
- package/plugins/default-plugins.js +21 -12
- package/plugins/subagent-plugin.js +319 -11
- package/plugins/telegram-plugin.js +2 -0
- package/plugins/tools-plugin.js +6 -4
- package/plugins/weixin-plugin.js +6 -4
- package/src/core/plugin-base.js +7 -0
- package/src/core/plugin-manager.js +70 -4
|
@@ -40,23 +40,30 @@
|
|
|
40
40
|
"Bash(node -c plugins/telegram-plugin.js && echo \"Syntax OK\")",
|
|
41
41
|
"WebSearch",
|
|
42
42
|
"Bash(npm ls:*)",
|
|
43
|
-
"Bash(cd node_modules/@
|
|
44
|
-
"Bash(cd node_modules/@
|
|
43
|
+
"Bash(cd node_modules/@chnak/weixin-bot && npm run build 2>&1)",
|
|
44
|
+
"Bash(cd node_modules/@chnak/weixin-bot && npx tsc 2>&1)",
|
|
45
45
|
"Bash(npm install:*)",
|
|
46
|
-
"Bash(cd node_modules/@
|
|
46
|
+
"Bash(cd node_modules/@chnak/weixin-bot && npx typescript --version && npx tsc 2>&1)",
|
|
47
47
|
"Bash(npx tsc:*)",
|
|
48
48
|
"Bash(node --check /d/Code/vb-agent/plugins/weixin-plugin.js 2>&1)",
|
|
49
49
|
"Bash(node --check /d/Code/vb-agent/plugins/telegram-plugin.js && node --check /d/Code/vb-agent/plugins/weixin-plugin.js && echo \"OK\")",
|
|
50
50
|
"Bash(node --check /d/Code/vb-agent/cli/src/ui/chat-ui.js && echo \"OK\")",
|
|
51
51
|
"Bash(npm uninstall:*)",
|
|
52
52
|
"Bash(rm -rf /tmp/weixin-bot && git clone https://github.com/chnak/weixin-bot.git /tmp/weixin-bot 2>&1)",
|
|
53
|
-
"Bash(mkdir -p node_modules/@
|
|
53
|
+
"Bash(mkdir -p node_modules/@chnak/weixin-bot && cp -r /tmp/weixin-bot/nodejs/* node_modules/@chnak/weixin-bot/ && cd node_modules/@chnak/weixin-bot && npm install 2>&1)",
|
|
54
54
|
"Bash(node --check /d/Code/vb-agent/plugins/weixin-plugin.js && echo \"OK\")",
|
|
55
|
-
"Bash(node -e \"import\\('@
|
|
55
|
+
"Bash(node -e \"import\\('@chnak/weixin-bot'\\).then\\(m => console.log\\('OK:', Object.keys\\(m\\)\\)\\).catch\\(e => console.error\\('Error:', e.message\\)\\)\")",
|
|
56
56
|
"Bash(npm run:*)",
|
|
57
|
-
"Bash(node -e \"const { WeixinBot } = require\\('@
|
|
58
|
-
"Bash(node -e \"import\\('@
|
|
59
|
-
"Bash(ls -la D:/code/vb-agent/cli/bin/ && cat D:/code/vb-agent/cli/bin/*.js 2>/dev/null | head -50)"
|
|
57
|
+
"Bash(node -e \"const { WeixinBot } = require\\('@chnak/weixin-bot'\\); console.log\\(typeof WeixinBot\\)\" 2>&1)",
|
|
58
|
+
"Bash(node -e \"import\\('@chnak/weixin-bot'\\).then\\(m => console.log\\('OK:', typeof m.WeixinBot\\)\\).catch\\(e => console.error\\('Error:', e.message\\)\\)\" 2>&1)",
|
|
59
|
+
"Bash(ls -la D:/code/vb-agent/cli/bin/ && cat D:/code/vb-agent/cli/bin/*.js 2>/dev/null | head -50)",
|
|
60
|
+
"Bash(npm config:*)",
|
|
61
|
+
"Bash(node -c plugins/subagent-plugin.js 2>&1)",
|
|
62
|
+
"Bash(node -c plugins/default-plugins.js 2>&1)",
|
|
63
|
+
"Bash(node -c plugins/default-plugins.js && node -c src/core/plugin-manager.js && node -c plugins/tools-plugin.js 2>&1)",
|
|
64
|
+
"Bash(node -c src/core/plugin-base.js && node -c src/core/plugin-manager.js 2>&1)",
|
|
65
|
+
"Bash(node -c plugins/telegram-plugin.js && node -c plugins/weixin-plugin.js 2>&1)",
|
|
66
|
+
"Bash(node -c src/core/plugin-manager.js 2>&1)"
|
|
60
67
|
]
|
|
61
68
|
}
|
|
62
69
|
}
|
package/README.md
CHANGED
|
@@ -11,15 +11,42 @@
|
|
|
11
11
|
- **技能系统** - 可扩展的 Skill 管理
|
|
12
12
|
- **会话管理** - 支持多会话切换
|
|
13
13
|
- **规则引擎** - 可配置的行为规则
|
|
14
|
+
- **子 Agent** - 支持多子 Agent 分工协作
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 方式一:npm 安装
|
|
20
|
+
npm install -g foliko
|
|
21
|
+
|
|
22
|
+
# 方式二:Windows 一键脚本安装
|
|
23
|
+
irm https://folikoai.com/install.ps1 | iex
|
|
24
|
+
|
|
25
|
+
# 方式三:Mac/Linux 一键脚本安装
|
|
26
|
+
curl -fsSL https://folikoai.com/install.sh | bash
|
|
27
|
+
```
|
|
14
28
|
|
|
15
29
|
## 快速开始
|
|
16
30
|
|
|
17
31
|
```bash
|
|
18
|
-
#
|
|
19
|
-
|
|
32
|
+
# 全局安装后
|
|
33
|
+
foliko chat
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## CLI 命令
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# 聊天模式(使用 .env 中的配置)
|
|
40
|
+
foliko chat
|
|
41
|
+
|
|
42
|
+
# 指定完整配置
|
|
43
|
+
foliko chat --provider deepseek --model deepseek-chat --base-url https://api.deepseek.com/v1 --api-key sk-xxx
|
|
44
|
+
|
|
45
|
+
# 只指定 provider(使用 provider 默认 model 和 baseURL)
|
|
46
|
+
foliko chat --provider deepseek --api-key sk-xxx
|
|
20
47
|
|
|
21
|
-
#
|
|
22
|
-
|
|
48
|
+
# 列出所有子Agent配置
|
|
49
|
+
foliko list
|
|
23
50
|
```
|
|
24
51
|
|
|
25
52
|
## 项目结构
|
|
@@ -30,7 +57,7 @@ foliko/
|
|
|
30
57
|
│ └── bin/foliko.js # CLI 入口
|
|
31
58
|
├── src/ # 核心框架
|
|
32
59
|
│ ├── core/ # 核心组件
|
|
33
|
-
│ ├── capabilities/
|
|
60
|
+
│ ├── capabilities/ # 能力插件
|
|
34
61
|
│ └── executors/ # 执行器
|
|
35
62
|
├── plugins/ # 内置插件
|
|
36
63
|
├── skills/ # 技能目录
|
|
@@ -50,19 +77,19 @@ FOLIKO_PROVIDER=minimax
|
|
|
50
77
|
# AI Model(可选,不填则使用 provider 默认值)
|
|
51
78
|
# MiniMax: MiniMax-M2.7
|
|
52
79
|
# DeepSeek: deepseek-chat, deepseek-coder 等
|
|
53
|
-
FOLIKO_MODEL=
|
|
80
|
+
FOLIKO_MODEL=MiniMax-M2.7
|
|
54
81
|
|
|
55
82
|
# API Base URL(可选,不填则使用 provider 默认值)
|
|
56
83
|
# MiniMax: https://api.minimaxi.com/v1
|
|
57
84
|
# DeepSeek: https://api.deepseek.com/v1
|
|
58
|
-
FOLIKO_BASE_URL=
|
|
85
|
+
FOLIKO_BASE_URL=https://api.minimaxi.com/v1
|
|
59
86
|
|
|
60
87
|
# API Key(通用,不填则使用 provider 专用 key)
|
|
61
|
-
FOLIKO_API_KEY=
|
|
88
|
+
FOLIKO_API_KEY=sk-your-minimax-api-key
|
|
62
89
|
|
|
63
90
|
# Provider 专用 API Key(可选)
|
|
64
|
-
DEEPSEEK_API_KEY=sk-your-deepseek-api-key
|
|
65
|
-
MINIMAX_API_KEY=sk-your-minimax-api-key
|
|
91
|
+
# DEEPSEEK_API_KEY=sk-your-deepseek-api-key
|
|
92
|
+
# MINIMAX_API_KEY=sk-your-minimax-api-key
|
|
66
93
|
```
|
|
67
94
|
|
|
68
95
|
**配置优先级**:命令行参数 > .env配置 > provider默认值
|
|
@@ -74,11 +101,49 @@ MINIMAX_API_KEY=sk-your-minimax-api-key
|
|
|
74
101
|
├── config # 配置文件
|
|
75
102
|
├── ai.json # AI 配置
|
|
76
103
|
├── mcp_config.json # MCP 服务器配置
|
|
104
|
+
├── agents/ # 子Agent配置目录
|
|
77
105
|
├── plugins/ # 用户插件目录
|
|
78
106
|
├── skills/ # 用户技能目录
|
|
79
107
|
└── data/ # 数据目录
|
|
80
108
|
```
|
|
81
109
|
|
|
110
|
+
### 子 Agent 配置 (.agent/agents/)
|
|
111
|
+
|
|
112
|
+
在 `.agent/agents/` 目录下放置子 Agent 配置文件,支持 `.js`、`.json`、`.md` 格式:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
.agent/agents/
|
|
116
|
+
├── backend-specialist.md
|
|
117
|
+
├── frontend-specialist.md
|
|
118
|
+
└── database-architect.md
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### JSON 格式
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"name": "backend-specialist",
|
|
126
|
+
"role": "Backend Specialist",
|
|
127
|
+
"description": "Expert in backend development",
|
|
128
|
+
"parentTools": ["read_file", "run_command"]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### Markdown 格式
|
|
133
|
+
|
|
134
|
+
```markdown
|
|
135
|
+
# Backend Specialist
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"name": "backend-specialist",
|
|
140
|
+
"role": "Backend Specialist",
|
|
141
|
+
"description": "Expert in backend development",
|
|
142
|
+
"parentTools": ["read_file", "run_command"]
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
```
|
|
146
|
+
|
|
82
147
|
### ai.json 格式
|
|
83
148
|
|
|
84
149
|
```json
|
|
@@ -191,37 +256,57 @@ allowed-tools: tool1,tool2
|
|
|
191
256
|
技能内容...
|
|
192
257
|
```
|
|
193
258
|
|
|
194
|
-
## CLI 命令
|
|
195
|
-
|
|
196
|
-
```bash
|
|
197
|
-
# 聊天模式(使用 .env 中的配置)
|
|
198
|
-
npm run chat
|
|
199
|
-
|
|
200
|
-
# 指定完整配置
|
|
201
|
-
foliko chat --provider deepseek --model deepseek-chat --base-url https://api.deepseek.com/v1 --api-key sk-xxx
|
|
202
|
-
|
|
203
|
-
# 只指定 provider(使用 provider 默认 model 和 baseURL)
|
|
204
|
-
foliko chat --provider deepseek --api-key sk-xxx
|
|
205
|
-
|
|
206
|
-
# 只指定 api-key(使用 FOLIKO_PROVIDER 设定的 provider)
|
|
207
|
-
foliko chat --api-key sk-xxx
|
|
208
|
-
```
|
|
209
|
-
|
|
210
259
|
## 内置工具
|
|
211
260
|
|
|
261
|
+
### 通用工具
|
|
262
|
+
|
|
212
263
|
| 工具 | 说明 |
|
|
213
264
|
|------|------|
|
|
214
265
|
| `loadSkill` | 加载技能 |
|
|
215
|
-
| `
|
|
216
|
-
| `
|
|
217
|
-
| `
|
|
266
|
+
| `list_plugins` | 列出所有插件 |
|
|
267
|
+
| `list_tools` | 列出所有工具 |
|
|
268
|
+
| `reload_plugins` | 重载插件 |
|
|
269
|
+
| `shell` | 执行 Shell 命令 |
|
|
270
|
+
| `powershell` | 执行 PowerShell 命令 |
|
|
271
|
+
| `python-execute` | 执行 Python 代码 |
|
|
272
|
+
| `python_script` | 执行 Python 脚本 |
|
|
273
|
+
| `pip_install` | 安装 Python 包 |
|
|
218
274
|
| `install` | 安装 npm 包 |
|
|
219
275
|
| `mcp_reload` | 重载 MCP 服务器 |
|
|
220
276
|
| `audit_query` | 查询审计日志 |
|
|
221
277
|
|
|
278
|
+
### 子 Agent 管理工具
|
|
279
|
+
|
|
280
|
+
| 工具 | 说明 |
|
|
281
|
+
|------|------|
|
|
282
|
+
| `subagent_list` | 列出所有子Agent |
|
|
283
|
+
| `subagent_call` | 调用指定的子Agent处理任务 |
|
|
284
|
+
| `subagent_reload` | 重新加载子Agent配置 |
|
|
285
|
+
|
|
286
|
+
### 会话工具
|
|
287
|
+
|
|
288
|
+
| 工具 | 说明 |
|
|
289
|
+
|------|------|
|
|
290
|
+
| `session_create` | 创建会话 |
|
|
291
|
+
| `session_get` | 获取会话 |
|
|
292
|
+
| `session_list` | 列出所有会话 |
|
|
293
|
+
| `session_delete` | 删除会话 |
|
|
294
|
+
| `session_history` | 获取会话历史 |
|
|
295
|
+
|
|
296
|
+
### 定时任务工具
|
|
297
|
+
|
|
298
|
+
| 工具 | 说明 |
|
|
299
|
+
|------|------|
|
|
300
|
+
| `schedule_task` | 创建定时任务 |
|
|
301
|
+
| `schedule_list` | 列出定时任务 |
|
|
302
|
+
| `schedule_cancel` | 取消定时任务 |
|
|
303
|
+
| `cron_examples` | 显示 cron 示例 |
|
|
304
|
+
|
|
222
305
|
## 工作目录
|
|
223
306
|
|
|
224
307
|
CLI 工作目录默认使用执行命令的目录。
|
|
308
|
+
- `.agent/` 目录会创建在工作目录下
|
|
309
|
+
- 配置文件、插件、技能等都放在工作目录的 `.agent/` 下
|
|
225
310
|
|
|
226
311
|
## 许可证
|
|
227
312
|
|
package/cli/src/commands/chat.js
CHANGED
|
@@ -37,11 +37,19 @@ function getEnvConfig() {
|
|
|
37
37
|
const provider = process.env.FOLIKO_PROVIDER || DEFAULT_CONFIG.provider
|
|
38
38
|
const providerDefaults = PROVIDER_DEFAULTS[provider] || PROVIDER_DEFAULTS.minimax
|
|
39
39
|
|
|
40
|
+
// 支持多种 API key 环境变量名
|
|
41
|
+
let apiKey = process.env.FOLIKO_API_KEY || null
|
|
42
|
+
if (!apiKey) {
|
|
43
|
+
// 根据 provider 查找对应的 API key
|
|
44
|
+
const upperProvider = provider.toUpperCase().replace(/-/g, '_')
|
|
45
|
+
apiKey = process.env[`${upperProvider}_API_KEY`] || null
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
return {
|
|
41
49
|
model: process.env.FOLIKO_MODEL || providerDefaults.model,
|
|
42
50
|
provider: provider,
|
|
43
51
|
baseURL: process.env.FOLIKO_BASE_URL || providerDefaults.baseURL,
|
|
44
|
-
apiKey:
|
|
52
|
+
apiKey: apiKey
|
|
45
53
|
}
|
|
46
54
|
}
|
|
47
55
|
|
|
@@ -73,8 +81,8 @@ function parseArgs(args) {
|
|
|
73
81
|
* Chat 命令入口
|
|
74
82
|
*/
|
|
75
83
|
async function chatCommand(args) {
|
|
76
|
-
const options = parseArgs(args)
|
|
77
84
|
|
|
85
|
+
const options = parseArgs(args)
|
|
78
86
|
console.log('=== Foliko 持续对话 ===\n')
|
|
79
87
|
console.log('输入 exit 或 quit 退出\n')
|
|
80
88
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List 命令实现
|
|
3
|
+
* 列出所有子Agent配置
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
const path = require('path')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 列出 .agent/agents 目录下的所有 Agent 配置
|
|
11
|
+
*/
|
|
12
|
+
async function listCommand() {
|
|
13
|
+
const agentsDir = path.resolve(process.cwd(), '.agent', 'agents')
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(agentsDir)) {
|
|
16
|
+
console.log('No .agent/agents directory found.')
|
|
17
|
+
console.log('Create agents by adding files to: .agent/agents/')
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const entries = fs.readdirSync(agentsDir, { withFileTypes: true })
|
|
22
|
+
const agents = []
|
|
23
|
+
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.json') || entry.name.endsWith('.md'))) {
|
|
26
|
+
const baseName = entry.name.replace(/\.(js|json|md)$/, '')
|
|
27
|
+
const filePath = path.join(agentsDir, entry.name)
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
let config = { name: baseName }
|
|
31
|
+
|
|
32
|
+
if (entry.name.endsWith('.json')) {
|
|
33
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
34
|
+
config = { ...config, ...JSON.parse(content) }
|
|
35
|
+
} else if (entry.name.endsWith('.md')) {
|
|
36
|
+
// 尝试从 markdown 中提取配置
|
|
37
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
38
|
+
const jsonMatch = content.match(/```json\n([\s\S]*?)\n```/)
|
|
39
|
+
if (jsonMatch) {
|
|
40
|
+
config = { ...config, ...JSON.parse(jsonMatch[1]) }
|
|
41
|
+
}
|
|
42
|
+
// 提取 name 行
|
|
43
|
+
const nameMatch = content.match(/^name:\s*(.+)$/m)
|
|
44
|
+
if (nameMatch) {
|
|
45
|
+
config.name = nameMatch[1].trim()
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
// .js 文件,尝试加载
|
|
49
|
+
delete require.cache[require.resolve(filePath)]
|
|
50
|
+
const mod = require(filePath)
|
|
51
|
+
config = typeof mod === 'function' ? mod() : mod
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
agents.push({
|
|
55
|
+
name: config.name || baseName,
|
|
56
|
+
role: config.role || '-',
|
|
57
|
+
description: config.description || '-',
|
|
58
|
+
file: entry.name
|
|
59
|
+
})
|
|
60
|
+
} catch (err) {
|
|
61
|
+
agents.push({
|
|
62
|
+
name: baseName,
|
|
63
|
+
role: '-',
|
|
64
|
+
description: `Error loading: ${err.message}`,
|
|
65
|
+
file: entry.name
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (agents.length === 0) {
|
|
72
|
+
console.log('No agents found in .agent/agents/')
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(`\nFound ${agents.length} agent(s) in .agent/agents/:\n`)
|
|
77
|
+
console.log('Name Role Description')
|
|
78
|
+
console.log('---------------- ------------------ ------------------------------------------')
|
|
79
|
+
|
|
80
|
+
for (const agent of agents) {
|
|
81
|
+
const name = (agent.name || '').padEnd(16).slice(0, 16)
|
|
82
|
+
const role = (agent.role || '-').padEnd(18).slice(0, 18)
|
|
83
|
+
const desc = (agent.description || '-').slice(0, 40)
|
|
84
|
+
console.log(`${name} ${role} ${desc}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log('')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { listCommand }
|
package/cli/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const { chatCommand } = require('./commands/chat')
|
|
6
|
+
const { listCommand } = require('./commands/list')
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* CLI 主入口
|
|
@@ -16,6 +17,11 @@ async function cli() {
|
|
|
16
17
|
await chatCommand(args.slice(1))
|
|
17
18
|
break
|
|
18
19
|
|
|
20
|
+
case 'list':
|
|
21
|
+
case 'ls':
|
|
22
|
+
await listCommand()
|
|
23
|
+
break
|
|
24
|
+
|
|
19
25
|
case 'help':
|
|
20
26
|
case '--help':
|
|
21
27
|
case '-h':
|
|
@@ -46,6 +52,7 @@ Usage: foliko <command> [options]
|
|
|
46
52
|
|
|
47
53
|
Commands:
|
|
48
54
|
chat 启动持续对话聊天
|
|
55
|
+
list 列出所有子Agent配置
|
|
49
56
|
help 显示帮助信息
|
|
50
57
|
version 显示版本号
|
|
51
58
|
|
|
@@ -58,6 +65,7 @@ Examples:
|
|
|
58
65
|
foliko chat
|
|
59
66
|
foliko chat --model MiniMax-M2.7
|
|
60
67
|
foliko chat --provider minimax --base-url https://api.minimaxi.com/v1
|
|
68
|
+
foliko list
|
|
61
69
|
`)
|
|
62
70
|
}
|
|
63
71
|
|
package/install.ps1
CHANGED
|
@@ -97,13 +97,33 @@ Write-Host "Cleaning npm cache..." -ForegroundColor Cyan
|
|
|
97
97
|
npm cache clean --force 2>$null | Out-Null
|
|
98
98
|
|
|
99
99
|
Write-Host "Installing Foliko..." -ForegroundColor Cyan
|
|
100
|
-
npm install -g foliko --ignore-scripts
|
|
100
|
+
npm install -g foliko --ignore-scripts 2>&1 | Out-Null
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
# Get npm global bin path
|
|
103
|
+
$npmPrefix = npm config get prefix 2>$null
|
|
104
|
+
$npmBinPath = $npmPrefix -replace "\\$", ""
|
|
105
|
+
|
|
106
|
+
# Add to PATH permanently for current user
|
|
107
|
+
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
108
|
+
if ($userPath -notlike "*$npmBinPath*") {
|
|
109
|
+
[Environment]::SetEnvironmentVariable("Path", "$userPath;$npmBinPath", "User")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Also add to current session PATH
|
|
113
|
+
$env:Path = "$npmBinPath;$env:Path"
|
|
114
|
+
Start-Sleep -Seconds 2
|
|
115
|
+
|
|
116
|
+
# Check if installation succeeded
|
|
117
|
+
if (Test-Path "$npmBinPath\foliko.cmd") {
|
|
103
118
|
Write-Host ""
|
|
104
119
|
Write-Host "Installation complete!" -ForegroundColor Green
|
|
105
|
-
Write-Host "
|
|
120
|
+
Write-Host ""
|
|
121
|
+
Write-Host "Please restart your terminal or run:" -ForegroundColor Cyan
|
|
122
|
+
Write-Host ' $env:Path = "C:\Users\Administrator\AppData\Roaming\npm;' + '$env:Path"' -ForegroundColor White
|
|
123
|
+
Write-Host ""
|
|
124
|
+
Write-Host "Then run: foliko chat" -ForegroundColor Yellow
|
|
106
125
|
} else {
|
|
107
126
|
Write-Host ""
|
|
108
|
-
Write-Host "Installation failed.
|
|
127
|
+
Write-Host "Installation may have failed." -ForegroundColor Yellow
|
|
128
|
+
Write-Host "Run: npm install -g foliko"
|
|
109
129
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foliko",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.26",
|
|
4
4
|
"description": "简约的插件化 Agent 框架",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"@ai-sdk/openai": "^3.0.41",
|
|
26
26
|
"@ai-sdk/openai-compatible": "^2.0.35",
|
|
27
27
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
28
|
-
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
29
28
|
"@chnak/weixin-bot": "^1.2.0",
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
30
30
|
"ai": "^6.0.116",
|
|
31
31
|
"dotenv": "^17.3.1",
|
|
32
32
|
"imap": "^0.8.19",
|
package/plugins/ai-plugin.js
CHANGED
|
@@ -17,7 +17,8 @@ function loadAgentConfig(agentDir = '.agent') {
|
|
|
17
17
|
ai: {},
|
|
18
18
|
mcpServers: {},
|
|
19
19
|
plugins: [],
|
|
20
|
-
skillsDirs: []
|
|
20
|
+
skillsDirs: [],
|
|
21
|
+
agentsDir: null // 子Agent配置目录
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const resolvedDir = path.resolve(process.cwd(), agentDir)
|
|
@@ -29,6 +30,13 @@ function loadAgentConfig(agentDir = '.agent') {
|
|
|
29
30
|
|
|
30
31
|
console.log(`[AgentConfig] Loading config from: ${resolvedDir}`)
|
|
31
32
|
|
|
33
|
+
// 添加 agents 目录(如果存在)
|
|
34
|
+
const agentsDir = path.join(resolvedDir, 'agents')
|
|
35
|
+
if (fs.existsSync(agentsDir)) {
|
|
36
|
+
config.agentsDir = agentsDir
|
|
37
|
+
console.log(`[AgentConfig] Found agents directory: ${agentsDir}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
// 加载 config 文件(key=value 格式)
|
|
33
41
|
const configFile = path.join(resolvedDir, 'config')
|
|
34
42
|
if (fs.existsSync(configFile)) {
|
|
@@ -242,10 +250,13 @@ async function bootstrapDefaults(framework, config = {}) {
|
|
|
242
250
|
// 跳过或已禁用
|
|
243
251
|
} else {
|
|
244
252
|
const { AIPlugin } = require('./ai-plugin')
|
|
253
|
+
// 根据 provider 获取对应的 API key
|
|
254
|
+
const upperProvider = (aiConfig.provider || 'deepseek').toUpperCase().replace(/-/g, '_')
|
|
255
|
+
const envApiKey = process.env[`${upperProvider}_API_KEY`] || process.env.FOLIKO_API_KEY
|
|
245
256
|
const aiPlugin = new AIPlugin({
|
|
246
257
|
provider: aiConfig.provider || 'deepseek',
|
|
247
258
|
model: aiConfig.model || 'deepseek-chat',
|
|
248
|
-
apiKey: aiConfig.apiKey ||
|
|
259
|
+
apiKey: aiConfig.apiKey || envApiKey,
|
|
249
260
|
baseURL: aiConfig.baseURL
|
|
250
261
|
})
|
|
251
262
|
await framework.loadPlugin(aiPlugin)
|
|
@@ -382,19 +393,18 @@ async function bootstrapDefaults(framework, config = {}) {
|
|
|
382
393
|
console.log('[Bootstrap] Python Plugin Loader loaded')
|
|
383
394
|
}
|
|
384
395
|
|
|
385
|
-
// 12.6 Telegram
|
|
396
|
+
// 12.6 Telegram 插件(默认禁用,需要在 plugins.json 中设置 enabled: true)
|
|
386
397
|
if (shouldLoad('telegram')) {
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
await framework.loadPlugin(new TelegramPlugin({ botToken: telegramToken }))
|
|
393
|
-
console.log('[Bootstrap] Telegram Plugin loaded')
|
|
398
|
+
const { Plugin } = require('../src/core/plugin-base')
|
|
399
|
+
const createTelegramPlugin = require('./telegram-plugin')
|
|
400
|
+
const TelegramPlugin = createTelegramPlugin(Plugin)
|
|
401
|
+
const telegramConfig = {
|
|
402
|
+
botToken: process.env.TELEGRAM_BOT_TOKEN || agentConfig.telegram?.botToken
|
|
394
403
|
}
|
|
404
|
+
await framework.loadPlugin(new TelegramPlugin(telegramConfig))
|
|
395
405
|
}
|
|
396
406
|
|
|
397
|
-
// 12.7 WeChat
|
|
407
|
+
// 12.7 WeChat 插件(默认禁用,需要在 plugins.json 中设置 enabled: true)
|
|
398
408
|
if (shouldLoad('weixin')) {
|
|
399
409
|
try {
|
|
400
410
|
const { Plugin } = require('../src/core/plugin-base')
|
|
@@ -406,7 +416,6 @@ async function bootstrapDefaults(framework, config = {}) {
|
|
|
406
416
|
allowedUsers: agentConfig.weixin?.allowedUsers || []
|
|
407
417
|
}
|
|
408
418
|
await framework.loadPlugin(new WeixinPlugin(weixinConfig))
|
|
409
|
-
console.log('[Bootstrap] WeChat Plugin loaded')
|
|
410
419
|
} catch (err) {
|
|
411
420
|
console.warn('[Bootstrap] WeChat Plugin not available:', err.message)
|
|
412
421
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* 创建具有独立工具集的子Agent,用于分工处理不同领域的任务
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
const path = require('path')
|
|
6
8
|
const { Plugin } = require('../src/core/plugin-base')
|
|
7
9
|
const { z } = require('zod')
|
|
8
10
|
const { Agent } = require('../src/core/agent')
|
|
@@ -38,7 +40,7 @@ class SubAgentPlugin extends Plugin {
|
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
start(framework) {
|
|
41
|
-
// 创建子Agent
|
|
43
|
+
// 创建子Agent(可能需要等待父agent准备好)
|
|
42
44
|
this._createSubAgent()
|
|
43
45
|
|
|
44
46
|
// 注册委托工具到主Agent(通过framework获取父agent)
|
|
@@ -54,7 +56,30 @@ class SubAgentPlugin extends Plugin {
|
|
|
54
56
|
// 获取父Agent(主Agent)
|
|
55
57
|
const parentAgent = this._getParentAgent()
|
|
56
58
|
if (!parentAgent) {
|
|
57
|
-
|
|
59
|
+
// 父agent还没创建,监听事件等其创建
|
|
60
|
+
if (this._framework) {
|
|
61
|
+
const framework = this._framework
|
|
62
|
+
framework.on('agent:created', (agent) => {
|
|
63
|
+
if (framework._mainAgent && agent === framework._mainAgent) {
|
|
64
|
+
// 父agent已创建,重新尝试创建子agent
|
|
65
|
+
this._doCreateSubAgent(agent)
|
|
66
|
+
// 注册委托工具
|
|
67
|
+
this._registerDelegateTool()
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this._doCreateSubAgent(parentAgent)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 实际执行创建子Agent
|
|
79
|
+
*/
|
|
80
|
+
_doCreateSubAgent(parentAgent) {
|
|
81
|
+
// 避免重复创建
|
|
82
|
+
if (this._agent) {
|
|
58
83
|
return
|
|
59
84
|
}
|
|
60
85
|
|
|
@@ -184,27 +209,32 @@ class SubAgentPlugin extends Plugin {
|
|
|
184
209
|
}
|
|
185
210
|
|
|
186
211
|
/**
|
|
187
|
-
*
|
|
212
|
+
* 注册委托工具到框架(会被同步到主Agent)
|
|
188
213
|
*/
|
|
189
214
|
_registerDelegateTool() {
|
|
190
|
-
const
|
|
191
|
-
if (!
|
|
215
|
+
const framework = this._framework
|
|
216
|
+
if (!framework) return
|
|
192
217
|
|
|
193
218
|
const agentName = this.config.name
|
|
194
219
|
const role = this.config.role
|
|
195
220
|
|
|
196
|
-
//
|
|
197
|
-
|
|
221
|
+
// 注册工具到框架工具注册表
|
|
222
|
+
framework.registerTool({
|
|
198
223
|
name: agentName,
|
|
199
224
|
description: `${role}:当需要${role}时,调用此工具执行任务。如:编译代码、运行测试、代码重构等。`,
|
|
200
225
|
inputSchema: z.object({
|
|
201
226
|
task: z.string().describe('给子Agent的具体任务描述')
|
|
202
227
|
}),
|
|
203
|
-
execute: async (args,
|
|
228
|
+
execute: async (args, fw) => {
|
|
204
229
|
if (!this._agent) {
|
|
205
230
|
return { error: `SubAgent ${agentName} not initialized` }
|
|
206
231
|
}
|
|
207
232
|
|
|
233
|
+
const parentAgent = fw._mainAgent
|
|
234
|
+
if (!parentAgent) {
|
|
235
|
+
return { error: `Parent agent not found` }
|
|
236
|
+
}
|
|
237
|
+
|
|
208
238
|
// 发射子Agent开始处理事件
|
|
209
239
|
parentAgent.emit('subagent:chat:start', {
|
|
210
240
|
parentAgent,
|
|
@@ -294,11 +324,14 @@ class SubAgentManagerPlugin extends Plugin {
|
|
|
294
324
|
this.priority = 10
|
|
295
325
|
|
|
296
326
|
this.config = {
|
|
297
|
-
|
|
327
|
+
agentsDir: config.agentsDir || null, // 子Agent配置目录
|
|
328
|
+
agents: config.agents || [] // 子Agent配置列表
|
|
298
329
|
}
|
|
299
330
|
|
|
300
331
|
this._framework = null
|
|
301
332
|
this._subAgents = new Map()
|
|
333
|
+
this._fileWatcher = null
|
|
334
|
+
this._lastFileStates = new Map() // 记录文件状态用于检测变化
|
|
302
335
|
}
|
|
303
336
|
|
|
304
337
|
install(framework) {
|
|
@@ -307,6 +340,9 @@ class SubAgentManagerPlugin extends Plugin {
|
|
|
307
340
|
}
|
|
308
341
|
|
|
309
342
|
start(framework) {
|
|
343
|
+
// 从 agentsDir 加载子Agent配置
|
|
344
|
+
this._loadAgentsFromDir()
|
|
345
|
+
|
|
310
346
|
// 创建所有配置的子Agent
|
|
311
347
|
for (const agentConfig of this.config.agents) {
|
|
312
348
|
const plugin = new SubAgentPlugin(agentConfig)
|
|
@@ -315,7 +351,20 @@ class SubAgentManagerPlugin extends Plugin {
|
|
|
315
351
|
this._subAgents.set(agentConfig.name, plugin)
|
|
316
352
|
}
|
|
317
353
|
|
|
318
|
-
//
|
|
354
|
+
// 注册管理工具到框架工具注册表
|
|
355
|
+
// 这样当 agent 创建时会通过 _syncTools() 自动同步过来
|
|
356
|
+
this._registerTools(framework)
|
|
357
|
+
|
|
358
|
+
// 启动文件监控
|
|
359
|
+
this._startFileWatcher()
|
|
360
|
+
|
|
361
|
+
return this
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 注册管理工具到框架工具注册表
|
|
366
|
+
*/
|
|
367
|
+
_registerTools(framework) {
|
|
319
368
|
framework.registerTool({
|
|
320
369
|
name: 'subagent_list',
|
|
321
370
|
description: '列出所有子Agent',
|
|
@@ -358,7 +407,251 @@ class SubAgentManagerPlugin extends Plugin {
|
|
|
358
407
|
}
|
|
359
408
|
})
|
|
360
409
|
|
|
361
|
-
|
|
410
|
+
framework.registerTool({
|
|
411
|
+
name: 'subagent_reload',
|
|
412
|
+
description: '重新加载子Agent配置(当新增或删除.agent/agents下的文件后使用)',
|
|
413
|
+
inputSchema: z.object({}),
|
|
414
|
+
execute: async () => {
|
|
415
|
+
try {
|
|
416
|
+
this.reload(this._framework)
|
|
417
|
+
return { success: true, message: '子Agent配置已重新加载' }
|
|
418
|
+
} catch (err) {
|
|
419
|
+
return { success: false, error: err.message }
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
console.log('[SubAgentManager] Management tools registered to framework')
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* 从 agentsDir 目录加载子Agent配置
|
|
429
|
+
*/
|
|
430
|
+
_loadAgentsFromDir() {
|
|
431
|
+
const agentsDir = this.config.agentsDir
|
|
432
|
+
console.log('[SubAgentManager] _loadAgentsFromDir called, agentsDir:', agentsDir)
|
|
433
|
+
if (!agentsDir || !fs.existsSync(agentsDir)) {
|
|
434
|
+
console.log('[SubAgentManager] agentsDir not found or does not exist')
|
|
435
|
+
return
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const entries = fs.readdirSync(agentsDir, { withFileTypes: true })
|
|
439
|
+
console.log('[SubAgentManager] Found entries:', entries.length)
|
|
440
|
+
|
|
441
|
+
for (const entry of entries) {
|
|
442
|
+
if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.json') || entry.name.endsWith('.md'))) {
|
|
443
|
+
const baseName = entry.name.replace(/\.(js|json|md)$/, '')
|
|
444
|
+
const filePath = path.join(agentsDir, entry.name)
|
|
445
|
+
|
|
446
|
+
// 跳过与已有配置同名的
|
|
447
|
+
if (this.config.agents.some(a => a.name === baseName)) {
|
|
448
|
+
continue
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
let agentConfig
|
|
453
|
+
if (entry.name.endsWith('.json')) {
|
|
454
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
455
|
+
agentConfig = JSON.parse(content)
|
|
456
|
+
} else if (entry.name.endsWith('.md')) {
|
|
457
|
+
// 解析 markdown 文件,提取 JSON 配置
|
|
458
|
+
agentConfig = this._parseMarkdownConfig(filePath, baseName)
|
|
459
|
+
console.log('[SubAgentManager] Parsed md:', baseName, agentConfig)
|
|
460
|
+
} else {
|
|
461
|
+
// 清除缓存并加载
|
|
462
|
+
delete require.cache[require.resolve(filePath)]
|
|
463
|
+
const mod = require(filePath)
|
|
464
|
+
agentConfig = typeof mod === 'function' ? mod() : mod
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (agentConfig && agentConfig.name) {
|
|
468
|
+
agentConfig._fromDir = true
|
|
469
|
+
this.config.agents.push(agentConfig)
|
|
470
|
+
console.log('[SubAgentManager] Loaded agent:', agentConfig.name)
|
|
471
|
+
} else {
|
|
472
|
+
console.warn('[SubAgentManager] Agent config has no name:', baseName, agentConfig)
|
|
473
|
+
}
|
|
474
|
+
} catch (err) {
|
|
475
|
+
console.warn(`[SubAgentManager] Failed to load agent config from ${filePath}:`, err.message, err.stack)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* 解析 markdown 文件中的配置
|
|
483
|
+
* 支持格式:
|
|
484
|
+
* ```json
|
|
485
|
+
* { "name": "xxx", "role": "xxx" }
|
|
486
|
+
* ```
|
|
487
|
+
* 或直接用 frontmatter 格式
|
|
488
|
+
*/
|
|
489
|
+
_parseMarkdownConfig(filePath, defaultName) {
|
|
490
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
491
|
+
console.log('[SubAgentManager] _parseMarkdownConfig:', filePath)
|
|
492
|
+
|
|
493
|
+
// 尝试从 code block 中提取 JSON
|
|
494
|
+
const jsonMatch = content.match(/```(?:json)?\s*\n([\s\S]*?)\n\s*```/)
|
|
495
|
+
if (jsonMatch) {
|
|
496
|
+
try {
|
|
497
|
+
const config = JSON.parse(jsonMatch[1].trim())
|
|
498
|
+
console.log('[SubAgentManager] Found JSON in code block')
|
|
499
|
+
return config
|
|
500
|
+
} catch (err) {
|
|
501
|
+
// JSON 解析失败,继续
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// 尝试从 frontmatter 格式中提取
|
|
506
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
507
|
+
if (frontmatterMatch) {
|
|
508
|
+
console.log('[SubAgentManager] Found frontmatter')
|
|
509
|
+
const frontmatterContent = frontmatterMatch[1]
|
|
510
|
+
const parsed = this._parseYamlLike(frontmatterContent)
|
|
511
|
+
if (parsed && parsed.name) {
|
|
512
|
+
console.log('[SubAgentManager] Parsed frontmatter:', parsed)
|
|
513
|
+
return { name: parsed.name || defaultName, ...parsed }
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// 从 markdown 内容中提取配置(key: value 格式)
|
|
518
|
+
const config = { name: defaultName }
|
|
519
|
+
const yamlMatch = content.match(/^(name|role|description|parentTools|tools):\s*(.+)$/m)
|
|
520
|
+
if (yamlMatch) {
|
|
521
|
+
config[yamlMatch[1]] = yamlMatch[2].trim()
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// 如果没有找到配置,使用文件名作为 name
|
|
525
|
+
return config
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* 解析类似 YAML 的配置
|
|
530
|
+
*/
|
|
531
|
+
_parseYamlLike(content) {
|
|
532
|
+
const result = {}
|
|
533
|
+
const lines = content.split('\n')
|
|
534
|
+
|
|
535
|
+
for (const line of lines) {
|
|
536
|
+
const match = line.match(/^(\w+):\s*(.+)$/)
|
|
537
|
+
if (match) {
|
|
538
|
+
const key = match[1]
|
|
539
|
+
let value = match[2].trim()
|
|
540
|
+
|
|
541
|
+
// 处理数组格式 [a, b, c] 或 [a b c]
|
|
542
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
543
|
+
value = value.slice(1, -1).split(/[,\s]+/).filter(Boolean)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
result[key] = value
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return result
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* 启动文件监控
|
|
555
|
+
*/
|
|
556
|
+
_startFileWatcher() {
|
|
557
|
+
const agentsDir = this.config.agentsDir
|
|
558
|
+
if (!agentsDir || !fs.existsSync(agentsDir)) {
|
|
559
|
+
return
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// 记录初始文件状态
|
|
563
|
+
this._updateFileStates()
|
|
564
|
+
|
|
565
|
+
// 使用 fs.watch 监控目录变化
|
|
566
|
+
this._fileWatcher = fs.watch(agentsDir, { recursive: false }, (eventType, filename) => {
|
|
567
|
+
if (filename && (filename.endsWith('.js') || filename.endsWith('.json') || filename.endsWith('.md'))) {
|
|
568
|
+
console.log(`[SubAgentManager] Detected change in agents dir: ${eventType} - ${filename}`)
|
|
569
|
+
|
|
570
|
+
// 防抖:延迟处理,等文件操作完成
|
|
571
|
+
setTimeout(() => {
|
|
572
|
+
this._checkAndReload()
|
|
573
|
+
}, 500)
|
|
574
|
+
}
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
console.log(`[SubAgentManager] Watching agents dir for changes: ${agentsDir}`)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* 更新文件状态
|
|
582
|
+
*/
|
|
583
|
+
_updateFileStates() {
|
|
584
|
+
const agentsDir = this.config.agentsDir
|
|
585
|
+
if (!agentsDir || !fs.existsSync(agentsDir)) {
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
this._lastFileStates.clear()
|
|
590
|
+
const entries = fs.readdirSync(agentsDir, { withFileTypes: true })
|
|
591
|
+
|
|
592
|
+
for (const entry of entries) {
|
|
593
|
+
if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.json') || entry.name.endsWith('.md'))) {
|
|
594
|
+
const filePath = path.join(agentsDir, entry.name)
|
|
595
|
+
try {
|
|
596
|
+
const stat = fs.statSync(filePath)
|
|
597
|
+
this._lastFileStates.set(entry.name, {
|
|
598
|
+
mtime: stat.mtime.getTime(),
|
|
599
|
+
size: stat.size
|
|
600
|
+
})
|
|
601
|
+
} catch (err) {
|
|
602
|
+
// 忽略
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* 检查并重载(如果有变化)
|
|
610
|
+
*/
|
|
611
|
+
_checkAndReload() {
|
|
612
|
+
const agentsDir = this.config.agentsDir
|
|
613
|
+
if (!agentsDir || !fs.existsSync(agentsDir)) {
|
|
614
|
+
return
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const currentStates = new Map()
|
|
618
|
+
const entries = fs.readdirSync(agentsDir, { withFileTypes: true })
|
|
619
|
+
let hasChanges = false
|
|
620
|
+
|
|
621
|
+
// 检查当前文件
|
|
622
|
+
for (const entry of entries) {
|
|
623
|
+
if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.json') || entry.name.endsWith('.md'))) {
|
|
624
|
+
const filePath = path.join(agentsDir, entry.name)
|
|
625
|
+
try {
|
|
626
|
+
const stat = fs.statSync(filePath)
|
|
627
|
+
currentStates.set(entry.name, {
|
|
628
|
+
mtime: stat.mtime.getTime(),
|
|
629
|
+
size: stat.size
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
const lastState = this._lastFileStates.get(entry.name)
|
|
633
|
+
if (!lastState || lastState.mtime !== stat.mtime.getTime() || lastState.size !== stat.size) {
|
|
634
|
+
hasChanges = true
|
|
635
|
+
}
|
|
636
|
+
} catch (err) {
|
|
637
|
+
// 忽略
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// 检查删除的文件
|
|
643
|
+
for (const [name] of this._lastFileStates) {
|
|
644
|
+
if (!currentStates.has(name)) {
|
|
645
|
+
hasChanges = true
|
|
646
|
+
break
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (hasChanges) {
|
|
651
|
+
console.log('[SubAgentManager] File changes detected, reloading...')
|
|
652
|
+
this._updateFileStates()
|
|
653
|
+
this.reload(this._framework)
|
|
654
|
+
}
|
|
362
655
|
}
|
|
363
656
|
|
|
364
657
|
/**
|
|
@@ -370,17 +663,32 @@ class SubAgentManagerPlugin extends Plugin {
|
|
|
370
663
|
}
|
|
371
664
|
|
|
372
665
|
reload(framework) {
|
|
666
|
+
// 停止文件监控
|
|
667
|
+
if (this._fileWatcher) {
|
|
668
|
+
this._fileWatcher.close()
|
|
669
|
+
this._fileWatcher = null
|
|
670
|
+
}
|
|
671
|
+
|
|
373
672
|
// 销毁旧的子Agent
|
|
374
673
|
for (const plugin of this._subAgents.values()) {
|
|
375
674
|
plugin.uninstall(framework)
|
|
376
675
|
}
|
|
377
676
|
this._subAgents.clear()
|
|
378
677
|
|
|
678
|
+
// 清空从目录加载的配置,重新加载
|
|
679
|
+
this.config.agents = this.config.agents.filter(a => !a._fromDir)
|
|
680
|
+
|
|
379
681
|
// 重新创建
|
|
380
682
|
this.start(framework)
|
|
381
683
|
}
|
|
382
684
|
|
|
383
685
|
uninstall(framework) {
|
|
686
|
+
// 停止文件监控
|
|
687
|
+
if (this._fileWatcher) {
|
|
688
|
+
this._fileWatcher.close()
|
|
689
|
+
this._fileWatcher = null
|
|
690
|
+
}
|
|
691
|
+
|
|
384
692
|
for (const plugin of this._subAgents.values()) {
|
|
385
693
|
plugin.uninstall(framework)
|
|
386
694
|
}
|
|
@@ -27,6 +27,8 @@ module.exports = function(Plugin) {
|
|
|
27
27
|
this.version = '2.0.0'
|
|
28
28
|
this.description = 'Telegram 对话插件,绑定主Agent进行持续对话'
|
|
29
29
|
this.priority = 80
|
|
30
|
+
// 默认不启用,需要在 plugins.json 中设置 enabled: true
|
|
31
|
+
this.enabled = false
|
|
30
32
|
|
|
31
33
|
this.config = {
|
|
32
34
|
botToken: config.botToken || process.env.TELEGRAM_BOT_TOKEN,
|
package/plugins/tools-plugin.js
CHANGED
|
@@ -61,18 +61,20 @@ class ToolsPlugin extends Plugin {
|
|
|
61
61
|
}
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
-
//
|
|
64
|
+
// 列出所有插件工具(包括未加载的)
|
|
65
65
|
framework.registerTool({
|
|
66
66
|
name: 'list_plugins',
|
|
67
|
-
description: '
|
|
67
|
+
description: '列出所有插件(包括未加载的)',
|
|
68
68
|
inputSchema: z.object({}),
|
|
69
69
|
execute: async () => {
|
|
70
|
-
const plugins = framework.pluginManager.
|
|
70
|
+
const plugins = framework.pluginManager.getAllKnown()
|
|
71
71
|
return {
|
|
72
72
|
success: true,
|
|
73
73
|
plugins: plugins.map(p => ({
|
|
74
74
|
name: p.name,
|
|
75
|
-
|
|
75
|
+
status: p.status,
|
|
76
|
+
enabled: p.enabled,
|
|
77
|
+
version: p.version
|
|
76
78
|
}))
|
|
77
79
|
}
|
|
78
80
|
}
|
package/plugins/weixin-plugin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WeChat 插件
|
|
3
|
-
* 使用 @
|
|
3
|
+
* 使用 @chnak/weixin-bot 实现微信对话
|
|
4
4
|
*
|
|
5
5
|
* 配置:
|
|
6
6
|
* - forceLogin: 是否强制重新扫码登录
|
|
@@ -18,6 +18,8 @@ module.exports = function(Plugin) {
|
|
|
18
18
|
this.version = '1.0.0'
|
|
19
19
|
this.description = '微信对话插件,使用微信网页账号进行对话'
|
|
20
20
|
this.priority = 80
|
|
21
|
+
// 默认不启用,需要在 plugins.json 中设置 enabled: true
|
|
22
|
+
this.enabled = false
|
|
21
23
|
|
|
22
24
|
this.config = {
|
|
23
25
|
forceLogin: config.forceLogin || process.env.WEIXIN_FORCE_LOGIN === 'true',
|
|
@@ -70,11 +72,11 @@ module.exports = function(Plugin) {
|
|
|
70
72
|
|
|
71
73
|
let WeixinBot
|
|
72
74
|
try {
|
|
73
|
-
const module = await import('@
|
|
75
|
+
const module = await import('@chnak/weixin-bot')
|
|
74
76
|
WeixinBot = module.WeixinBot
|
|
75
77
|
} catch (err) {
|
|
76
|
-
console.warn('[WeChat] Failed to load @
|
|
77
|
-
console.warn('[WeChat] Make sure @
|
|
78
|
+
console.warn('[WeChat] Failed to load @chnak/weixin-bot:', err.message)
|
|
79
|
+
console.warn('[WeChat] Make sure @chnak/weixin-bot is installed: npm install @chnak/weixin-bot qrcode-terminal')
|
|
78
80
|
return
|
|
79
81
|
}
|
|
80
82
|
|
package/src/core/plugin-base.js
CHANGED
|
@@ -16,6 +16,7 @@ class PluginManager {
|
|
|
16
16
|
this._plugins = new Map()
|
|
17
17
|
this._loading = false
|
|
18
18
|
this._stateFile = path.join(process.cwd(), '.agent', 'data', 'plugins-state.json')
|
|
19
|
+
this._knownPlugins = new Set() // 已知插件列表(包括未加载的)
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -89,10 +90,13 @@ class PluginManager {
|
|
|
89
90
|
pluginInstance.config = { ...pluginInstance.config, ...savedConfig }
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
// 使用保存的状态,否则使用插件实例的 enabled,默认 true
|
|
94
|
+
const finalEnabled = savedEnabled !== undefined ? savedEnabled : (pluginInstance.enabled !== undefined ? pluginInstance.enabled : true)
|
|
95
|
+
|
|
92
96
|
this._plugins.set(pluginInstance.name, {
|
|
93
97
|
instance: pluginInstance,
|
|
94
98
|
status: 'registered',
|
|
95
|
-
enabled:
|
|
99
|
+
enabled: finalEnabled
|
|
96
100
|
})
|
|
97
101
|
|
|
98
102
|
this.framework.emit('plugin:registered', pluginInstance)
|
|
@@ -116,7 +120,7 @@ class PluginManager {
|
|
|
116
120
|
pluginInstance = this._createFromObject(plugin)
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
//
|
|
123
|
+
// 如果已注册,使用已注册的实例和状态
|
|
120
124
|
const existing = this._plugins.get(pluginInstance.name)
|
|
121
125
|
if (existing) {
|
|
122
126
|
pluginInstance = existing.instance
|
|
@@ -424,12 +428,59 @@ class PluginManager {
|
|
|
424
428
|
.map(e => ({ name: e.instance.name, instance: e.instance }))
|
|
425
429
|
}
|
|
426
430
|
|
|
431
|
+
/**
|
|
432
|
+
* 注册一个已知插件(但不加载)
|
|
433
|
+
* 用于显示所有可用插件列表
|
|
434
|
+
* @param {string} name - 插件名称
|
|
435
|
+
* @param {Object} info - 插件信息
|
|
436
|
+
*/
|
|
437
|
+
registerKnownPlugin(name, info = {}) {
|
|
438
|
+
this._knownPlugins.add(name)
|
|
439
|
+
// 如果插件还没注册过,记录它的信息
|
|
440
|
+
if (!this._plugins.has(name)) {
|
|
441
|
+
this._plugins.set(name, {
|
|
442
|
+
instance: { name, ...info },
|
|
443
|
+
status: 'known',
|
|
444
|
+
enabled: info.enabled !== undefined ? info.enabled : false
|
|
445
|
+
})
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* 获取所有已知插件(包括未加载的)
|
|
451
|
+
* @returns {Array} 插件列表,包含 name, status, enabled
|
|
452
|
+
*/
|
|
453
|
+
getAllKnown() {
|
|
454
|
+
const result = []
|
|
455
|
+
for (const name of this._knownPlugins) {
|
|
456
|
+
const entry = this._plugins.get(name)
|
|
457
|
+
result.push({
|
|
458
|
+
name,
|
|
459
|
+
status: entry?.status || 'unknown',
|
|
460
|
+
enabled: entry?.enabled || false,
|
|
461
|
+
version: entry?.instance?.version
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
// 也加入已加载但不在 knownPlugins 中的
|
|
465
|
+
for (const [name, entry] of this._plugins) {
|
|
466
|
+
if (!result.find(p => p.name === name)) {
|
|
467
|
+
result.push({
|
|
468
|
+
name,
|
|
469
|
+
status: entry.status,
|
|
470
|
+
enabled: entry.enabled,
|
|
471
|
+
version: entry.instance?.version
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return result
|
|
476
|
+
}
|
|
477
|
+
|
|
427
478
|
/**
|
|
428
479
|
* 检查插件是否存在
|
|
429
480
|
* @param {string} name - 插件名称
|
|
430
481
|
*/
|
|
431
482
|
has(name) {
|
|
432
|
-
return this._plugins.has(name)
|
|
483
|
+
return this._plugins.has(name) || this._knownPlugins.has(name)
|
|
433
484
|
}
|
|
434
485
|
|
|
435
486
|
/**
|
|
@@ -464,8 +515,12 @@ class PluginManager {
|
|
|
464
515
|
}
|
|
465
516
|
|
|
466
517
|
entry.enabled = true
|
|
518
|
+
// 同步更新插件实例的 enabled 属性,避免 load() 时被跳过
|
|
519
|
+
if (entry.instance) {
|
|
520
|
+
entry.instance.enabled = true
|
|
521
|
+
}
|
|
467
522
|
|
|
468
|
-
//
|
|
523
|
+
// 如果插件已加载,尝试重新启动
|
|
469
524
|
if (entry.status === 'loaded') {
|
|
470
525
|
try {
|
|
471
526
|
// 如果之前已经启动过,先调用 stop 停止旧实例
|
|
@@ -481,6 +536,13 @@ class PluginManager {
|
|
|
481
536
|
} catch (err) {
|
|
482
537
|
console.error(`[PluginManager] Enable/reload failed for '${name}':`, err.message)
|
|
483
538
|
}
|
|
539
|
+
} else if (entry.status === 'registered' || entry.status === 'known') {
|
|
540
|
+
// 插件只注册过但未加载,现在加载它
|
|
541
|
+
try {
|
|
542
|
+
await this.load(entry.instance)
|
|
543
|
+
} catch (err) {
|
|
544
|
+
console.error(`[PluginManager] Enable/load failed for '${name}':`, err.message)
|
|
545
|
+
}
|
|
484
546
|
}
|
|
485
547
|
|
|
486
548
|
this.framework.emit('plugin:enabled', entry.instance)
|
|
@@ -504,6 +566,10 @@ class PluginManager {
|
|
|
504
566
|
}
|
|
505
567
|
|
|
506
568
|
entry.enabled = false
|
|
569
|
+
// 同步更新插件实例的 enabled 属性
|
|
570
|
+
if (entry.instance) {
|
|
571
|
+
entry.instance.enabled = false
|
|
572
|
+
}
|
|
507
573
|
|
|
508
574
|
// 如果插件正在运行,停止它
|
|
509
575
|
if (entry.instance._started) {
|