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.
- {auto_coder-0.1.287.dist-info → auto_coder-0.1.289.dist-info}/METADATA +1 -1
- {auto_coder-0.1.287.dist-info → auto_coder-0.1.289.dist-info}/RECORD +26 -17
- autocoder/chat_auto_coder.py +265 -82
- autocoder/chat_auto_coder_lang.py +25 -21
- autocoder/commands/auto_web.py +1062 -0
- autocoder/common/__init__.py +1 -2
- autocoder/common/anything2img.py +113 -43
- autocoder/common/auto_coder_lang.py +40 -1
- autocoder/common/computer_use.py +931 -0
- autocoder/common/mcp_hub.py +99 -77
- autocoder/common/mcp_server.py +162 -61
- autocoder/index/filter/quick_filter.py +373 -3
- autocoder/plugins/__init__.py +1123 -0
- autocoder/plugins/dynamic_completion_example.py +148 -0
- autocoder/plugins/git_helper_plugin.py +252 -0
- autocoder/plugins/sample_plugin.py +160 -0
- autocoder/plugins/token_helper_plugin.py +343 -0
- autocoder/plugins/utils.py +9 -0
- autocoder/rag/long_context_rag.py +22 -9
- autocoder/rag/relevant_utils.py +1 -1
- autocoder/rag/searchable.py +58 -0
- autocoder/version.py +1 -1
- {auto_coder-0.1.287.dist-info → auto_coder-0.1.289.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.287.dist-info → auto_coder-0.1.289.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.287.dist-info → auto_coder-0.1.289.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.287.dist-info → auto_coder-0.1.289.dist-info}/top_level.txt +0 -0
|
@@ -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}")
|