auto-coder 0.1.310__py3-none-any.whl → 0.1.312__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.

@@ -7,6 +7,7 @@ import sys
7
7
  import time
8
8
  import threading
9
9
  import queue
10
+ import json
10
11
  from datetime import datetime
11
12
  from typing import List, Dict, Optional, Any, Tuple, Set
12
13
  from loguru import logger as global_logger
@@ -35,13 +36,14 @@ class ActiveFileContext(BaseModel):
35
36
  active_md_path: str = Field(..., description="活动文件路径")
36
37
  content: str = Field(..., description="文件完整内容")
37
38
  sections: ActiveFileSections = Field(..., description="文件内容各部分")
38
- files: List[str] = Field(default_factory=list, description="与该活动文件相关的源文件列表")
39
+ files: List[str] = Field(default_factory=list,
40
+ description="与该活动文件相关的源文件列表")
39
41
 
40
42
 
41
43
  class FileContextsResult(BaseModel):
42
44
  """文件上下文查询结果"""
43
45
  contexts: Dict[str, ActiveFileContext] = Field(
44
- default_factory=dict,
46
+ default_factory=dict,
45
47
  description="键为目录路径,值为活动文件上下文"
46
48
  )
47
49
  not_found_files: List[str] = Field(
@@ -53,34 +55,35 @@ class FileContextsResult(BaseModel):
53
55
  class ActiveContextManager:
54
56
  """
55
57
  ActiveContextManager是活动上下文跟踪子系统的主要接口。
56
-
58
+
57
59
  该类负责:
58
60
  1. 从YAML文件加载任务数据
59
61
  2. 映射目录结构
60
62
  3. 生成活动上下文文档
61
63
  4. 管理异步任务执行
62
-
64
+ 5. 持久化任务信息
65
+
63
66
  该类实现了单例模式,确保系统中只有一个实例。
64
67
  """
65
-
68
+
66
69
  # 类变量,用于存储单例实例
67
70
  _instance = None
68
71
  _is_initialized = False
69
-
72
+
70
73
  # 任务队列和队列处理线程
71
74
  _task_queue = None
72
75
  _queue_thread = None
73
76
  _queue_lock = None
74
77
  _is_processing = False
75
-
78
+
76
79
  def __new__(cls, llm: byzerllm.ByzerLLM = None, args: AutoCoderArgs = None):
77
80
  """
78
81
  实现单例模式,确保只创建一个实例
79
-
82
+
80
83
  Args:
81
84
  llm: ByzerLLM实例,用于生成文档内容
82
85
  args: AutoCoderArgs实例,包含配置信息
83
-
86
+
84
87
  Returns:
85
88
  ActiveContextManager: 单例实例
86
89
  """
@@ -88,11 +91,11 @@ class ActiveContextManager:
88
91
  cls._instance = super(ActiveContextManager, cls).__new__(cls)
89
92
  cls._instance._is_initialized = False
90
93
  return cls._instance
91
-
92
- def __init__(self, llm: byzerllm.ByzerLLM,source_dir:str):
94
+
95
+ def __init__(self, llm: byzerllm.ByzerLLM, source_dir: str):
93
96
  """
94
97
  初始化活动上下文管理器
95
-
98
+
96
99
  Args:
97
100
  llm: ByzerLLM实例,用于生成文档内容
98
101
  """
@@ -104,39 +107,128 @@ class ActiveContextManager:
104
107
  log_dir = os.path.join(source_dir, ".auto-coder", "active-context")
105
108
  os.makedirs(log_dir, exist_ok=True)
106
109
  log_file = os.path.join(log_dir, "active.log")
107
-
110
+
108
111
  # 配置全局日志输出到文件,不输出到控制台
109
112
  global_logger.configure(
110
113
  handlers=[
111
114
  # 移除控制台输出,只保留文件输出
112
- {"sink": log_file, "level": "DEBUG", "rotation": "10 MB", "retention": "1 week",
115
+ {"sink": log_file, "level": "DEBUG", "rotation": "10 MB", "retention": "1 week",
113
116
  "format": "{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}"}
114
117
  ]
115
118
  )
116
-
119
+
117
120
  # 创建专用的logger实例
118
121
  self.logger = global_logger.bind(name="ActiveContextManager")
119
122
  self.logger.info(f"初始化 ActiveContextManager,日志输出到 {log_file}")
120
-
121
- self.llm = llm
122
- self.directory_mapper = DirectoryMapper()
123
- self.active_package = ActivePackage(llm)
123
+
124
+ self.llm = llm
125
+ self.directory_mapper = DirectoryMapper()
124
126
  self.async_processor = AsyncProcessor()
125
127
  self.yml_manager = ActionYmlFileManager(source_dir)
126
- self.tasks = {} # 用于跟踪任务状态
127
- self.printer = Printer()
128
128
 
129
+ # 任务持久化文件路径
130
+ self.tasks_file_path = os.path.join(source_dir, ".auto-coder", "active-context", "tasks.json")
131
+
132
+ # 加载已存在的任务
133
+ self.tasks = self._load_tasks_from_disk()
134
+ self.tasks_lock = threading.Lock() # 添加锁以保护任务字典的操作
135
+
136
+ self.printer = Printer()
137
+
129
138
  # 初始化任务队列和锁
130
139
  self.__class__._task_queue = queue.Queue()
131
140
  self.__class__._queue_lock = threading.Lock()
132
-
141
+
133
142
  # 启动队列处理线程
134
- self.__class__._queue_thread = threading.Thread(target=self._process_queue, daemon=True)
143
+ self.__class__._queue_thread = threading.Thread(
144
+ target=self._process_queue, daemon=True)
135
145
  self.__class__._queue_thread.start()
136
-
146
+
137
147
  # 标记为已初始化
138
- self._is_initialized = True
139
-
148
+ self._is_initialized = True
149
+
150
+ def _load_tasks_from_disk(self) -> Dict[str, Dict[str, Any]]:
151
+ """
152
+ 从磁盘加载任务信息
153
+
154
+ Returns:
155
+ Dict: 任务字典
156
+ """
157
+ try:
158
+ if os.path.exists(self.tasks_file_path):
159
+ with open(self.tasks_file_path, 'r', encoding='utf-8') as f:
160
+ tasks_json = json.load(f)
161
+
162
+ # 转换时间字符串为datetime对象
163
+ for task_id, task in tasks_json.items():
164
+ if 'start_time' in task and task['start_time']:
165
+ try:
166
+ task['start_time'] = datetime.fromisoformat(task['start_time'])
167
+ except:
168
+ task['start_time'] = None
169
+
170
+ if 'completion_time' in task and task['completion_time']:
171
+ try:
172
+ task['completion_time'] = datetime.fromisoformat(task['completion_time'])
173
+ except:
174
+ task['completion_time'] = None
175
+
176
+ self.logger.info(f"从 {self.tasks_file_path} 加载了 {len(tasks_json)} 个任务")
177
+ return tasks_json
178
+ else:
179
+ self.logger.info(f"任务文件 {self.tasks_file_path} 不存在,将创建新文件")
180
+ return {}
181
+ except Exception as e:
182
+ self.logger.error(f"加载任务文件失败: {e}")
183
+ return {}
184
+
185
+ def _save_tasks_to_disk(self):
186
+ """
187
+ 将任务信息保存到磁盘
188
+ """
189
+ try:
190
+ with self.tasks_lock: # 使用锁确保线程安全
191
+ # 创建任务字典的副本并进行序列化处理
192
+ tasks_copy = {}
193
+ for task_id, task in self.tasks.items():
194
+ # 深拷贝并转换不可序列化的对象
195
+ task_copy = {}
196
+ for k, v in task.items():
197
+ if k in ['start_time', 'completion_time'] and isinstance(v, datetime):
198
+ task_copy[k] = v.isoformat()
199
+ else:
200
+ task_copy[k] = v
201
+ tasks_copy[task_id] = task_copy
202
+
203
+ # 确保目录存在
204
+ os.makedirs(os.path.dirname(self.tasks_file_path), exist_ok=True)
205
+
206
+ # 写入JSON文件
207
+ with open(self.tasks_file_path, 'w', encoding='utf-8') as f:
208
+ json.dump(tasks_copy, f, ensure_ascii=False, indent=2)
209
+
210
+ self.logger.debug(f"成功保存 {len(tasks_copy)} 个任务到 {self.tasks_file_path}")
211
+ except Exception as e:
212
+ self.logger.error(f"保存任务到磁盘失败: {e}")
213
+
214
+ def _update_task(self, task_id: str, **kwargs):
215
+ """
216
+ 更新任务信息并持久化到磁盘
217
+
218
+ Args:
219
+ task_id: 任务ID
220
+ **kwargs: 要更新的任务属性
221
+ """
222
+ with self.tasks_lock:
223
+ if task_id in self.tasks:
224
+ self.tasks[task_id].update(kwargs)
225
+ self.logger.debug(f"更新任务 {task_id} 信息: {kwargs.keys()}")
226
+ else:
227
+ self.logger.warning(f"尝试更新不存在的任务: {task_id}")
228
+
229
+ # 持久化到磁盘
230
+ self._save_tasks_to_disk()
231
+
140
232
  def _process_queue(self):
141
233
  """
142
234
  处理任务队列的后台线程
@@ -149,90 +241,102 @@ class ActiveContextManager:
149
241
  if task is None:
150
242
  # None 是退出信号
151
243
  break
152
-
244
+
153
245
  # 设置处理标志
154
246
  with self._queue_lock:
155
247
  self.__class__._is_processing = True
156
-
248
+
157
249
  # 解包任务参数
158
250
  task_id, query, changed_urls, current_urls = task
159
-
251
+
160
252
  # 更新任务状态为运行中
161
- self.tasks[task_id]['status'] = 'running'
162
-
163
- self._process_changes_async(task_id, query, changed_urls, current_urls)
164
-
253
+ self._update_task(task_id, status='running')
254
+
255
+ self._process_changes_async(
256
+ task_id, query, changed_urls, current_urls)
257
+
165
258
  # 重置处理标志
166
259
  with self._queue_lock:
167
260
  self.__class__._is_processing = False
168
-
261
+
169
262
  # 标记任务完成
170
263
  self._task_queue.task_done()
171
-
264
+
172
265
  except Exception as e:
173
266
  self.logger.error(f"Error in queue processing thread: {e}")
174
267
  # 重置处理标志,确保队列可以继续处理
175
268
  with self._queue_lock:
176
269
  self.__class__._is_processing = False
177
-
178
- def process_changes(self, args:AutoCoderArgs) -> str:
270
+
271
+ def process_changes(self, args: AutoCoderArgs) -> str:
179
272
  """
180
273
  处理代码变更,创建活动上下文(非阻塞)
181
-
274
+
182
275
  Args:
183
276
  args: AutoCoderArgs实例,包含配置信息
184
-
277
+
185
278
  Returns:
186
279
  str: 任务ID,可用于后续查询任务状态
187
280
  """
188
281
  try:
189
282
  # 使用参数中的文件或者指定的文件
190
283
  if not args.file:
191
- raise ValueError("action file is required")
192
-
284
+ raise ValueError("action file is required")
285
+
193
286
  file_name = os.path.basename(args.file)
194
287
  # 从YAML文件加载数据
195
288
  yaml_content = self.yml_manager.load_yaml_content(file_name)
196
-
289
+
197
290
  # 提取需要的信息
198
291
  query = yaml_content.get('query', '')
199
292
  changed_urls = yaml_content.get('add_updated_urls', [])
200
- current_urls = yaml_content.get('urls', []) + yaml_content.get('dynamic_urls', [])
201
-
293
+ current_urls = yaml_content.get(
294
+ 'urls', []) + yaml_content.get('dynamic_urls', [])
295
+
202
296
  # 创建任务ID
203
297
  task_id = f"active_context_{int(time.time())}_{file_name}"
204
-
298
+
205
299
  # 更新任务状态
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
-
300
+ with self.tasks_lock:
301
+ self.tasks[task_id] = {
302
+ 'status': 'queued',
303
+ 'start_time': datetime.now(),
304
+ 'file_name': file_name,
305
+ 'query': query,
306
+ 'changed_urls': changed_urls,
307
+ 'current_urls': current_urls,
308
+ 'queue_position': self._task_queue.qsize() + (1 if self._is_processing else 0),
309
+ 'total_tokens': 0, # 初始化token计数
310
+ 'input_tokens': 0, # 初始化输入token计数
311
+ 'output_tokens': 0, # 初始化输出token计数
312
+ 'cost': 0.0, # 初始化费用
313
+ }
314
+ # 持久化任务信息
315
+ self._save_tasks_to_disk()
316
+
216
317
  # 直接启动后台线程处理任务,不通过队列
217
318
  thread = threading.Thread(
218
319
  target=self._execute_task_in_background,
219
- args=(task_id, query, changed_urls, current_urls),
320
+ args=(task_id, query, changed_urls, current_urls, args),
220
321
  daemon=True # 使用守护线程,主程序退出时自动结束
221
322
  )
222
323
  thread.start()
223
-
324
+
224
325
  # 记录任务已启动,并立即返回
225
326
  self.logger.info(f"Task {task_id} started in background thread")
226
327
  return task_id
227
-
328
+
228
329
  except Exception as e:
229
- self.logger.error(f"Error in process_changes: {e}")
330
+ self.logger.error(f"Error in process_changes: {e}")
230
331
  raise
231
-
232
- def _execute_task_in_background(self, task_id: str, query: str, changed_urls: List[str], current_urls: List[str]):
332
+
333
+ def _execute_task_in_background(self, task_id: str,
334
+ query: str,
335
+ changed_urls: List[str], current_urls: List[str],
336
+ args: AutoCoderArgs):
233
337
  """
234
338
  在后台线程中执行任务,处理所有日志重定向
235
-
339
+
236
340
  Args:
237
341
  task_id: 任务ID
238
342
  query: 用户查询
@@ -241,47 +345,48 @@ class ActiveContextManager:
241
345
  """
242
346
  try:
243
347
  # 更新任务状态为运行中
244
- self.tasks[task_id]['status'] = 'running'
245
-
348
+ self._update_task(task_id, status='running')
349
+
246
350
  # 重定向输出并执行任务
247
- self._process_changes_async(task_id, query, changed_urls, current_urls)
248
-
351
+ self._process_changes_async(
352
+ task_id, query, changed_urls, current_urls, args)
353
+
249
354
  # 更新任务状态为已完成
250
- self.tasks[task_id]['status'] = 'completed'
251
- self.tasks[task_id]['completion_time'] = datetime.now()
252
-
355
+ self._update_task(task_id, status='completed', completion_time=datetime.now())
356
+
253
357
  except Exception as e:
254
358
  # 记录错误,但不允许异常传播到主线程
255
359
  error_msg = f"Background task {task_id} failed: {str(e)}"
256
360
  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]):
361
+ self._update_task(task_id, status='failed', error=error_msg)
362
+
363
+ def _process_changes_async(self, task_id: str, query: str, changed_urls: List[str], current_urls: List[str], args: AutoCoderArgs):
261
364
  """
262
365
  实际处理变更的异步方法
263
-
366
+
264
367
  Args:
265
368
  task_id: 任务ID
266
369
  query: 用户查询/需求
267
370
  changed_urls: 变更的文件路径列表
268
371
  current_urls: 当前相关的文件路径列表
372
+ args: AutoCoderArgs实例,包含配置信息
269
373
  """
270
374
  try:
271
375
  self.logger.info(f"==== 开始处理任务 {task_id} ====")
272
376
  self.logger.info(f"查询内容: {query}")
273
377
  self.logger.info(f"变更文件数量: {len(changed_urls)}")
274
378
  self.logger.info(f"相关文件数量: {len(current_urls)}")
275
-
379
+
276
380
  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
-
381
+ self.logger.debug(
382
+ f"变更文件列表: {', '.join(changed_urls[:5])}{'...' if len(changed_urls) > 5 else ''}")
383
+
384
+ self._update_task(task_id, status='running')
385
+
281
386
  # 获取当前任务的文件名
282
387
  file_name = self.tasks[task_id].get('file_name')
283
388
  self.logger.info(f"任务关联文件: {file_name}")
284
-
389
+
285
390
  # 获取文件变更信息
286
391
  file_changes = {}
287
392
  if file_name:
@@ -294,60 +399,98 @@ class ActiveContextManager:
294
399
  self.logger.info(f"成功获取到 {len(file_changes)} 个文件变更")
295
400
  else:
296
401
  self.logger.warning("未找到提交变更信息")
297
-
402
+
298
403
  # 1. 映射目录
299
404
  self.logger.info("开始映射目录结构...")
300
405
  directory_contexts = self.directory_mapper.map_directories(
301
406
  self.source_dir, changed_urls, current_urls
302
407
  )
303
408
  self.logger.info(f"目录映射完成,找到 {len(directory_contexts)} 个相关目录")
304
-
409
+
305
410
  # 2. 处理每个目录
306
411
  processed_dirs = []
412
+ total_tokens = 0
413
+ input_tokens = 0
414
+ output_tokens = 0
415
+ cost = 0.0
416
+
307
417
  for i, context in enumerate(directory_contexts):
308
418
  dir_path = context['directory_path']
309
- self.logger.info(f"[{i+1}/{len(directory_contexts)}] 开始处理目录: {dir_path}")
419
+ self.logger.info(
420
+ f"[{i+1}/{len(directory_contexts)}] 开始处理目录: {dir_path}")
310
421
  try:
311
- self._process_directory_context(context, query, file_changes)
422
+ result = self._process_directory_context(
423
+ context, query, file_changes, args)
424
+
425
+ # 如果返回了token和费用信息,则累加
426
+ if isinstance(result, dict):
427
+ dir_tokens = result.get('total_tokens', 0)
428
+ dir_input_tokens = result.get('input_tokens', 0)
429
+ dir_output_tokens = result.get('output_tokens', 0)
430
+ dir_cost = result.get('cost', 0.0)
431
+
432
+ total_tokens += dir_tokens
433
+ input_tokens += dir_input_tokens
434
+ output_tokens += dir_output_tokens
435
+ cost += dir_cost
436
+
437
+ self.logger.info(f"目录 {dir_path} 处理完成,使用了 {dir_tokens} tokens,费用 {dir_cost:.6f}")
438
+
312
439
  processed_dirs.append(os.path.basename(dir_path))
313
- self.logger.info(f"目录 {dir_path} 处理完成")
440
+
314
441
  except Exception as e:
315
442
  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
443
 
322
- duration = (datetime.now() - self.tasks[task_id]['start_time']).total_seconds()
444
+ # 更新任务的token和费用信息
445
+ self._update_task(
446
+ task_id,
447
+ total_tokens=total_tokens,
448
+ input_tokens=input_tokens,
449
+ output_tokens=output_tokens,
450
+ cost=cost,
451
+ processed_dirs=processed_dirs
452
+ )
453
+
454
+ # 3. 更新任务状态
455
+ self._update_task(task_id,
456
+ status='completed',
457
+ completion_time=datetime.now())
458
+
459
+ duration = (datetime.now() -
460
+ self.tasks[task_id]['start_time']).total_seconds()
323
461
  self.logger.info(f"==== 任务 {task_id} 处理完成 ====")
324
462
  self.logger.info(f"总耗时: {duration:.2f}秒")
325
463
  self.logger.info(f"处理的目录数: {len(processed_dirs)}")
326
-
464
+ self.logger.info(f"使用总tokens: {total_tokens} (输入: {input_tokens}, 输出: {output_tokens})")
465
+ self.logger.info(f"总费用: {cost:.6f}")
466
+
327
467
  except Exception as e:
328
468
  # 记录错误
329
469
  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):
470
+ self._update_task(task_id, status='failed', error=str(e))
471
+
472
+ def _process_directory_context(self, context: Dict[str, Any], query: str, file_changes: Dict[str, Tuple[str, str]] = None, args: AutoCoderArgs = None):
334
473
  """
335
474
  处理单个目录上下文
336
-
475
+
337
476
  Args:
338
477
  context: 目录上下文字典
339
478
  query: 用户查询/需求
340
479
  file_changes: 文件变更字典,键为文件路径,值为(变更前内容, 变更后内容)的元组
480
+ args: AutoCoderArgs实例,包含配置信息
481
+
482
+ Returns:
483
+ Dict: 包含token和费用信息的字典
341
484
  """
342
485
  try:
343
486
  directory_path = context['directory_path']
344
487
  self.logger.debug(f"--- 处理目录上下文开始: {directory_path} ---")
345
-
488
+
346
489
  # 1. 确保目录存在
347
490
  target_dir = self._get_active_context_path(directory_path)
348
491
  os.makedirs(target_dir, exist_ok=True)
349
492
  self.logger.debug(f"目标目录准备完成: {target_dir}")
350
-
493
+
351
494
  # 2. 检查是否有现有的active.md文件
352
495
  existing_file_path = os.path.join(target_dir, "active.md")
353
496
  if os.path.exists(existing_file_path):
@@ -355,19 +498,20 @@ class ActiveContextManager:
355
498
  try:
356
499
  with open(existing_file_path, 'r', encoding='utf-8') as f:
357
500
  existing_content_preview = f.read(500)
358
- self.logger.debug(f"现有文件内容预览: {existing_content_preview[:100]}...")
501
+ self.logger.debug(
502
+ f"现有文件内容预览: {existing_content_preview[:100]}...")
359
503
  except Exception as e:
360
504
  self.logger.warning(f"无法读取现有文件内容: {str(e)}")
361
505
  else:
362
506
  existing_file_path = None
363
507
  self.logger.info(f"目录 {directory_path} 没有找到现有的 active.md 文件")
364
-
508
+
365
509
  # 记录目录中的文件信息
366
510
  changed_files = context.get('changed_files', [])
367
511
  current_files = context.get('current_files', [])
368
512
  self.logger.debug(f"目录中变更文件数: {len(changed_files)}")
369
513
  self.logger.debug(f"目录中当前文件数: {len(current_files)}")
370
-
514
+
371
515
  # 过滤出当前目录相关的文件变更
372
516
  directory_changes = {}
373
517
  if file_changes:
@@ -378,69 +522,95 @@ class ActiveContextManager:
378
522
  file_path = file_info['path']
379
523
  dir_files.append(file_path)
380
524
  self.logger.debug(f"添加变更文件: {file_path}")
381
-
525
+
382
526
  for file_info in current_files:
383
527
  file_path = file_info['path']
384
528
  dir_files.append(file_path)
385
529
  self.logger.debug(f"添加当前文件: {file_path}")
386
-
530
+
387
531
  # 从file_changes中获取当前目录文件的变更
388
532
  for file_path, change_info in file_changes.items():
389
533
  if file_path in dir_files:
390
534
  directory_changes[file_path] = change_info
391
535
  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 "(空)"
536
+ old_preview = old_content[:
537
+ 50] if old_content else "(空)"
538
+ new_preview = new_content[:
539
+ 50] if new_content else "(空)"
394
540
  self.logger.debug(f"文件变更: {file_path}")
395
541
  self.logger.debug(f" 旧内容: {old_preview}...")
396
542
  self.logger.debug(f" 新内容: {new_preview}...")
397
-
398
- self.logger.info(f"找到 {len(directory_changes)} 个与目录 {directory_path} 相关的文件变更")
543
+
544
+ self.logger.info(
545
+ f"找到 {len(directory_changes)} 个与目录 {directory_path} 相关的文件变更")
399
546
  else:
400
547
  self.logger.debug("没有提供文件变更信息")
401
-
548
+
402
549
  # 3. 生成活动文件内容
403
550
  self.logger.info(f"开始为目录 {directory_path} 生成活动文件内容...")
404
- markdown_content = self.active_package.generate_active_file(
405
- context,
551
+
552
+ active_package = ActivePackage(self.llm, args.product_mode)
553
+
554
+ # 调用生成方法,捕获token和费用信息
555
+ generation_result = active_package.generate_active_file(
556
+ context,
406
557
  query,
407
558
  existing_file_path=existing_file_path,
408
- file_changes=directory_changes
559
+ file_changes=directory_changes,
560
+ args=args
409
561
  )
410
562
 
563
+ # 检查返回值类型,兼容现有逻辑
564
+ if isinstance(generation_result, tuple) and len(generation_result) >= 2:
565
+ markdown_content = generation_result[0]
566
+ tokens_info = generation_result[1]
567
+ else:
568
+ markdown_content = generation_result
569
+ tokens_info = {
570
+ "total_tokens": 0,
571
+ "input_tokens": 0,
572
+ "output_tokens": 0,
573
+ "cost": 0.0
574
+ }
575
+
411
576
  content_length = len(markdown_content)
412
577
  self.logger.debug(f"生成的活动文件内容长度: {content_length} 字符")
413
578
  if content_length > 0:
414
579
  self.logger.debug(f"内容预览: {markdown_content[:200]}...")
415
-
580
+
416
581
  # 4. 写入文件
417
582
  active_md_path = os.path.join(target_dir, "active.md")
418
583
  self.logger.info(f"正在写入活动文件: {active_md_path}")
419
584
  with open(active_md_path, "w", encoding="utf-8") as f:
420
585
  f.write(markdown_content)
421
-
586
+
422
587
  self.logger.info(f"成功创建/更新目录 {directory_path} 的活动文件")
423
588
  self.logger.debug(f"--- 处理目录上下文完成: {directory_path} ---")
424
589
 
590
+ # 返回token和费用信息
591
+ return tokens_info
592
+
425
593
  except Exception as e:
426
- self.logger.error(f"处理目录 {context.get('directory_path', 'unknown')} 时出错: {str(e)}", exc_info=True)
594
+ self.logger.error(
595
+ f"处理目录 {context.get('directory_path', 'unknown')} 时出错: {str(e)}", exc_info=True)
427
596
  raise
428
-
597
+
429
598
  def get_task_status(self, task_id: str) -> Dict[str, Any]:
430
599
  """
431
600
  获取任务状态
432
-
601
+
433
602
  Args:
434
603
  task_id: 任务ID
435
-
604
+
436
605
  Returns:
437
606
  Dict: 任务状态信息
438
607
  """
439
608
  if task_id not in self.tasks:
440
609
  return {'status': 'not_found', 'task_id': task_id}
441
-
442
- task = self.tasks[task_id]
443
-
610
+
611
+ with self.tasks_lock:
612
+ task = self.tasks[task_id]
613
+
444
614
  # 计算任务运行时间
445
615
  start_time = task.get('start_time')
446
616
  if task.get('status') == 'completed':
@@ -456,15 +626,16 @@ class ActiveContextManager:
456
626
  else:
457
627
  elapsed = None
458
628
  duration = None
459
-
629
+
460
630
  # 构建日志文件路径
461
631
  log_file_path = None
462
632
  if 'file_name' in task:
463
- log_dir = os.path.join(self.source_dir, '.auto-coder', 'active-context', 'logs')
633
+ log_dir = os.path.join(
634
+ self.source_dir, '.auto-coder', 'active-context', 'logs')
464
635
  log_file_path = os.path.join(log_dir, f'{task_id}.log')
465
636
  if not os.path.exists(log_file_path):
466
637
  log_file_path = None
467
-
638
+
468
639
  # 构建返回结果
469
640
  result = {
470
641
  'task_id': task_id,
@@ -472,75 +643,81 @@ class ActiveContextManager:
472
643
  'file_name': task.get('file_name'),
473
644
  'start_time': start_time.strftime("%Y-%m-%d %H:%M:%S") if start_time else None,
474
645
  }
475
-
646
+
476
647
  # 添加可选信息
477
648
  if 'completion_time' in task:
478
- result['completion_time'] = task['completion_time'].strftime("%Y-%m-%d %H:%M:%S")
479
-
649
+ result['completion_time'] = task['completion_time'].strftime(
650
+ "%Y-%m-%d %H:%M:%S")
651
+
480
652
  if elapsed is not None:
481
653
  mins, secs = divmod(elapsed, 60)
482
654
  hrs, mins = divmod(mins, 60)
483
655
  result['elapsed'] = f"{int(hrs):02d}:{int(mins):02d}:{int(secs):02d}"
484
-
656
+
485
657
  if duration is not None:
486
658
  mins, secs = divmod(duration, 60)
487
659
  hrs, mins = divmod(mins, 60)
488
660
  result['duration'] = f"{int(hrs):02d}:{int(mins):02d}:{int(secs):02d}"
489
-
661
+
490
662
  if 'processed_dirs' in task:
491
663
  result['processed_dirs'] = task['processed_dirs']
492
664
  result['processed_dirs_count'] = len(task['processed_dirs'])
493
-
665
+
494
666
  if 'error' in task:
495
667
  result['error'] = task['error']
496
-
668
+
669
+ # 添加token和费用信息
670
+ for key in ['total_tokens', 'input_tokens', 'output_tokens', 'cost']:
671
+ if key in task:
672
+ result[key] = task[key]
673
+
497
674
  if log_file_path and os.path.exists(log_file_path):
498
675
  result['log_file'] = log_file_path
499
-
676
+
500
677
  # 尝试获取日志文件大小
501
678
  try:
502
679
  file_size = os.path.getsize(log_file_path)
503
680
  result['log_file_size'] = f"{file_size / 1024:.2f} KB"
504
681
  except:
505
682
  pass
506
-
683
+
507
684
  return result
508
-
685
+
509
686
  def get_all_tasks(self) -> List[Dict[str, Any]]:
510
687
  """
511
688
  获取所有任务状态
512
-
689
+
513
690
  Returns:
514
691
  List[Dict]: 所有任务的状态信息
515
692
  """
516
693
  return [{'task_id': tid, **task} for tid, task in self.tasks.items()]
517
-
694
+
518
695
  def get_running_tasks(self) -> List[Dict[str, Any]]:
519
696
  """
520
697
  获取所有正在运行的任务
521
-
698
+
522
699
  Returns:
523
700
  List[Dict]: 所有正在运行的任务的状态信息
524
701
  """
525
- return [{'task_id': tid, **task} for tid, task in self.tasks.items()
702
+ return [{'task_id': tid, **task} for tid, task in self.tasks.items()
526
703
  if task['status'] in ['running', 'queued']]
527
-
704
+
528
705
  def load_active_contexts_for_files(self, file_paths: List[str]) -> FileContextsResult:
529
706
  """
530
707
  根据文件路径列表,找到并加载对应的活动上下文文件
531
-
708
+
532
709
  Args:
533
710
  file_paths: 文件路径列表
534
-
711
+
535
712
  Returns:
536
713
  FileContextsResult: 包含活动上下文信息的结构化结果
537
714
  """
538
715
  try:
539
716
  result = FileContextsResult()
540
-
717
+
541
718
  # 记录未找到对应活动上下文的文件
542
719
  found_files: Set[str] = set()
543
-
720
+
544
721
  # 1. 获取文件所在的唯一目录列表
545
722
  directories = set()
546
723
  for file_path in file_paths:
@@ -548,30 +725,31 @@ class ActiveContextManager:
548
725
  dir_path = os.path.dirname(file_path)
549
726
  if os.path.exists(dir_path):
550
727
  directories.add(dir_path)
551
-
728
+
552
729
  # 2. 查找每个目录的活动上下文文件
553
730
  for dir_path in directories:
554
731
  # 获取活动上下文目录路径
555
732
  active_context_dir = self._get_active_context_path(dir_path)
556
733
  active_md_path = os.path.join(active_context_dir, "active.md")
557
-
734
+
558
735
  # 检查active.md文件是否存在
559
736
  if os.path.exists(active_md_path):
560
737
  try:
561
738
  # 读取文件内容
562
739
  with open(active_md_path, 'r', encoding='utf-8') as f:
563
740
  content = f.read()
564
-
741
+
565
742
  # 解析文件内容
566
743
  sections_dict = self._parse_active_md_content(content)
567
744
  sections = ActiveFileSections(**sections_dict)
568
-
745
+
569
746
  # 找到相关的文件
570
- related_files = [f for f in file_paths if dir_path in f]
571
-
747
+ related_files = [
748
+ f for f in file_paths if dir_path in f]
749
+
572
750
  # 记录找到了对应活动上下文的文件
573
751
  found_files.update(related_files)
574
-
752
+
575
753
  # 创建活动文件上下文
576
754
  active_context = ActiveFileContext(
577
755
  directory_path=dir_path,
@@ -580,30 +758,32 @@ class ActiveContextManager:
580
758
  sections=sections,
581
759
  files=related_files
582
760
  )
583
-
761
+
584
762
  # 添加到结果
585
763
  result.contexts[dir_path] = active_context
586
-
764
+
587
765
  self.logger.info(f"已加载目录 {dir_path} 的活动上下文文件")
588
766
  except Exception as e:
589
- self.logger.error(f"读取活动上下文文件 {active_md_path} 时出错: {e}")
590
-
767
+ self.logger.error(
768
+ f"读取活动上下文文件 {active_md_path} 时出错: {e}")
769
+
591
770
  # 3. 记录未找到对应活动上下文的文件
592
- result.not_found_files = [f for f in file_paths if f not in found_files]
593
-
771
+ result.not_found_files = [
772
+ f for f in file_paths if f not in found_files]
773
+
594
774
  return result
595
-
775
+
596
776
  except Exception as e:
597
777
  self.logger.error(f"加载活动上下文失败: {e}")
598
778
  return FileContextsResult(not_found_files=file_paths)
599
-
779
+
600
780
  def _parse_active_md_content(self, content: str) -> Dict[str, str]:
601
781
  """
602
782
  解析活动上下文文件内容,提取各个部分
603
-
783
+
604
784
  Args:
605
785
  content: 活动上下文文件内容
606
-
786
+
607
787
  Returns:
608
788
  Dict[str, str]: 包含标题、当前变更和文档部分的字典
609
789
  """
@@ -613,37 +793,39 @@ class ActiveContextManager:
613
793
  'current_change': '',
614
794
  'document': ''
615
795
  }
616
-
796
+
617
797
  # 提取标题部分(到第一个二级标题之前)
618
798
  header_match = re.search(r'^(.*?)(?=\n## )', content, re.DOTALL)
619
799
  if header_match:
620
800
  result['header'] = header_match.group(1).strip()
621
-
801
+
622
802
  # 提取当前变更部分
623
- current_change_match = re.search(r'## 当前变更\s*\n(.*?)(?=\n## |$)', content, re.DOTALL)
803
+ current_change_match = re.search(
804
+ r'## 当前变更\s*\n(.*?)(?=\n## |$)', content, re.DOTALL)
624
805
  if current_change_match:
625
- result['current_change'] = current_change_match.group(1).strip()
626
-
806
+ result['current_change'] = current_change_match.group(
807
+ 1).strip()
808
+
627
809
  # 提取文档部分
628
- document_match = re.search(r'## 文档\s*\n(.*?)(?=\n## |$)', content, re.DOTALL)
810
+ document_match = re.search(
811
+ r'## 文档\s*\n(.*?)(?=\n## |$)', content, re.DOTALL)
629
812
  if document_match:
630
813
  result['document'] = document_match.group(1).strip()
631
-
814
+
632
815
  return result
633
816
  except Exception as e:
634
817
  self.logger.error(f"解析活动上下文文件内容时出错: {e}")
635
818
  return {'header': '', 'current_change': '', 'document': ''}
636
-
819
+
637
820
  def _get_active_context_path(self, directory_path: str) -> str:
638
821
  """
639
822
  获取活动上下文中对应的目录路径
640
-
823
+
641
824
  Args:
642
825
  directory_path: 原始目录路径
643
-
826
+
644
827
  Returns:
645
828
  str: 活动上下文中对应的目录路径
646
829
  """
647
830
  relative_path = os.path.relpath(directory_path, self.source_dir)
648
831
  return os.path.join(self.source_dir, ".auto-coder", "active-context", relative_path)
649
-