tabby-ai-assistant 1.0.12 → 1.0.15

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.
Files changed (47) hide show
  1. package/.editorconfig +18 -0
  2. package/README.md +113 -55
  3. package/dist/index.js +1 -1
  4. package/package.json +6 -4
  5. package/src/components/chat/ai-sidebar.component.scss +220 -9
  6. package/src/components/chat/ai-sidebar.component.ts +364 -29
  7. package/src/components/chat/chat-input.component.ts +36 -4
  8. package/src/components/chat/chat-interface.component.ts +225 -5
  9. package/src/components/chat/chat-message.component.ts +6 -1
  10. package/src/components/settings/context-settings.component.ts +91 -91
  11. package/src/components/terminal/ai-toolbar-button.component.ts +4 -2
  12. package/src/components/terminal/command-suggestion.component.ts +148 -6
  13. package/src/index.ts +0 -6
  14. package/src/providers/tabby/ai-toolbar-button.provider.ts +7 -3
  15. package/src/services/chat/ai-sidebar.service.ts +414 -410
  16. package/src/services/chat/chat-session.service.ts +36 -12
  17. package/src/services/context/compaction.ts +110 -134
  18. package/src/services/context/manager.ts +27 -7
  19. package/src/services/context/memory.ts +17 -33
  20. package/src/services/context/summary.service.ts +136 -0
  21. package/src/services/core/ai-assistant.service.ts +1060 -37
  22. package/src/services/core/ai-provider-manager.service.ts +154 -25
  23. package/src/services/core/checkpoint.service.ts +218 -18
  24. package/src/services/core/config-provider.service.ts +4 -12
  25. package/src/services/core/toast.service.ts +106 -106
  26. package/src/services/providers/anthropic-provider.service.ts +126 -202
  27. package/src/services/providers/base-provider.service.ts +315 -21
  28. package/src/services/providers/glm-provider.service.ts +151 -233
  29. package/src/services/providers/minimax-provider.service.ts +55 -238
  30. package/src/services/providers/ollama-provider.service.ts +117 -188
  31. package/src/services/providers/openai-compatible.service.ts +165 -177
  32. package/src/services/providers/openai-provider.service.ts +170 -177
  33. package/src/services/providers/vllm-provider.service.ts +116 -188
  34. package/src/services/terminal/terminal-context.service.ts +265 -5
  35. package/src/services/terminal/terminal-manager.service.ts +748 -748
  36. package/src/services/terminal/terminal-tools.service.ts +612 -441
  37. package/src/types/ai.types.ts +156 -3
  38. package/src/types/provider.types.ts +206 -75
  39. package/src/utils/cost.utils.ts +249 -0
  40. package/src/utils/validation.utils.ts +306 -2
  41. package/dist/index.js.LICENSE.txt +0 -18
  42. package/src/index.ts.backup +0 -165
  43. package/src/services/chat/chat-history.service.ts.backup +0 -239
  44. package/src/services/terminal/command-analyzer.service.ts +0 -43
  45. package/src/services/terminal/context-menu.service.ts +0 -45
  46. package/src/services/terminal/hotkey.service.ts +0 -53
  47. package/webpack.config.js.backup +0 -57
@@ -1,106 +1,106 @@
1
- import { Injectable } from '@angular/core';
2
- import { Subject } from 'rxjs';
3
-
4
- export interface ToastMessage {
5
- id: string;
6
- type: 'success' | 'error' | 'warning' | 'info';
7
- message: string;
8
- duration?: number;
9
- }
10
-
11
- @Injectable({ providedIn: 'root' })
12
- export class ToastService {
13
- private toastSubject = new Subject<ToastMessage>();
14
- toast$ = this.toastSubject.asObservable();
15
-
16
- show(type: ToastMessage['type'], message: string, duration = 3000): void {
17
- const id = `toast-${Date.now()}`;
18
-
19
- // 确保容器存在
20
- let container = document.getElementById('ai-toast-container');
21
- if (!container) {
22
- container = document.createElement('div');
23
- container.id = 'ai-toast-container';
24
- container.className = 'ai-toast-container';
25
- container.style.cssText = `
26
- position: fixed;
27
- bottom: 20px;
28
- right: 20px;
29
- z-index: 99999;
30
- display: flex;
31
- flex-direction: column;
32
- gap: 10px;
33
- pointer-events: none;
34
- `;
35
- document.body.appendChild(container);
36
- }
37
-
38
- // 创建 Toast 元素
39
- const toast = document.createElement('div');
40
- toast.id = id;
41
- toast.className = `ai-toast ai-toast-${type}`;
42
- toast.style.cssText = `
43
- padding: 12px 16px;
44
- border-radius: 8px;
45
- color: white;
46
- font-size: 14px;
47
- display: flex;
48
- align-items: center;
49
- gap: 8px;
50
- animation: toastSlideIn 0.3s ease;
51
- cursor: pointer;
52
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
53
- pointer-events: auto;
54
- min-width: 200px;
55
- max-width: 350px;
56
- ${type === 'success' ? 'background: linear-gradient(135deg, #22c55e, #16a34a);' : ''}
57
- ${type === 'error' ? 'background: linear-gradient(135deg, #ef4444, #dc2626);' : ''}
58
- ${type === 'warning' ? 'background: linear-gradient(135deg, #f59e0b, #d97706);' : ''}
59
- ${type === 'info' ? 'background: linear-gradient(135deg, #3b82f6, #2563eb);' : ''}
60
- `;
61
-
62
- const icon = type === 'success' ? '✓' : type === 'error' ? '✗' : type === 'warning' ? '⚠' : 'ℹ';
63
- toast.innerHTML = `<span style="font-size: 16px;">${icon}</span><span>${message}</span>`;
64
-
65
- toast.onclick = () => {
66
- this.removeToast(toast);
67
- };
68
-
69
- container.appendChild(toast);
70
-
71
- // 自动消失
72
- setTimeout(() => {
73
- this.removeToast(toast);
74
- }, duration);
75
-
76
- // 发射事件(兼容现有订阅)
77
- this.toastSubject.next({ id, type, message, duration });
78
- }
79
-
80
- private removeToast(toast: HTMLElement): void {
81
- if (toast && toast.parentNode) {
82
- toast.style.animation = 'toastSlideOut 0.3s ease forwards';
83
- setTimeout(() => {
84
- if (toast.parentNode) {
85
- toast.remove();
86
- }
87
- }, 300);
88
- }
89
- }
90
-
91
- success(message: string, duration = 3000): void {
92
- this.show('success', message, duration);
93
- }
94
-
95
- error(message: string, duration = 5000): void {
96
- this.show('error', message, duration);
97
- }
98
-
99
- warning(message: string, duration = 4000): void {
100
- this.show('warning', message, duration);
101
- }
102
-
103
- info(message: string, duration = 3000): void {
104
- this.show('info', message, duration);
105
- }
106
- }
1
+ import { Injectable } from '@angular/core';
2
+ import { Subject } from 'rxjs';
3
+
4
+ export interface ToastMessage {
5
+ id: string;
6
+ type: 'success' | 'error' | 'warning' | 'info';
7
+ message: string;
8
+ duration?: number;
9
+ }
10
+
11
+ @Injectable({ providedIn: 'root' })
12
+ export class ToastService {
13
+ private toastSubject = new Subject<ToastMessage>();
14
+ toast$ = this.toastSubject.asObservable();
15
+
16
+ show(type: ToastMessage['type'], message: string, duration = 3000): void {
17
+ const id = `toast-${Date.now()}`;
18
+
19
+ // 确保容器存在
20
+ let container = document.getElementById('ai-toast-container');
21
+ if (!container) {
22
+ container = document.createElement('div');
23
+ container.id = 'ai-toast-container';
24
+ container.className = 'ai-toast-container';
25
+ container.style.cssText = `
26
+ position: fixed;
27
+ bottom: 20px;
28
+ right: 20px;
29
+ z-index: 99999;
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: 10px;
33
+ pointer-events: none;
34
+ `;
35
+ document.body.appendChild(container);
36
+ }
37
+
38
+ // 创建 Toast 元素
39
+ const toast = document.createElement('div');
40
+ toast.id = id;
41
+ toast.className = `ai-toast ai-toast-${type}`;
42
+ toast.style.cssText = `
43
+ padding: 12px 16px;
44
+ border-radius: 8px;
45
+ color: white;
46
+ font-size: 14px;
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 8px;
50
+ animation: toastSlideIn 0.3s ease;
51
+ cursor: pointer;
52
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
53
+ pointer-events: auto;
54
+ min-width: 200px;
55
+ max-width: 350px;
56
+ ${type === 'success' ? 'background: linear-gradient(135deg, #22c55e, #16a34a);' : ''}
57
+ ${type === 'error' ? 'background: linear-gradient(135deg, #ef4444, #dc2626);' : ''}
58
+ ${type === 'warning' ? 'background: linear-gradient(135deg, #f59e0b, #d97706);' : ''}
59
+ ${type === 'info' ? 'background: linear-gradient(135deg, #3b82f6, #2563eb);' : ''}
60
+ `;
61
+
62
+ const icon = type === 'success' ? '✓' : type === 'error' ? '✗' : type === 'warning' ? '⚠' : 'ℹ';
63
+ toast.innerHTML = `<span style="font-size: 16px;">${icon}</span><span>${message}</span>`;
64
+
65
+ toast.onclick = () => {
66
+ this.removeToast(toast);
67
+ };
68
+
69
+ container.appendChild(toast);
70
+
71
+ // 自动消失
72
+ setTimeout(() => {
73
+ this.removeToast(toast);
74
+ }, duration);
75
+
76
+ // 发射事件(兼容现有订阅)
77
+ this.toastSubject.next({ id, type, message, duration });
78
+ }
79
+
80
+ private removeToast(toast: HTMLElement): void {
81
+ if (toast && toast.parentNode) {
82
+ toast.style.animation = 'toastSlideOut 0.3s ease forwards';
83
+ setTimeout(() => {
84
+ if (toast.parentNode) {
85
+ toast.remove();
86
+ }
87
+ }, 300);
88
+ }
89
+ }
90
+
91
+ success(message: string, duration = 3000): void {
92
+ this.show('success', message, duration);
93
+ }
94
+
95
+ error(message: string, duration = 5000): void {
96
+ this.show('error', message, duration);
97
+ }
98
+
99
+ warning(message: string, duration = 4000): void {
100
+ this.show('warning', message, duration);
101
+ }
102
+
103
+ info(message: string, duration = 3000): void {
104
+ this.show('info', message, duration);
105
+ }
106
+ }
@@ -1,9 +1,9 @@
1
1
  import { Injectable } from '@angular/core';
2
- import { Observable, from } from 'rxjs';
2
+ import { Observable, Observer } from 'rxjs';
3
3
  import { Anthropic } from '@anthropic-ai/sdk';
4
4
  import { BaseAiProvider } from './base-provider.service';
5
- import { ProviderCapability, HealthStatus, ValidationResult } from '../../types/provider.types';
6
- import { ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse, MessageRole } from '../../types/ai.types';
5
+ import { ProviderCapability, ValidationResult } from '../../types/provider.types';
6
+ import { ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse, MessageRole, StreamEvent } from '../../types/ai.types';
7
7
  import { LoggerService } from '../core/logger.service';
8
8
 
9
9
  /**
@@ -93,11 +93,119 @@ export class AnthropicProviderService extends BaseAiProvider {
93
93
  }
94
94
 
95
95
  /**
96
- * 流式聊天功能 - 暂未实现,回退到非流式
96
+ * 流式聊天功能 - 支持工具调用事件
97
97
  */
98
- chatStream(request: ChatRequest): Observable<any> {
99
- // 回退到非流式
100
- return from(this.chat(request));
98
+ chatStream(request: ChatRequest): Observable<StreamEvent> {
99
+ return new Observable<StreamEvent>((subscriber: Observer<StreamEvent>) => {
100
+ if (!this.client) {
101
+ const error = new Error('Anthropic client not initialized');
102
+ subscriber.next({ type: 'error', error: error.message });
103
+ subscriber.error(error);
104
+ return;
105
+ }
106
+
107
+ let currentToolId = '';
108
+ let currentToolName = '';
109
+ let currentToolInput = '';
110
+ let fullContent = '';
111
+
112
+ const abortController = new AbortController();
113
+
114
+ const runStream = async () => {
115
+ try {
116
+ const stream = await this.client!.messages.stream({
117
+ model: this.config?.model || 'claude-3-sonnet',
118
+ max_tokens: request.maxTokens || 1000,
119
+ system: request.systemPrompt || this.getDefaultSystemPrompt(),
120
+ messages: this.transformMessages(request.messages),
121
+ temperature: request.temperature || 1.0,
122
+ });
123
+
124
+ for await (const event of stream) {
125
+ if (abortController.signal.aborted) break;
126
+
127
+ const eventAny = event as any;
128
+ this.logger.debug('Stream event', { type: event.type });
129
+
130
+ // 处理文本增量
131
+ if (event.type === 'content_block_delta' && eventAny.delta?.type === 'text_delta') {
132
+ const textDelta = eventAny.delta.text;
133
+ fullContent += textDelta;
134
+ subscriber.next({
135
+ type: 'text_delta',
136
+ textDelta
137
+ });
138
+ }
139
+ // 处理工具调用开始
140
+ else if (event.type === 'content_block_start' && eventAny.content_block?.type === 'tool_use') {
141
+ currentToolId = eventAny.content_block.id || `tool_${Date.now()}`;
142
+ currentToolName = eventAny.content_block.name;
143
+ currentToolInput = '';
144
+ subscriber.next({
145
+ type: 'tool_use_start',
146
+ toolCall: {
147
+ id: currentToolId,
148
+ name: currentToolName,
149
+ input: {}
150
+ }
151
+ });
152
+ this.logger.debug('Stream event', { type: 'tool_use_start', name: currentToolName });
153
+ }
154
+ // 处理工具调用参数
155
+ else if (event.type === 'content_block_delta' && eventAny.delta?.type === 'input_json_delta') {
156
+ currentToolInput += eventAny.delta.partial_json || '';
157
+ }
158
+ // 处理工具调用结束
159
+ else if (event.type === 'content_block_stop') {
160
+ if (currentToolId && currentToolName) {
161
+ let parsedInput = {};
162
+ try {
163
+ parsedInput = JSON.parse(currentToolInput || '{}');
164
+ } catch (e) {
165
+ // 使用原始输入
166
+ }
167
+ subscriber.next({
168
+ type: 'tool_use_end',
169
+ toolCall: {
170
+ id: currentToolId,
171
+ name: currentToolName,
172
+ input: parsedInput
173
+ }
174
+ });
175
+ this.logger.debug('Stream event', { type: 'tool_use_end', name: currentToolName });
176
+ currentToolId = '';
177
+ currentToolName = '';
178
+ currentToolInput = '';
179
+ }
180
+ }
181
+ }
182
+
183
+ subscriber.next({
184
+ type: 'message_end',
185
+ message: {
186
+ id: this.generateId(),
187
+ role: MessageRole.ASSISTANT,
188
+ content: fullContent,
189
+ timestamp: new Date()
190
+ }
191
+ });
192
+ this.logger.debug('Stream event', { type: 'message_end', contentLength: fullContent.length });
193
+ subscriber.complete();
194
+
195
+ } catch (error) {
196
+ if ((error as any).name !== 'AbortError') {
197
+ const errorMessage = `Anthropic stream failed: ${error instanceof Error ? error.message : String(error)}`;
198
+ this.logger.error('Stream error', error);
199
+ subscriber.next({ type: 'error', error: errorMessage });
200
+ subscriber.error(new Error(errorMessage));
201
+ }
202
+ }
203
+ };
204
+
205
+ runStream();
206
+
207
+ return () => abortController.abort();
208
+ });
101
209
  }
102
210
 
103
211
  async generateCommand(request: CommandRequest): Promise<CommandResponse> {
@@ -160,31 +268,19 @@ export class AnthropicProviderService extends BaseAiProvider {
160
268
  return this.parseAnalysisResponse(response.message.content);
161
269
  }
162
270
 
163
- async healthCheck(): Promise<HealthStatus> {
164
- try {
165
- if (!this.client) {
166
- return HealthStatus.UNHEALTHY;
167
- }
168
-
169
- const response = await this.client.messages.create({
170
- model: this.config?.model || 'claude-3-sonnet',
171
- max_tokens: 1,
172
- messages: [
173
- {
174
- role: 'user',
175
- content: 'Hi'
176
- }
177
- ]
178
- });
271
+ protected async sendTestRequest(request: ChatRequest): Promise<ChatResponse> {
272
+ if (!this.client) {
273
+ throw new Error('Anthropic client not initialized');
274
+ }
179
275
 
180
- this.lastHealthCheck = { status: HealthStatus.HEALTHY, timestamp: new Date() };
181
- return HealthStatus.HEALTHY;
276
+ const response = await this.client.messages.create({
277
+ model: this.config?.model || 'claude-3-sonnet',
278
+ max_tokens: request.maxTokens || 1,
279
+ messages: this.transformMessages(request.messages),
280
+ temperature: request.temperature || 0
281
+ });
182
282
 
183
- } catch (error) {
184
- this.logger.error('Anthropic health check failed', error);
185
- this.lastHealthCheck = { status: HealthStatus.UNHEALTHY, timestamp: new Date() };
186
- return HealthStatus.UNHEALTHY;
187
- }
283
+ return this.transformChatResponse(response);
188
284
  }
189
285
 
190
286
  validateConfig(): ValidationResult {
@@ -208,10 +304,6 @@ export class AnthropicProviderService extends BaseAiProvider {
208
304
  return result;
209
305
  }
210
306
 
211
- protected getDefaultBaseURL(): string {
212
- return 'https://api.anthropic.com';
213
- }
214
-
215
307
  protected transformMessages(messages: any[]): any[] {
216
308
  return messages.map(msg => ({
217
309
  role: msg.role,
@@ -237,172 +329,4 @@ export class AnthropicProviderService extends BaseAiProvider {
237
329
  } : undefined
238
330
  };
239
331
  }
240
-
241
- private buildCommandPrompt(request: CommandRequest): string {
242
- let prompt = `请将以下自然语言描述转换为准确的终端命令:\n\n"${request.naturalLanguage}"\n\n`;
243
-
244
- if (request.context) {
245
- prompt += `当前环境:\n`;
246
- if (request.context.currentDirectory) {
247
- prompt += `- 当前目录:${request.context.currentDirectory}\n`;
248
- }
249
- if (request.context.operatingSystem) {
250
- prompt += `- 操作系统:${request.context.operatingSystem}\n`;
251
- }
252
- if (request.context.shell) {
253
- prompt += `- Shell:${request.context.shell}\n`;
254
- }
255
- }
256
-
257
- prompt += `\n请直接返回JSON格式:\n`;
258
- prompt += `{\n`;
259
- prompt += ` "command": "具体命令",\n`;
260
- prompt += ` "explanation": "命令解释",\n`;
261
- prompt += ` "confidence": 0.95\n`;
262
- prompt += `}\n`;
263
-
264
- return prompt;
265
- }
266
-
267
- private buildExplainPrompt(request: ExplainRequest): string {
268
- let prompt = `请详细解释以下终端命令:\n\n\`${request.command}\`\n\n`;
269
-
270
- if (request.context?.currentDirectory) {
271
- prompt += `当前目录:${request.context.currentDirectory}\n`;
272
- }
273
- if (request.context?.operatingSystem) {
274
- prompt += `操作系统:${request.context.operatingSystem}\n`;
275
- }
276
-
277
- prompt += `\n请按以下JSON格式返回:\n`;
278
- prompt += `{\n`;
279
- prompt += ` "explanation": "整体解释",\n`;
280
- prompt += ` "breakdown": [\n`;
281
- prompt += ` {"part": "命令部分", "description": "说明"}\n`;
282
- prompt += ` ],\n`;
283
- prompt += ` "examples": ["使用示例"]\n`;
284
- prompt += `}\n`;
285
-
286
- return prompt;
287
- }
288
-
289
- private buildAnalysisPrompt(request: AnalysisRequest): string {
290
- let prompt = `请分析以下命令执行结果:\n\n`;
291
- prompt += `命令:${request.command}\n`;
292
- prompt += `退出码:${request.exitCode}\n`;
293
- prompt += `输出:\n${request.output}\n\n`;
294
-
295
- if (request.context?.workingDirectory) {
296
- prompt += `工作目录:${request.context.workingDirectory}\n`;
297
- }
298
-
299
- prompt += `\n请按以下JSON格式返回:\n`;
300
- prompt += `{\n`;
301
- prompt += ` "summary": "结果总结",\n`;
302
- prompt += ` "insights": ["洞察1", "洞察2"],\n`;
303
- prompt += ` "success": true/false,\n`;
304
- prompt += ` "issues": [\n`;
305
- prompt += ` {"severity": "warning|error|info", "message": "问题描述", "suggestion": "建议"}\n`;
306
- prompt += ` ]\n`;
307
- prompt += `}\n`;
308
-
309
- return prompt;
310
- }
311
-
312
- private parseCommandResponse(content: string): CommandResponse {
313
- try {
314
- const match = content.match(/\{[\s\S]*\}/);
315
- if (match) {
316
- const parsed = JSON.parse(match[0]);
317
- return {
318
- command: parsed.command || '',
319
- explanation: parsed.explanation || '',
320
- confidence: parsed.confidence || 0.5
321
- };
322
- }
323
- } catch (error) {
324
- this.logger.warn('Failed to parse command response as JSON', error);
325
- }
326
-
327
- const lines = content.split('\n').map(l => l.trim()).filter(l => l);
328
- return {
329
- command: lines[0] || '',
330
- explanation: lines.slice(1).join(' ') || 'AI生成的命令',
331
- confidence: 0.5
332
- };
333
- }
334
-
335
- private parseExplainResponse(content: string): ExplainResponse {
336
- try {
337
- const match = content.match(/\{[\s\S]*\}/);
338
- if (match) {
339
- const parsed = JSON.parse(match[0]);
340
- return {
341
- explanation: parsed.explanation || '',
342
- breakdown: parsed.breakdown || [],
343
- examples: parsed.examples || []
344
- };
345
- }
346
- } catch (error) {
347
- this.logger.warn('Failed to parse explain response as JSON', error);
348
- }
349
-
350
- return {
351
- explanation: content,
352
- breakdown: []
353
- };
354
- }
355
-
356
- private parseAnalysisResponse(content: string): AnalysisResponse {
357
- try {
358
- const match = content.match(/\{[\s\S]*\}/);
359
- if (match) {
360
- const parsed = JSON.parse(match[0]);
361
- return {
362
- summary: parsed.summary || '',
363
- insights: parsed.insights || [],
364
- success: parsed.success !== false,
365
- issues: parsed.issues || []
366
- };
367
- }
368
- } catch (error) {
369
- this.logger.warn('Failed to parse analysis response as JSON', error);
370
- }
371
-
372
- return {
373
- summary: content,
374
- insights: [],
375
- success: true
376
- };
377
- }
378
-
379
- private getDefaultSystemPrompt(): string {
380
- return `你是一个专业的终端命令助手,运行在 Tabby 终端中。
381
-
382
- ## 核心能力
383
- 你可以通过以下工具直接操作终端:
384
- - write_to_terminal: 向终端写入并执行命令
385
- - read_terminal_output: 读取终端输出
386
- - get_terminal_list: 获取所有终端列表
387
- - get_terminal_cwd: 获取当前工作目录
388
- - focus_terminal: 切换到指定索引的终端(需要参数 terminal_index)
389
- - get_terminal_selection: 获取终端中选中的文本
390
-
391
- ## 重要规则
392
- 1. 当用户请求执行命令(如"查看当前目录"、"列出文件"等),你必须使用 write_to_terminal 工具来执行
393
- 2. **当用户请求切换终端(如"切换到终端0"、"打开终端4"等),你必须使用 focus_terminal 工具**
394
- 3. 不要只是描述你"将要做什么",而是直接调用工具执行
395
- 4. 执行命令后,使用 read_terminal_output 读取结果并报告给用户
396
- 5. 如果不确定当前目录或终端状态,先使用 get_terminal_cwd 或 get_terminal_list 获取信息
397
- 6. **永远不要假装执行了操作,必须真正调用工具**
398
-
399
- ## 示例
400
- 用户:"查看当前目录的文件"
401
- 正确做法:调用 write_to_terminal 工具,参数 { "command": "dir", "execute": true }
402
- 错误做法:仅回复文字"我将执行 dir 命令"
403
-
404
- 用户:"切换到终端4"
405
- 正确做法:调用 focus_terminal 工具,参数 { "terminal_index": 4 }
406
- 错误做法:仅回复文字"已切换到终端4"(不调用工具)`;
407
- }
408
332
  }