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.
@@ -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
 
@@ -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 { render, renderLine } = require('../utils/markdown')
8
+ const { renderLine } = require('../utils/markdown')
10
9
 
11
- class ChatUI extends EventEmitter {
10
+ class ChatUI {
12
11
  constructor(agent) {
13
- super()
14
12
  this.agent = agent
15
13
  this.rl = null
16
- this.currentLine = ''
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
- // 使用 keypress 事件支持特殊按键
47
- readline.emitKeypressEvents(process.stdin)
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('Ctrl+Enter', DIM)} 换行 | ${colored('Enter', DIM)} 发送\n`)
36
+ console.log(`${colored('Ctrl+C', DIM)} 退出 | ${colored('连续两次回车', DIM)} 发送多行 | ${colored('!!', DIM)} 立即发送\n`)
77
37
  }
78
38
 
79
39
  /**
80
- * 显示提示符
40
+ * 获取多行输入
81
41
  */
82
- prompt() {
83
- const prefix = this.isFirstLine ? colored('You: ', GREEN) : colored('> ', DIM)
84
- process.stdout.write(`\r${CLEAR_LINE}${prefix}${this.currentLine}`)
85
- }
86
-
87
- /**
88
- * 处理按键
89
- */
90
- handleKey(str, key) {
91
- // Ctrl+C 退出
92
- if (key.ctrl && key.name === 'c') {
93
- console.log('\n再见!')
94
- if (process.stdin.isTTY) {
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
- // Ctrl+Enter 换行
108
- if (key.ctrl && key.name === 'return') {
109
- this.lines.push(this.currentLine)
110
- this.currentLine = ''
111
- this.isFirstLine = false
112
- this.prompt()
113
- return
114
- }
56
+ // 空行:结束输入并发送(第一次空行就发送)
57
+ if (input.trim() === '') {
58
+ const result = lines.join('\n').trim()
59
+ resolve(result)
60
+ return
61
+ }
115
62
 
116
- // Backspace 退格
117
- if (key.name === 'backspace') {
118
- if (this.currentLine.length > 0) {
119
- this.currentLine = this.currentLine.slice(0, -1)
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
- if (str && !key.ctrl && !key.meta) {
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 handleSubmit() {
160
- const content = this.lines.length > 0
161
- ? [...this.lines, this.currentLine].join('\n')
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
- if (trimmed.toLowerCase() === 'exit' || trimmed.toLowerCase() === 'quit') {
171
- console.log('再见!')
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
- this.resetInput()
85
+ // 退出命令
86
+ if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
87
+ console.log('再见!')
88
+ this.rl.close()
89
+ return
90
+ }
187
91
 
188
- // 发送消息
189
- await this.sendMessage(trimmed)
190
- }
92
+ // 发送消息
93
+ await this.sendMessage(input)
191
94
 
192
- /**
193
- * 重置输入状态
194
- */
195
- resetInput() {
196
- this.currentLine = ''
197
- this.lines = []
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
- const hasOrphanedSurrogate = (str) => {
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
 
@@ -52,7 +52,7 @@ async function main() {
52
52
  const lines = []
53
53
 
54
54
  const question = () => {
55
- rl.question(lines.length === 0 ? 'You: ' : '> ', (input) => {
55
+ rl.question(lines.length === 0 ? '> ' : '- ', (input) => {
56
56
  // 输入 !! 立即结束
57
57
  if (input.trim() === '!!') {
58
58
  const result = lines.join('\n').trim()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foliko",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "简约的插件化 Agent 框架",
5
5
  "main": "src/index.js",
6
6
  "bin": {