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.

Files changed (60) hide show
  1. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/METADATA +1 -1
  2. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/RECORD +60 -45
  3. autocoder/agent/agentic_filter.py +1 -1
  4. autocoder/auto_coder.py +8 -0
  5. autocoder/auto_coder_rag.py +37 -1
  6. autocoder/auto_coder_runner.py +58 -77
  7. autocoder/chat/conf_command.py +270 -0
  8. autocoder/chat/models_command.py +485 -0
  9. autocoder/chat_auto_coder.py +29 -24
  10. autocoder/chat_auto_coder_lang.py +26 -2
  11. autocoder/commands/auto_command.py +60 -132
  12. autocoder/commands/auto_web.py +1 -1
  13. autocoder/commands/tools.py +1 -1
  14. autocoder/common/__init__.py +3 -1
  15. autocoder/common/command_completer.py +58 -12
  16. autocoder/common/command_completer_v2.py +576 -0
  17. autocoder/common/conversations/__init__.py +52 -0
  18. autocoder/common/conversations/compatibility.py +303 -0
  19. autocoder/common/conversations/conversation_manager.py +502 -0
  20. autocoder/common/conversations/example.py +152 -0
  21. autocoder/common/file_monitor/__init__.py +5 -0
  22. autocoder/common/file_monitor/monitor.py +383 -0
  23. autocoder/common/global_cancel.py +53 -16
  24. autocoder/common/ignorefiles/__init__.py +4 -0
  25. autocoder/common/ignorefiles/ignore_file_utils.py +103 -0
  26. autocoder/common/ignorefiles/test_ignore_file_utils.py +91 -0
  27. autocoder/common/rulefiles/__init__.py +15 -0
  28. autocoder/common/rulefiles/autocoderrules_utils.py +173 -0
  29. autocoder/common/save_formatted_log.py +54 -0
  30. autocoder/common/v2/agent/agentic_edit.py +10 -39
  31. autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +1 -1
  32. autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +73 -43
  33. autocoder/common/v2/code_agentic_editblock_manager.py +9 -9
  34. autocoder/common/v2/code_diff_manager.py +2 -2
  35. autocoder/common/v2/code_editblock_manager.py +31 -18
  36. autocoder/common/v2/code_strict_diff_manager.py +3 -2
  37. autocoder/dispacher/actions/action.py +6 -6
  38. autocoder/dispacher/actions/plugins/action_regex_project.py +2 -2
  39. autocoder/events/event_manager_singleton.py +1 -1
  40. autocoder/index/index.py +3 -3
  41. autocoder/models.py +22 -9
  42. autocoder/rag/api_server.py +14 -2
  43. autocoder/rag/cache/local_byzer_storage_cache.py +1 -1
  44. autocoder/rag/cache/local_duckdb_storage_cache.py +8 -0
  45. autocoder/rag/cache/simple_cache.py +63 -33
  46. autocoder/rag/loaders/docx_loader.py +1 -1
  47. autocoder/rag/loaders/filter_utils.py +133 -76
  48. autocoder/rag/loaders/image_loader.py +15 -3
  49. autocoder/rag/loaders/pdf_loader.py +2 -2
  50. autocoder/rag/long_context_rag.py +11 -0
  51. autocoder/rag/qa_conversation_strategy.py +5 -31
  52. autocoder/rag/utils.py +21 -2
  53. autocoder/utils/_markitdown.py +66 -25
  54. autocoder/utils/auto_coder_utils/chat_stream_out.py +4 -4
  55. autocoder/utils/thread_utils.py +9 -27
  56. autocoder/version.py +1 -1
  57. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/LICENSE +0 -0
  58. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/WHEEL +0 -0
  59. {auto_coder-0.1.353.dist-info → auto_coder-0.1.355.dist-info}/entry_points.txt +0 -0
  60. {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
- @property
19
- def requested(self) -> bool:
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._global_flag
28
+ return self._active_tokens.copy()
23
29
 
24
30
  def is_requested(self, token: Optional[str] = None) -> bool:
25
- """检查是否请求了特定token或全局的取消"""
26
- with self._lock:
27
- # 全局标志总是优先
28
- if self._global_flag:
29
- return True
30
- # 如果提供了token,检查该token的标志
31
- if token is not None and token in self._token_flags:
32
- return self._token_flags[token]
33
- return False
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,4 @@
1
+
2
+ from .ignore_file_utils import should_ignore
3
+
4
+ __all__ = ["should_ignore"]
@@ -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)