auto-coder 0.1.353__py3-none-any.whl → 0.1.355__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.353.dist-info → auto_coder-0.1.355.dist-info}/METADATA +1 -1
- {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/RECORD +60 -45
- autocoder/agent/agentic_filter.py +1 -1
- autocoder/auto_coder.py +8 -0
- autocoder/auto_coder_rag.py +37 -1
- autocoder/auto_coder_runner.py +58 -77
- autocoder/chat/conf_command.py +270 -0
- autocoder/chat/models_command.py +485 -0
- autocoder/chat_auto_coder.py +29 -24
- autocoder/chat_auto_coder_lang.py +26 -2
- autocoder/commands/auto_command.py +60 -132
- autocoder/commands/auto_web.py +1 -1
- autocoder/commands/tools.py +1 -1
- autocoder/common/__init__.py +3 -1
- autocoder/common/command_completer.py +58 -12
- autocoder/common/command_completer_v2.py +576 -0
- autocoder/common/conversations/__init__.py +52 -0
- autocoder/common/conversations/compatibility.py +303 -0
- autocoder/common/conversations/conversation_manager.py +502 -0
- autocoder/common/conversations/example.py +152 -0
- autocoder/common/file_monitor/__init__.py +5 -0
- autocoder/common/file_monitor/monitor.py +383 -0
- autocoder/common/global_cancel.py +53 -16
- autocoder/common/ignorefiles/__init__.py +4 -0
- autocoder/common/ignorefiles/ignore_file_utils.py +103 -0
- autocoder/common/ignorefiles/test_ignore_file_utils.py +91 -0
- autocoder/common/rulefiles/__init__.py +15 -0
- autocoder/common/rulefiles/autocoderrules_utils.py +173 -0
- autocoder/common/save_formatted_log.py +54 -0
- autocoder/common/v2/agent/agentic_edit.py +10 -39
- autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +1 -1
- autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +73 -43
- autocoder/common/v2/code_agentic_editblock_manager.py +9 -9
- autocoder/common/v2/code_diff_manager.py +2 -2
- autocoder/common/v2/code_editblock_manager.py +31 -18
- autocoder/common/v2/code_strict_diff_manager.py +3 -2
- autocoder/dispacher/actions/action.py +6 -6
- autocoder/dispacher/actions/plugins/action_regex_project.py +2 -2
- autocoder/events/event_manager_singleton.py +1 -1
- autocoder/index/index.py +3 -3
- autocoder/models.py +22 -9
- autocoder/rag/api_server.py +14 -2
- autocoder/rag/cache/local_byzer_storage_cache.py +1 -1
- autocoder/rag/cache/local_duckdb_storage_cache.py +8 -0
- autocoder/rag/cache/simple_cache.py +63 -33
- autocoder/rag/loaders/docx_loader.py +1 -1
- autocoder/rag/loaders/filter_utils.py +133 -76
- autocoder/rag/loaders/image_loader.py +15 -3
- autocoder/rag/loaders/pdf_loader.py +2 -2
- autocoder/rag/long_context_rag.py +11 -0
- autocoder/rag/qa_conversation_strategy.py +5 -31
- autocoder/rag/utils.py +21 -2
- autocoder/utils/_markitdown.py +66 -25
- autocoder/utils/auto_coder_utils/chat_stream_out.py +4 -4
- autocoder/utils/thread_utils.py +9 -27
- autocoder/version.py +1 -1
- {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
import fnmatch
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable, Dict, List, Set, Tuple, Union, Optional, Any
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
# 尝试导入 watchfiles,如果失败则提示用户安装
|
|
12
|
+
try:
|
|
13
|
+
from watchfiles import watch, Change
|
|
14
|
+
except ImportError:
|
|
15
|
+
logger.error("错误:需要安装 'watchfiles' 库。请运行: pip install watchfiles")
|
|
16
|
+
# 可以选择抛出异常或退出,这里仅打印信息
|
|
17
|
+
# raise ImportError("watchfiles is required for FileMonitor")
|
|
18
|
+
# 或者提供一个空的实现或禁用该功能
|
|
19
|
+
Change = None # type: ignore
|
|
20
|
+
watch = None # type: ignore
|
|
21
|
+
|
|
22
|
+
# 尝试导入 pathspec,如果失败则提示用户安装
|
|
23
|
+
try:
|
|
24
|
+
import pathspec
|
|
25
|
+
except ImportError:
|
|
26
|
+
logger.error("错误:需要安装 'pathspec' 库。请运行: pip install pathspec")
|
|
27
|
+
pathspec = None # type: ignore
|
|
28
|
+
|
|
29
|
+
# 用于区分普通路径和模式路径
|
|
30
|
+
class PathType:
|
|
31
|
+
LITERAL = 'literal' # 普通的精确路径
|
|
32
|
+
PATTERN = 'pattern' # 模式路径(glob, gitignore等)
|
|
33
|
+
|
|
34
|
+
# 注册的路径信息结构
|
|
35
|
+
class RegisteredPath:
|
|
36
|
+
def __init__(self, path: str, path_type: str, spec=None):
|
|
37
|
+
self.path = path
|
|
38
|
+
self.path_type = path_type
|
|
39
|
+
self.spec = spec # pathspec规范对象,用于高效匹配
|
|
40
|
+
|
|
41
|
+
def __eq__(self, other):
|
|
42
|
+
if not isinstance(other, RegisteredPath):
|
|
43
|
+
return False
|
|
44
|
+
return self.path == other.path and self.path_type == other.path_type
|
|
45
|
+
|
|
46
|
+
def __hash__(self):
|
|
47
|
+
return hash((self.path, self.path_type))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class FileMonitor:
|
|
51
|
+
"""
|
|
52
|
+
使用 watchfiles 库监控指定根目录下文件或目录的变化。
|
|
53
|
+
|
|
54
|
+
允许动态注册特定路径的回调函数,当这些路径发生变化时触发。
|
|
55
|
+
支持多种路径模式,包括glob模式,如 "**/*.py" 匹配任何Python文件。
|
|
56
|
+
使用pathspec库实现高效的路径匹配。
|
|
57
|
+
|
|
58
|
+
此类实现了单例模式,确保全局只有一个监控实例。
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# 单例实例
|
|
62
|
+
_instance = None
|
|
63
|
+
_instance_lock = threading.Lock()
|
|
64
|
+
|
|
65
|
+
def __new__(cls, root_dir: Optional[str] = None):
|
|
66
|
+
"""
|
|
67
|
+
实现单例模式。确保只创建一个 FileMonitor 实例。
|
|
68
|
+
|
|
69
|
+
:param root_dir: 需要监控的根目录。如果已存在实例且提供了新的根目录,不会更改现有实例的根目录。
|
|
70
|
+
:return: FileMonitor 的单例实例
|
|
71
|
+
"""
|
|
72
|
+
with cls._instance_lock:
|
|
73
|
+
if cls._instance is None:
|
|
74
|
+
if root_dir is None:
|
|
75
|
+
raise ValueError("First initialization of FileMonitor requires a valid root_dir")
|
|
76
|
+
cls._instance = super(FileMonitor, cls).__new__(cls)
|
|
77
|
+
cls._instance._initialized = False # 标记是否已初始化
|
|
78
|
+
elif root_dir is not None and cls._instance.root_dir != os.path.abspath(root_dir):
|
|
79
|
+
logger.warning(f"FileMonitor is already initialized with root directory '{cls._instance.root_dir}'.")
|
|
80
|
+
logger.warning(f"New root directory '{root_dir}' will be ignored.")
|
|
81
|
+
return cls._instance
|
|
82
|
+
|
|
83
|
+
def __init__(self, root_dir: str):
|
|
84
|
+
"""
|
|
85
|
+
初始化 FileMonitor。由于是单例,只有首次创建实例时才会执行初始化。
|
|
86
|
+
|
|
87
|
+
:param root_dir: 需要监控的根目录。watchfiles 将监控此目录及其所有子目录。
|
|
88
|
+
"""
|
|
89
|
+
# 如果已经初始化过,则跳过
|
|
90
|
+
if hasattr(self, '_initialized') and self._initialized:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
if watch is None:
|
|
94
|
+
raise ImportError("watchfiles is not installed or could not be imported.")
|
|
95
|
+
|
|
96
|
+
if pathspec is None:
|
|
97
|
+
raise ImportError("pathspec is not installed or could not be imported.")
|
|
98
|
+
|
|
99
|
+
self.root_dir = os.path.abspath(root_dir)
|
|
100
|
+
if not os.path.isdir(self.root_dir):
|
|
101
|
+
raise ValueError(f"Root directory '{self.root_dir}' does not exist or is not a directory.")
|
|
102
|
+
|
|
103
|
+
# 存储回调: {registered_path: [callback1, callback2, ...]}
|
|
104
|
+
# 回调函数签名: callback(change_type: Change, changed_path: str)
|
|
105
|
+
self._callbacks: Dict[RegisteredPath, List[Callable[[Change, str], None]]] = defaultdict(list)
|
|
106
|
+
self._callback_lock = threading.Lock() # 保护 _callbacks 的访问
|
|
107
|
+
|
|
108
|
+
self._stop_event = threading.Event() # 用于通知监控循环停止
|
|
109
|
+
self._monitor_thread: Optional[threading.Thread] = None
|
|
110
|
+
self._watch_stop_event = threading.Event() # watchfiles 停止事件
|
|
111
|
+
|
|
112
|
+
self._initialized = True
|
|
113
|
+
logger.info(f"FileMonitor singleton initialized for root directory: {self.root_dir}")
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def get_instance(cls) -> Optional['FileMonitor']:
|
|
117
|
+
"""
|
|
118
|
+
获取 FileMonitor 的单例实例。
|
|
119
|
+
|
|
120
|
+
:return: FileMonitor 实例,如果尚未初始化则返回 None
|
|
121
|
+
"""
|
|
122
|
+
return cls._instance
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def reset_instance(cls):
|
|
126
|
+
"""
|
|
127
|
+
重置单例实例。
|
|
128
|
+
如果当前实例正在运行,则先停止它。
|
|
129
|
+
"""
|
|
130
|
+
with cls._instance_lock:
|
|
131
|
+
if cls._instance is not None:
|
|
132
|
+
if cls._instance.is_running():
|
|
133
|
+
cls._instance.stop()
|
|
134
|
+
cls._instance = None
|
|
135
|
+
logger.info("FileMonitor singleton has been reset.")
|
|
136
|
+
|
|
137
|
+
def _is_pattern(self, path: str) -> bool:
|
|
138
|
+
"""
|
|
139
|
+
判断一个路径是否为模式(包含通配符)。
|
|
140
|
+
|
|
141
|
+
:param path: 要检查的路径
|
|
142
|
+
:return: 如果路径包含通配符,则返回True
|
|
143
|
+
"""
|
|
144
|
+
# 检查是否包含通配符字符
|
|
145
|
+
return any(c in path for c in ['*', '?', '[', ']'])
|
|
146
|
+
|
|
147
|
+
def _create_pathspec(self, pattern: str) -> Any:
|
|
148
|
+
"""
|
|
149
|
+
创建一个pathspec匹配器。
|
|
150
|
+
|
|
151
|
+
:param pattern: 匹配模式
|
|
152
|
+
:return: pathspec匹配器对象
|
|
153
|
+
"""
|
|
154
|
+
# 将单个模式转换为pathspec格式
|
|
155
|
+
# 使用GitWildMatchPattern,它支持.gitignore样式的通配符,功能最全面
|
|
156
|
+
return pathspec.PathSpec.from_patterns([pattern], pathspec.patterns.GitWildMatchPattern)
|
|
157
|
+
|
|
158
|
+
def register(self, path: Union[str, Path], callback: Callable[[Change, str], None]):
|
|
159
|
+
"""
|
|
160
|
+
注册一个文件或目录路径以及对应的回调函数。
|
|
161
|
+
|
|
162
|
+
支持多种模式路径,如 "**/*.py" 匹配任何Python文件。
|
|
163
|
+
如果注册的是目录,则该目录本身或其内部任何文件的变化都会触发回调。
|
|
164
|
+
路径必须位于初始化时指定的 root_dir 内部。
|
|
165
|
+
|
|
166
|
+
:param path: 要监控的文件或目录的路径(绝对或相对于当前工作目录)。
|
|
167
|
+
支持多种模式,如 "src/**/*.py"。
|
|
168
|
+
:param callback: 当路径发生变化时调用的回调函数。
|
|
169
|
+
接收两个参数:变化类型 (watchfiles.Change) 和变化的文件/目录路径 (str)。
|
|
170
|
+
"""
|
|
171
|
+
path_str = str(path)
|
|
172
|
+
|
|
173
|
+
# 检查是否是模式路径
|
|
174
|
+
is_pattern = self._is_pattern(path_str)
|
|
175
|
+
|
|
176
|
+
# 对于非模式路径,路径必须是绝对路径或相对于当前工作目录的路径
|
|
177
|
+
if not is_pattern:
|
|
178
|
+
abs_path = os.path.abspath(path_str)
|
|
179
|
+
|
|
180
|
+
# 检查路径是否在 root_dir 内部
|
|
181
|
+
if not abs_path.startswith(self.root_dir):
|
|
182
|
+
logger.warning(f"Path '{abs_path}' is outside the monitored root directory '{self.root_dir}' and cannot be registered.")
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
reg_path = RegisteredPath(abs_path, PathType.LITERAL)
|
|
186
|
+
|
|
187
|
+
with self._callback_lock:
|
|
188
|
+
self._callbacks[reg_path].append(callback)
|
|
189
|
+
logger.info(f"Registered callback for literal path: {abs_path}")
|
|
190
|
+
else:
|
|
191
|
+
# 对于模式路径,先处理路径格式
|
|
192
|
+
if os.path.isabs(path_str):
|
|
193
|
+
# 如果是绝对路径,检查是否在监控根目录下
|
|
194
|
+
if not path_str.startswith(self.root_dir):
|
|
195
|
+
logger.warning(f"Pattern '{path_str}' is outside the monitored root directory '{self.root_dir}' and cannot be registered.")
|
|
196
|
+
return
|
|
197
|
+
# 转换为相对于root_dir的路径用于pathspec匹配
|
|
198
|
+
pattern = os.path.relpath(path_str, self.root_dir)
|
|
199
|
+
else:
|
|
200
|
+
# 对于相对路径,直接使用
|
|
201
|
+
pattern = path_str
|
|
202
|
+
|
|
203
|
+
# 创建pathspec匹配器
|
|
204
|
+
path_spec = self._create_pathspec(pattern)
|
|
205
|
+
|
|
206
|
+
# 注册带有pathspec的模式
|
|
207
|
+
reg_path = RegisteredPath(pattern, PathType.PATTERN, spec=path_spec)
|
|
208
|
+
|
|
209
|
+
with self._callback_lock:
|
|
210
|
+
self._callbacks[reg_path].append(callback)
|
|
211
|
+
logger.info(f"Registered callback for pattern: {pattern}")
|
|
212
|
+
|
|
213
|
+
def unregister(self, path: Union[str, Path], callback: Optional[Callable[[Change, str], None]] = None):
|
|
214
|
+
"""
|
|
215
|
+
取消注册一个文件或目录路径的回调函数。
|
|
216
|
+
|
|
217
|
+
:param path: 要取消注册的文件或目录路径,包括模式路径。
|
|
218
|
+
:param callback: 要取消注册的特定回调函数。如果为 None,则移除该路径的所有回调。
|
|
219
|
+
"""
|
|
220
|
+
path_str = str(path)
|
|
221
|
+
is_pattern = self._is_pattern(path_str)
|
|
222
|
+
|
|
223
|
+
# 查找匹配的注册路径
|
|
224
|
+
target_reg_path = None
|
|
225
|
+
with self._callback_lock:
|
|
226
|
+
if not is_pattern:
|
|
227
|
+
abs_path = os.path.abspath(path_str)
|
|
228
|
+
for reg_path in self._callbacks.keys():
|
|
229
|
+
if reg_path.path_type == PathType.LITERAL and reg_path.path == abs_path:
|
|
230
|
+
target_reg_path = reg_path
|
|
231
|
+
break
|
|
232
|
+
else:
|
|
233
|
+
# 对于模式路径,尝试找到完全匹配的模式
|
|
234
|
+
if os.path.isabs(path_str):
|
|
235
|
+
pattern = os.path.relpath(path_str, self.root_dir)
|
|
236
|
+
else:
|
|
237
|
+
pattern = path_str
|
|
238
|
+
|
|
239
|
+
for reg_path in self._callbacks.keys():
|
|
240
|
+
if reg_path.path_type == PathType.PATTERN and reg_path.path == pattern:
|
|
241
|
+
target_reg_path = reg_path
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
# 如果找到匹配的路径,执行取消注册
|
|
245
|
+
if target_reg_path:
|
|
246
|
+
if callback:
|
|
247
|
+
try:
|
|
248
|
+
self._callbacks[target_reg_path].remove(callback)
|
|
249
|
+
logger.info(f"Unregistered specific callback for path: {path_str}")
|
|
250
|
+
if not self._callbacks[target_reg_path]: # 如果列表为空,则删除键
|
|
251
|
+
del self._callbacks[target_reg_path]
|
|
252
|
+
except ValueError:
|
|
253
|
+
logger.warning(f"Callback not found for path: {path_str}")
|
|
254
|
+
else:
|
|
255
|
+
del self._callbacks[target_reg_path]
|
|
256
|
+
logger.info(f"Unregistered all callbacks for path: {path_str}")
|
|
257
|
+
else:
|
|
258
|
+
logger.warning(f"No callbacks registered for path: {path_str}")
|
|
259
|
+
|
|
260
|
+
def _path_matches(self, file_path: str, reg_path: RegisteredPath) -> bool:
|
|
261
|
+
"""
|
|
262
|
+
检查文件路径是否匹配注册的路径。
|
|
263
|
+
|
|
264
|
+
:param file_path: 要检查的文件路径
|
|
265
|
+
:param reg_path: 注册的路径对象
|
|
266
|
+
:return: 如果路径匹配,则返回True
|
|
267
|
+
"""
|
|
268
|
+
if reg_path.path_type == PathType.LITERAL:
|
|
269
|
+
# 对于精确路径,检查完全匹配或者是否在目录内
|
|
270
|
+
if file_path == reg_path.path:
|
|
271
|
+
return True
|
|
272
|
+
if os.path.isdir(reg_path.path) and file_path.startswith(reg_path.path + os.sep):
|
|
273
|
+
return True
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
elif reg_path.path_type == PathType.PATTERN:
|
|
277
|
+
# 对于模式路径,使用pathspec进行匹配
|
|
278
|
+
if reg_path.spec:
|
|
279
|
+
# 获取相对于根目录的路径进行匹配
|
|
280
|
+
rel_path = os.path.relpath(file_path, self.root_dir)
|
|
281
|
+
return reg_path.spec.match_file(rel_path)
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
def _monitor_loop(self):
|
|
287
|
+
"""
|
|
288
|
+
监控线程的主循环,使用 watchfiles.watch。
|
|
289
|
+
"""
|
|
290
|
+
logger.info(f"File monitor loop started for {self.root_dir}...")
|
|
291
|
+
try:
|
|
292
|
+
# watchfiles.watch 会阻塞直到 stop_event 被设置或发生错误
|
|
293
|
+
for changes in watch(self.root_dir, stop_event=self._watch_stop_event, yield_on_timeout=True):
|
|
294
|
+
if self._stop_event.is_set(): # 检查外部停止信号
|
|
295
|
+
logger.info("External stop signal received.")
|
|
296
|
+
break
|
|
297
|
+
|
|
298
|
+
if not changes: # 超时时 changes 可能为空
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
# changes 是一个集合: {(Change.added, '/path/to/file'), (Change.modified, '/path/to/another')}
|
|
302
|
+
triggered_callbacks: List[Tuple[Callable, Change, str]] = []
|
|
303
|
+
|
|
304
|
+
with self._callback_lock:
|
|
305
|
+
# 检查每个变化是否与注册的路径匹配
|
|
306
|
+
for change_type, changed_path in changes:
|
|
307
|
+
abs_changed_path = os.path.abspath(changed_path)
|
|
308
|
+
|
|
309
|
+
# 遍历所有注册的路径和回调
|
|
310
|
+
for reg_path, callbacks in self._callbacks.items():
|
|
311
|
+
# 使用优化后的路径匹配方法
|
|
312
|
+
if self._path_matches(abs_changed_path, reg_path):
|
|
313
|
+
for cb in callbacks:
|
|
314
|
+
# 避免重复添加同一回调对于同一事件
|
|
315
|
+
if (cb, change_type, abs_changed_path) not in triggered_callbacks:
|
|
316
|
+
triggered_callbacks.append((cb, change_type, abs_changed_path))
|
|
317
|
+
|
|
318
|
+
# 在锁外部执行回调,避免阻塞监控循环
|
|
319
|
+
if triggered_callbacks:
|
|
320
|
+
for cb, ct, cp in triggered_callbacks:
|
|
321
|
+
try:
|
|
322
|
+
cb(ct, cp)
|
|
323
|
+
except Exception as e:
|
|
324
|
+
logger.error(f"Error executing callback {getattr(cb, '__name__', str(cb))} for change {ct} on {cp}: {e}")
|
|
325
|
+
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.error(f"Error in file monitor loop: {e}")
|
|
328
|
+
finally:
|
|
329
|
+
logger.info("File monitor loop stopped.")
|
|
330
|
+
|
|
331
|
+
def start(self):
|
|
332
|
+
"""
|
|
333
|
+
启动文件监控后台线程。
|
|
334
|
+
如果监控已在运行,则不执行任何操作。
|
|
335
|
+
"""
|
|
336
|
+
if self._monitor_thread is not None and self._monitor_thread.is_alive():
|
|
337
|
+
logger.info("Monitor is already running.")
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
logger.info("Starting file monitor...")
|
|
341
|
+
self._stop_event.clear() # 重置外部停止事件
|
|
342
|
+
self._watch_stop_event.clear() # 重置 watchfiles 停止事件
|
|
343
|
+
self._monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True)
|
|
344
|
+
self._monitor_thread.start()
|
|
345
|
+
logger.info("File monitor started in background thread.")
|
|
346
|
+
|
|
347
|
+
def stop(self):
|
|
348
|
+
"""
|
|
349
|
+
停止文件监控线程。
|
|
350
|
+
"""
|
|
351
|
+
if self._monitor_thread is None or not self._monitor_thread.is_alive():
|
|
352
|
+
logger.info("Monitor is not running.")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
logger.info("Stopping file monitor...")
|
|
356
|
+
self._stop_event.set() # 设置外部停止标志
|
|
357
|
+
self._watch_stop_event.set() # 触发 watchfiles 内部停止
|
|
358
|
+
|
|
359
|
+
if self._monitor_thread:
|
|
360
|
+
# 等待一小段时间让 watch() 循环检测到事件并退出
|
|
361
|
+
# join() 超时是为了防止 watch() 因某些原因卡住导致主线程无限等待
|
|
362
|
+
self._monitor_thread.join(timeout=5.0)
|
|
363
|
+
if self._monitor_thread.is_alive():
|
|
364
|
+
logger.warning("Monitor thread did not stop gracefully after 5 seconds.")
|
|
365
|
+
else:
|
|
366
|
+
logger.info("Monitor thread joined.")
|
|
367
|
+
|
|
368
|
+
self._monitor_thread = None
|
|
369
|
+
logger.info("File monitor stopped.")
|
|
370
|
+
|
|
371
|
+
def is_running(self) -> bool:
|
|
372
|
+
"""
|
|
373
|
+
检查监控线程是否正在运行。
|
|
374
|
+
"""
|
|
375
|
+
return self._monitor_thread is not None and self._monitor_thread.is_alive()
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def get_file_monitor(root_dir: str) -> FileMonitor:
|
|
379
|
+
"""
|
|
380
|
+
获取 FileMonitor 的单例实例。
|
|
381
|
+
"""
|
|
382
|
+
return FileMonitor(root_dir)
|
|
383
|
+
|
|
@@ -14,32 +14,41 @@ class GlobalCancel:
|
|
|
14
14
|
self._token_flags: Dict[str, bool] = {}
|
|
15
15
|
self._lock = threading.Lock()
|
|
16
16
|
self._context: Dict[str, Any] = {} # 存储与取消相关的上下文信息
|
|
17
|
+
self._active_tokens: set[str] = set() # 存储当前正在运行的token
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
def register_token(self, token: str) -> None:
|
|
20
|
+
"""注册一个 token,表示一个操作开始,但尚未请求取消"""
|
|
21
|
+
with self._lock:
|
|
22
|
+
self._token_flags[token] = False
|
|
23
|
+
self._active_tokens.add(token)
|
|
24
|
+
|
|
25
|
+
def get_active_tokens(self) -> set[str]:
|
|
26
|
+
"""获取当前正在运行的token"""
|
|
21
27
|
with self._lock:
|
|
22
|
-
return self.
|
|
28
|
+
return self._active_tokens.copy()
|
|
23
29
|
|
|
24
30
|
def is_requested(self, token: Optional[str] = None) -> bool:
|
|
25
|
-
"""检查是否请求了特定token或全局的取消"""
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
"""检查是否请求了特定token或全局的取消"""
|
|
32
|
+
if token is not None and token in self._token_flags:
|
|
33
|
+
return self._token_flags[token]
|
|
34
|
+
|
|
35
|
+
if self._global_flag:
|
|
36
|
+
return True
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
def set_active_tokens(self) -> None:
|
|
40
|
+
"""启用所有活跃的token"""
|
|
41
|
+
for token in self._active_tokens:
|
|
42
|
+
self.set(token)
|
|
43
|
+
|
|
35
44
|
def set(self, token: Optional[str] = None, context: Optional[Dict[str, Any]] = None) -> None:
|
|
36
45
|
"""设置特定token或全局的取消标志"""
|
|
37
46
|
with self._lock:
|
|
38
47
|
if token is None:
|
|
39
48
|
self._global_flag = True
|
|
40
49
|
else:
|
|
41
|
-
self._token_flags[token] = True
|
|
42
|
-
|
|
50
|
+
self._token_flags[token] = True
|
|
51
|
+
|
|
43
52
|
# 存储上下文
|
|
44
53
|
if context:
|
|
45
54
|
if token is None:
|
|
@@ -49,6 +58,21 @@ class GlobalCancel:
|
|
|
49
58
|
self._context["tokens"] = {}
|
|
50
59
|
self._context["tokens"][token] = context
|
|
51
60
|
|
|
61
|
+
def reset_global(self) -> None:
|
|
62
|
+
"""重置全局取消标志"""
|
|
63
|
+
with self._lock:
|
|
64
|
+
self._global_flag = False
|
|
65
|
+
|
|
66
|
+
def reset_token(self, token: str) -> None:
|
|
67
|
+
"""重置特定token的取消标志"""
|
|
68
|
+
with self._lock:
|
|
69
|
+
if token in self._token_flags:
|
|
70
|
+
del self._token_flags[token]
|
|
71
|
+
if "tokens" in self._context and token in self._context["tokens"]:
|
|
72
|
+
del self._context["tokens"][token]
|
|
73
|
+
if token:
|
|
74
|
+
self._active_tokens.discard(token) # 从活跃集合中移除
|
|
75
|
+
|
|
52
76
|
def reset(self, token: Optional[str] = None) -> None:
|
|
53
77
|
"""重置特定token或全局的取消标志"""
|
|
54
78
|
with self._lock:
|
|
@@ -57,12 +81,21 @@ class GlobalCancel:
|
|
|
57
81
|
self._global_flag = False
|
|
58
82
|
self._token_flags.clear()
|
|
59
83
|
self._context.clear()
|
|
84
|
+
self._active_tokens.clear() # 清空活跃集合
|
|
60
85
|
else:
|
|
61
86
|
# 特定token重置
|
|
62
87
|
if token in self._token_flags:
|
|
63
88
|
del self._token_flags[token]
|
|
64
89
|
if "tokens" in self._context and token in self._context["tokens"]:
|
|
65
90
|
del self._context["tokens"][token]
|
|
91
|
+
if token:
|
|
92
|
+
self._active_tokens.discard(token) # 从活跃集合中移除
|
|
93
|
+
|
|
94
|
+
def reset_active_tokens(self) -> None:
|
|
95
|
+
"""重置所有活跃的token"""
|
|
96
|
+
with self._lock:
|
|
97
|
+
for token in self._active_tokens.copy():
|
|
98
|
+
self.reset_token(token)
|
|
66
99
|
|
|
67
100
|
def get_context(self, token: Optional[str] = None) -> Dict[str, Any]:
|
|
68
101
|
"""获取与取消相关的上下文信息"""
|
|
@@ -77,6 +110,10 @@ class GlobalCancel:
|
|
|
77
110
|
"""检查是否请求了取消,如果是则抛出异常"""
|
|
78
111
|
if self.is_requested(token):
|
|
79
112
|
context = self.get_context(token)
|
|
113
|
+
if token:
|
|
114
|
+
self.reset_token(token)
|
|
115
|
+
else:
|
|
116
|
+
self.reset_global()
|
|
80
117
|
raise CancelRequestedException(token, context.get("message", "Operation was cancelled"))
|
|
81
118
|
|
|
82
119
|
global_cancel = GlobalCancel()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from threading import Lock
|
|
5
|
+
import pathspec
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
# 尝试导入 FileMonitor
|
|
9
|
+
try:
|
|
10
|
+
from autocoder.common.file_monitor.monitor import FileMonitor, Change
|
|
11
|
+
except ImportError:
|
|
12
|
+
# 如果导入失败,提供一个空的实现
|
|
13
|
+
print("警告: 无法导入 FileMonitor,忽略文件变更监控将不可用")
|
|
14
|
+
FileMonitor = None
|
|
15
|
+
Change = None
|
|
16
|
+
|
|
17
|
+
DEFAULT_EXCLUDES = [
|
|
18
|
+
'.git', '.auto-coder', 'node_modules', '.mvn', '.idea',
|
|
19
|
+
'__pycache__', '.venv', 'venv', 'dist', 'build', '.gradle',".next"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class IgnoreFileManager:
|
|
24
|
+
_instance = None
|
|
25
|
+
_lock = Lock()
|
|
26
|
+
|
|
27
|
+
def __new__(cls):
|
|
28
|
+
if not cls._instance:
|
|
29
|
+
with cls._lock:
|
|
30
|
+
if not cls._instance:
|
|
31
|
+
cls._instance = super(IgnoreFileManager, cls).__new__(cls)
|
|
32
|
+
cls._instance._initialized = False
|
|
33
|
+
return cls._instance
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
if self._initialized:
|
|
37
|
+
return
|
|
38
|
+
self._initialized = True
|
|
39
|
+
self._spec = None
|
|
40
|
+
self._ignore_file_path = None
|
|
41
|
+
self._file_monitor = None
|
|
42
|
+
self._load_ignore_spec()
|
|
43
|
+
self._setup_file_monitor()
|
|
44
|
+
|
|
45
|
+
def _load_ignore_spec(self):
|
|
46
|
+
"""加载忽略规则文件并解析规则"""
|
|
47
|
+
ignore_patterns = []
|
|
48
|
+
project_root = Path(os.getcwd())
|
|
49
|
+
|
|
50
|
+
ignore_file_paths = [
|
|
51
|
+
project_root / '.autocoderignore',
|
|
52
|
+
project_root / '.auto-coder' / '.autocoderignore'
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
for ignore_file in ignore_file_paths:
|
|
56
|
+
if ignore_file.is_file():
|
|
57
|
+
with open(ignore_file, 'r', encoding='utf-8') as f:
|
|
58
|
+
ignore_patterns = f.read().splitlines()
|
|
59
|
+
self._ignore_file_path = str(ignore_file)
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
# 添加默认排除目录
|
|
63
|
+
ignore_patterns.extend(DEFAULT_EXCLUDES)
|
|
64
|
+
|
|
65
|
+
self._spec = pathspec.PathSpec.from_lines('gitwildmatch', ignore_patterns)
|
|
66
|
+
|
|
67
|
+
def _setup_file_monitor(self):
|
|
68
|
+
"""设置文件监控,当忽略文件变化时重新加载规则"""
|
|
69
|
+
if FileMonitor is None or not self._ignore_file_path:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
# 获取或创建 FileMonitor 实例
|
|
74
|
+
root_dir = os.path.dirname(self._ignore_file_path)
|
|
75
|
+
self._file_monitor = FileMonitor(root_dir=root_dir)
|
|
76
|
+
|
|
77
|
+
# 注册忽略文件的回调
|
|
78
|
+
self._file_monitor.register(self._ignore_file_path, self._on_ignore_file_changed)
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
print(f"设置忽略文件监控时出错: {e}")
|
|
82
|
+
|
|
83
|
+
def _on_ignore_file_changed(self, change_type: Change, changed_path: str):
|
|
84
|
+
"""当忽略文件发生变化时的回调函数"""
|
|
85
|
+
if os.path.abspath(changed_path) == os.path.abspath(self._ignore_file_path):
|
|
86
|
+
print(f"检测到忽略文件变化 ({change_type.name}): {changed_path}")
|
|
87
|
+
self._load_ignore_spec()
|
|
88
|
+
print("已重新加载忽略规则")
|
|
89
|
+
|
|
90
|
+
def should_ignore(self, path: str) -> bool:
|
|
91
|
+
"""判断指定路径是否应该被忽略"""
|
|
92
|
+
rel_path = os.path.relpath(path, os.getcwd())
|
|
93
|
+
# 标准化分隔符
|
|
94
|
+
rel_path = rel_path.replace(os.sep, '/')
|
|
95
|
+
return self._spec.match_file(rel_path)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# 对外提供单例
|
|
99
|
+
_ignore_manager = IgnoreFileManager()
|
|
100
|
+
|
|
101
|
+
def should_ignore(path: str) -> bool:
|
|
102
|
+
"""判断指定路径是否应该被忽略"""
|
|
103
|
+
return _ignore_manager.should_ignore(path)
|