tabby-ai-assistant 1.0.13 → 1.0.16
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/.editorconfig +18 -0
- package/README.md +40 -10
- package/dist/index.js +1 -1
- package/package.json +5 -3
- package/src/components/chat/ai-sidebar.component.scss +220 -9
- package/src/components/chat/ai-sidebar.component.ts +379 -29
- package/src/components/chat/chat-input.component.ts +36 -4
- package/src/components/chat/chat-interface.component.ts +225 -5
- package/src/components/chat/chat-message.component.ts +6 -1
- package/src/components/settings/context-settings.component.ts +91 -91
- package/src/components/terminal/ai-toolbar-button.component.ts +4 -2
- package/src/components/terminal/command-suggestion.component.ts +148 -6
- package/src/index.ts +81 -19
- package/src/providers/tabby/ai-toolbar-button.provider.ts +7 -3
- package/src/services/chat/ai-sidebar.service.ts +448 -410
- package/src/services/chat/chat-session.service.ts +36 -12
- package/src/services/context/compaction.ts +110 -134
- package/src/services/context/manager.ts +27 -7
- package/src/services/context/memory.ts +17 -33
- package/src/services/context/summary.service.ts +136 -0
- package/src/services/core/ai-assistant.service.ts +1060 -37
- package/src/services/core/ai-provider-manager.service.ts +154 -25
- package/src/services/core/checkpoint.service.ts +218 -18
- package/src/services/core/toast.service.ts +106 -106
- package/src/services/providers/anthropic-provider.service.ts +126 -30
- package/src/services/providers/base-provider.service.ts +90 -7
- package/src/services/providers/glm-provider.service.ts +151 -38
- package/src/services/providers/minimax-provider.service.ts +55 -40
- package/src/services/providers/ollama-provider.service.ts +117 -28
- package/src/services/providers/openai-compatible.service.ts +164 -34
- package/src/services/providers/openai-provider.service.ts +169 -34
- package/src/services/providers/vllm-provider.service.ts +116 -28
- package/src/services/terminal/terminal-context.service.ts +265 -5
- package/src/services/terminal/terminal-manager.service.ts +845 -748
- package/src/services/terminal/terminal-tools.service.ts +612 -441
- package/src/types/ai.types.ts +156 -3
- package/src/utils/cost.utils.ts +249 -0
- package/src/utils/validation.utils.ts +306 -2
- package/dist/index.js.LICENSE.txt +0 -18
- package/src/services/terminal/command-analyzer.service.ts +0 -43
- package/src/services/terminal/context-menu.service.ts +0 -45
- package/src/services/terminal/hotkey.service.ts +0 -53
|
@@ -1,441 +1,612 @@
|
|
|
1
|
-
import { Injectable } from '@angular/core';
|
|
2
|
-
import { TerminalManagerService, TerminalInfo } from './terminal-manager.service';
|
|
3
|
-
import { LoggerService } from '../core/logger.service';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 终端工具定义
|
|
7
|
-
*/
|
|
8
|
-
export interface ToolDefinition {
|
|
9
|
-
name: string;
|
|
10
|
-
description: string;
|
|
11
|
-
input_schema: {
|
|
12
|
-
type: string;
|
|
13
|
-
properties: Record<string, any>;
|
|
14
|
-
required?: string[];
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 工具调用请求
|
|
20
|
-
*/
|
|
21
|
-
export interface ToolCall {
|
|
22
|
-
id: string;
|
|
23
|
-
name: string;
|
|
24
|
-
input: Record<string, any>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 工具调用结果
|
|
29
|
-
*/
|
|
30
|
-
export interface ToolResult {
|
|
31
|
-
tool_use_id: string;
|
|
32
|
-
content: string;
|
|
33
|
-
is_error?: boolean;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
this.logger.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { TerminalManagerService, TerminalInfo } from './terminal-manager.service';
|
|
3
|
+
import { LoggerService } from '../core/logger.service';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 终端工具定义
|
|
7
|
+
*/
|
|
8
|
+
export interface ToolDefinition {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
input_schema: {
|
|
12
|
+
type: string;
|
|
13
|
+
properties: Record<string, any>;
|
|
14
|
+
required?: string[];
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 工具调用请求
|
|
20
|
+
*/
|
|
21
|
+
export interface ToolCall {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
input: Record<string, any>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 工具调用结果
|
|
29
|
+
*/
|
|
30
|
+
export interface ToolResult {
|
|
31
|
+
tool_use_id: string;
|
|
32
|
+
content: string;
|
|
33
|
+
is_error?: boolean;
|
|
34
|
+
isTaskComplete?: boolean; // 特殊标记:task_complete 工具调用
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 终端工具服务
|
|
39
|
+
* 定义 AI 可调用的终端相关工具
|
|
40
|
+
*/
|
|
41
|
+
@Injectable({ providedIn: 'root' })
|
|
42
|
+
export class TerminalToolsService {
|
|
43
|
+
// ========== 智能等待配置 ==========
|
|
44
|
+
// 命令类型与预估等待时间映射(毫秒)
|
|
45
|
+
private readonly COMMAND_WAIT_TIMES: Record<string, number> = {
|
|
46
|
+
// 快速命令 (< 500ms)
|
|
47
|
+
'cd': 200,
|
|
48
|
+
'pwd': 200,
|
|
49
|
+
'echo': 200,
|
|
50
|
+
'set': 300,
|
|
51
|
+
'export': 200,
|
|
52
|
+
'cls': 100,
|
|
53
|
+
'clear': 100,
|
|
54
|
+
'date': 200,
|
|
55
|
+
'time': 200,
|
|
56
|
+
|
|
57
|
+
// 标准命令 (500-1500ms)
|
|
58
|
+
'dir': 500,
|
|
59
|
+
'ls': 500,
|
|
60
|
+
'cat': 500,
|
|
61
|
+
'type': 500,
|
|
62
|
+
'mkdir': 300,
|
|
63
|
+
'rm': 500,
|
|
64
|
+
'del': 500,
|
|
65
|
+
'copy': 800,
|
|
66
|
+
'xcopy': 1000,
|
|
67
|
+
'move': 800,
|
|
68
|
+
'ren': 300,
|
|
69
|
+
'rename': 300,
|
|
70
|
+
'tree': 1000,
|
|
71
|
+
'find': 600,
|
|
72
|
+
'grep': 500,
|
|
73
|
+
'head': 200,
|
|
74
|
+
'tail': 200,
|
|
75
|
+
|
|
76
|
+
// 慢速命令 (1500-5000ms)
|
|
77
|
+
'git': 3000,
|
|
78
|
+
'npm': 5000,
|
|
79
|
+
'yarn': 5000,
|
|
80
|
+
'pnpm': 5000,
|
|
81
|
+
'pip': 4000,
|
|
82
|
+
'conda': 3000,
|
|
83
|
+
'docker': 4000,
|
|
84
|
+
'kubectl': 3000,
|
|
85
|
+
'terraform': 4000,
|
|
86
|
+
'make': 2000,
|
|
87
|
+
'cmake': 3000,
|
|
88
|
+
|
|
89
|
+
// 非常慢的命令 (> 5000ms)
|
|
90
|
+
'systeminfo': 8000,
|
|
91
|
+
'ipconfig': 2000,
|
|
92
|
+
'ifconfig': 2000,
|
|
93
|
+
'netstat': 3000,
|
|
94
|
+
'ss': 2000,
|
|
95
|
+
'ping': 10000,
|
|
96
|
+
'tracert': 15000,
|
|
97
|
+
'tracepath': 10000,
|
|
98
|
+
'nslookup': 3000,
|
|
99
|
+
'dig': 3000,
|
|
100
|
+
'choco': 5000,
|
|
101
|
+
'scoop': 5000,
|
|
102
|
+
'apt-get': 5000,
|
|
103
|
+
'apt': 4000,
|
|
104
|
+
'yum': 5000,
|
|
105
|
+
'dnf': 5000,
|
|
106
|
+
'brew': 5000,
|
|
107
|
+
'pacman': 5000,
|
|
108
|
+
|
|
109
|
+
// 默认等待时间
|
|
110
|
+
'__default__': 1500
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// 工具定义
|
|
114
|
+
private tools: ToolDefinition[] = [
|
|
115
|
+
// ========== 任务完成工具 ==========
|
|
116
|
+
{
|
|
117
|
+
name: 'task_complete',
|
|
118
|
+
description: `【重要】当你完成了用户请求的所有任务后,必须调用此工具来结束任务循环。
|
|
119
|
+
调用此工具后,Agent 将停止继续执行,你的 summary 将作为最终回复展示给用户。
|
|
120
|
+
使用场景:
|
|
121
|
+
- 所有工具调用都成功完成
|
|
122
|
+
- 遇到无法解决的问题需要停止
|
|
123
|
+
- 用户请求已被完整满足
|
|
124
|
+
注意:如果还有未完成的任务,请先完成它们再调用此工具。`,
|
|
125
|
+
input_schema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
summary: {
|
|
129
|
+
type: 'string',
|
|
130
|
+
description: '任务完成总结,描述做了什么、结果如何'
|
|
131
|
+
},
|
|
132
|
+
success: {
|
|
133
|
+
type: 'boolean',
|
|
134
|
+
description: '是否成功完成所有任务'
|
|
135
|
+
},
|
|
136
|
+
next_steps: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
description: '可选,建议用户的后续操作'
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
required: ['summary', 'success']
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
// ========== 终端操作工具 ==========
|
|
145
|
+
{
|
|
146
|
+
name: 'read_terminal_output',
|
|
147
|
+
description: '读取指定终端的最近输出内容。用于获取命令执行结果或终端状态。',
|
|
148
|
+
input_schema: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
lines: {
|
|
152
|
+
type: 'number',
|
|
153
|
+
description: '要读取的行数,默认为 50'
|
|
154
|
+
},
|
|
155
|
+
terminal_index: {
|
|
156
|
+
type: 'number',
|
|
157
|
+
description: '目标终端索引。如不指定则读取活动终端。'
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
required: []
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'write_to_terminal',
|
|
165
|
+
description: '向终端写入命令。可以指定终端索引或使用当前活动终端。',
|
|
166
|
+
input_schema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
command: {
|
|
170
|
+
type: 'string',
|
|
171
|
+
description: '要写入的命令'
|
|
172
|
+
},
|
|
173
|
+
execute: {
|
|
174
|
+
type: 'boolean',
|
|
175
|
+
description: '是否立即执行命令(添加回车),默认为 true'
|
|
176
|
+
},
|
|
177
|
+
terminal_index: {
|
|
178
|
+
type: 'number',
|
|
179
|
+
description: '目标终端索引(从 0 开始)。如不指定则使用当前活动终端。'
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
required: ['command']
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'get_terminal_list',
|
|
187
|
+
description: '获取所有打开的终端列表,包括终端 ID、标题、活动状态等。',
|
|
188
|
+
input_schema: {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {},
|
|
191
|
+
required: []
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'get_terminal_cwd',
|
|
196
|
+
description: '获取当前终端的工作目录。',
|
|
197
|
+
input_schema: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: {},
|
|
200
|
+
required: []
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: 'get_terminal_selection',
|
|
205
|
+
description: '获取当前终端中选中的文本。',
|
|
206
|
+
input_schema: {
|
|
207
|
+
type: 'object',
|
|
208
|
+
properties: {},
|
|
209
|
+
required: []
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'focus_terminal',
|
|
214
|
+
description: '切换到指定索引的终端,使其成为活动终端。',
|
|
215
|
+
input_schema: {
|
|
216
|
+
type: 'object',
|
|
217
|
+
properties: {
|
|
218
|
+
terminal_index: {
|
|
219
|
+
type: 'number',
|
|
220
|
+
description: '目标终端索引(从 0 开始)'
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
required: ['terminal_index']
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
// 终端输出缓存
|
|
229
|
+
private outputBuffer: string[] = [];
|
|
230
|
+
private maxBufferLines = 500;
|
|
231
|
+
|
|
232
|
+
constructor(
|
|
233
|
+
private terminalManager: TerminalManagerService,
|
|
234
|
+
private logger: LoggerService
|
|
235
|
+
) {
|
|
236
|
+
// 不再需要静态订阅输出,直接从 xterm buffer 动态读取
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 获取所有工具定义
|
|
241
|
+
*/
|
|
242
|
+
getToolDefinitions(): ToolDefinition[] {
|
|
243
|
+
return this.tools;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 执行工具调用
|
|
248
|
+
*/
|
|
249
|
+
async executeToolCall(toolCall: ToolCall): Promise<ToolResult> {
|
|
250
|
+
this.logger.info('Executing tool call', { name: toolCall.name, input: toolCall.input });
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
let result: string;
|
|
254
|
+
let isTaskComplete = false;
|
|
255
|
+
|
|
256
|
+
switch (toolCall.name) {
|
|
257
|
+
// ========== 任务完成工具 ==========
|
|
258
|
+
case 'task_complete': {
|
|
259
|
+
const input = toolCall.input;
|
|
260
|
+
const successStatus = input.success ? '成功' : '未能';
|
|
261
|
+
const nextStepsText = input.next_steps
|
|
262
|
+
? `\n\n建议后续操作:${input.next_steps}`
|
|
263
|
+
: '';
|
|
264
|
+
result = `任务${successStatus}完成。\n\n${input.summary}${nextStepsText}`;
|
|
265
|
+
isTaskComplete = true;
|
|
266
|
+
this.logger.info('Task completed via task_complete tool', { success: input.success });
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
// ========== 终端操作工具 ==========
|
|
270
|
+
case 'read_terminal_output':
|
|
271
|
+
result = this.readTerminalOutput(
|
|
272
|
+
toolCall.input.lines || 50,
|
|
273
|
+
toolCall.input.terminal_index
|
|
274
|
+
);
|
|
275
|
+
break;
|
|
276
|
+
case 'write_to_terminal':
|
|
277
|
+
result = await this.writeToTerminal(
|
|
278
|
+
toolCall.input.command,
|
|
279
|
+
toolCall.input.execute ?? true,
|
|
280
|
+
toolCall.input.terminal_index
|
|
281
|
+
);
|
|
282
|
+
break;
|
|
283
|
+
case 'get_terminal_list':
|
|
284
|
+
result = this.getTerminalList();
|
|
285
|
+
break;
|
|
286
|
+
case 'get_terminal_cwd':
|
|
287
|
+
result = this.getTerminalCwd();
|
|
288
|
+
break;
|
|
289
|
+
case 'get_terminal_selection':
|
|
290
|
+
result = this.getTerminalSelection();
|
|
291
|
+
break;
|
|
292
|
+
case 'focus_terminal':
|
|
293
|
+
result = this.focusTerminal(toolCall.input.terminal_index);
|
|
294
|
+
break;
|
|
295
|
+
default:
|
|
296
|
+
throw new Error(`Unknown tool: ${toolCall.name}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this.logger.info('Tool call completed', { name: toolCall.name, resultLength: result.length });
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
tool_use_id: toolCall.id,
|
|
303
|
+
content: result,
|
|
304
|
+
isTaskComplete
|
|
305
|
+
};
|
|
306
|
+
} catch (error) {
|
|
307
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
308
|
+
this.logger.error('Tool call failed', { name: toolCall.name, error: errorMessage });
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
tool_use_id: toolCall.id,
|
|
312
|
+
content: `错误: ${errorMessage}`,
|
|
313
|
+
is_error: true
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* 从 xterm buffer 读取内容
|
|
320
|
+
* 包含详细调试日志以定位白屏问题
|
|
321
|
+
*/
|
|
322
|
+
private readFromXtermBuffer(terminal: any, lines: number): string {
|
|
323
|
+
try {
|
|
324
|
+
// === 调试代码:记录终端结构 ===
|
|
325
|
+
this.logger.info('【DEBUG】Terminal structure debug', {
|
|
326
|
+
hasTerminal: !!terminal,
|
|
327
|
+
terminalType: terminal?.constructor?.name,
|
|
328
|
+
hasFrontend: !!terminal?.frontend,
|
|
329
|
+
frontendType: terminal?.frontend?.constructor?.name,
|
|
330
|
+
frontendKeys: terminal?.frontend ? Object.keys(terminal.frontend).slice(0, 15) : [],
|
|
331
|
+
hasXterm: !!terminal?.frontend?.xterm,
|
|
332
|
+
xtermType: terminal?.frontend?.xterm?.constructor?.name,
|
|
333
|
+
hasBuffer: !!terminal?.frontend?.xterm?.buffer,
|
|
334
|
+
bufferActive: !!terminal?.frontend?.xterm?.buffer?.active
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// 尝试多种可能的 xterm buffer 访问路径
|
|
338
|
+
let buffer: any = null;
|
|
339
|
+
let bufferSource = '';
|
|
340
|
+
|
|
341
|
+
// 路径1: frontend.xterm.buffer.active (xterm.js 标准)
|
|
342
|
+
if (terminal.frontend?.xterm?.buffer?.active) {
|
|
343
|
+
buffer = terminal.frontend.xterm.buffer.active;
|
|
344
|
+
bufferSource = 'frontend.xterm.buffer.active';
|
|
345
|
+
this.logger.info('【DEBUG】Using buffer path: ' + bufferSource);
|
|
346
|
+
}
|
|
347
|
+
// 路径2: frontend.buffer (可能是直接暴露)
|
|
348
|
+
else if (terminal.frontend?.buffer?.active) {
|
|
349
|
+
buffer = terminal.frontend.buffer.active;
|
|
350
|
+
bufferSource = 'frontend.buffer.active';
|
|
351
|
+
this.logger.info('【DEBUG】Using buffer path: ' + bufferSource);
|
|
352
|
+
}
|
|
353
|
+
// 路径3: frontend._core.buffer (私有属性)
|
|
354
|
+
else if (terminal.frontend?._core?.buffer?.active) {
|
|
355
|
+
buffer = terminal.frontend._core.buffer.active;
|
|
356
|
+
bufferSource = 'frontend._core.buffer.active';
|
|
357
|
+
this.logger.info('【DEBUG】Using buffer path: ' + bufferSource);
|
|
358
|
+
}
|
|
359
|
+
// 路径4: 尝试通过 terminal 上的其他属性
|
|
360
|
+
else {
|
|
361
|
+
this.logger.warn('【DEBUG】No standard buffer path found, trying alternatives', {
|
|
362
|
+
hasContent: !!terminal.content,
|
|
363
|
+
hasContent$: !!terminal.content$,
|
|
364
|
+
hasSession: !!terminal.session,
|
|
365
|
+
allFrontendKeys: terminal?.frontend ? Object.keys(terminal.frontend) : []
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// 如果有 content 属性,尝试使用它
|
|
369
|
+
if (terminal.content) {
|
|
370
|
+
return `[DEBUG] 终端内容:\n${terminal.content}`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return '(无法访问终端 buffer,请检查终端是否就绪)';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (!buffer) {
|
|
377
|
+
this.logger.warn('【DEBUG】Buffer is null after all path attempts');
|
|
378
|
+
return '(无法访问终端 buffer,buffer 为空)';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const totalLines = buffer.length || 0;
|
|
382
|
+
this.logger.info('【DEBUG】Buffer info', {
|
|
383
|
+
totalLines,
|
|
384
|
+
requestedLines: lines,
|
|
385
|
+
bufferSource
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (totalLines === 0) {
|
|
389
|
+
return '(终端 buffer 为空)';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const startLine = Math.max(0, totalLines - lines);
|
|
393
|
+
const result: string[] = [];
|
|
394
|
+
|
|
395
|
+
for (let i = startLine; i < totalLines; i++) {
|
|
396
|
+
try {
|
|
397
|
+
const line = buffer.getLine(i);
|
|
398
|
+
if (line && typeof line.translateToString === 'function') {
|
|
399
|
+
result.push(line.translateToString(true));
|
|
400
|
+
}
|
|
401
|
+
} catch (e) {
|
|
402
|
+
this.logger.warn('【DEBUG】Failed to read line ' + i, e);
|
|
403
|
+
// 跳过无法读取的行
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const finalOutput = result.join('\n') || '(终端输出为空)';
|
|
408
|
+
this.logger.info('【DEBUG】Read completed', {
|
|
409
|
+
linesRead: result.length,
|
|
410
|
+
outputLength: finalOutput.length
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
return finalOutput;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
this.logger.error('【DEBUG】Failed to read xterm buffer', {
|
|
416
|
+
error: error instanceof Error ? error.message : String(error),
|
|
417
|
+
stack: error instanceof Error ? error.stack : ''
|
|
418
|
+
});
|
|
419
|
+
return '(读取终端失败,请重试)';
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* 读取终端输出
|
|
425
|
+
*/
|
|
426
|
+
private readTerminalOutput(lines: number, terminalIndex?: number): string {
|
|
427
|
+
// 尝试从缓冲区获取
|
|
428
|
+
if (this.outputBuffer.length > 0) {
|
|
429
|
+
const recentLines = this.outputBuffer.slice(-lines);
|
|
430
|
+
return recentLines.join('\n');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 直接从指定终端的 xterm buffer 读取
|
|
434
|
+
const terminals = this.terminalManager.getAllTerminals();
|
|
435
|
+
const terminal = terminalIndex !== undefined
|
|
436
|
+
? terminals[terminalIndex]
|
|
437
|
+
: this.terminalManager.getActiveTerminal();
|
|
438
|
+
|
|
439
|
+
if (!terminal) {
|
|
440
|
+
return '(无可用终端)';
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return this.readFromXtermBuffer(terminal, lines);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* 写入终端 - 带执行反馈和智能等待
|
|
448
|
+
*/
|
|
449
|
+
private async writeToTerminal(command: string, execute: boolean, terminalIndex?: number): Promise<string> {
|
|
450
|
+
this.logger.info('writeToTerminal called', { command, execute, terminalIndex });
|
|
451
|
+
|
|
452
|
+
let success: boolean;
|
|
453
|
+
let targetTerminalIndex: number;
|
|
454
|
+
|
|
455
|
+
if (terminalIndex !== undefined) {
|
|
456
|
+
// 向指定索引的终端写入
|
|
457
|
+
this.logger.info('Sending command to terminal index', { terminalIndex });
|
|
458
|
+
success = this.terminalManager.sendCommandToIndex(terminalIndex, command, execute);
|
|
459
|
+
targetTerminalIndex = terminalIndex;
|
|
460
|
+
this.logger.info('sendCommandToIndex result', { success });
|
|
461
|
+
} else {
|
|
462
|
+
// 向当前活动终端写入
|
|
463
|
+
this.logger.info('Sending command to active terminal');
|
|
464
|
+
success = this.terminalManager.sendCommand(command, execute);
|
|
465
|
+
targetTerminalIndex = 0; // 默认活动终端
|
|
466
|
+
this.logger.info('sendCommand result', { success });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (!success) {
|
|
470
|
+
throw new Error(terminalIndex !== undefined
|
|
471
|
+
? `无法写入终端 ${terminalIndex},索引无效或终端不可用`
|
|
472
|
+
: '无法写入终端,请确保有活动的终端窗口');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ========== 智能等待机制 ==========
|
|
476
|
+
const baseCommand = this.extractBaseCommand(command);
|
|
477
|
+
const waitTime = this.getWaitTimeForCommand(baseCommand);
|
|
478
|
+
|
|
479
|
+
this.logger.info('Smart wait for command', { command, baseCommand, waitTime });
|
|
480
|
+
|
|
481
|
+
// 初始等待
|
|
482
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
483
|
+
|
|
484
|
+
// 直接从 xterm buffer 读取
|
|
485
|
+
const terminals = this.terminalManager.getAllTerminals();
|
|
486
|
+
const terminal = terminalIndex !== undefined
|
|
487
|
+
? terminals[terminalIndex]
|
|
488
|
+
: this.terminalManager.getActiveTerminal();
|
|
489
|
+
|
|
490
|
+
let output = '(终端输出为空)';
|
|
491
|
+
if (terminal) {
|
|
492
|
+
output = this.readFromXtermBuffer(terminal, 50);
|
|
493
|
+
|
|
494
|
+
// 对于慢命令,轮询检查是否完成
|
|
495
|
+
if (waitTime >= 3000) {
|
|
496
|
+
let retryCount = 0;
|
|
497
|
+
const maxRetries = 3;
|
|
498
|
+
|
|
499
|
+
while (retryCount < maxRetries && !this.isCommandComplete(output)) {
|
|
500
|
+
this.logger.info(`Command still running, retry ${retryCount + 1}/${maxRetries}`);
|
|
501
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
502
|
+
output = this.readFromXtermBuffer(terminal, 50);
|
|
503
|
+
retryCount++;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// 返回执行结果
|
|
509
|
+
return [
|
|
510
|
+
`✅ 命令已执行: ${command}`,
|
|
511
|
+
`⏱️ 等待时间: ${waitTime}ms`,
|
|
512
|
+
'',
|
|
513
|
+
'=== 终端输出 ===',
|
|
514
|
+
output,
|
|
515
|
+
'=== 输出结束 ==='
|
|
516
|
+
].join('\n');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* 提取命令基础名称
|
|
521
|
+
*/
|
|
522
|
+
private extractBaseCommand(command: string): string {
|
|
523
|
+
const trimmed = command.trim().toLowerCase();
|
|
524
|
+
// 处理 Windows 路径 (如 C:\Windows\System32\systeminfo.exe)
|
|
525
|
+
const parts = trimmed.split(/[\s\/\\]+/);
|
|
526
|
+
const executable = parts[0].replace(/\.exe$/i, '');
|
|
527
|
+
// 移除常见前缀
|
|
528
|
+
return executable.replace(/^(winpty|busybox|gtimeout|command|-)/, '');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* 获取命令等待时间
|
|
533
|
+
*/
|
|
534
|
+
private getWaitTimeForCommand(baseCommand: string): number {
|
|
535
|
+
return this.COMMAND_WAIT_TIMES[baseCommand] || this.COMMAND_WAIT_TIMES['__default__'];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* 检查命令是否完成(检测提示符)
|
|
540
|
+
*/
|
|
541
|
+
private isCommandComplete(output: string): boolean {
|
|
542
|
+
const promptPatterns = [
|
|
543
|
+
/\n[A-Za-z]:.*>\s*$/, // Windows: C:\Users\xxx>
|
|
544
|
+
/\$\s*$/, // Linux/Mac: $
|
|
545
|
+
/\n#\s*$/, // Root: #
|
|
546
|
+
/\n.*@.*:\~.*\$\s*$/, // bash: user@host:~$
|
|
547
|
+
/PS\s+[A-Za-z]:.*>\s*$/, // PowerShell: PS C:\>
|
|
548
|
+
/\[.*@\S+\s+.*\]\$\s*$/, // modern bash
|
|
549
|
+
];
|
|
550
|
+
|
|
551
|
+
return promptPatterns.some(pattern => pattern.test(output));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* 获取终端列表
|
|
556
|
+
*/
|
|
557
|
+
private getTerminalList(): string {
|
|
558
|
+
const terminals: TerminalInfo[] = this.terminalManager.getAllTerminalInfo();
|
|
559
|
+
if (terminals.length === 0) {
|
|
560
|
+
return '(没有打开的终端)';
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// 检测操作系统
|
|
564
|
+
const platform = process.platform;
|
|
565
|
+
const isWindows = platform === 'win32';
|
|
566
|
+
const osInfo = isWindows ? 'Windows' : (platform === 'darwin' ? 'macOS' : 'Linux');
|
|
567
|
+
|
|
568
|
+
const list = terminals.map((t, i) =>
|
|
569
|
+
`[${i}] ${t.title}${t.isActive ? ' (活动)' : ''}${t.cwd ? ` - ${t.cwd}` : ''}`
|
|
570
|
+
).join('\n');
|
|
571
|
+
|
|
572
|
+
return `操作系统: ${osInfo}\n共 ${terminals.length} 个终端:\n${list}\n\n注意: ${isWindows ? '请使用 Windows 命令 (如 dir, cd, type 等)' : '请使用 Unix 命令 (如 ls, cd, cat 等)'}`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* 获取终端工作目录
|
|
579
|
+
*/
|
|
580
|
+
private getTerminalCwd(): string {
|
|
581
|
+
const cwd = this.terminalManager.getTerminalCwd();
|
|
582
|
+
if (cwd) {
|
|
583
|
+
return `当前工作目录: ${cwd}`;
|
|
584
|
+
} else {
|
|
585
|
+
return '(无法获取工作目录)';
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* 获取终端选中文本
|
|
591
|
+
*/
|
|
592
|
+
private getTerminalSelection(): string {
|
|
593
|
+
const selection = this.terminalManager.getSelection();
|
|
594
|
+
if (selection) {
|
|
595
|
+
return selection;
|
|
596
|
+
} else {
|
|
597
|
+
return '(没有选中的文本)';
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* 切换终端焦点
|
|
603
|
+
*/
|
|
604
|
+
private focusTerminal(index: number): string {
|
|
605
|
+
const success = this.terminalManager.focusTerminal(index);
|
|
606
|
+
if (success) {
|
|
607
|
+
return `✅ 已切换到终端 ${index}`;
|
|
608
|
+
} else {
|
|
609
|
+
return `❌ 无法切换到终端 ${index},索引无效`;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|