foliko 1.0.8 → 1.0.10
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 +5 -1
- package/README.md +29 -2
- package/cli/src/ui/chat-ui.js +56 -187
- package/examples/test-chat.js +1 -1
- package/package.json +1 -1
- package/plugins/default-plugins.js +139 -59
- package/plugins/install-plugin.js +115 -12
- package/skills/vb-agent-dev/AGENTS.md +81 -10
- package/skills/vb-agent-dev/SKILL.md +149 -25
- package/src/core/plugin-manager.js +104 -21
|
@@ -30,7 +30,11 @@
|
|
|
30
30
|
"Bash(cd D:/Code/vb-agent && timeout 8 node test-tg.js 2>&1 || true)",
|
|
31
31
|
"Bash(cd D:/Code/vb-agent && timeout 10 node test-tg.js 2>&1 || true)",
|
|
32
32
|
"Bash(find /d/Code/vb-agent -name \"*email*\" -type f 2>/dev/null | grep -v node_modules | grep -v .git)",
|
|
33
|
-
"Bash(find /d/Code/vb-agent -maxdepth 2 -name \"*.md\" -type f 2>/dev/null | grep -v node_modules)"
|
|
33
|
+
"Bash(find /d/Code/vb-agent -maxdepth 2 -name \"*.md\" -type f 2>/dev/null | grep -v node_modules)",
|
|
34
|
+
"Bash(node -c plugins/default-plugins.js && node -c src/core/plugin-manager.js && echo \"Syntax OK\")",
|
|
35
|
+
"Bash(node -c plugins/install-plugin.js && echo \"Syntax OK\")",
|
|
36
|
+
"Bash(node -c cli/src/ui/chat-ui.js && echo \"Syntax OK\")",
|
|
37
|
+
"Bash(node -c plugins/default-plugins.js && echo \"Syntax OK\")"
|
|
34
38
|
]
|
|
35
39
|
}
|
|
36
40
|
}
|
package/README.md
CHANGED
|
@@ -87,8 +87,27 @@ ai_provider: minimax
|
|
|
87
87
|
|
|
88
88
|
### 用户插件(.agent/plugins/)
|
|
89
89
|
|
|
90
|
+
插件支持两种结构:**文件夹结构**(推荐)和**单文件结构**。
|
|
91
|
+
|
|
92
|
+
#### 文件夹结构(推荐)
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
.agent/plugins/my-plugin/
|
|
96
|
+
├── package.json # 可选,main 字段指定入口
|
|
97
|
+
├── index.js # 默认入口
|
|
98
|
+
└── node_modules/ # 可选,插件私有依赖
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
// package.json 示例
|
|
103
|
+
{
|
|
104
|
+
"name": "my-plugin",
|
|
105
|
+
"main": "index.js"
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
90
109
|
```javascript
|
|
91
|
-
// .agent/plugins/my-plugin.js
|
|
110
|
+
// .agent/plugins/my-plugin/index.js
|
|
92
111
|
module.exports = function(Plugin) {
|
|
93
112
|
return class MyPlugin extends Plugin {
|
|
94
113
|
constructor(config = {}) {
|
|
@@ -117,9 +136,17 @@ module.exports = function(Plugin) {
|
|
|
117
136
|
}
|
|
118
137
|
```
|
|
119
138
|
|
|
139
|
+
#### 单文件结构(兼容)
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
.agent/plugins/my-plugin.js
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
如果同时存在文件夹和同名 `.js` 文件,**文件夹优先**。
|
|
146
|
+
|
|
120
147
|
**注意**:如果插件需要第三方库(如 `zod`),需要先安装:
|
|
121
148
|
|
|
122
|
-
1.
|
|
149
|
+
1. 创建插件文件/文件夹
|
|
123
150
|
2. 调用 `install` 工具安装依赖
|
|
124
151
|
3. 热重载插件
|
|
125
152
|
|
package/cli/src/ui/chat-ui.js
CHANGED
|
@@ -1,71 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 聊天界面组件
|
|
3
|
-
*
|
|
3
|
+
* 使用 readline question() 实现多行输入
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const readline = require('readline')
|
|
7
|
-
const { EventEmitter } = require('events')
|
|
8
7
|
const { CLEAR_LINE, CYAN, DIM, GREEN, RED, YELLOW, colored } = require('../utils/ansi')
|
|
9
|
-
const {
|
|
8
|
+
const { renderLine } = require('../utils/markdown')
|
|
10
9
|
|
|
11
|
-
class ChatUI
|
|
10
|
+
class ChatUI {
|
|
12
11
|
constructor(agent) {
|
|
13
|
-
super()
|
|
14
12
|
this.agent = agent
|
|
15
13
|
this.rl = null
|
|
16
|
-
this.
|
|
17
|
-
this.lines = []
|
|
18
|
-
this.pasteId = 0
|
|
19
|
-
this.isFirstLine = true
|
|
20
|
-
|
|
21
|
-
// 粘贴检测
|
|
22
|
-
this.lastKeyTime = 0
|
|
23
|
-
this.pasteBuffer = ''
|
|
24
|
-
this.isPasting = false
|
|
14
|
+
this.lines = [] // 多行输入的累积
|
|
25
15
|
}
|
|
26
16
|
|
|
27
17
|
/**
|
|
28
18
|
* 启动聊天界面
|
|
29
19
|
*/
|
|
30
20
|
start() {
|
|
31
|
-
this.setupReadline()
|
|
32
|
-
this.printWelcome()
|
|
33
|
-
this.prompt()
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* 设置 readline
|
|
38
|
-
*/
|
|
39
|
-
setupReadline() {
|
|
40
21
|
this.rl = readline.createInterface({
|
|
41
22
|
input: process.stdin,
|
|
42
23
|
output: process.stdout,
|
|
43
24
|
crlfDelay: Infinity
|
|
44
25
|
})
|
|
45
26
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (process.stdin.isTTY) {
|
|
50
|
-
process.stdin.setRawMode(true)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// 监听按键
|
|
54
|
-
process.stdin.on('keypress', this.handleKey.bind(this))
|
|
55
|
-
|
|
56
|
-
// 退出时清理
|
|
57
|
-
process.on('exit', () => {
|
|
58
|
-
if (process.stdin.isTTY) {
|
|
59
|
-
process.stdin.setRawMode(false)
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
process.on('SIGINT', () => {
|
|
64
|
-
if (process.stdin.isTTY) {
|
|
65
|
-
process.stdin.setRawMode(false)
|
|
66
|
-
}
|
|
67
|
-
process.exit(0)
|
|
68
|
-
})
|
|
27
|
+
this.printWelcome()
|
|
28
|
+
this.promptUser()
|
|
69
29
|
}
|
|
70
30
|
|
|
71
31
|
/**
|
|
@@ -73,139 +33,77 @@ class ChatUI extends EventEmitter {
|
|
|
73
33
|
*/
|
|
74
34
|
printWelcome() {
|
|
75
35
|
console.log(`${colored('Foliko', CYAN)} - 持续对话聊天`)
|
|
76
|
-
console.log(`${colored('Ctrl+C', DIM)} 退出 | ${colored('
|
|
36
|
+
console.log(`${colored('Ctrl+C', DIM)} 退出 | ${colored('连续两次回车', DIM)} 发送多行 | ${colored('!!', DIM)} 立即发送\n`)
|
|
77
37
|
}
|
|
78
38
|
|
|
79
39
|
/**
|
|
80
|
-
*
|
|
40
|
+
* 获取多行输入
|
|
81
41
|
*/
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
process.stdin.setRawMode(false)
|
|
96
|
-
}
|
|
97
|
-
process.exit(0)
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Enter 发送
|
|
102
|
-
if (key.name === 'return') {
|
|
103
|
-
this.handleSubmit()
|
|
104
|
-
return
|
|
105
|
-
}
|
|
42
|
+
getMultilineInput() {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const lines = []
|
|
45
|
+
|
|
46
|
+
const question = (isFirst) => {
|
|
47
|
+
const prompt = isFirst ? colored('> ', GREEN) : colored('- ', DIM)
|
|
48
|
+
this.rl.question(prompt, (input) => {
|
|
49
|
+
// 输入 !! 立即结束
|
|
50
|
+
if (input.trim() === '!!') {
|
|
51
|
+
const result = lines.join('\n').trim()
|
|
52
|
+
resolve(result)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
106
55
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return
|
|
114
|
-
}
|
|
56
|
+
// 空行:结束输入并发送(第一次空行就发送)
|
|
57
|
+
if (input.trim() === '') {
|
|
58
|
+
const result = lines.join('\n').trim()
|
|
59
|
+
resolve(result)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
115
62
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
this.prompt()
|
|
121
|
-
} else if (this.lines.length > 0) {
|
|
122
|
-
this.currentLine = this.lines.pop()
|
|
123
|
-
this.isFirstLine = this.lines.length === 0
|
|
124
|
-
this.prompt()
|
|
63
|
+
// 非空行:添加到 lines,继续输入
|
|
64
|
+
lines.push(input)
|
|
65
|
+
question(false)
|
|
66
|
+
})
|
|
125
67
|
}
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
68
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// 直接添加到当前行
|
|
132
|
-
this.currentLine += str
|
|
133
|
-
this.prompt()
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* 处理粘贴
|
|
139
|
-
*/
|
|
140
|
-
handlePaste() {
|
|
141
|
-
if (this.pasteBuffer.includes('\n')) {
|
|
142
|
-
const allLines = this.pasteBuffer.split('\n')
|
|
143
|
-
this.lines.push(...allLines.slice(0, -1))
|
|
144
|
-
this.currentLine = allLines[allLines.length - 1]
|
|
145
|
-
this.isFirstLine = false
|
|
146
|
-
this.pasteId++
|
|
147
|
-
|
|
148
|
-
console.log(`\n${CLEAR_LINE}${colored(`[Pasted text #${this.pasteId} +${this.lines.length + 1} lines]`, CYAN)}`)
|
|
149
|
-
} else {
|
|
150
|
-
this.currentLine = this.pasteBuffer
|
|
151
|
-
this.lines = []
|
|
152
|
-
}
|
|
153
|
-
this.pasteBuffer = ''
|
|
69
|
+
question(true)
|
|
70
|
+
})
|
|
154
71
|
}
|
|
155
72
|
|
|
156
73
|
/**
|
|
157
|
-
*
|
|
74
|
+
* 提示用户输入
|
|
158
75
|
*/
|
|
159
|
-
async
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
: this.currentLine
|
|
163
|
-
|
|
164
|
-
const trimmed = content.trim()
|
|
165
|
-
|
|
166
|
-
// 显示空行
|
|
167
|
-
console.log()
|
|
76
|
+
async promptUser() {
|
|
77
|
+
try {
|
|
78
|
+
const input = await this.getMultilineInput()
|
|
168
79
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (process.stdin.isTTY) {
|
|
173
|
-
process.stdin.setRawMode(false)
|
|
80
|
+
if (!input) {
|
|
81
|
+
await this.promptUser()
|
|
82
|
+
return
|
|
174
83
|
}
|
|
175
|
-
process.exit(0)
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (!trimmed) {
|
|
180
|
-
this.resetInput()
|
|
181
|
-
this.prompt()
|
|
182
|
-
return
|
|
183
|
-
}
|
|
184
84
|
|
|
185
|
-
|
|
186
|
-
|
|
85
|
+
// 退出命令
|
|
86
|
+
if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
|
|
87
|
+
console.log('再见!')
|
|
88
|
+
this.rl.close()
|
|
89
|
+
return
|
|
90
|
+
}
|
|
187
91
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
92
|
+
// 发送消息
|
|
93
|
+
await this.sendMessage(input)
|
|
191
94
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
this.isFirstLine = true
|
|
199
|
-
this.pasteId = 0
|
|
200
|
-
this.isPasting = false
|
|
201
|
-
this.pasteBuffer = ''
|
|
95
|
+
// 继续等待下一条消息
|
|
96
|
+
await this.promptUser()
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(`\n${colored('[错误]', RED)} ${err.message}`)
|
|
99
|
+
await this.promptUser()
|
|
100
|
+
}
|
|
202
101
|
}
|
|
203
102
|
|
|
204
103
|
/**
|
|
205
104
|
* 发送消息并显示响应
|
|
206
105
|
*/
|
|
207
106
|
async sendMessage(message) {
|
|
208
|
-
console.log(colored('Agent:', GREEN))
|
|
209
107
|
console.log()
|
|
210
108
|
|
|
211
109
|
// 用于打断的标志
|
|
@@ -224,43 +122,18 @@ class ChatUI extends EventEmitter {
|
|
|
224
122
|
|
|
225
123
|
try {
|
|
226
124
|
let lineBuffer = ''
|
|
227
|
-
// 渲染状态追踪
|
|
228
125
|
const renderState = { inThink: false, inCodeBlock: false }
|
|
229
126
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (!str || str.length === 0) return false
|
|
233
|
-
let i = 0
|
|
234
|
-
while (i < str.length) {
|
|
235
|
-
const code = str.charCodeAt(i)
|
|
236
|
-
if (code >= 0xD800 && code <= 0xDBFF) {
|
|
237
|
-
const nextCode = str.charCodeAt(i + 1)
|
|
238
|
-
if (nextCode >= 0xDC00 && nextCode <= 0xDFFF) {
|
|
239
|
-
i += 2
|
|
240
|
-
continue
|
|
241
|
-
}
|
|
242
|
-
return true // 孤立的高代理
|
|
243
|
-
}
|
|
244
|
-
i++
|
|
245
|
-
}
|
|
246
|
-
return false
|
|
247
|
-
}
|
|
127
|
+
console.log(colored('Agent:', GREEN))
|
|
128
|
+
console.log()
|
|
248
129
|
|
|
249
130
|
for await (const chunk of this.agent.chatStream(message)) {
|
|
250
|
-
// 检查是否被打断
|
|
251
131
|
if (interrupted) break
|
|
252
132
|
|
|
253
133
|
if (chunk.type === 'text') {
|
|
254
134
|
lineBuffer += chunk.text
|
|
255
135
|
|
|
256
|
-
// 如果有孤立代理对,等待下一个chunk补充
|
|
257
|
-
if (hasOrphanedSurrogate(lineBuffer)) {
|
|
258
|
-
continue
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// 当有一行完整内容时,渲染并输出
|
|
262
136
|
while (lineBuffer.includes('\n')) {
|
|
263
|
-
// 检查是否被打断
|
|
264
137
|
if (interrupted) break
|
|
265
138
|
|
|
266
139
|
const nlIndex = lineBuffer.indexOf('\n')
|
|
@@ -277,7 +150,6 @@ class ChatUI extends EventEmitter {
|
|
|
277
150
|
}
|
|
278
151
|
}
|
|
279
152
|
|
|
280
|
-
// 输出剩余内容
|
|
281
153
|
if (lineBuffer.trim() && !interrupted) {
|
|
282
154
|
console.log(renderLine(lineBuffer, renderState))
|
|
283
155
|
}
|
|
@@ -286,11 +158,8 @@ class ChatUI extends EventEmitter {
|
|
|
286
158
|
console.error(`\n${colored('[错误]', RED)} ${err.message}\n`)
|
|
287
159
|
}
|
|
288
160
|
} finally {
|
|
289
|
-
// 移除监听器
|
|
290
161
|
process.removeListener('SIGINT', interruptHandler)
|
|
291
162
|
}
|
|
292
|
-
|
|
293
|
-
this.prompt()
|
|
294
163
|
}
|
|
295
164
|
}
|
|
296
165
|
|
package/examples/test-chat.js
CHANGED
|
@@ -52,7 +52,7 @@ async function main() {
|
|
|
52
52
|
const lines = []
|
|
53
53
|
|
|
54
54
|
const question = () => {
|
|
55
|
-
rl.question(lines.length === 0 ? '
|
|
55
|
+
rl.question(lines.length === 0 ? '> ' : '- ', (input) => {
|
|
56
56
|
// 输入 !! 立即结束
|
|
57
57
|
if (input.trim() === '!!') {
|
|
58
58
|
const result = lines.join('\n').trim()
|