tabby-ai-assistant 1.0.11 → 1.0.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tabby-ai-assistant",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Tabby终端AI助手插件 - 支持多AI提供商(OpenAI、Anthropic、Minimax、GLM、Ollama、vLLM)",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -16,7 +16,7 @@ export interface AiSidebarConfig {
16
16
 
17
17
  /**
18
18
  * AI Sidebar 服务 - 管理 AI 聊天侧边栏的生命周期
19
- *
19
+ *
20
20
  * 采用 Flexbox 布局方式,将 sidebar 插入到 app-root 作为第一个子元素,
21
21
  * app-root 变为水平 flex 容器,sidebar 在左侧
22
22
  */
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
2
2
  import { Subject, Observable } from 'rxjs';
3
3
  import { LoggerService } from './logger.service';
4
4
  import { SecurityConfig } from '../../types/security.types';
5
- import { ProviderConfig } from '../../types/provider.types';
5
+ import { ProviderConfig, PROVIDER_DEFAULTS, ProviderConfigUtils } from '../../types/provider.types';
6
6
  import { ContextConfig } from '../../types/ai.types';
7
7
 
8
8
  /**
@@ -217,17 +217,9 @@ export class ConfigProviderService {
217
217
  if (contextWindow && contextWindow > 0) {
218
218
  return contextWindow;
219
219
  }
220
- // 返回供应商默认值
221
- const defaults: { [key: string]: number } = {
222
- 'openai': 128000,
223
- 'anthropic': 200000,
224
- 'minimax': 128000,
225
- 'glm': 128000,
226
- 'ollama': 8192,
227
- 'vllm': 8192,
228
- 'openai-compatible': 128000
229
- };
230
- return defaults[activeProvider] || 200000;
220
+ // 从统一默认值获取
221
+ const defaults = PROVIDER_DEFAULTS[activeProvider];
222
+ return defaults?.contextWindow || 200000;
231
223
  }
232
224
 
233
225
  /**
@@ -15,9 +15,79 @@ export class ToastService {
15
15
 
16
16
  show(type: ToastMessage['type'], message: string, duration = 3000): void {
17
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
+ // 发射事件(兼容现有订阅)
18
77
  this.toastSubject.next({ id, type, message, duration });
19
78
  }
20
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
+
21
91
  success(message: string, duration = 3000): void {
22
92
  this.show('success', message, duration);
23
93
  }
@@ -208,10 +208,6 @@ export class AnthropicProviderService extends BaseAiProvider {
208
208
  return result;
209
209
  }
210
210
 
211
- protected getDefaultBaseURL(): string {
212
- return 'https://api.anthropic.com';
213
- }
214
-
215
211
  protected transformMessages(messages: any[]): any[] {
216
212
  return messages.map(msg => ({
217
213
  role: msg.role,
@@ -237,172 +233,4 @@ export class AnthropicProviderService extends BaseAiProvider {
237
233
  } : undefined
238
234
  };
239
235
  }
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
236
  }
@@ -1,15 +1,15 @@
1
1
  import { Injectable } from '@angular/core';
2
2
  import { Observable } from 'rxjs';
3
- import { BaseAiProvider as IBaseAiProvider, ProviderConfig, AuthConfig, ProviderCapability, HealthStatus, ValidationResult } from '../../types/provider.types';
3
+ import { IBaseAiProvider, ProviderConfig, AuthConfig, ProviderCapability, HealthStatus, ValidationResult, ProviderInfo, PROVIDER_DEFAULTS } from '../../types/provider.types';
4
4
  import { ChatRequest, ChatResponse, CommandRequest, CommandResponse, ExplainRequest, ExplainResponse, AnalysisRequest, AnalysisResponse, StreamEvent } from '../../types/ai.types';
5
5
  import { LoggerService } from '../core/logger.service';
6
6
 
7
7
  /**
8
8
  * 基础AI提供商抽象类
9
- * 所有AI提供商都应该继承此类
9
+ * 所有AI提供商都应该实现此接口
10
10
  */
11
11
  @Injectable()
12
- export abstract class BaseAiProvider extends IBaseAiProvider {
12
+ export abstract class BaseAiProvider implements IBaseAiProvider {
13
13
  abstract readonly name: string;
14
14
  abstract readonly displayName: string;
15
15
  abstract readonly capabilities: ProviderCapability[];
@@ -19,9 +19,7 @@ export abstract class BaseAiProvider extends IBaseAiProvider {
19
19
  protected isInitialized = false;
20
20
  protected lastHealthCheck: { status: HealthStatus; timestamp: Date } | null = null;
21
21
 
22
- constructor(protected logger: LoggerService) {
23
- super();
24
- }
22
+ constructor(protected logger: LoggerService) {}
25
23
 
26
24
  /**
27
25
  * 配置提供商
@@ -135,7 +133,7 @@ export abstract class BaseAiProvider extends IBaseAiProvider {
135
133
  /**
136
134
  * 获取提供商信息
137
135
  */
138
- getInfo(): any {
136
+ getInfo(): ProviderInfo {
139
137
  return {
140
138
  name: this.name,
141
139
  displayName: this.displayName,
@@ -145,7 +143,7 @@ export abstract class BaseAiProvider extends IBaseAiProvider {
145
143
  authConfig: this.authConfig,
146
144
  supportedModels: this.config?.model ? [this.config.model] : [],
147
145
  configured: this.isInitialized,
148
- lastHealthCheck: this.lastHealthCheck
146
+ lastHealthCheck: this.lastHealthCheck ?? undefined
149
147
  };
150
148
  }
151
149
 
@@ -306,19 +304,40 @@ export abstract class BaseAiProvider extends IBaseAiProvider {
306
304
  * 获取基础URL
307
305
  */
308
306
  protected getBaseURL(): string {
309
- return this.config?.baseURL || this.getDefaultBaseURL();
307
+ if (this.config?.baseURL) {
308
+ return this.config.baseURL;
309
+ }
310
+ // 从统一默认值获取
311
+ const defaults = PROVIDER_DEFAULTS[this.name];
312
+ return defaults?.baseURL || '';
313
+ }
314
+
315
+ /**
316
+ * 获取默认模型
317
+ */
318
+ protected getDefaultModel(): string {
319
+ if (this.config?.model) {
320
+ return this.config.model;
321
+ }
322
+ // 从统一默认值获取
323
+ const defaults = PROVIDER_DEFAULTS[this.name];
324
+ return defaults?.model || 'default';
310
325
  }
311
326
 
312
327
  /**
313
- * 获取默认基础URL - 子类必须实现
328
+ * 获取默认超时时间
314
329
  */
315
- protected abstract getDefaultBaseURL(): string;
330
+ protected getDefaultTimeout(): number {
331
+ const defaults = PROVIDER_DEFAULTS[this.name];
332
+ return defaults?.timeout || 30000;
333
+ }
316
334
 
317
335
  /**
318
- * 获取默认模型 - 子类可以重写
336
+ * 获取默认重试次数
319
337
  */
320
- protected getDefaultModel(): string {
321
- return this.config?.model || 'default';
338
+ protected getDefaultRetries(): number {
339
+ const defaults = PROVIDER_DEFAULTS[this.name];
340
+ return defaults?.retries || 3;
322
341
  }
323
342
 
324
343
  /**
@@ -372,4 +391,196 @@ export abstract class BaseAiProvider extends IBaseAiProvider {
372
391
  }
373
392
  return 'Unknown error';
374
393
  }
394
+
395
+ // ==================== 通用命令处理方法 ====================
396
+
397
+ /**
398
+ * 构建命令生成提示 - 通用实现
399
+ */
400
+ protected buildCommandPrompt(request: CommandRequest): string {
401
+ let prompt = `请将以下自然语言描述转换为准确的终端命令:\n\n"${request.naturalLanguage}"\n\n`;
402
+
403
+ if (request.context) {
404
+ prompt += `当前环境:\n`;
405
+ if (request.context.currentDirectory) {
406
+ prompt += `- 当前目录:${request.context.currentDirectory}\n`;
407
+ }
408
+ if (request.context.operatingSystem) {
409
+ prompt += `- 操作系统:${request.context.operatingSystem}\n`;
410
+ }
411
+ if (request.context.shell) {
412
+ prompt += `- Shell:${request.context.shell}\n`;
413
+ }
414
+ }
415
+
416
+ prompt += `\n请直接返回JSON格式:\n`;
417
+ prompt += `{\n`;
418
+ prompt += ` "command": "具体命令",\n`;
419
+ prompt += ` "explanation": "命令解释",\n`;
420
+ prompt += ` "confidence": 0.95\n`;
421
+ prompt += `}\n`;
422
+
423
+ return prompt;
424
+ }
425
+
426
+ /**
427
+ * 构建命令解释提示 - 通用实现
428
+ */
429
+ protected buildExplainPrompt(request: ExplainRequest): string {
430
+ let prompt = `请详细解释以下终端命令:\n\n\`${request.command}\`\n\n`;
431
+
432
+ if (request.context?.currentDirectory) {
433
+ prompt += `当前目录:${request.context.currentDirectory}\n`;
434
+ }
435
+ if (request.context?.operatingSystem) {
436
+ prompt += `操作系统:${request.context.operatingSystem}\n`;
437
+ }
438
+
439
+ prompt += `\n请按以下JSON格式返回:\n`;
440
+ prompt += `{\n`;
441
+ prompt += ` "explanation": "整体解释",\n`;
442
+ prompt += ` "breakdown": [\n`;
443
+ prompt += ` {"part": "命令部分", "description": "说明"}\n`;
444
+ prompt += ` ],\n`;
445
+ prompt += ` "examples": ["使用示例"]\n`;
446
+ prompt += `}\n`;
447
+
448
+ return prompt;
449
+ }
450
+
451
+ /**
452
+ * 构建结果分析提示 - 通用实现
453
+ */
454
+ protected buildAnalysisPrompt(request: AnalysisRequest): string {
455
+ let prompt = `请分析以下命令执行结果:\n\n`;
456
+ prompt += `命令:${request.command}\n`;
457
+ prompt += `退出码:${request.exitCode}\n`;
458
+ prompt += `输出:\n${request.output}\n\n`;
459
+
460
+ if (request.context?.workingDirectory) {
461
+ prompt += `工作目录:${request.context.workingDirectory}\n`;
462
+ }
463
+
464
+ prompt += `\n请按以下JSON格式返回:\n`;
465
+ prompt += `{\n`;
466
+ prompt += ` "summary": "结果总结",\n`;
467
+ prompt += ` "insights": ["洞察1", "洞察2"],\n`;
468
+ prompt += ` "success": true/false,\n`;
469
+ prompt += ` "issues": [\n`;
470
+ prompt += ` {"severity": "warning|error|info", "message": "问题描述", "suggestion": "建议"}\n`;
471
+ prompt += ` ]\n`;
472
+ prompt += `}\n`;
473
+
474
+ return prompt;
475
+ }
476
+
477
+ /**
478
+ * 解析命令响应 - 通用实现
479
+ */
480
+ protected parseCommandResponse(content: string): CommandResponse {
481
+ try {
482
+ const match = content.match(/\{[\s\S]*\}/);
483
+ if (match) {
484
+ const parsed = JSON.parse(match[0]);
485
+ return {
486
+ command: parsed.command || '',
487
+ explanation: parsed.explanation || '',
488
+ confidence: parsed.confidence || 0.5
489
+ };
490
+ }
491
+ } catch (error) {
492
+ this.logger.warn('Failed to parse command response as JSON', error);
493
+ }
494
+
495
+ // 备用解析
496
+ const lines = content.split('\n').map(l => l.trim()).filter(l => l);
497
+ return {
498
+ command: lines[0] || '',
499
+ explanation: lines.slice(1).join(' ') || 'AI生成的命令',
500
+ confidence: 0.5
501
+ };
502
+ }
503
+
504
+ /**
505
+ * 解析解释响应 - 通用实现
506
+ */
507
+ protected parseExplainResponse(content: string): ExplainResponse {
508
+ try {
509
+ const match = content.match(/\{[\s\S]*\}/);
510
+ if (match) {
511
+ const parsed = JSON.parse(match[0]);
512
+ return {
513
+ explanation: parsed.explanation || '',
514
+ breakdown: parsed.breakdown || [],
515
+ examples: parsed.examples || []
516
+ };
517
+ }
518
+ } catch (error) {
519
+ this.logger.warn('Failed to parse explain response as JSON', error);
520
+ }
521
+
522
+ return {
523
+ explanation: content,
524
+ breakdown: []
525
+ };
526
+ }
527
+
528
+ /**
529
+ * 解析分析响应 - 通用实现
530
+ */
531
+ protected parseAnalysisResponse(content: string): AnalysisResponse {
532
+ try {
533
+ const match = content.match(/\{[\s\S]*\}/);
534
+ if (match) {
535
+ const parsed = JSON.parse(match[0]);
536
+ return {
537
+ summary: parsed.summary || '',
538
+ insights: parsed.insights || [],
539
+ success: parsed.success !== false,
540
+ issues: parsed.issues || []
541
+ };
542
+ }
543
+ } catch (error) {
544
+ this.logger.warn('Failed to parse analysis response as JSON', error);
545
+ }
546
+
547
+ return {
548
+ summary: content,
549
+ insights: [],
550
+ success: true
551
+ };
552
+ }
553
+
554
+ /**
555
+ * 获取默认系统提示 - 子类可重写
556
+ */
557
+ protected getDefaultSystemPrompt(): string {
558
+ return `你是一个专业的终端命令助手,运行在 Tabby 终端中。
559
+
560
+ ## 核心能力
561
+ 你可以通过以下工具直接操作终端:
562
+ - write_to_terminal: 向终端写入并执行命令
563
+ - read_terminal_output: 读取终端输出
564
+ - get_terminal_list: 获取所有终端列表
565
+ - get_terminal_cwd: 获取当前工作目录
566
+ - focus_terminal: 切换到指定索引的终端(需要参数 terminal_index)
567
+ - get_terminal_selection: 获取终端中选中的文本
568
+
569
+ ## 重要规则
570
+ 1. 当用户请求执行命令(如"查看当前目录"、"列出文件"等),你必须使用 write_to_terminal 工具来执行
571
+ 2. **当用户请求切换终端(如"切换到终端0"、"打开终端4"等),你必须使用 focus_terminal 工具**
572
+ 3. 不要只是描述你"将要做什么",而是直接调用工具执行
573
+ 4. 执行命令后,使用 read_terminal_output 读取结果并报告给用户
574
+ 5. 如果不确定当前目录或终端状态,先使用 get_terminal_cwd 或 get_terminal_list 获取信息
575
+ 6. **永远不要假装执行了操作,必须真正调用工具**
576
+
577
+ ## 示例
578
+ 用户:"查看当前目录的文件"
579
+ 正确做法:调用 write_to_terminal 工具,参数 { "command": "dir", "execute": true }
580
+ 错误做法:仅回复文字"我将执行 dir 命令"
581
+
582
+ 用户:"切换到终端4"
583
+ 正确做法:调用 focus_terminal 工具,参数 { "terminal_index": 4 }
584
+ 错误做法:仅回复文字"已切换到终端4"(不调用工具)`;
585
+ }
375
586
  }