foliko 1.0.9 → 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/cli/src/ui/chat-ui.js +53 -223
- package/package.json +1 -1
package/cli/src/ui/chat-ui.js
CHANGED
|
@@ -1,75 +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
|
|
25
|
-
|
|
26
|
-
// 多行输入:记录上次 Enter 时间,检测连续两次空回车
|
|
27
|
-
this.lastEnterTime = 0
|
|
28
|
-
this.LINE_ENDING_INTERVAL = 800 // 毫秒内连续两次空回车结束输入
|
|
14
|
+
this.lines = [] // 多行输入的累积
|
|
29
15
|
}
|
|
30
16
|
|
|
31
17
|
/**
|
|
32
18
|
* 启动聊天界面
|
|
33
19
|
*/
|
|
34
20
|
start() {
|
|
35
|
-
this.setupReadline()
|
|
36
|
-
this.printWelcome()
|
|
37
|
-
this.prompt()
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* 设置 readline
|
|
42
|
-
*/
|
|
43
|
-
setupReadline() {
|
|
44
21
|
this.rl = readline.createInterface({
|
|
45
22
|
input: process.stdin,
|
|
46
23
|
output: process.stdout,
|
|
47
24
|
crlfDelay: Infinity
|
|
48
25
|
})
|
|
49
26
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (process.stdin.isTTY) {
|
|
54
|
-
process.stdin.setRawMode(true)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// 监听按键
|
|
58
|
-
process.stdin.on('keypress', this.handleKey.bind(this))
|
|
59
|
-
|
|
60
|
-
// 退出时清理
|
|
61
|
-
process.on('exit', () => {
|
|
62
|
-
if (process.stdin.isTTY) {
|
|
63
|
-
process.stdin.setRawMode(false)
|
|
64
|
-
}
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
process.on('SIGINT', () => {
|
|
68
|
-
if (process.stdin.isTTY) {
|
|
69
|
-
process.stdin.setRawMode(false)
|
|
70
|
-
}
|
|
71
|
-
process.exit(0)
|
|
72
|
-
})
|
|
27
|
+
this.printWelcome()
|
|
28
|
+
this.promptUser()
|
|
73
29
|
}
|
|
74
30
|
|
|
75
31
|
/**
|
|
@@ -77,174 +33,77 @@ class ChatUI extends EventEmitter {
|
|
|
77
33
|
*/
|
|
78
34
|
printWelcome() {
|
|
79
35
|
console.log(`${colored('Foliko', CYAN)} - 持续对话聊天`)
|
|
80
|
-
console.log(`${colored('Ctrl+C', DIM)} 退出 | ${colored('
|
|
36
|
+
console.log(`${colored('Ctrl+C', DIM)} 退出 | ${colored('连续两次回车', DIM)} 发送多行 | ${colored('!!', DIM)} 立即发送\n`)
|
|
81
37
|
}
|
|
82
38
|
|
|
83
39
|
/**
|
|
84
|
-
*
|
|
40
|
+
* 获取多行输入
|
|
85
41
|
*/
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
process.stdin.setRawMode(false)
|
|
100
|
-
}
|
|
101
|
-
process.exit(0)
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Enter 发送
|
|
106
|
-
if (key.name === 'return') {
|
|
107
|
-
this.handleSubmit()
|
|
108
|
-
return
|
|
109
|
-
}
|
|
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
|
+
}
|
|
110
55
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return
|
|
118
|
-
}
|
|
56
|
+
// 空行:结束输入并发送(第一次空行就发送)
|
|
57
|
+
if (input.trim() === '') {
|
|
58
|
+
const result = lines.join('\n').trim()
|
|
59
|
+
resolve(result)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
119
62
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.prompt()
|
|
125
|
-
} else if (this.lines.length > 0) {
|
|
126
|
-
this.currentLine = this.lines.pop()
|
|
127
|
-
this.isFirstLine = this.lines.length === 0
|
|
128
|
-
this.prompt()
|
|
63
|
+
// 非空行:添加到 lines,继续输入
|
|
64
|
+
lines.push(input)
|
|
65
|
+
question(false)
|
|
66
|
+
})
|
|
129
67
|
}
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 普通字符
|
|
134
|
-
if (str && !key.ctrl && !key.meta) {
|
|
135
|
-
// 直接添加到当前行
|
|
136
|
-
this.currentLine += str
|
|
137
|
-
this.prompt()
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* 处理粘贴
|
|
143
|
-
*/
|
|
144
|
-
handlePaste() {
|
|
145
|
-
if (this.pasteBuffer.includes('\n')) {
|
|
146
|
-
const allLines = this.pasteBuffer.split('\n')
|
|
147
|
-
this.lines.push(...allLines.slice(0, -1))
|
|
148
|
-
this.currentLine = allLines[allLines.length - 1]
|
|
149
|
-
this.isFirstLine = false
|
|
150
|
-
this.pasteId++
|
|
151
68
|
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
this.currentLine = this.pasteBuffer
|
|
155
|
-
this.lines = []
|
|
156
|
-
}
|
|
157
|
-
this.pasteBuffer = ''
|
|
69
|
+
question(true)
|
|
70
|
+
})
|
|
158
71
|
}
|
|
159
72
|
|
|
160
73
|
/**
|
|
161
|
-
*
|
|
74
|
+
* 提示用户输入
|
|
162
75
|
*/
|
|
163
|
-
async
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
? [...this.lines, this.currentLine].join('\n')
|
|
167
|
-
: this.currentLine
|
|
168
|
-
|
|
169
|
-
const trimmed = content.trim()
|
|
170
|
-
|
|
171
|
-
// 检测连续两次空回车(多行输入结束)
|
|
172
|
-
if (this.currentLine.trim() === '' && this.lines.length > 0) {
|
|
173
|
-
if (now - this.lastEnterTime < this.LINE_ENDING_INTERVAL) {
|
|
174
|
-
// 第二次空回车,结束输入
|
|
175
|
-
const finalContent = this.lines.join('\n').trim()
|
|
176
|
-
this.resetInput()
|
|
177
|
-
console.log()
|
|
178
|
-
|
|
179
|
-
if (!finalContent) {
|
|
180
|
-
this.prompt()
|
|
181
|
-
return
|
|
182
|
-
}
|
|
76
|
+
async promptUser() {
|
|
77
|
+
try {
|
|
78
|
+
const input = await this.getMultilineInput()
|
|
183
79
|
|
|
184
|
-
|
|
80
|
+
if (!input) {
|
|
81
|
+
await this.promptUser()
|
|
185
82
|
return
|
|
186
83
|
}
|
|
187
|
-
}
|
|
188
|
-
this.lastEnterTime = now
|
|
189
|
-
|
|
190
|
-
// 输入 !! 立即结束多行输入
|
|
191
|
-
if (this.currentLine.trim() === '!!') {
|
|
192
|
-
const finalContent = this.lines.join('\n').trim()
|
|
193
|
-
this.resetInput()
|
|
194
|
-
console.log()
|
|
195
84
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
this.
|
|
85
|
+
// 退出命令
|
|
86
|
+
if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
|
|
87
|
+
console.log('再见!')
|
|
88
|
+
this.rl.close()
|
|
89
|
+
return
|
|
200
90
|
}
|
|
201
|
-
return
|
|
202
|
-
}
|
|
203
91
|
|
|
204
|
-
|
|
205
|
-
|
|
92
|
+
// 发送消息
|
|
93
|
+
await this.sendMessage(input)
|
|
206
94
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
process.exit(0)
|
|
214
|
-
return
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (!trimmed) {
|
|
218
|
-
this.resetInput()
|
|
219
|
-
this.prompt()
|
|
220
|
-
return
|
|
95
|
+
// 继续等待下一条消息
|
|
96
|
+
await this.promptUser()
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(`\n${colored('[错误]', RED)} ${err.message}`)
|
|
99
|
+
await this.promptUser()
|
|
221
100
|
}
|
|
222
|
-
|
|
223
|
-
// 重置输入
|
|
224
|
-
this.resetInput()
|
|
225
|
-
|
|
226
|
-
// 发送消息
|
|
227
|
-
await this.sendMessage(trimmed)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* 重置输入状态
|
|
232
|
-
*/
|
|
233
|
-
resetInput() {
|
|
234
|
-
this.currentLine = ''
|
|
235
|
-
this.lines = []
|
|
236
|
-
this.isFirstLine = true
|
|
237
|
-
this.pasteId = 0
|
|
238
|
-
this.isPasting = false
|
|
239
|
-
this.pasteBuffer = ''
|
|
240
|
-
this.lastEnterTime = 0
|
|
241
101
|
}
|
|
242
102
|
|
|
243
103
|
/**
|
|
244
104
|
* 发送消息并显示响应
|
|
245
105
|
*/
|
|
246
106
|
async sendMessage(message) {
|
|
247
|
-
console.log(colored('Agent:', GREEN))
|
|
248
107
|
console.log()
|
|
249
108
|
|
|
250
109
|
// 用于打断的标志
|
|
@@ -263,43 +122,18 @@ class ChatUI extends EventEmitter {
|
|
|
263
122
|
|
|
264
123
|
try {
|
|
265
124
|
let lineBuffer = ''
|
|
266
|
-
// 渲染状态追踪
|
|
267
125
|
const renderState = { inThink: false, inCodeBlock: false }
|
|
268
126
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (!str || str.length === 0) return false
|
|
272
|
-
let i = 0
|
|
273
|
-
while (i < str.length) {
|
|
274
|
-
const code = str.charCodeAt(i)
|
|
275
|
-
if (code >= 0xD800 && code <= 0xDBFF) {
|
|
276
|
-
const nextCode = str.charCodeAt(i + 1)
|
|
277
|
-
if (nextCode >= 0xDC00 && nextCode <= 0xDFFF) {
|
|
278
|
-
i += 2
|
|
279
|
-
continue
|
|
280
|
-
}
|
|
281
|
-
return true // 孤立的高代理
|
|
282
|
-
}
|
|
283
|
-
i++
|
|
284
|
-
}
|
|
285
|
-
return false
|
|
286
|
-
}
|
|
127
|
+
console.log(colored('Agent:', GREEN))
|
|
128
|
+
console.log()
|
|
287
129
|
|
|
288
130
|
for await (const chunk of this.agent.chatStream(message)) {
|
|
289
|
-
// 检查是否被打断
|
|
290
131
|
if (interrupted) break
|
|
291
132
|
|
|
292
133
|
if (chunk.type === 'text') {
|
|
293
134
|
lineBuffer += chunk.text
|
|
294
135
|
|
|
295
|
-
// 如果有孤立代理对,等待下一个chunk补充
|
|
296
|
-
if (hasOrphanedSurrogate(lineBuffer)) {
|
|
297
|
-
continue
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// 当有一行完整内容时,渲染并输出
|
|
301
136
|
while (lineBuffer.includes('\n')) {
|
|
302
|
-
// 检查是否被打断
|
|
303
137
|
if (interrupted) break
|
|
304
138
|
|
|
305
139
|
const nlIndex = lineBuffer.indexOf('\n')
|
|
@@ -316,7 +150,6 @@ class ChatUI extends EventEmitter {
|
|
|
316
150
|
}
|
|
317
151
|
}
|
|
318
152
|
|
|
319
|
-
// 输出剩余内容
|
|
320
153
|
if (lineBuffer.trim() && !interrupted) {
|
|
321
154
|
console.log(renderLine(lineBuffer, renderState))
|
|
322
155
|
}
|
|
@@ -325,11 +158,8 @@ class ChatUI extends EventEmitter {
|
|
|
325
158
|
console.error(`\n${colored('[错误]', RED)} ${err.message}\n`)
|
|
326
159
|
}
|
|
327
160
|
} finally {
|
|
328
|
-
// 移除监听器
|
|
329
161
|
process.removeListener('SIGINT', interruptHandler)
|
|
330
162
|
}
|
|
331
|
-
|
|
332
|
-
this.prompt()
|
|
333
163
|
}
|
|
334
164
|
}
|
|
335
165
|
|