auto-coder 0.1.311__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.
- {auto_coder-0.1.311.dist-info → auto_coder-0.1.312.dist-info}/METADATA +1 -1
- {auto_coder-0.1.311.dist-info → auto_coder-0.1.312.dist-info}/RECORD +11 -10
- autocoder/auto_coder_runner.py +22 -8
- autocoder/common/token_cost_caculate.py +200 -0
- autocoder/memory/active_context_manager.py +353 -171
- autocoder/memory/active_package.py +175 -34
- autocoder/version.py +1 -1
- {auto_coder-0.1.311.dist-info → auto_coder-0.1.312.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.311.dist-info → auto_coder-0.1.312.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.311.dist-info → auto_coder-0.1.312.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.311.dist-info → auto_coder-0.1.312.dist-info}/top_level.txt +0 -0
|
@@ -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,
|
|
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(
|
|
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.
|
|
162
|
-
|
|
163
|
-
self._process_changes_async(
|
|
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(
|
|
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.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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,
|
|
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.
|
|
245
|
-
|
|
348
|
+
self._update_task(task_id, status='running')
|
|
349
|
+
|
|
246
350
|
# 重定向输出并执行任务
|
|
247
|
-
self._process_changes_async(
|
|
248
|
-
|
|
351
|
+
self._process_changes_async(
|
|
352
|
+
task_id, query, changed_urls, current_urls, args)
|
|
353
|
+
|
|
249
354
|
# 更新任务状态为已完成
|
|
250
|
-
self.
|
|
251
|
-
|
|
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.
|
|
258
|
-
|
|
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(
|
|
278
|
-
|
|
279
|
-
|
|
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(
|
|
419
|
+
self.logger.info(
|
|
420
|
+
f"[{i+1}/{len(directory_contexts)}] 开始处理目录: {dir_path}")
|
|
310
421
|
try:
|
|
311
|
-
self._process_directory_context(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
331
|
-
|
|
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(
|
|
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[:
|
|
393
|
-
|
|
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(
|
|
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
|
-
|
|
405
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 = [
|
|
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(
|
|
590
|
-
|
|
767
|
+
self.logger.error(
|
|
768
|
+
f"读取活动上下文文件 {active_md_path} 时出错: {e}")
|
|
769
|
+
|
|
591
770
|
# 3. 记录未找到对应活动上下文的文件
|
|
592
|
-
result.not_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(
|
|
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(
|
|
626
|
-
|
|
806
|
+
result['current_change'] = current_change_match.group(
|
|
807
|
+
1).strip()
|
|
808
|
+
|
|
627
809
|
# 提取文档部分
|
|
628
|
-
document_match = re.search(
|
|
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
|
-
|