auto-coder 0.1.287__py3-none-any.whl → 0.1.289__py3-none-any.whl

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.

Potentially problematic release.


This version of auto-coder might be problematic. Click here for more details.

@@ -0,0 +1,1123 @@
1
+ """
2
+ Plugin system for Chat Auto Coder.
3
+ This module provides the base classes and functionality for creating and managing plugins.
4
+ """
5
+
6
+ import importlib
7
+ import inspect
8
+ import os
9
+ import sys
10
+ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union
11
+ from autocoder.plugins.utils import load_json_file, save_json_file
12
+
13
+
14
+ class Plugin:
15
+ """Base class for all plugins."""
16
+
17
+ name: str = "base_plugin" # 插件名称
18
+ description: str = "Base plugin class" # 插件描述
19
+ version: str = "0.1.0" # 插件版本
20
+ manager: "PluginManager" # 插件管理器
21
+ dynamic_cmds: List[str] = [] # 需要动态补全的命令列表
22
+
23
+ @classmethod
24
+ def id_name(cls) -> str:
25
+ """返回插件的唯一标识符,包括插件目录和插件文件名"""
26
+ return f"{cls.__module__}.{cls.__name__}"
27
+
28
+ @classmethod
29
+ def plugin_name(cls) -> str:
30
+ """返回插件的名称,不包括插件目录和插件文件名"""
31
+ return cls.__name__
32
+
33
+ def __init__(
34
+ self,
35
+ manager: "PluginManager",
36
+ config: Optional[Dict[str, Any]] = None,
37
+ config_path: Optional[str] = None,
38
+ ):
39
+ """Initialize the plugin.
40
+
41
+ Args:
42
+ manager: The plugin manager instance
43
+ config: Optional configuration dictionary for the plugin
44
+ config_path: Optional path to the configuration file
45
+ """
46
+ self.config = config or {}
47
+ self.config_path = config_path
48
+ self.manager = manager
49
+
50
+ def initialize(self) -> bool:
51
+ """Initialize the plugin.
52
+
53
+ This method is called after the plugin instance is created but before
54
+ it is registered with the plugin manager. Override this method to
55
+ perform any initialization tasks.
56
+
57
+ Returns:
58
+ True if initialization was successful, False otherwise
59
+ """
60
+ return True
61
+
62
+ def get_commands(self) -> Dict[str, Tuple[Callable, str]]:
63
+ """Get the commands provided by this plugin.
64
+
65
+ Returns:
66
+ A dictionary mapping command names to (handler, description) tuples
67
+ """
68
+ return {}
69
+
70
+ def get_keybindings(self) -> List[Tuple[str, Callable, str]]:
71
+ """Get the keybindings provided by this plugin.
72
+
73
+ Returns:
74
+ A list of (key_combination, handler, description) tuples
75
+ """
76
+ return []
77
+
78
+ def get_completions(self) -> Dict[str, List[str]]:
79
+ """Get the completions provided by this plugin.
80
+
81
+ Returns:
82
+ A dictionary mapping command prefixes to lists of completion options
83
+ """
84
+ return {}
85
+
86
+ def load_config(self, config_path: Optional[str] = None) -> bool:
87
+ """加载插件配置。
88
+
89
+ Args:
90
+ config_path: 配置文件路径。如果提供,将覆盖插件的 config_path 属性。
91
+
92
+ Returns:
93
+ 加载成功返回 True,否则返回 False
94
+ """
95
+ # 如果提供了新的配置路径,则更新 self.config_path
96
+ if config_path:
97
+ self.config_path = config_path
98
+
99
+ # 如果没有配置路径,则无法加载
100
+ if not self.config_path:
101
+ return False
102
+
103
+ # 尝试从文件加载配置
104
+ try:
105
+ import json
106
+ import os
107
+
108
+ if os.path.exists(self.config_path):
109
+ with open(self.config_path, "r", encoding="utf-8") as f:
110
+ self.config = json.load(f)
111
+ return True
112
+ else:
113
+ # 配置文件不存在,但路径有效,视为成功
114
+ return True
115
+ except Exception as e:
116
+ print(f"Error loading plugin config from {self.config_path}: {e}")
117
+ return False
118
+
119
+ def export_config(
120
+ self, config_path: Optional[str] = None
121
+ ) -> Optional[Dict[str, Any]]:
122
+ """导出插件配置,用于持久化存储。
123
+
124
+ 默认实现会返回插件的 self.config 属性,并将配置保存到 self.config_path 或提供的 config_path。
125
+ 子类可以覆盖此方法,以提供自定义的配置导出逻辑。
126
+
127
+ Args:
128
+ config_path: 配置文件保存路径。如果提供,将覆盖插件的 config_path 属性。
129
+
130
+ Returns:
131
+ 包含插件配置的字典,如果没有配置需要导出则返回 None
132
+ """
133
+ # 如果没有配置,则返回 None
134
+ if not self.config:
135
+ return None
136
+
137
+ # 更新配置路径(如果提供)
138
+ if config_path:
139
+ self.config_path = config_path
140
+
141
+ # 通过插件管理器获取插件配置路径
142
+ config_path = self.manager.get_plugin_config_path(self.id_name())
143
+ if config_path:
144
+ self.config_path = config_path
145
+
146
+ # 如果有配置路径,则保存至文件
147
+ if self.config_path:
148
+ try:
149
+ import json
150
+ import os
151
+
152
+ # 确保目录存在
153
+ os.makedirs(
154
+ os.path.dirname(os.path.abspath(self.config_path)), exist_ok=True
155
+ )
156
+
157
+ # 保存配置到文件
158
+ with open(self.config_path, "w", encoding="utf-8") as f:
159
+ json.dump(self.config, f, indent=2, ensure_ascii=False)
160
+ except Exception as e:
161
+ print(f"Error saving plugin config to {self.config_path}: {e}")
162
+
163
+ return self.config
164
+
165
+ def get_dynamic_completions(
166
+ self, command: str, current_input: str
167
+ ) -> List[Tuple[str, str]]:
168
+ """Get dynamic completions based on the current command context.
169
+
170
+ This method provides context-aware completions for commands that need
171
+ dynamic options based on the current state or user input.
172
+
173
+ Args:
174
+ command: The base command (e.g., "/example select")
175
+ current_input: The full current input including the command
176
+
177
+ Returns:
178
+ A list of tuples containing (completion_text, display_text)
179
+ """
180
+ return []
181
+
182
+ def intercept_command(
183
+ self, command: str, args: str
184
+ ) -> Tuple[bool, Optional[str], Optional[str]]:
185
+ """Intercept a command before it's processed.
186
+
187
+ Args:
188
+ command: The command name (without the /)
189
+ args: The command arguments
190
+
191
+ Returns:
192
+ A tuple of (should_continue, modified_command, modified_args)
193
+ If should_continue is False, the original command processing will be skipped
194
+ """
195
+ return True, command, args
196
+
197
+ def intercept_function(
198
+ self, func_name: str, args: List[Any], kwargs: Dict[str, Any]
199
+ ) -> Tuple[bool, List[Any], Dict[str, Any]]:
200
+ """Intercept a function call before it's executed.
201
+
202
+ Args:
203
+ func_name: The name of the function
204
+ args: The positional arguments
205
+ kwargs: The keyword arguments
206
+
207
+ Returns:
208
+ A tuple of (should_continue, modified_args, modified_kwargs)
209
+ If should_continue is False, the original function call will be skipped
210
+ """
211
+ return True, args, kwargs
212
+
213
+ def post_function(self, func_name: str, result: Any) -> Any:
214
+ """Process a function result after it's executed.
215
+
216
+ Args:
217
+ func_name: The name of the function
218
+ result: The result of the function call
219
+
220
+ Returns:
221
+ The possibly modified result
222
+ """
223
+ return result
224
+
225
+ def shutdown(self) -> None:
226
+ """Shutdown the plugin. Called when the application is exiting."""
227
+ pass
228
+
229
+
230
+ class PluginManager:
231
+ """Manages plugins for the Chat Auto Coder."""
232
+
233
+ def __init__(self):
234
+ """Initialize the plugin manager."""
235
+ self.plugins: Dict[str, Plugin] = {}
236
+ self.command_handlers: Dict[str, Tuple[Callable, str, str]] = (
237
+ {}
238
+ ) # command -> (handler, description, plugin_name)
239
+ self.intercepted_functions: Dict[str, List[str]] = (
240
+ {}
241
+ ) # function_name -> [plugin_names]
242
+ self.global_plugin_dirs: List[str] = []
243
+ self.plugin_dirs: List[str] = []
244
+ self._discover_plugins_cache: List[Type[Plugin]] = None # type: ignore
245
+
246
+ # built-in commands
247
+ self._builtin_commands = [
248
+ "/plugins",
249
+ "/plugins/dirs",
250
+ ]
251
+ # 内置的动态命令列表
252
+ self._builtin_dynamic_cmds = [
253
+ "/plugins /load",
254
+ "/plugins /unload",
255
+ "/plugins/dirs /add",
256
+ "/plugins/dirs /remove",
257
+ "/plugins/dirs /clear",
258
+ ]
259
+
260
+ @property
261
+ def cached_discover_plugins(self) -> List[Type[Plugin]]:
262
+ if self._discover_plugins_cache is None:
263
+ self._discover_plugins_cache = self.discover_plugins()
264
+ return self._discover_plugins_cache
265
+
266
+ def load_global_plugin_dirs(self) -> None:
267
+ """Read global plugin dirs from ~/.auto-coder/plugins/global_plugin_dirs"""
268
+ global_plugin_dirs_path = os.path.expanduser("~/.auto-coder/plugins/global_plugin_dirs")
269
+ if os.path.exists(global_plugin_dirs_path):
270
+ with open(global_plugin_dirs_path, "r", encoding="utf-8") as f:
271
+ for line in f:
272
+ ok, msg = self.add_global_plugin_directory(line.strip())
273
+ if not ok:
274
+ print(f"🚫 Error adding global plugin directory: {msg}")
275
+
276
+ def save_global_plugin_dirs(self) -> None:
277
+ """Save global plugin dirs to ~/.auto-coder/plugins/global_plugin_dirs"""
278
+ global_plugin_dirs_path = os.path.expanduser("~/.auto-coder/plugins/global_plugin_dirs")
279
+ # 确保目录存在
280
+ os.makedirs(os.path.dirname(global_plugin_dirs_path), exist_ok=True)
281
+ with open(global_plugin_dirs_path, "w", encoding="utf-8") as f:
282
+ for plugin_dir in self.global_plugin_dirs:
283
+ f.write(plugin_dir + "\n")
284
+ print(f"Saved global plugin dirs to {global_plugin_dirs_path}")
285
+
286
+ def add_global_plugin_directory(self, directory: str) -> Tuple[bool, str]:
287
+ """Add a directory to search for plugins.
288
+
289
+ Args:
290
+ directory: The directory path
291
+
292
+ Returns:
293
+ Tuple of (success: bool, message: str)
294
+ """
295
+ normalized_dir = os.path.abspath(os.path.normpath(directory))
296
+ if os.path.isdir(normalized_dir):
297
+ if normalized_dir not in self.global_plugin_dirs:
298
+ self.global_plugin_dirs.append(normalized_dir)
299
+ if normalized_dir not in sys.path:
300
+ sys.path.append(normalized_dir)
301
+ self._discover_plugins_cache = None # type: ignore
302
+ self.save_global_plugin_dirs()
303
+ return True, f"Added global directory: {normalized_dir}"
304
+ return False, f"Invalid directory: {normalized_dir}"
305
+
306
+ def add_plugin_directory(self, directory: str) -> Tuple[bool, str]:
307
+ """Add a directory to search for plugins.
308
+
309
+ Args:
310
+ directory: The directory path
311
+
312
+ Returns:
313
+ Tuple of (success: bool, message: str)
314
+ """
315
+ normalized_dir = os.path.abspath(os.path.normpath(directory))
316
+ if os.path.isdir(normalized_dir):
317
+ if normalized_dir not in self.plugin_dirs and normalized_dir not in self.global_plugin_dirs:
318
+ self.plugin_dirs.append(normalized_dir)
319
+ if normalized_dir not in sys.path:
320
+ sys.path.append(normalized_dir)
321
+ self._discover_plugins_cache = None # type: ignore
322
+ return True, f"Added directory: {normalized_dir}"
323
+ return False, f"Directory already exists: {normalized_dir}"
324
+ return False, f"Invalid directory: {normalized_dir}"
325
+
326
+ def remove_plugin_directory(self, directory: str) -> str:
327
+ """Remove a plugin directory.
328
+
329
+ Args:
330
+ directory: The directory path to remove
331
+
332
+ Returns:
333
+ Result message
334
+ """
335
+ normalized_dir = os.path.normpath(directory)
336
+ if normalized_dir in self.plugin_dirs:
337
+ self.plugin_dirs.remove(normalized_dir)
338
+ if normalized_dir in sys.path:
339
+ sys.path.remove(normalized_dir)
340
+ self._discover_plugins_cache = None # type: ignore
341
+ return f"Removed directory: {normalized_dir}"
342
+ return f"Directory not found: {normalized_dir}"
343
+
344
+ def clear_plugin_directories(self) -> str:
345
+ """Clear all plugin directories.
346
+
347
+ Returns:
348
+ Result message
349
+ """
350
+ count = len(self.plugin_dirs)
351
+ for directory in self.plugin_dirs:
352
+ if directory in sys.path:
353
+ sys.path.remove(directory)
354
+ self.plugin_dirs.clear()
355
+ self._discover_plugins_cache = None # type: ignore
356
+ return f"Cleared all directories ({count} removed)"
357
+
358
+ def discover_plugins(self) -> List[Type[Plugin]]:
359
+ """Discover available plugins in the plugin directories.
360
+
361
+ Returns:
362
+ A list of plugin classes
363
+ """
364
+ discovered_plugins: List[Type[Plugin]] = []
365
+
366
+ plugin_dirs = set(self.plugin_dirs)
367
+ plugin_dirs.update(self.global_plugin_dirs)
368
+
369
+ for plugin_dir in plugin_dirs:
370
+ for filename in os.listdir(plugin_dir):
371
+ if filename.endswith(".py") and not filename.startswith("_"):
372
+ module_name = filename[:-3]
373
+ try:
374
+ # Fully qualify module name with path information
375
+ plugin_dir_basename = os.path.basename(plugin_dir)
376
+ parent_dir = os.path.basename(os.path.dirname(plugin_dir))
377
+
378
+ if (
379
+ parent_dir == "autocoder"
380
+ and plugin_dir_basename == "plugins"
381
+ ):
382
+ # For built-in plugins
383
+ full_module_name = f"autocoder.plugins.{module_name}"
384
+ else:
385
+ # For external plugins
386
+ full_module_name = module_name
387
+
388
+ # Try to import using the determined module name
389
+ try:
390
+ module = importlib.import_module(full_module_name)
391
+ except ImportError:
392
+ # Fallback to direct module name
393
+ module = importlib.import_module(module_name)
394
+
395
+ for name, obj in inspect.getmembers(module):
396
+ if (
397
+ inspect.isclass(obj)
398
+ and issubclass(obj, Plugin)
399
+ and obj is not Plugin
400
+ ):
401
+ discovered_plugins.append(obj)
402
+ except Exception as e:
403
+ print(f"Error loading plugin module {module_name}: {e}")
404
+
405
+ return discovered_plugins
406
+
407
+ def load_plugin(
408
+ self, plugin_class: Type[Plugin], config: Optional[Dict[str, Any]] = None
409
+ ) -> bool:
410
+ """Load and initialize a plugin.
411
+
412
+ Args:
413
+ plugin_class: The plugin class to load
414
+ config: Optional configuration for the plugin
415
+
416
+ Returns:
417
+ True if the plugin was loaded successfully, False otherwise
418
+ """
419
+ try:
420
+ # 获取插件名称和插件ID
421
+ # plugin_name = plugin_class.plugin_name()
422
+ plugin_id = plugin_class.id_name()
423
+
424
+ # 获取插件配置路径
425
+ config_path = self.get_plugin_config_path(plugin_id)
426
+
427
+ # 创建插件实例,传入 manager(self) 作为第一个参数
428
+ plugin = plugin_class(self, config, config_path)
429
+
430
+ # 如果未提供配置但配置路径存在,尝试加载配置
431
+ if not config and config_path:
432
+ plugin.load_config()
433
+
434
+ # 调用插件的 initialize 方法,只有成功初始化的插件才会被添加
435
+ if not plugin.initialize():
436
+ print(f"Plugin {plugin_id} initialization failed")
437
+ return False
438
+
439
+ # 将插件添加到已加载插件字典中
440
+ self.plugins[plugin_id] = plugin
441
+
442
+ # Register commands
443
+ for cmd, (handler, desc) in plugin.get_commands().items():
444
+ self.command_handlers[f"/{cmd}"] = (handler, desc, plugin_id)
445
+
446
+ return True
447
+ except Exception as e:
448
+ print(f"Error loading plugin {plugin_class.__name__}: {e}")
449
+ return False
450
+
451
+ def load_plugins_from_config(self, config: Dict[str, Any]) -> None:
452
+ """Load plugins based on configuration.
453
+
454
+ Args:
455
+ config: Configuration dictionary with plugin settings
456
+ """
457
+ if "plugin_dirs" in config:
458
+ for directory in config["plugin_dirs"]:
459
+ self.add_plugin_directory(directory)
460
+
461
+ if "plugins" in config:
462
+ discovered_plugins = {p.id_name(): p for p in self.cached_discover_plugins}
463
+
464
+ for plugin_id in config["plugins"]:
465
+ if plugin_id in discovered_plugins:
466
+ self.load_plugin(discovered_plugins[plugin_id])
467
+
468
+ def get_plugin(self, name: str) -> Optional[Plugin]:
469
+ """Get a plugin by name or full class name.
470
+
471
+ Args:
472
+ name: The name or full class name of the plugin
473
+
474
+ Returns:
475
+ The plugin instance, or None if not found
476
+ """
477
+ # 直接通过全类名查找 (优先), name 是 plugin_id
478
+ if name in self.plugins:
479
+ return self.plugins[name]
480
+
481
+ # 如果没找到,尝试通过简单名称查找
482
+ for plugin in self.plugins.values():
483
+ if plugin.plugin_name() == name or plugin.name == name:
484
+ return plugin
485
+
486
+ return None
487
+
488
+ def process_command(
489
+ self, full_command: str
490
+ ) -> Optional[Tuple[str, Optional[Callable], List[str]]]:
491
+ """Process a command, allowing plugins to intercept it.
492
+
493
+ Args:
494
+ full_command: The full command string including the /
495
+
496
+ Returns:
497
+ A tuple of (plugin_name, handler, args) if a plugin should handle the command,
498
+ None if it should be handled by the main program
499
+ """
500
+ if not full_command.startswith("/"):
501
+ return None
502
+
503
+ parts = full_command.split(maxsplit=1)
504
+ command = parts[0]
505
+ args = parts[1] if len(parts) > 1 else ""
506
+
507
+ # Check if any plugin wants to intercept this command
508
+ command_without_slash = command[1:] if command.startswith("/") else command
509
+
510
+ for plugin_name, plugin in self.plugins.items():
511
+ should_continue, modified_command, modified_args = plugin.intercept_command(
512
+ command_without_slash, args
513
+ )
514
+ if not should_continue:
515
+ if modified_command and modified_command in self.command_handlers:
516
+ handler, _, handler_plugin = self.command_handlers[modified_command]
517
+ return (
518
+ handler_plugin,
519
+ handler,
520
+ [modified_args] if modified_args is not None else [""],
521
+ )
522
+ return plugin_name, None, [modified_command or "", modified_args or ""]
523
+
524
+ # Check if this is a registered plugin command
525
+ if command in self.command_handlers:
526
+ handler, _, plugin_name = self.command_handlers[command]
527
+ return plugin_name, handler, [args]
528
+
529
+ return None
530
+
531
+ def wrap_function(self, original_func: Callable, func_name: str) -> Callable:
532
+ """Wrap a function to allow plugins to intercept it.
533
+
534
+ Args:
535
+ original_func: The original function
536
+ func_name: The name of the function
537
+
538
+ Returns:
539
+ A wrapped function that allows plugin interception
540
+ """
541
+
542
+ def wrapped(*args, **kwargs):
543
+ # Pre-processing by plugins
544
+ should_continue = True
545
+ for plugin_name in self.intercepted_functions.get(func_name, []):
546
+ plugin = self.plugins[plugin_name]
547
+ plugin_continue, args, kwargs = plugin.intercept_function(
548
+ func_name, list(args), kwargs
549
+ )
550
+ should_continue = should_continue and plugin_continue
551
+
552
+ # Execute the original function if no plugin cancelled it
553
+ if should_continue:
554
+ result = original_func(*args, **kwargs)
555
+
556
+ # Post-processing by plugins
557
+ for plugin_name in self.intercepted_functions.get(func_name, []):
558
+ plugin = self.plugins[plugin_name]
559
+ result = plugin.post_function(func_name, result)
560
+
561
+ return result
562
+ return None
563
+
564
+ return wrapped
565
+
566
+ def register_function_interception(self, plugin_name: str, func_name: str) -> None:
567
+ """Register a plugin's interest in intercepting a function.
568
+
569
+ Args:
570
+ plugin_name: The name of the plugin
571
+ func_name: The name of the function to intercept
572
+ """
573
+ if func_name not in self.intercepted_functions:
574
+ self.intercepted_functions[func_name] = []
575
+
576
+ if plugin_name not in self.intercepted_functions[func_name]:
577
+ self.intercepted_functions[func_name].append(plugin_name)
578
+
579
+ def get_all_commands(self) -> Dict[str, Tuple[str, str]]:
580
+ """Get all commands from all plugins.
581
+
582
+ Returns:
583
+ A dictionary mapping command names to (description, plugin_name) tuples
584
+ """
585
+ return {
586
+ cmd: (desc, plugin)
587
+ for cmd, (_, desc, plugin) in self.command_handlers.items()
588
+ }
589
+
590
+ def get_all_commands_with_prefix(self, prefix: str) :
591
+ """Get all commands from all plugins and built-in commands match the prefix.
592
+
593
+ Args:
594
+ prefix: The prefix to match
595
+
596
+ Returns:
597
+ A list of command names
598
+ """
599
+ # check prefix of built-in commands + plugin commands
600
+ for cmd in (self._builtin_commands + list(self.command_handlers.keys())):
601
+ if cmd.startswith(prefix):
602
+ yield cmd
603
+
604
+ def get_plugin_completions(self) -> Dict[str, List[str]]:
605
+ """Get command completions from all plugins.
606
+
607
+ Returns:
608
+ A dictionary mapping command prefixes to lists of completion options
609
+ """
610
+ completions = {
611
+ "/plugins": ["/list", "/load", "/unload"],
612
+ "/plugins/dirs": ["/add", "/remove", "/clear"],
613
+ }
614
+
615
+ # Get completions from plugins
616
+ for plugin in self.plugins.values():
617
+ plugin_completions = plugin.get_completions()
618
+ for prefix, options in plugin_completions.items():
619
+ if prefix not in completions:
620
+ completions[prefix] = []
621
+ completions[prefix].extend(options)
622
+ return completions
623
+
624
+ def get_dynamic_completions(
625
+ self, command: str, current_input: str
626
+ ) -> List[Tuple[str, str]]:
627
+ """Get dynamic completions based on the current command context.
628
+
629
+ This method provides context-aware completions for commands that need
630
+ dynamic options based on the current state or user input.
631
+
632
+ Args:
633
+ command: The base command (e.g., "/plugins /load")
634
+ current_input: The full current input including the command
635
+
636
+ Returns:
637
+ A list of tuples containing (completion_text, display_text)
638
+ """
639
+ # print(f'command: {command}')
640
+
641
+ command = command.strip()
642
+ completions = self._get_manager_dynamic_completions(command, current_input)
643
+
644
+ # 检查是否有插件提供了此命令的动态补全
645
+ for plugin in self.plugins.values():
646
+ # 检查插件是否有 dynamic_completions
647
+ plugin_completions = plugin.get_dynamic_completions(command, current_input)
648
+ if plugin_completions:
649
+ completions.extend(plugin_completions)
650
+
651
+ return completions
652
+
653
+ def _get_manager_dynamic_completions(self, command: str, current_input: str) -> List[Tuple[str, str]]:
654
+ """获取插件管理器的动态补全。
655
+
656
+ Args:
657
+ command: 当前命令
658
+ current_input: 当前输入
659
+ """
660
+ # Split the input to analyze command parts
661
+ parts = current_input.split(maxsplit=2)
662
+ completions = []
663
+
664
+ # Handle built-in /plugins subcommands
665
+ if command == "/plugins /load":
666
+ # 提供可用插件列表作为补全选项
667
+ plugin_prefix = ""
668
+ if len(parts) > 2:
669
+ plugin_prefix = parts[2]
670
+
671
+ # 获取所有可用的插件
672
+ discovered_plugins = self.cached_discover_plugins
673
+
674
+ # 记录已经添加的显示名称,避免重复
675
+ added_display_names = set()
676
+
677
+ # 过滤出与前缀匹配的插件名称
678
+ for plugin_class in discovered_plugins:
679
+ plugin_name = plugin_class.name
680
+ plugin_class_name = plugin_class.plugin_name()
681
+ display_name = f"{plugin_class_name} ({plugin_name})"
682
+
683
+ # 首先尝试匹配插件短名称
684
+ if (
685
+ plugin_name.startswith(plugin_prefix)
686
+ and display_name not in added_display_names
687
+ ):
688
+ completions.append((plugin_name, display_name))
689
+ added_display_names.add(display_name)
690
+ # 如果类名与短名称不同,也尝试匹配类名
691
+ elif (
692
+ plugin_class_name.startswith(plugin_prefix)
693
+ and plugin_class_name != plugin_name
694
+ and display_name not in added_display_names
695
+ ):
696
+ completions.append((plugin_class_name, display_name))
697
+ added_display_names.add(display_name)
698
+
699
+ elif command == "/plugins /unload":
700
+ # 提供已加载插件列表作为补全选项
701
+ plugin_prefix = ""
702
+ if len(parts) > 2:
703
+ plugin_prefix = parts[2]
704
+
705
+ # 记录已经添加的显示名称,避免重复
706
+ added_display_names = set()
707
+
708
+ # 获取所有已加载的插件
709
+ for plugin_id, plugin in self.plugins.items():
710
+ plugin_name = plugin.name
711
+ plugin_class_name = plugin.plugin_name()
712
+ display_name = f"{plugin_class_name} ({plugin_name})"
713
+
714
+ # 首先尝试匹配插件短名称
715
+ if (
716
+ plugin_name.startswith(plugin_prefix)
717
+ and display_name not in added_display_names
718
+ ):
719
+ completions.append((plugin_name, display_name))
720
+ added_display_names.add(display_name)
721
+ # 如果类名与短名称不同,也尝试匹配类名
722
+ elif (
723
+ plugin_class_name.startswith(plugin_prefix)
724
+ and plugin_class_name != plugin_name
725
+ and display_name not in added_display_names
726
+ ):
727
+ completions.append((plugin_class_name, display_name))
728
+ added_display_names.add(display_name)
729
+
730
+ elif command == "/plugins/dirs /add":
731
+ # 如果没有前缀,从当前目录开始补全
732
+ prefix = ""
733
+ if len(parts) > 2:
734
+ prefix = " ".join(parts[2:])
735
+ prefix = prefix.strip()
736
+
737
+ # 获取搜索目标目录,如果 prefix 为空,则从当前目录开始搜索
738
+ target_dir = "."
739
+ if prefix:
740
+ target_dir = os.path.dirname(prefix)
741
+ # 获取文件名前缀
742
+ file_prefix = os.path.basename(prefix) if prefix else ""
743
+ # 如果父目录存在,列出其内容
744
+ if os.path.isdir(target_dir):
745
+ for entry in os.listdir(target_dir):
746
+ full_path = os.path.join(target_dir, entry)
747
+ if os.path.isdir(full_path) and entry.startswith(file_prefix):
748
+ completions.append((full_path, entry))
749
+
750
+ elif command == "/plugins/dirs /remove":
751
+ # 如果没有前缀,显示所有插件目录
752
+ prefix = ""
753
+ if len(parts) > 3:
754
+ prefix = " ".join(parts[3:])
755
+
756
+ if not prefix:
757
+ for directory in self.plugin_dirs:
758
+ completions.append((directory, directory))
759
+ else:
760
+ # 如果有前缀,过滤匹配的目录
761
+ for directory in self.plugin_dirs:
762
+ if directory.startswith(prefix):
763
+ # 如果目录以 prefix 开头,添加到补全列表
764
+ completions.append((directory, directory))
765
+
766
+ return completions
767
+
768
+ def register_dynamic_completion_provider(
769
+ self, plugin_name: str, command_prefixes: List[str]
770
+ ) -> None:
771
+ """注册一个插件作为特定命令的动态补全提供者。
772
+
773
+ Args:
774
+ plugin_name: 插件名称
775
+ command_prefixes: 需要提供动态补全的命令前缀列表
776
+ """
777
+ # 此功能可以在未来拓展,例如维护一个映射
778
+ # 从命令前缀到能够提供其动态补全的插件
779
+ pass
780
+
781
+ def project_root(self) -> Optional[str]:
782
+ """检查当前是否在项目根目录。如果是,返回目录,否则返回None"""
783
+ current_dir = os.getcwd()
784
+ _root = os.path.join(current_dir, ".auto-coder")
785
+ if os.path.exists(_root):
786
+ return _root
787
+ return None
788
+
789
+ def load_runtime_cfg(self) -> None:
790
+ """从项目根目录加载运行时配置。
791
+
792
+ 只加载插件目录和插件列表信息,具体配置由插件自行加载。
793
+ """
794
+ # 检查是否有项目根目录
795
+ project_root = self.project_root()
796
+ if not project_root:
797
+ return
798
+ # 加载插件列表和目录
799
+ plugins_json_path = os.path.join(project_root, "plugins.json")
800
+ if not os.path.exists(plugins_json_path):
801
+ return
802
+
803
+ try:
804
+ config = load_json_file(plugins_json_path)
805
+
806
+ # 添加插件目录
807
+ if "plugin_dirs" in config:
808
+ for directory in config["plugin_dirs"]:
809
+ if os.path.isdir(directory):
810
+ self.add_plugin_directory(directory)
811
+
812
+ # 加载插件 - 在 load_plugin 方法中会自动加载插件的配置
813
+ if "plugins" in config:
814
+ discovered_plugins = {
815
+ p.id_name(): p for p in self.cached_discover_plugins
816
+ }
817
+
818
+ for plugin_id in config["plugins"]:
819
+ if (
820
+ plugin_id in discovered_plugins
821
+ and plugin_id not in self.plugins
822
+ ):
823
+ # 加载插件 - 配置会在 load_plugin 方法中处理
824
+ self.load_plugin(discovered_plugins[plugin_id])
825
+ # print(f"Successfully loaded plugins: {list(self.plugins.keys())}")
826
+ except Exception as e:
827
+ print(f"Error loading plugin configuration: {e}")
828
+
829
+ def save_runtime_cfg(self) -> None:
830
+ """将当前插件配置保存到运行时配置文件。
831
+
832
+ 只保存插件目录和插件列表信息,具体配置由插件自行保存。
833
+ """
834
+ # 检查是否有项目根目录
835
+ project_root = self.project_root()
836
+ if not project_root:
837
+ return
838
+
839
+ # 保存插件目录和加载的插件列表
840
+ plugins_json_path = os.path.join(project_root, "plugins.json")
841
+ config = {"plugin_dirs": self.plugin_dirs, "plugins": list(self.plugins.keys())}
842
+
843
+ try:
844
+ # 仅当有插件配置变化时保存
845
+ for plugin in self.plugins.values():
846
+ plugin.export_config()
847
+ save_json_file(plugins_json_path, config)
848
+ except Exception as e:
849
+ print(f"Error saving plugins list: {e}")
850
+
851
+ def shutdown_all(self) -> None:
852
+ """Shutdown all plugins."""
853
+ # 保存配置
854
+ self.save_runtime_cfg()
855
+ if not self.plugins:
856
+ return
857
+ for plugin in self.plugins.values():
858
+ try:
859
+ plugin.shutdown()
860
+ except Exception as e:
861
+ print(f"Error shutting down plugin {plugin.name}: {e}")
862
+ print("All plugins shutdown")
863
+
864
+ def handle_plugins_command(self, args: List[str]) -> str:
865
+ """处理 /plugins 命令。
866
+
867
+ 此方法处理插件的列出、加载和卸载等操作。
868
+
869
+ Args:
870
+ args: 命令参数列表,例如 ["list"]、["load", "plugin_name"] 等
871
+
872
+ Returns:
873
+ 命令的输出结果
874
+ """
875
+ import io
876
+
877
+ output = io.StringIO()
878
+
879
+ if not args:
880
+ # 列出所有已加载的插件
881
+ print("\033[1;34mLoaded Plugins:\033[0m", file=output)
882
+ for plugin_id, plugin in self.plugins.items():
883
+ print(
884
+ f" - {plugin.name} (v{plugin.version}): {plugin.description}",
885
+ file=output,
886
+ )
887
+
888
+ elif args[0] == "list":
889
+ # 列出所有可用的插件
890
+ discovered_plugins = self.discover_plugins()
891
+ print("\033[1;34mAvailable Plugins:\033[0m", file=output)
892
+ for plugin_class in discovered_plugins:
893
+ # 显示插件的短名称而不是完整ID
894
+ print(
895
+ f" - {plugin_class.plugin_name()} ({plugin_class.name}): {plugin_class.description}",
896
+ file=output,
897
+ )
898
+
899
+ elif args[0] == "/list":
900
+ # 列出所有可用的插件
901
+ discovered_plugins = self.discover_plugins()
902
+ print("\033[1;34mAvailable Plugins:\033[0m", file=output)
903
+ for plugin_class in discovered_plugins:
904
+ # 显示插件的短名称而不是完整ID
905
+ print(
906
+ f" - {plugin_class.plugin_name()} ({plugin_class.name}): {plugin_class.description}",
907
+ file=output,
908
+ )
909
+
910
+ elif args[0] == "load" or args[0] == "/load":
911
+ if len(args) <= 1:
912
+ print("Usage: /plugins /load <plugin_name>", file=output)
913
+ return output.getvalue()
914
+
915
+ # 加载特定的插件
916
+ plugin_name = args[1]
917
+ discovered_plugins = self.cached_discover_plugins
918
+
919
+ # 使用简短名称查找插件
920
+ found = False
921
+ for plugin_class in discovered_plugins:
922
+ if (
923
+ plugin_class.plugin_name() == plugin_name
924
+ or plugin_class.name == plugin_name
925
+ ):
926
+ if self.load_plugin(plugin_class):
927
+ print(
928
+ f"Plugin '{plugin_name}' loaded successfully", file=output
929
+ )
930
+ # 加载插件后已在 load_plugin 方法中保存配置
931
+ else:
932
+ print(f"Failed to load plugin '{plugin_name}'", file=output)
933
+ found = True
934
+ break
935
+
936
+ if not found:
937
+ print(f"Plugin '{plugin_name}' not found", file=output)
938
+
939
+ elif args[0] == "unload" or args[0] == "/unload":
940
+ if len(args) <= 1:
941
+ print("Usage: /plugins /unload <plugin_name>", file=output)
942
+ return output.getvalue()
943
+
944
+ # 卸载特定的插件
945
+ plugin_name = args[1]
946
+ found = False
947
+
948
+ # 使用简短名称查找插件
949
+ for plugin_id, plugin in list(self.plugins.items()):
950
+ if plugin.plugin_name() == plugin_name or plugin.name == plugin_name:
951
+ plugin = self.plugins.pop(plugin_id)
952
+ plugin.shutdown()
953
+ print(f"Plugin '{plugin_name}' unloaded", file=output)
954
+ # 卸载插件后保存配置
955
+ self.save_runtime_cfg()
956
+ found = True
957
+ break
958
+
959
+ if not found:
960
+ print(f"Plugin '{plugin_name}' not loaded", file=output)
961
+
962
+ elif args[0] == "dirs" or args[0] == "/dirs":
963
+ if len(args) < 2:
964
+ # 列出所有插件目录
965
+ print("\033[1;34mPlugin Directories:\033[0m", file=output)
966
+ # global plugin dirs
967
+ print("\033[33mGlobal Plugin Directories:\033[0m", file=output)
968
+ for idx, directory in enumerate(self.global_plugin_dirs, 1):
969
+ status = (
970
+ "\033[32m✓\033[0m"
971
+ if os.path.exists(directory)
972
+ else "\033[31m✗\033[0m"
973
+ )
974
+ print(f" {idx}. {status} {directory}", file=output)
975
+ # project plugin dirs
976
+ print("\033[1;34mProject Plugin Directories:\033[0m", file=output)
977
+ for idx, directory in enumerate(self.plugin_dirs, 1):
978
+ status = (
979
+ "\033[32m✓\033[0m"
980
+ if os.path.exists(directory)
981
+ else "\033[31m✗\033[0m"
982
+ )
983
+ print(f" {idx}. {status} {directory}", file=output)
984
+ return output.getvalue()
985
+
986
+ subcmd = args[1]
987
+ if (subcmd == "add" or subcmd == "/add") and len(args) > 2:
988
+ path = " ".join(args[2:])
989
+ success, msg = self.add_plugin_directory(path)
990
+ status = (
991
+ "\033[32mSUCCESS\033[0m" if success else "\033[31mFAILED\033[0m"
992
+ )
993
+ print(f"{status}: {msg}", file=output)
994
+ elif (subcmd == "remove" or subcmd == "/remove") and len(args) > 2:
995
+ path = " ".join(args[2:])
996
+ msg = self.remove_plugin_directory(path)
997
+ print(f"\033[33m{msg}\033[0m", file=output)
998
+ elif subcmd == "clear" or subcmd == "/clear":
999
+ msg = self.clear_plugin_directories()
1000
+ print(f"\033[33m{msg}\033[0m", file=output)
1001
+ else:
1002
+ print(
1003
+ "Usage: /plugins/dirs [/add <path>|/remove <path>|/clear]", file=output
1004
+ )
1005
+
1006
+ else:
1007
+ # 在找不到命令的情况下显示用法信息
1008
+ print(
1009
+ "Usage: /plugins [/list|/load <name>|/unload <name>|/dirs ...]", file=output
1010
+ )
1011
+
1012
+ return output.getvalue()
1013
+
1014
+ def apply_keybindings(self, kb) -> None:
1015
+ """将所有插件的键盘绑定应用到提供的键盘绑定器对象。
1016
+
1017
+ 此方法迭代所有已加载的插件,获取它们的键盘绑定,并将这些绑定应用到键盘绑定器。
1018
+ 这样可以将键盘绑定的处理逻辑集中在 PluginManager 中,减少外部代码的耦合。
1019
+
1020
+ Args:
1021
+ kb: 键盘绑定器对象,必须有一个 add 方法,该方法返回一个可调用对象用于注册处理程序
1022
+ """
1023
+ # 检查键盘绑定器是否有 add 方法
1024
+ if not hasattr(kb, "add") or not callable(getattr(kb, "add")):
1025
+ raise ValueError("键盘绑定器必须有一个可调用的 add 方法")
1026
+
1027
+ # 迭代所有插件
1028
+ for plugin_key, plugin in self.plugins.items():
1029
+ # 获取插件的键盘绑定
1030
+ for key_combination, handler, description in plugin.get_keybindings():
1031
+ # 应用键盘绑定
1032
+ try:
1033
+ kb.add(key_combination)(handler)
1034
+ except Exception as e:
1035
+ print(
1036
+ f"Error applying keybinding '{key_combination}' from plugin '{plugin_key}': {e}"
1037
+ )
1038
+
1039
+ return
1040
+
1041
+ def get_plugin_config_path(self, plugin_id: str) -> Optional[str]:
1042
+ """获取插件配置文件的路径。
1043
+
1044
+ Args:
1045
+ plugin_id: 插件的唯一标识符 (plugin.id_name())
1046
+
1047
+ Returns:
1048
+ 配置文件路径,如果项目根目录不存在则返回 None
1049
+ """
1050
+ # 检查是否有项目根目录
1051
+ project_root = self.project_root()
1052
+ if not project_root:
1053
+ return None
1054
+
1055
+ # 创建配置目录和文件路径
1056
+ config_dir = os.path.join(project_root, "plugins", plugin_id)
1057
+ os.makedirs(config_dir, exist_ok=True)
1058
+ return os.path.join(config_dir, "config.json")
1059
+
1060
+ def get_dynamic_cmds(self) -> List[str]:
1061
+ """获取所有需要动态补全的命令列表。
1062
+
1063
+ 包括内置的动态命令和所有插件提供的动态命令。
1064
+
1065
+ Returns:
1066
+ 需要动态补全的命令列表
1067
+ """
1068
+ dynamic_cmds = self._builtin_dynamic_cmds.copy()
1069
+
1070
+ # 收集所有插件提供的动态命令
1071
+ for plugin in self.plugins.values():
1072
+ if hasattr(plugin, "dynamic_cmds"):
1073
+ dynamic_cmds.extend(plugin.dynamic_cmds)
1074
+
1075
+ return dynamic_cmds
1076
+
1077
+ def process_dynamic_completions(
1078
+ self, command: str, current_input: str
1079
+ ) -> List[Tuple[str, str]]:
1080
+ """处理动态补全命令
1081
+
1082
+ Args:
1083
+ command: 基础命令,如 /plugins
1084
+ current_input: 当前完整的输入,如 /plugins/dirs /remove /usr
1085
+
1086
+ Returns:
1087
+ List[Tuple[str, str]]: 补全选项列表,每个选项为 (补全文本, 显示文本)
1088
+ """
1089
+ # 获取动态补全选项
1090
+ completions = self.get_dynamic_completions(command, current_input)
1091
+
1092
+ # 处理补全选项
1093
+ processed_completions = []
1094
+ parts = current_input.split()
1095
+ existing_input = ""
1096
+
1097
+ # 如果输入包含子命令和参数
1098
+ if len(parts) > 2:
1099
+ # 获取最后一个部分作为补全前缀
1100
+ existing_input = parts[-1]
1101
+
1102
+ # 只提供未输入部分作为补全
1103
+ for completion_text, display_text in completions:
1104
+ if completion_text.startswith(existing_input):
1105
+ remaining_text = completion_text[len(existing_input) :]
1106
+ processed_completions.append((remaining_text, display_text))
1107
+
1108
+ return processed_completions
1109
+
1110
+
1111
+ def register_global_plugin_dir(plugin_dir: str) -> None:
1112
+ """注册一个全局插件目录。
1113
+
1114
+ Args:
1115
+ plugin_dir: 插件目录路径
1116
+ """
1117
+ plugin_dir = os.path.abspath(os.path.normpath(plugin_dir))
1118
+ if not os.path.exists(plugin_dir):
1119
+ print(f"Plugin directory does not exist: {plugin_dir}")
1120
+ return
1121
+ plugin_manager = PluginManager()
1122
+ plugin_manager.add_global_plugin_directory(plugin_dir)
1123
+ print(f"Registered global plugin directory: {plugin_dir}")