auto-coder 0.1.305__py3-none-any.whl → 0.1.306__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 (39) hide show
  1. {auto_coder-0.1.305.dist-info → auto_coder-0.1.306.dist-info}/METADATA +1 -1
  2. {auto_coder-0.1.305.dist-info → auto_coder-0.1.306.dist-info}/RECORD +39 -34
  3. autocoder/agent/auto_learn_from_commit.py +3 -1
  4. autocoder/agent/auto_review_commit.py +3 -1
  5. autocoder/auto_coder.py +3 -2
  6. autocoder/auto_coder_runner.py +116 -3
  7. autocoder/chat_auto_coder.py +9 -1
  8. autocoder/chat_auto_coder_lang.py +552 -278
  9. autocoder/common/__init__.py +4 -0
  10. autocoder/common/auto_coder_lang.py +737 -401
  11. autocoder/common/code_auto_generate.py +104 -16
  12. autocoder/common/code_auto_generate_diff.py +101 -10
  13. autocoder/common/code_auto_generate_editblock.py +103 -9
  14. autocoder/common/code_auto_generate_strict_diff.py +99 -9
  15. autocoder/common/code_auto_merge.py +8 -0
  16. autocoder/common/code_auto_merge_diff.py +8 -0
  17. autocoder/common/code_auto_merge_editblock.py +7 -0
  18. autocoder/common/code_auto_merge_strict_diff.py +5 -0
  19. autocoder/common/code_modification_ranker.py +4 -2
  20. autocoder/common/command_completer.py +12 -0
  21. autocoder/common/command_generator.py +5 -4
  22. autocoder/common/git_utils.py +13 -7
  23. autocoder/common/stream_out_type.py +5 -1
  24. autocoder/common/utils_code_auto_generate.py +29 -3
  25. autocoder/dispacher/__init__.py +18 -19
  26. autocoder/dispacher/actions/action.py +0 -132
  27. autocoder/index/filter/quick_filter.py +6 -3
  28. autocoder/memory/__init__.py +7 -0
  29. autocoder/memory/active_context_manager.py +649 -0
  30. autocoder/memory/active_package.py +469 -0
  31. autocoder/memory/async_processor.py +161 -0
  32. autocoder/memory/directory_mapper.py +67 -0
  33. autocoder/utils/auto_coder_utils/chat_stream_out.py +5 -0
  34. autocoder/utils/project_structure.py +35 -1
  35. autocoder/version.py +1 -1
  36. {auto_coder-0.1.305.dist-info → auto_coder-0.1.306.dist-info}/LICENSE +0 -0
  37. {auto_coder-0.1.305.dist-info → auto_coder-0.1.306.dist-info}/WHEEL +0 -0
  38. {auto_coder-0.1.305.dist-info → auto_coder-0.1.306.dist-info}/entry_points.txt +0 -0
  39. {auto_coder-0.1.305.dist-info → auto_coder-0.1.306.dist-info}/top_level.txt +0 -0
@@ -120,7 +120,8 @@ class QuickFilter():
120
120
  self.index_manager.index_filter_llm,
121
121
  [{"role": "user", "content": self.quick_filter_files.prompt(
122
122
  chunk, self.args.query)}],
123
- {}
123
+ {},
124
+ self.args
124
125
  )
125
126
  full_response, last_meta = stream_out(
126
127
  stream_generator,
@@ -485,7 +486,8 @@ class QuickFilter():
485
486
  stream_generator = stream_chat_with_continue(
486
487
  self.index_manager.index_filter_llm,
487
488
  [{"role": "user", "content": query}],
488
- {}
489
+ {},
490
+ self.args
489
491
  )
490
492
 
491
493
  def extract_file_number_list(content: str) -> str:
@@ -760,7 +762,8 @@ class QuickFilter():
760
762
  stream_generator = stream_chat_with_continue(
761
763
  self.index_manager.index_filter_llm,
762
764
  [{"role": "user", "content": prompt}],
763
- {}
765
+ {},
766
+ self.args
764
767
  )
765
768
 
766
769
  def extract_file_number_list(content: str) -> str:
@@ -0,0 +1,7 @@
1
+ """
2
+ 记忆子系统 - 用于跟踪和记录代码变更上下文
3
+ """
4
+
5
+ from autocoder.memory.active_context_manager import ActiveContextManager
6
+
7
+ __all__ = ["ActiveContextManager"]
@@ -0,0 +1,649 @@
1
+ """
2
+ 活动上下文管理器 - 子系统的主要接口
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ import time
8
+ import threading
9
+ import queue
10
+ from datetime import datetime
11
+ from typing import List, Dict, Optional, Any, Tuple, Set
12
+ from loguru import logger as global_logger
13
+ import byzerllm
14
+ import re
15
+ from pydantic import BaseModel, Field
16
+
17
+ from autocoder.common import AutoCoderArgs
18
+ from autocoder.common.action_yml_file_manager import ActionYmlFileManager
19
+ from autocoder.common.printer import Printer
20
+ from autocoder.memory.directory_mapper import DirectoryMapper
21
+ from autocoder.memory.active_package import ActivePackage
22
+ from autocoder.memory.async_processor import AsyncProcessor
23
+
24
+
25
+ class ActiveFileSections(BaseModel):
26
+ """活动文件内容的各个部分"""
27
+ header: str = Field(default="", description="文件标题部分")
28
+ current_change: str = Field(default="", description="当前变更部分")
29
+ document: str = Field(default="", description="文档部分")
30
+
31
+
32
+ class ActiveFileContext(BaseModel):
33
+ """单个活动文件的上下文信息"""
34
+ directory_path: str = Field(..., description="目录路径")
35
+ active_md_path: str = Field(..., description="活动文件路径")
36
+ content: str = Field(..., description="文件完整内容")
37
+ sections: ActiveFileSections = Field(..., description="文件内容各部分")
38
+ files: List[str] = Field(default_factory=list, description="与该活动文件相关的源文件列表")
39
+
40
+
41
+ class FileContextsResult(BaseModel):
42
+ """文件上下文查询结果"""
43
+ contexts: Dict[str, ActiveFileContext] = Field(
44
+ default_factory=dict,
45
+ description="键为目录路径,值为活动文件上下文"
46
+ )
47
+ not_found_files: List[str] = Field(
48
+ default_factory=list,
49
+ description="未找到对应活动上下文的文件列表"
50
+ )
51
+
52
+
53
+ class ActiveContextManager:
54
+ """
55
+ ActiveContextManager是活动上下文跟踪子系统的主要接口。
56
+
57
+ 该类负责:
58
+ 1. 从YAML文件加载任务数据
59
+ 2. 映射目录结构
60
+ 3. 生成活动上下文文档
61
+ 4. 管理异步任务执行
62
+
63
+ 该类实现了单例模式,确保系统中只有一个实例。
64
+ """
65
+
66
+ # 类变量,用于存储单例实例
67
+ _instance = None
68
+ _is_initialized = False
69
+
70
+ # 任务队列和队列处理线程
71
+ _task_queue = None
72
+ _queue_thread = None
73
+ _queue_lock = None
74
+ _is_processing = False
75
+
76
+ def __new__(cls, llm: byzerllm.ByzerLLM = None, args: AutoCoderArgs = None):
77
+ """
78
+ 实现单例模式,确保只创建一个实例
79
+
80
+ Args:
81
+ llm: ByzerLLM实例,用于生成文档内容
82
+ args: AutoCoderArgs实例,包含配置信息
83
+
84
+ Returns:
85
+ ActiveContextManager: 单例实例
86
+ """
87
+ if cls._instance is None:
88
+ cls._instance = super(ActiveContextManager, cls).__new__(cls)
89
+ cls._instance._is_initialized = False
90
+ return cls._instance
91
+
92
+ def __init__(self, llm: byzerllm.ByzerLLM, args: AutoCoderArgs):
93
+ """
94
+ 初始化活动上下文管理器
95
+
96
+ Args:
97
+ llm: ByzerLLM实例,用于生成文档内容
98
+ args: AutoCoderArgs实例,包含配置信息
99
+ """
100
+ # 如果已经初始化过,则直接返回
101
+ if self._is_initialized:
102
+ return
103
+
104
+ # 设置日志目录和文件
105
+ log_dir = os.path.join(args.source_dir, ".auto-coder", "active-context")
106
+ os.makedirs(log_dir, exist_ok=True)
107
+ log_file = os.path.join(log_dir, "active.log")
108
+
109
+ # 配置全局日志输出到文件,不输出到控制台
110
+ global_logger.configure(
111
+ handlers=[
112
+ # 移除控制台输出,只保留文件输出
113
+ {"sink": log_file, "level": "DEBUG", "rotation": "10 MB", "retention": "1 week",
114
+ "format": "{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}"}
115
+ ]
116
+ )
117
+
118
+ # 创建专用的logger实例
119
+ self.logger = global_logger.bind(name="ActiveContextManager")
120
+ self.logger.info(f"初始化 ActiveContextManager,日志输出到 {log_file}")
121
+
122
+ self.llm = llm
123
+ self.args = args
124
+ self.directory_mapper = DirectoryMapper()
125
+ self.active_package = ActivePackage(llm)
126
+ self.async_processor = AsyncProcessor()
127
+ self.yml_manager = ActionYmlFileManager(args.source_dir)
128
+ self.tasks = {} # 用于跟踪任务状态
129
+ self.printer = Printer()
130
+
131
+ # 初始化任务队列和锁
132
+ self.__class__._task_queue = queue.Queue()
133
+ self.__class__._queue_lock = threading.Lock()
134
+
135
+ # 启动队列处理线程
136
+ self.__class__._queue_thread = threading.Thread(target=self._process_queue, daemon=True)
137
+ self.__class__._queue_thread.start()
138
+
139
+ # 标记为已初始化
140
+ self._is_initialized = True
141
+
142
+ def _process_queue(self):
143
+ """
144
+ 处理任务队列的后台线程
145
+ 确保一次只有一个任务在运行
146
+ """
147
+ while True:
148
+ try:
149
+ # 从队列中获取任务
150
+ task = self._task_queue.get()
151
+ if task is None:
152
+ # None 是退出信号
153
+ break
154
+
155
+ # 设置处理标志
156
+ with self._queue_lock:
157
+ self.__class__._is_processing = True
158
+
159
+ # 解包任务参数
160
+ task_id, query, changed_urls, current_urls = task
161
+
162
+ # 更新任务状态为运行中
163
+ self.tasks[task_id]['status'] = 'running'
164
+
165
+ self._process_changes_async(task_id, query, changed_urls, current_urls)
166
+
167
+ # 重置处理标志
168
+ with self._queue_lock:
169
+ self.__class__._is_processing = False
170
+
171
+ # 标记任务完成
172
+ self._task_queue.task_done()
173
+
174
+ except Exception as e:
175
+ self.logger.error(f"Error in queue processing thread: {e}")
176
+ # 重置处理标志,确保队列可以继续处理
177
+ with self._queue_lock:
178
+ self.__class__._is_processing = False
179
+
180
+ def process_changes(self, file_name: Optional[str] = None) -> str:
181
+ """
182
+ 处理代码变更,创建活动上下文(非阻塞)
183
+
184
+ Args:
185
+ file_name: YAML文件名,如果为None则使用args.file
186
+
187
+ Returns:
188
+ str: 任务ID,可用于后续查询任务状态
189
+ """
190
+ try:
191
+ # 使用参数中的文件或者指定的文件
192
+ file_name = file_name or os.path.basename(self.args.file)
193
+
194
+ # 从YAML文件加载数据
195
+ yaml_content = self.yml_manager.load_yaml_content(file_name)
196
+
197
+ # 提取需要的信息
198
+ query = yaml_content.get('query', '')
199
+ changed_urls = yaml_content.get('add_updated_urls', [])
200
+ current_urls = yaml_content.get('urls', []) + yaml_content.get('dynamic_urls', [])
201
+
202
+ # 创建任务ID
203
+ task_id = f"active_context_{int(time.time())}_{file_name}"
204
+
205
+ # 更新任务状态
206
+ self.tasks[task_id] = {
207
+ 'status': 'queued',
208
+ 'start_time': datetime.now(),
209
+ 'file_name': file_name,
210
+ 'query': query,
211
+ 'changed_urls': changed_urls,
212
+ 'current_urls': current_urls,
213
+ 'queue_position': self._task_queue.qsize() + (1 if self._is_processing else 0)
214
+ }
215
+
216
+ # 直接启动后台线程处理任务,不通过队列
217
+ thread = threading.Thread(
218
+ target=self._execute_task_in_background,
219
+ args=(task_id, query, changed_urls, current_urls),
220
+ daemon=True # 使用守护线程,主程序退出时自动结束
221
+ )
222
+ thread.start()
223
+
224
+ # 记录任务已启动,并立即返回
225
+ self.logger.info(f"Task {task_id} started in background thread")
226
+ return task_id
227
+
228
+ except Exception as e:
229
+ self.logger.error(f"Error in process_changes: {e}")
230
+ raise
231
+
232
+ def _execute_task_in_background(self, task_id: str, query: str, changed_urls: List[str], current_urls: List[str]):
233
+ """
234
+ 在后台线程中执行任务,处理所有日志重定向
235
+
236
+ Args:
237
+ task_id: 任务ID
238
+ query: 用户查询
239
+ changed_urls: 变更的文件列表
240
+ current_urls: 当前相关的文件列表
241
+ """
242
+ try:
243
+ # 更新任务状态为运行中
244
+ self.tasks[task_id]['status'] = 'running'
245
+
246
+ # 重定向输出并执行任务
247
+ self._process_changes_async(task_id, query, changed_urls, current_urls)
248
+
249
+ # 更新任务状态为已完成
250
+ self.tasks[task_id]['status'] = 'completed'
251
+ self.tasks[task_id]['completion_time'] = datetime.now()
252
+
253
+ except Exception as e:
254
+ # 记录错误,但不允许异常传播到主线程
255
+ error_msg = f"Background task {task_id} failed: {str(e)}"
256
+ self.logger.error(error_msg)
257
+ self.tasks[task_id]['status'] = 'failed'
258
+ self.tasks[task_id]['error'] = error_msg
259
+
260
+ def _process_changes_async(self, task_id: str, query: str, changed_urls: List[str], current_urls: List[str]):
261
+ """
262
+ 实际处理变更的异步方法
263
+
264
+ Args:
265
+ task_id: 任务ID
266
+ query: 用户查询/需求
267
+ changed_urls: 变更的文件路径列表
268
+ current_urls: 当前相关的文件路径列表
269
+ """
270
+ try:
271
+ self.logger.info(f"==== 开始处理任务 {task_id} ====")
272
+ self.logger.info(f"查询内容: {query}")
273
+ self.logger.info(f"变更文件数量: {len(changed_urls)}")
274
+ self.logger.info(f"相关文件数量: {len(current_urls)}")
275
+
276
+ if changed_urls:
277
+ self.logger.debug(f"变更文件列表: {', '.join(changed_urls[:5])}{'...' if len(changed_urls) > 5 else ''}")
278
+
279
+ self.tasks[task_id]['status'] = 'running'
280
+
281
+ # 获取当前任务的文件名
282
+ file_name = self.tasks[task_id].get('file_name')
283
+ self.logger.info(f"任务关联文件: {file_name}")
284
+
285
+ # 获取文件变更信息
286
+ file_changes = {}
287
+ if file_name:
288
+ self.logger.info(f"正在获取提交变更信息...")
289
+ commit_changes = self.yml_manager.get_commit_changes(file_name)
290
+ if commit_changes and len(commit_changes) > 0:
291
+ # commit_changes结构为 [(query, urls, changes)]
292
+ _, _, changes = commit_changes[0]
293
+ file_changes = changes
294
+ self.logger.info(f"成功获取到 {len(file_changes)} 个文件变更")
295
+ else:
296
+ self.logger.warning("未找到提交变更信息")
297
+
298
+ # 1. 映射目录
299
+ self.logger.info("开始映射目录结构...")
300
+ directory_contexts = self.directory_mapper.map_directories(
301
+ self.args.source_dir, changed_urls, current_urls
302
+ )
303
+ self.logger.info(f"目录映射完成,找到 {len(directory_contexts)} 个相关目录")
304
+
305
+ # 2. 处理每个目录
306
+ processed_dirs = []
307
+ for i, context in enumerate(directory_contexts):
308
+ dir_path = context['directory_path']
309
+ self.logger.info(f"[{i+1}/{len(directory_contexts)}] 开始处理目录: {dir_path}")
310
+ try:
311
+ self._process_directory_context(context, query, file_changes)
312
+ processed_dirs.append(os.path.basename(dir_path))
313
+ self.logger.info(f"目录 {dir_path} 处理完成")
314
+ except Exception as e:
315
+ self.logger.error(f"处理目录 {dir_path} 时出错: {str(e)}")
316
+
317
+ # 3. 更新任务状态
318
+ self.tasks[task_id]['status'] = 'completed'
319
+ self.tasks[task_id]['completion_time'] = datetime.now()
320
+ self.tasks[task_id]['processed_dirs'] = processed_dirs
321
+
322
+ duration = (datetime.now() - self.tasks[task_id]['start_time']).total_seconds()
323
+ self.logger.info(f"==== 任务 {task_id} 处理完成 ====")
324
+ self.logger.info(f"总耗时: {duration:.2f}秒")
325
+ self.logger.info(f"处理的目录数: {len(processed_dirs)}")
326
+
327
+ except Exception as e:
328
+ # 记录错误
329
+ self.logger.error(f"任务 {task_id} 失败: {str(e)}", exc_info=True)
330
+ self.tasks[task_id]['status'] = 'failed'
331
+ self.tasks[task_id]['error'] = str(e)
332
+
333
+ def _process_directory_context(self, context: Dict[str, Any], query: str, file_changes: Dict[str, Tuple[str, str]] = None):
334
+ """
335
+ 处理单个目录上下文
336
+
337
+ Args:
338
+ context: 目录上下文字典
339
+ query: 用户查询/需求
340
+ file_changes: 文件变更字典,键为文件路径,值为(变更前内容, 变更后内容)的元组
341
+ """
342
+ try:
343
+ directory_path = context['directory_path']
344
+ self.logger.debug(f"--- 处理目录上下文开始: {directory_path} ---")
345
+
346
+ # 1. 确保目录存在
347
+ target_dir = self._get_active_context_path(directory_path)
348
+ os.makedirs(target_dir, exist_ok=True)
349
+ self.logger.debug(f"目标目录准备完成: {target_dir}")
350
+
351
+ # 2. 检查是否有现有的active.md文件
352
+ existing_file_path = os.path.join(target_dir, "active.md")
353
+ if os.path.exists(existing_file_path):
354
+ self.logger.info(f"找到现有 active.md 文件: {existing_file_path}")
355
+ try:
356
+ with open(existing_file_path, 'r', encoding='utf-8') as f:
357
+ existing_content_preview = f.read(500)
358
+ self.logger.debug(f"现有文件内容预览: {existing_content_preview[:100]}...")
359
+ except Exception as e:
360
+ self.logger.warning(f"无法读取现有文件内容: {str(e)}")
361
+ else:
362
+ existing_file_path = None
363
+ self.logger.info(f"目录 {directory_path} 没有找到现有的 active.md 文件")
364
+
365
+ # 记录目录中的文件信息
366
+ changed_files = context.get('changed_files', [])
367
+ current_files = context.get('current_files', [])
368
+ self.logger.debug(f"目录中变更文件数: {len(changed_files)}")
369
+ self.logger.debug(f"目录中当前文件数: {len(current_files)}")
370
+
371
+ # 过滤出当前目录相关的文件变更
372
+ directory_changes = {}
373
+ if file_changes:
374
+ self.logger.debug(f"开始筛选与目录相关的文件变更...")
375
+ # 获取当前目录下的所有文件路径
376
+ dir_files = []
377
+ for file_info in changed_files:
378
+ file_path = file_info['path']
379
+ dir_files.append(file_path)
380
+ self.logger.debug(f"添加变更文件: {file_path}")
381
+
382
+ for file_info in current_files:
383
+ file_path = file_info['path']
384
+ dir_files.append(file_path)
385
+ self.logger.debug(f"添加当前文件: {file_path}")
386
+
387
+ # 从file_changes中获取当前目录文件的变更
388
+ for file_path, change_info in file_changes.items():
389
+ if file_path in dir_files:
390
+ directory_changes[file_path] = change_info
391
+ old_content, new_content = change_info
392
+ old_preview = old_content[:50] if old_content else "(空)"
393
+ new_preview = new_content[:50] if new_content else "(空)"
394
+ self.logger.debug(f"文件变更: {file_path}")
395
+ self.logger.debug(f" 旧内容: {old_preview}...")
396
+ self.logger.debug(f" 新内容: {new_preview}...")
397
+
398
+ self.logger.info(f"找到 {len(directory_changes)} 个与目录 {directory_path} 相关的文件变更")
399
+ else:
400
+ self.logger.debug("没有提供文件变更信息")
401
+
402
+ # 3. 生成活动文件内容
403
+ self.logger.info(f"开始为目录 {directory_path} 生成活动文件内容...")
404
+ markdown_content = self.active_package.generate_active_file(
405
+ context,
406
+ query,
407
+ existing_file_path=existing_file_path,
408
+ file_changes=directory_changes
409
+ )
410
+
411
+ content_length = len(markdown_content)
412
+ self.logger.debug(f"生成的活动文件内容长度: {content_length} 字符")
413
+ if content_length > 0:
414
+ self.logger.debug(f"内容预览: {markdown_content[:200]}...")
415
+
416
+ # 4. 写入文件
417
+ active_md_path = os.path.join(target_dir, "active.md")
418
+ self.logger.info(f"正在写入活动文件: {active_md_path}")
419
+ with open(active_md_path, "w", encoding="utf-8") as f:
420
+ f.write(markdown_content)
421
+
422
+ self.logger.info(f"成功创建/更新目录 {directory_path} 的活动文件")
423
+ self.logger.debug(f"--- 处理目录上下文完成: {directory_path} ---")
424
+
425
+ except Exception as e:
426
+ self.logger.error(f"处理目录 {context.get('directory_path', 'unknown')} 时出错: {str(e)}", exc_info=True)
427
+ raise
428
+
429
+ def get_task_status(self, task_id: str) -> Dict[str, Any]:
430
+ """
431
+ 获取任务状态
432
+
433
+ Args:
434
+ task_id: 任务ID
435
+
436
+ Returns:
437
+ Dict: 任务状态信息
438
+ """
439
+ if task_id not in self.tasks:
440
+ return {'status': 'not_found', 'task_id': task_id}
441
+
442
+ task = self.tasks[task_id]
443
+
444
+ # 计算任务运行时间
445
+ start_time = task.get('start_time')
446
+ if task.get('status') == 'completed':
447
+ completion_time = task.get('completion_time')
448
+ if start_time and completion_time:
449
+ duration = (completion_time - start_time).total_seconds()
450
+ else:
451
+ duration = None
452
+ elapsed = None
453
+ else:
454
+ if start_time:
455
+ elapsed = (datetime.now() - start_time).total_seconds()
456
+ else:
457
+ elapsed = None
458
+ duration = None
459
+
460
+ # 构建日志文件路径
461
+ log_file_path = None
462
+ if 'file_name' in task:
463
+ log_dir = os.path.join(self.args.source_dir, '.auto-coder', 'active-context', 'logs')
464
+ log_file_path = os.path.join(log_dir, f'{task_id}.log')
465
+ if not os.path.exists(log_file_path):
466
+ log_file_path = None
467
+
468
+ # 构建返回结果
469
+ result = {
470
+ 'task_id': task_id,
471
+ 'status': task.get('status', 'unknown'),
472
+ 'file_name': task.get('file_name'),
473
+ 'start_time': start_time.strftime("%Y-%m-%d %H:%M:%S") if start_time else None,
474
+ }
475
+
476
+ # 添加可选信息
477
+ if 'completion_time' in task:
478
+ result['completion_time'] = task['completion_time'].strftime("%Y-%m-%d %H:%M:%S")
479
+
480
+ if elapsed is not None:
481
+ mins, secs = divmod(elapsed, 60)
482
+ hrs, mins = divmod(mins, 60)
483
+ result['elapsed'] = f"{int(hrs):02d}:{int(mins):02d}:{int(secs):02d}"
484
+
485
+ if duration is not None:
486
+ mins, secs = divmod(duration, 60)
487
+ hrs, mins = divmod(mins, 60)
488
+ result['duration'] = f"{int(hrs):02d}:{int(mins):02d}:{int(secs):02d}"
489
+
490
+ if 'processed_dirs' in task:
491
+ result['processed_dirs'] = task['processed_dirs']
492
+ result['processed_dirs_count'] = len(task['processed_dirs'])
493
+
494
+ if 'error' in task:
495
+ result['error'] = task['error']
496
+
497
+ if log_file_path and os.path.exists(log_file_path):
498
+ result['log_file'] = log_file_path
499
+
500
+ # 尝试获取日志文件大小
501
+ try:
502
+ file_size = os.path.getsize(log_file_path)
503
+ result['log_file_size'] = f"{file_size / 1024:.2f} KB"
504
+ except:
505
+ pass
506
+
507
+ return result
508
+
509
+ def get_all_tasks(self) -> List[Dict[str, Any]]:
510
+ """
511
+ 获取所有任务状态
512
+
513
+ Returns:
514
+ List[Dict]: 所有任务的状态信息
515
+ """
516
+ return [{'task_id': tid, **task} for tid, task in self.tasks.items()]
517
+
518
+ def get_running_tasks(self) -> List[Dict[str, Any]]:
519
+ """
520
+ 获取所有正在运行的任务
521
+
522
+ Returns:
523
+ List[Dict]: 所有正在运行的任务的状态信息
524
+ """
525
+ return [{'task_id': tid, **task} for tid, task in self.tasks.items()
526
+ if task['status'] in ['running', 'queued']]
527
+
528
+ def load_active_contexts_for_files(self, file_paths: List[str]) -> FileContextsResult:
529
+ """
530
+ 根据文件路径列表,找到并加载对应的活动上下文文件
531
+
532
+ Args:
533
+ file_paths: 文件路径列表
534
+
535
+ Returns:
536
+ FileContextsResult: 包含活动上下文信息的结构化结果
537
+ """
538
+ try:
539
+ result = FileContextsResult()
540
+
541
+ # 记录未找到对应活动上下文的文件
542
+ found_files: Set[str] = set()
543
+
544
+ # 1. 获取文件所在的唯一目录列表
545
+ directories = set()
546
+ for file_path in file_paths:
547
+ # 获取文件所在的目录
548
+ dir_path = os.path.dirname(file_path)
549
+ if os.path.exists(dir_path):
550
+ directories.add(dir_path)
551
+
552
+ # 2. 查找每个目录的活动上下文文件
553
+ for dir_path in directories:
554
+ # 获取活动上下文目录路径
555
+ active_context_dir = self._get_active_context_path(dir_path)
556
+ active_md_path = os.path.join(active_context_dir, "active.md")
557
+
558
+ # 检查active.md文件是否存在
559
+ if os.path.exists(active_md_path):
560
+ try:
561
+ # 读取文件内容
562
+ with open(active_md_path, 'r', encoding='utf-8') as f:
563
+ content = f.read()
564
+
565
+ # 解析文件内容
566
+ sections_dict = self._parse_active_md_content(content)
567
+ sections = ActiveFileSections(**sections_dict)
568
+
569
+ # 找到相关的文件
570
+ related_files = [f for f in file_paths if dir_path in f]
571
+
572
+ # 记录找到了对应活动上下文的文件
573
+ found_files.update(related_files)
574
+
575
+ # 创建活动文件上下文
576
+ active_context = ActiveFileContext(
577
+ directory_path=dir_path,
578
+ active_md_path=active_md_path,
579
+ content=content,
580
+ sections=sections,
581
+ files=related_files
582
+ )
583
+
584
+ # 添加到结果
585
+ result.contexts[dir_path] = active_context
586
+
587
+ self.logger.info(f"已加载目录 {dir_path} 的活动上下文文件")
588
+ except Exception as e:
589
+ self.logger.error(f"读取活动上下文文件 {active_md_path} 时出错: {e}")
590
+
591
+ # 3. 记录未找到对应活动上下文的文件
592
+ result.not_found_files = [f for f in file_paths if f not in found_files]
593
+
594
+ return result
595
+
596
+ except Exception as e:
597
+ self.logger.error(f"加载活动上下文失败: {e}")
598
+ return FileContextsResult(not_found_files=file_paths)
599
+
600
+ def _parse_active_md_content(self, content: str) -> Dict[str, str]:
601
+ """
602
+ 解析活动上下文文件内容,提取各个部分
603
+
604
+ Args:
605
+ content: 活动上下文文件内容
606
+
607
+ Returns:
608
+ Dict[str, str]: 包含标题、当前变更和文档部分的字典
609
+ """
610
+ try:
611
+ result = {
612
+ 'header': '',
613
+ 'current_change': '',
614
+ 'document': ''
615
+ }
616
+
617
+ # 提取标题部分(到第一个二级标题之前)
618
+ header_match = re.search(r'^(.*?)(?=\n## )', content, re.DOTALL)
619
+ if header_match:
620
+ result['header'] = header_match.group(1).strip()
621
+
622
+ # 提取当前变更部分
623
+ current_change_match = re.search(r'## 当前变更\s*\n(.*?)(?=\n## |$)', content, re.DOTALL)
624
+ if current_change_match:
625
+ result['current_change'] = current_change_match.group(1).strip()
626
+
627
+ # 提取文档部分
628
+ document_match = re.search(r'## 文档\s*\n(.*?)(?=\n## |$)', content, re.DOTALL)
629
+ if document_match:
630
+ result['document'] = document_match.group(1).strip()
631
+
632
+ return result
633
+ except Exception as e:
634
+ self.logger.error(f"解析活动上下文文件内容时出错: {e}")
635
+ return {'header': '', 'current_change': '', 'document': ''}
636
+
637
+ def _get_active_context_path(self, directory_path: str) -> str:
638
+ """
639
+ 获取活动上下文中对应的目录路径
640
+
641
+ Args:
642
+ directory_path: 原始目录路径
643
+
644
+ Returns:
645
+ str: 活动上下文中对应的目录路径
646
+ """
647
+ relative_path = os.path.relpath(directory_path, self.args.source_dir)
648
+ return os.path.join(self.args.source_dir, ".auto-coder", "active-context", relative_path)
649
+