|
|
|
@ -3,10 +3,25 @@ import json
|
|
|
|
import re
|
|
|
|
import re
|
|
|
|
import shutil
|
|
|
|
import shutil
|
|
|
|
import subprocess
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
import zipfile
|
|
|
|
from collections import defaultdict
|
|
|
|
from collections import defaultdict
|
|
|
|
from datetime import datetime
|
|
|
|
from datetime import datetime
|
|
|
|
from pathlib import Path
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- ЦВЕТА ДЛЯ КОНСОЛИ (НЕЙТРАЛЬНАЯ ПАЛИТРА) ----------
|
|
|
|
|
|
|
|
COLOR_RESET = "\033[0m"
|
|
|
|
|
|
|
|
COLOR_WHITE = "\033[97m" # белый (для заголовков)
|
|
|
|
|
|
|
|
COLOR_LIGHT_GRAY = "\033[37m" # светло-серый (для меню)
|
|
|
|
|
|
|
|
COLOR_GRAY = "\033[90m" # тёмно-серый (для информации)
|
|
|
|
|
|
|
|
COLOR_YELLOW = "\033[93m" # жёлтый (для ввода/предупреждений)
|
|
|
|
|
|
|
|
COLOR_GREEN = "\033[32m" # обычный зелёный (успех)
|
|
|
|
|
|
|
|
COLOR_RED = "\033[31m" # красный (ошибки)
|
|
|
|
|
|
|
|
COLOR_CYAN = "\033[36m" # голубой (акценты, откат)
|
|
|
|
|
|
|
|
COLOR_BOLD = "\033[1m"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def color_text(text, color=COLOR_RESET):
|
|
|
|
|
|
|
|
return f"{color}{text}{COLOR_RESET}"
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- Функции сбора проекта ----------
|
|
|
|
# ---------- Функции сбора проекта ----------
|
|
|
|
def collect_project_files(root_dir, extensions=('.py', '.html'),
|
|
|
|
def collect_project_files(root_dir, extensions=('.py', '.html'),
|
|
|
|
exclude_dirs=('venv', '__pycache__', '.git', 'env', 'node_modules')):
|
|
|
|
exclude_dirs=('venv', '__pycache__', '.git', 'env', 'node_modules')):
|
|
|
|
@ -146,11 +161,101 @@ def extract_json_from_text(text):
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- Применение изменений ----------
|
|
|
|
# ---------- Полное резервное копирование проекта ----------
|
|
|
|
def apply_changes(project_root, actions):
|
|
|
|
def create_full_backup(project_root, ai_response_dir):
|
|
|
|
|
|
|
|
"""Создаёт ZIP-архив всего проекта (исключая папку _ai_response)."""
|
|
|
|
|
|
|
|
backup_full_dir = os.path.join(ai_response_dir, 'backup', 'full')
|
|
|
|
|
|
|
|
os.makedirs(backup_full_dir, exist_ok=True)
|
|
|
|
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
|
|
|
|
zip_filename = f"project_backup_{timestamp}.zip"
|
|
|
|
|
|
|
|
zip_path = os.path.join(backup_full_dir, zip_filename)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ignore_dir = os.path.basename(ai_response_dir)
|
|
|
|
|
|
|
|
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
|
|
|
|
|
|
for root, dirs, files in os.walk(project_root):
|
|
|
|
|
|
|
|
if ignore_dir in root.split(os.sep):
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
for file in files:
|
|
|
|
|
|
|
|
file_path = os.path.join(root, file)
|
|
|
|
|
|
|
|
arcname = os.path.relpath(file_path, project_root)
|
|
|
|
|
|
|
|
zipf.write(file_path, arcname)
|
|
|
|
|
|
|
|
return zip_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def list_full_backups(ai_response_dir):
|
|
|
|
|
|
|
|
backup_full_dir = os.path.join(ai_response_dir, 'backup', 'full')
|
|
|
|
|
|
|
|
if not os.path.exists(backup_full_dir):
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
backups = []
|
|
|
|
|
|
|
|
for f in os.listdir(backup_full_dir):
|
|
|
|
|
|
|
|
if f.startswith('project_backup_') and f.endswith('.zip'):
|
|
|
|
|
|
|
|
full_path = os.path.join(backup_full_dir, f)
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
time_str = f.replace('project_backup_', '').replace('.zip', '')
|
|
|
|
|
|
|
|
dt = datetime.strptime(time_str, "%Y%m%d_%H%M%S")
|
|
|
|
|
|
|
|
except:
|
|
|
|
|
|
|
|
dt = datetime.fromtimestamp(os.path.getmtime(full_path))
|
|
|
|
|
|
|
|
backups.append((full_path, dt, f))
|
|
|
|
|
|
|
|
backups.sort(key=lambda x: x[1], reverse=True)
|
|
|
|
|
|
|
|
return backups
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def restore_full_backup(project_root, backup_path):
|
|
|
|
|
|
|
|
for item in os.listdir(project_root):
|
|
|
|
|
|
|
|
if item == os.path.basename(os.path.join(project_root, '_ai_response')):
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
item_path = os.path.join(project_root, item)
|
|
|
|
|
|
|
|
if os.path.isfile(item_path):
|
|
|
|
|
|
|
|
os.remove(item_path)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
shutil.rmtree(item_path)
|
|
|
|
|
|
|
|
with zipfile.ZipFile(backup_path, 'r') as zipf:
|
|
|
|
|
|
|
|
zipf.extractall(project_root)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- Бэкапирование отдельных файлов (для отката) ----------
|
|
|
|
|
|
|
|
def backup_file(abs_path, ai_response_dir):
|
|
|
|
|
|
|
|
backup_root = os.path.join(ai_response_dir, 'backup')
|
|
|
|
|
|
|
|
os.makedirs(backup_root, exist_ok=True)
|
|
|
|
|
|
|
|
project_root = os.getcwd()
|
|
|
|
|
|
|
|
rel_path = os.path.relpath(abs_path, project_root)
|
|
|
|
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
|
|
|
|
backup_rel = f"{rel_path}.{timestamp}.bak"
|
|
|
|
|
|
|
|
backup_abs = os.path.join(backup_root, backup_rel)
|
|
|
|
|
|
|
|
os.makedirs(os.path.dirname(backup_abs), exist_ok=True)
|
|
|
|
|
|
|
|
shutil.copy2(abs_path, backup_abs)
|
|
|
|
|
|
|
|
return backup_abs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- Применение изменений (с бэкапами и полным архивом) ----------
|
|
|
|
|
|
|
|
def apply_changes(project_root, actions, ai_response_dir, user_prompt=''):
|
|
|
|
reports = []
|
|
|
|
reports = []
|
|
|
|
errors = False
|
|
|
|
errors = False
|
|
|
|
project_root = os.path.abspath(project_root)
|
|
|
|
project_root = os.path.abspath(project_root)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Добавляем запрос пользователя в отчёт
|
|
|
|
|
|
|
|
if user_prompt:
|
|
|
|
|
|
|
|
reports.append(f"📝 Запрос пользователя: {user_prompt}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Создаём полный бэкап проекта
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
backup_zip = create_full_backup(project_root, ai_response_dir)
|
|
|
|
|
|
|
|
reports.append(f"📦 Полный бэкап проекта сохранён: {backup_zip}")
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
reports.append(f"❌ Ошибка при создании полного бэкапа: {e}")
|
|
|
|
|
|
|
|
errors = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Сохраняем данные о применении (включая запрос)
|
|
|
|
|
|
|
|
last_apply_file = os.path.join(ai_response_dir, 'last_apply.json')
|
|
|
|
|
|
|
|
apply_data = {
|
|
|
|
|
|
|
|
'user_prompt': user_prompt,
|
|
|
|
|
|
|
|
'actions': actions,
|
|
|
|
|
|
|
|
'timestamp': datetime.now().isoformat()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
with open(last_apply_file, 'w', encoding='utf-8') as f:
|
|
|
|
|
|
|
|
json.dump(apply_data, f, ensure_ascii=False, indent=2)
|
|
|
|
|
|
|
|
|
|
|
|
for action in actions:
|
|
|
|
for action in actions:
|
|
|
|
action_type = action.get('action')
|
|
|
|
action_type = action.get('action')
|
|
|
|
file_path = action.get('file_path')
|
|
|
|
file_path = action.get('file_path')
|
|
|
|
@ -176,11 +281,14 @@ def apply_changes(project_root, actions):
|
|
|
|
if not os.path.exists(abs_path):
|
|
|
|
if not os.path.exists(abs_path):
|
|
|
|
reports.append(f"⚠️ Файл {file_path} не существует, создаём новый (как create)")
|
|
|
|
reports.append(f"⚠️ Файл {file_path} не существует, создаём новый (как create)")
|
|
|
|
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
|
|
|
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
backup_file(abs_path, ai_response_dir)
|
|
|
|
with open(abs_path, 'w', encoding='utf-8') as f:
|
|
|
|
with open(abs_path, 'w', encoding='utf-8') as f:
|
|
|
|
f.write(content)
|
|
|
|
f.write(content)
|
|
|
|
reports.append(f"✅ Обновлён: {file_path} — {description}")
|
|
|
|
reports.append(f"✅ Обновлён: {file_path} — {description}")
|
|
|
|
elif action_type == 'delete':
|
|
|
|
elif action_type == 'delete':
|
|
|
|
if os.path.exists(abs_path):
|
|
|
|
if os.path.exists(abs_path):
|
|
|
|
|
|
|
|
backup_file(abs_path, ai_response_dir)
|
|
|
|
os.remove(abs_path)
|
|
|
|
os.remove(abs_path)
|
|
|
|
reports.append(f"✅ Удалён: {file_path} — {description}")
|
|
|
|
reports.append(f"✅ Удалён: {file_path} — {description}")
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
@ -194,6 +302,85 @@ def apply_changes(project_root, actions):
|
|
|
|
return reports, errors
|
|
|
|
return reports, errors
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- Откат изменений (только последнего набора) ----------
|
|
|
|
|
|
|
|
def rollback_last_apply(project_root, ai_response_dir):
|
|
|
|
|
|
|
|
reports = []
|
|
|
|
|
|
|
|
errors = False
|
|
|
|
|
|
|
|
last_apply_file = os.path.join(ai_response_dir, 'last_apply.json')
|
|
|
|
|
|
|
|
if not os.path.exists(last_apply_file):
|
|
|
|
|
|
|
|
reports.append("❌ Нет сохранённых действий для отката (файл last_apply.json не найден).")
|
|
|
|
|
|
|
|
return reports, True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
with open(last_apply_file, 'r', encoding='utf-8') as f:
|
|
|
|
|
|
|
|
data = json.load(f)
|
|
|
|
|
|
|
|
actions = data.get('actions', [])
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
reports.append(f"❌ Ошибка чтения last_apply.json: {e}")
|
|
|
|
|
|
|
|
return reports, True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for action in reversed(actions):
|
|
|
|
|
|
|
|
action_type = action.get('action')
|
|
|
|
|
|
|
|
file_path = action.get('file_path')
|
|
|
|
|
|
|
|
description = action.get('description', '')
|
|
|
|
|
|
|
|
if not action_type or not file_path:
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
norm_path = os.path.normpath(file_path.replace('/', os.sep))
|
|
|
|
|
|
|
|
abs_path = os.path.join(project_root, norm_path)
|
|
|
|
|
|
|
|
if not os.path.abspath(abs_path).startswith(project_root):
|
|
|
|
|
|
|
|
reports.append(f"❌ Опасный путь: {file_path} — выход за пределы проекта")
|
|
|
|
|
|
|
|
errors = True
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
if action_type == 'create':
|
|
|
|
|
|
|
|
if os.path.exists(abs_path):
|
|
|
|
|
|
|
|
os.remove(abs_path)
|
|
|
|
|
|
|
|
reports.append(f"🗑️ Удалён (откат create): {file_path} — {description}")
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
reports.append(f"⚠️ Файл {file_path} не найден, пропускаем удаление (откат create)")
|
|
|
|
|
|
|
|
elif action_type == 'update':
|
|
|
|
|
|
|
|
backup_root = os.path.join(ai_response_dir, 'backup')
|
|
|
|
|
|
|
|
backup_files = []
|
|
|
|
|
|
|
|
for root, dirs, files in os.walk(backup_root):
|
|
|
|
|
|
|
|
for f in files:
|
|
|
|
|
|
|
|
if f.startswith(norm_path + '.') and f.endswith('.bak'):
|
|
|
|
|
|
|
|
full = os.path.join(root, f)
|
|
|
|
|
|
|
|
backup_files.append((full, os.path.getmtime(full)))
|
|
|
|
|
|
|
|
if backup_files:
|
|
|
|
|
|
|
|
backup_files.sort(key=lambda x: x[1], reverse=True)
|
|
|
|
|
|
|
|
latest_backup = backup_files[0][0]
|
|
|
|
|
|
|
|
shutil.copy2(latest_backup, abs_path)
|
|
|
|
|
|
|
|
reports.append(f"♻️ Восстановлен (откат update): {file_path} — из {os.path.basename(latest_backup)}")
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
reports.append(f"❌ Не найден бэкап для {file_path}, пропускаем (откат update)")
|
|
|
|
|
|
|
|
errors = True
|
|
|
|
|
|
|
|
elif action_type == 'delete':
|
|
|
|
|
|
|
|
backup_root = os.path.join(ai_response_dir, 'backup')
|
|
|
|
|
|
|
|
backup_files = []
|
|
|
|
|
|
|
|
for root, dirs, files in os.walk(backup_root):
|
|
|
|
|
|
|
|
for f in files:
|
|
|
|
|
|
|
|
if f.startswith(norm_path + '.') and f.endswith('.bak'):
|
|
|
|
|
|
|
|
full = os.path.join(root, f)
|
|
|
|
|
|
|
|
backup_files.append((full, os.path.getmtime(full)))
|
|
|
|
|
|
|
|
if backup_files:
|
|
|
|
|
|
|
|
backup_files.sort(key=lambda x: x[1], reverse=True)
|
|
|
|
|
|
|
|
latest_backup = backup_files[0][0]
|
|
|
|
|
|
|
|
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
|
|
|
|
|
|
|
shutil.copy2(latest_backup, abs_path)
|
|
|
|
|
|
|
|
reports.append(f"♻️ Восстановлен (откат delete): {file_path} — из {os.path.basename(latest_backup)}")
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
reports.append(f"❌ Не найден бэкап для {file_path}, пропускаем (откат delete)")
|
|
|
|
|
|
|
|
errors = True
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
reports.append(f"❌ Неизвестное действие '{action_type}' для {file_path} (откат)")
|
|
|
|
|
|
|
|
errors = True
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
reports.append(f"❌ Ошибка при откате {file_path}: {e}")
|
|
|
|
|
|
|
|
errors = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return reports, errors
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- Вспомогательные функции ----------
|
|
|
|
# ---------- Вспомогательные функции ----------
|
|
|
|
def get_downloads_folder():
|
|
|
|
def get_downloads_folder():
|
|
|
|
home = Path.home()
|
|
|
|
home = Path.home()
|
|
|
|
@ -246,7 +433,7 @@ def run_tests(project_root, test_path=None):
|
|
|
|
return False, reports, str(e)
|
|
|
|
return False, reports, str(e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- Логирование в суточный файл (только для APPLY и кратких записей о тестах) ----------
|
|
|
|
# ---------- Логирование в суточный файл ----------
|
|
|
|
def write_log_entry(log_file_path, entry_type, lines, extra_info=''):
|
|
|
|
def write_log_entry(log_file_path, entry_type, lines, extra_info=''):
|
|
|
|
with open(log_file_path, 'a', encoding='utf-8') as f:
|
|
|
|
with open(log_file_path, 'a', encoding='utf-8') as f:
|
|
|
|
f.write("=" * 80 + "\n")
|
|
|
|
f.write("=" * 80 + "\n")
|
|
|
|
@ -260,97 +447,145 @@ def write_log_entry(log_file_path, entry_type, lines, extra_info=''):
|
|
|
|
f.write("=" * 80 + "\n\n")
|
|
|
|
f.write("=" * 80 + "\n\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------- ОСНОВНАЯ ФУНКЦИЯ ----------
|
|
|
|
# ---------- ОСНОВНАЯ ФУНКЦИЯ (ЦИКЛ) ----------
|
|
|
|
def main():
|
|
|
|
def main():
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
|
|
print(" Инструмент для работы с DeepSeek")
|
|
|
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mode = input(
|
|
|
|
|
|
|
|
"Выберите режим:\n"
|
|
|
|
|
|
|
|
" 1 — Создать промт (собрать проект и инструкцию)\n"
|
|
|
|
|
|
|
|
" 2 — Применить ответ DeepSeek (автоматически найти JSON в Downloads)\n"
|
|
|
|
|
|
|
|
" 3 — Запустить тесты (все или указать папку/файл)\n"
|
|
|
|
|
|
|
|
"По умолчанию (Enter) — режим 1\n"
|
|
|
|
|
|
|
|
"Ваш выбор: "
|
|
|
|
|
|
|
|
).strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
project_root = os.getcwd()
|
|
|
|
project_root = os.getcwd()
|
|
|
|
ai_response_dir = os.path.join(project_root, '_ai_response')
|
|
|
|
ai_response_dir = os.path.join(project_root, '_ai_response')
|
|
|
|
os.makedirs(ai_response_dir, exist_ok=True)
|
|
|
|
os.makedirs(ai_response_dir, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
# Суточный лог-файл (только для изменений и кратких записей о тестах)
|
|
|
|
print(color_text("=" * 60, COLOR_WHITE + COLOR_BOLD))
|
|
|
|
today = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
print(color_text(" Инструмент для работы с DeepSeek", COLOR_WHITE + COLOR_BOLD))
|
|
|
|
log_file = os.path.join(ai_response_dir, f'ai_log_{today}.txt')
|
|
|
|
print(color_text("=" * 60, COLOR_WHITE + COLOR_BOLD))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
|
|
print("\n" + color_text("Выберите режим:", COLOR_LIGHT_GRAY + COLOR_BOLD))
|
|
|
|
|
|
|
|
print(color_text(" 0 — Выход", COLOR_GRAY))
|
|
|
|
|
|
|
|
print(color_text(" 1 — Создать промт (собрать проект и инструкцию)", COLOR_LIGHT_GRAY))
|
|
|
|
|
|
|
|
print(color_text(" 2 — Применить ответ DeepSeek (автоматически найти JSON в Downloads)", COLOR_LIGHT_GRAY))
|
|
|
|
|
|
|
|
print(color_text(" 3 — Запустить тесты (все или указать папку/файл)", COLOR_LIGHT_GRAY))
|
|
|
|
|
|
|
|
print(color_text(" 4 — Откатить последние изменения (восстановить из бэкапов файлов)", COLOR_LIGHT_GRAY))
|
|
|
|
|
|
|
|
print(color_text(" 5 — Восстановить проект из полного бэкапа (архива)", COLOR_LIGHT_GRAY))
|
|
|
|
|
|
|
|
mode = input(color_text("Ваш выбор (0-5): ", COLOR_LIGHT_GRAY + COLOR_BOLD)).strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if mode == '0':
|
|
|
|
|
|
|
|
print(color_text("Выход. До свидания!", COLOR_GREEN))
|
|
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if mode == '1':
|
|
|
|
|
|
|
|
print(color_text("\nСбор информации о проекте для отправки в DeepSeek.", COLOR_LIGHT_GRAY))
|
|
|
|
|
|
|
|
main_prompt = input(color_text("Введите ваш промт (одна строка, Enter для завершения): ", COLOR_YELLOW)).strip()
|
|
|
|
|
|
|
|
if not main_prompt:
|
|
|
|
|
|
|
|
print(color_text("Промт не может быть пустым. Завершение.", COLOR_RED))
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Сохраняем запрос пользователя для последующего логирования
|
|
|
|
|
|
|
|
prompt_file = os.path.join(ai_response_dir, 'last_user_prompt.txt')
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
with open(prompt_file, 'w', encoding='utf-8') as f:
|
|
|
|
|
|
|
|
f.write(main_prompt)
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
print(color_text(f"⚠️ Не удалось сохранить запрос: {e}", COLOR_YELLOW))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print(color_text(f"\nСканируем папку: {project_root}", COLOR_GRAY))
|
|
|
|
|
|
|
|
files_data = collect_project_files(project_root)
|
|
|
|
|
|
|
|
full_prompt = build_prompt(main_prompt, files_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
output_file = os.path.join(ai_response_dir, "ai_promt.txt")
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
|
|
|
|
|
|
f.write(full_prompt)
|
|
|
|
|
|
|
|
print(color_text(f"\n✅ Готово! Промт сохранён в файл: {output_file}", COLOR_GREEN))
|
|
|
|
|
|
|
|
print(color_text(f"Всего собрано файлов: {len(files_data)}", COLOR_GRAY))
|
|
|
|
|
|
|
|
print(color_text("\nТеперь скопируйте содержимое этого файла и отправьте в DeepSeek.", COLOR_YELLOW))
|
|
|
|
|
|
|
|
print(color_text("DeepSeek вернёт ответ с описанием и JSON-блоком.", COLOR_YELLOW))
|
|
|
|
|
|
|
|
print(color_text("Скопируйте весь ответ и сохраните в файл с именем deepseek_json_YYYYMMDD_HHMMSS.json в папке Downloads.", COLOR_YELLOW))
|
|
|
|
|
|
|
|
print(color_text("Затем запустите этот скрипт в режиме 2 для автоматического применения.", COLOR_YELLOW))
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
print(color_text(f"❌ Ошибка при сохранении: {e}", COLOR_RED))
|
|
|
|
|
|
|
|
|
|
|
|
if mode == '2':
|
|
|
|
elif mode == '2':
|
|
|
|
# ---- РЕЖИМ 2: Применение изменений (пишем в суточный лог) ----
|
|
|
|
|
|
|
|
downloads_dir = get_downloads_folder()
|
|
|
|
downloads_dir = get_downloads_folder()
|
|
|
|
json_file = find_latest_json_file(downloads_dir)
|
|
|
|
json_file = find_latest_json_file(downloads_dir)
|
|
|
|
if json_file is None:
|
|
|
|
if json_file is None:
|
|
|
|
print(f"❌ Не найден JSON-файл с префиксом 'deepseek_json_' в папке {downloads_dir}")
|
|
|
|
print(color_text(f"❌ Не найден JSON-файл с префиксом 'deepseek_json_' в папке {downloads_dir}", COLOR_RED))
|
|
|
|
return
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
print(f"📂 Найден файл: {json_file}")
|
|
|
|
print(color_text(f"📂 Найден файл: {json_file}", COLOR_GRAY))
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
with open(json_file, 'r', encoding='utf-8') as f:
|
|
|
|
with open(json_file, 'r', encoding='utf-8') as f:
|
|
|
|
text = f.read()
|
|
|
|
text = f.read()
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
print(f"❌ Ошибка чтения файла: {e}")
|
|
|
|
print(color_text(f"❌ Ошибка чтения файла: {e}", COLOR_RED))
|
|
|
|
return
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
data = extract_json_from_text(text)
|
|
|
|
data = extract_json_from_text(text)
|
|
|
|
if data is None:
|
|
|
|
if data is None:
|
|
|
|
print("❌ Не удалось извлечь JSON из ответа. Убедитесь, что в ответе есть блок ```json ... ``` или валидный JSON.")
|
|
|
|
print(color_text("❌ Не удалось извлечь JSON из ответа. Убедитесь, что в ответе есть блок ```json ... ``` или валидный JSON.", COLOR_RED))
|
|
|
|
return
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
actions = data.get('actions')
|
|
|
|
actions = data.get('actions')
|
|
|
|
if actions is None:
|
|
|
|
if actions is None:
|
|
|
|
print("❌ В JSON отсутствует поле 'actions'.")
|
|
|
|
print(color_text("❌ В JSON отсутствует поле 'actions'.", COLOR_RED))
|
|
|
|
return
|
|
|
|
continue
|
|
|
|
if not isinstance(actions, list):
|
|
|
|
if not isinstance(actions, list):
|
|
|
|
print("❌ Поле 'actions' должно быть массивом.")
|
|
|
|
print(color_text("❌ Поле 'actions' должно быть массивом.", COLOR_RED))
|
|
|
|
return
|
|
|
|
continue
|
|
|
|
if len(actions) == 0:
|
|
|
|
if len(actions) == 0:
|
|
|
|
print("ℹ️ Нет действий для применения.")
|
|
|
|
print(color_text("ℹ️ Нет действий для применения.", COLOR_YELLOW))
|
|
|
|
return
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
print(f"\n📁 Применяем изменения в: {project_root}")
|
|
|
|
# Читаем сохранённый запрос пользователя
|
|
|
|
reports, errors = apply_changes(project_root, actions)
|
|
|
|
user_prompt = ''
|
|
|
|
|
|
|
|
prompt_file = os.path.join(ai_response_dir, 'last_user_prompt.txt')
|
|
|
|
|
|
|
|
if os.path.exists(prompt_file):
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
with open(prompt_file, 'r', encoding='utf-8') as f:
|
|
|
|
|
|
|
|
user_prompt = f.read().strip()
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
print(color_text(f"⚠️ Не удалось прочитать запрос пользователя: {e}", COLOR_YELLOW))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print(color_text(f"\n📁 Применяем изменения в: {project_root}", COLOR_LIGHT_GRAY))
|
|
|
|
|
|
|
|
reports, errors = apply_changes(project_root, actions, ai_response_dir, user_prompt)
|
|
|
|
|
|
|
|
|
|
|
|
# Перемещаем JSON-файл в _ai_response
|
|
|
|
|
|
|
|
dest_filename = f"applied_{os.path.basename(json_file)}"
|
|
|
|
dest_filename = f"applied_{os.path.basename(json_file)}"
|
|
|
|
dest_path = os.path.join(ai_response_dir, dest_filename)
|
|
|
|
dest_path = os.path.join(ai_response_dir, dest_filename)
|
|
|
|
shutil.move(json_file, dest_path)
|
|
|
|
shutil.move(json_file, dest_path)
|
|
|
|
move_info = f"JSON перемещён в {dest_path}"
|
|
|
|
move_info = f"JSON перемещён в {dest_path}"
|
|
|
|
|
|
|
|
|
|
|
|
# Пишем в суточный лог
|
|
|
|
today = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
|
|
|
|
log_file = os.path.join(ai_response_dir, f'ai_log_{today}.txt')
|
|
|
|
write_log_entry(log_file, 'APPLY', reports, extra_info=move_info)
|
|
|
|
write_log_entry(log_file, 'APPLY', reports, extra_info=move_info)
|
|
|
|
|
|
|
|
|
|
|
|
# Вывод в консоль
|
|
|
|
print(color_text("\n" + "=" * 60, COLOR_WHITE + COLOR_BOLD))
|
|
|
|
print("\n" + "=" * 60)
|
|
|
|
print(color_text("ОТЧЁТ О ВЫПОЛНЕННЫХ ДЕЙСТВИЯХ", COLOR_WHITE + COLOR_BOLD))
|
|
|
|
print("ОТЧЁТ О ВЫПОЛНЕННЫХ ДЕЙСТВИЯХ")
|
|
|
|
print(color_text("=" * 60, COLOR_WHITE + COLOR_BOLD))
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
|
|
for line in reports:
|
|
|
|
for line in reports:
|
|
|
|
|
|
|
|
if line.startswith("✅"):
|
|
|
|
|
|
|
|
print(color_text(line, COLOR_GREEN))
|
|
|
|
|
|
|
|
elif line.startswith("❌"):
|
|
|
|
|
|
|
|
print(color_text(line, COLOR_RED))
|
|
|
|
|
|
|
|
elif line.startswith("⚠️"):
|
|
|
|
|
|
|
|
print(color_text(line, COLOR_YELLOW))
|
|
|
|
|
|
|
|
elif line.startswith("📦"):
|
|
|
|
|
|
|
|
print(color_text(line, COLOR_CYAN))
|
|
|
|
|
|
|
|
else:
|
|
|
|
print(line)
|
|
|
|
print(line)
|
|
|
|
|
|
|
|
|
|
|
|
if errors:
|
|
|
|
if errors:
|
|
|
|
print("\n❌ Были ошибки при применении изменений.")
|
|
|
|
print(color_text("\n❌ Были ошибки при применении изменений.", COLOR_RED))
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
print("\n✅ Все изменения применены успешно.")
|
|
|
|
print(color_text("\n✅ Все изменения применены успешно.", COLOR_GREEN))
|
|
|
|
print(f"\n📄 Лог изменений записан в: {log_file}")
|
|
|
|
print(color_text(f"\n📄 Лог изменений записан в: {log_file}", COLOR_GRAY))
|
|
|
|
|
|
|
|
|
|
|
|
elif mode == '3':
|
|
|
|
elif mode == '3':
|
|
|
|
# ---- РЕЖИМ 3: Тестирование (отдельный файл + краткая запись в суточный лог) ----
|
|
|
|
test_path = input(color_text("Введите относительный путь к папке или файлу с тестами (оставьте пустым для всех тестов): ", COLOR_YELLOW)).strip()
|
|
|
|
test_path = input("Введите относительный путь к папке или файлу с тестами (оставьте пустым для всех тестов): ").strip()
|
|
|
|
|
|
|
|
if test_path == '':
|
|
|
|
if test_path == '':
|
|
|
|
test_path = None
|
|
|
|
test_path = None
|
|
|
|
|
|
|
|
|
|
|
|
print("\n🔍 Запуск тестов...")
|
|
|
|
print(color_text("\n🔍 Запуск тестов...", COLOR_LIGHT_GRAY))
|
|
|
|
success, reports, output = run_tests(project_root, test_path)
|
|
|
|
success, reports, output = run_tests(project_root, test_path)
|
|
|
|
|
|
|
|
|
|
|
|
# Сохраняем полный вывод в отдельный файл
|
|
|
|
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
test_report_file = os.path.join(ai_response_dir, f"test_report_{timestamp}.txt")
|
|
|
|
test_report_file = os.path.join(ai_response_dir, f"test_report_{timestamp}.txt")
|
|
|
|
with open(test_report_file, 'w', encoding='utf-8') as f:
|
|
|
|
with open(test_report_file, 'w', encoding='utf-8') as f:
|
|
|
|
@ -361,7 +596,8 @@ def main():
|
|
|
|
f.write("=" * 60 + "\n")
|
|
|
|
f.write("=" * 60 + "\n")
|
|
|
|
f.write(output)
|
|
|
|
f.write(output)
|
|
|
|
|
|
|
|
|
|
|
|
# Пишем краткую запись в суточный лог
|
|
|
|
today = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
|
|
|
|
log_file = os.path.join(ai_response_dir, f'ai_log_{today}.txt')
|
|
|
|
brief_lines = [
|
|
|
|
brief_lines = [
|
|
|
|
f"Тесты запущены для пути: {test_path if test_path else 'все тесты'}",
|
|
|
|
f"Тесты запущены для пути: {test_path if test_path else 'все тесты'}",
|
|
|
|
f"Результат: {'✅ УСПЕШНО' if success else '❌ ОШИБКА'}",
|
|
|
|
f"Результат: {'✅ УСПЕШНО' if success else '❌ ОШИБКА'}",
|
|
|
|
@ -369,46 +605,94 @@ def main():
|
|
|
|
]
|
|
|
|
]
|
|
|
|
write_log_entry(log_file, 'TEST', brief_lines, extra_info=f"Путь: {test_path if test_path else 'все тесты'}")
|
|
|
|
write_log_entry(log_file, 'TEST', brief_lines, extra_info=f"Путь: {test_path if test_path else 'все тесты'}")
|
|
|
|
|
|
|
|
|
|
|
|
# Вывод в консоль
|
|
|
|
print(color_text("\n" + "=" * 60, COLOR_WHITE + COLOR_BOLD))
|
|
|
|
print("\n" + "=" * 60)
|
|
|
|
print(color_text("РЕЗУЛЬТАТ ТЕСТИРОВАНИЯ", COLOR_WHITE + COLOR_BOLD))
|
|
|
|
print("РЕЗУЛЬТАТ ТЕСТИРОВАНИЯ")
|
|
|
|
print(color_text("=" * 60, COLOR_WHITE + COLOR_BOLD))
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
|
|
for line in reports:
|
|
|
|
for line in reports:
|
|
|
|
|
|
|
|
if line.startswith("✅"):
|
|
|
|
|
|
|
|
print(color_text(line, COLOR_GREEN))
|
|
|
|
|
|
|
|
elif line.startswith("❌"):
|
|
|
|
|
|
|
|
print(color_text(line, COLOR_RED))
|
|
|
|
|
|
|
|
else:
|
|
|
|
print(line)
|
|
|
|
print(line)
|
|
|
|
|
|
|
|
|
|
|
|
if success:
|
|
|
|
if success:
|
|
|
|
print("\n✅ Тесты прошли успешно.")
|
|
|
|
print(color_text("\n✅ Тесты прошли успешно.", COLOR_GREEN))
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
print("\n❌ Тесты завершились с ошибками. Проверьте полный отчёт.")
|
|
|
|
print(color_text("\n❌ Тесты завершились с ошибками. Проверьте полный отчёт.", COLOR_RED))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print(color_text(f"\n📄 Полный отчёт сохранён в: {test_report_file}", COLOR_GRAY))
|
|
|
|
|
|
|
|
print(color_text(f"📄 Краткая запись добавлена в суточный лог: {log_file}", COLOR_GRAY))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif mode == '4':
|
|
|
|
|
|
|
|
print(color_text("\n🔄 Откат последних применённых изменений...", COLOR_LIGHT_GRAY))
|
|
|
|
|
|
|
|
reports, errors = rollback_last_apply(project_root, ai_response_dir)
|
|
|
|
|
|
|
|
|
|
|
|
print(f"\n📄 Полный отчёт сохранён в: {test_report_file}")
|
|
|
|
today = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
print(f"📄 Краткая запись добавлена в суточный лог: {log_file}")
|
|
|
|
log_file = os.path.join(ai_response_dir, f'ai_log_{today}.txt')
|
|
|
|
|
|
|
|
write_log_entry(log_file, 'ROLLBACK', reports, extra_info="Откат выполненных изменений")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print(color_text("\n" + "=" * 60, COLOR_WHITE + COLOR_BOLD))
|
|
|
|
|
|
|
|
print(color_text("ОТЧЁТ ОБ ОТКАТЕ", COLOR_WHITE + COLOR_BOLD))
|
|
|
|
|
|
|
|
print(color_text("=" * 60, COLOR_WHITE + COLOR_BOLD))
|
|
|
|
|
|
|
|
for line in reports:
|
|
|
|
|
|
|
|
if line.startswith("✅"):
|
|
|
|
|
|
|
|
print(color_text(line, COLOR_GREEN))
|
|
|
|
|
|
|
|
elif line.startswith("❌"):
|
|
|
|
|
|
|
|
print(color_text(line, COLOR_RED))
|
|
|
|
|
|
|
|
elif line.startswith("⚠️"):
|
|
|
|
|
|
|
|
print(color_text(line, COLOR_YELLOW))
|
|
|
|
|
|
|
|
elif line.startswith("🗑️") or line.startswith("♻️"):
|
|
|
|
|
|
|
|
print(color_text(line, COLOR_CYAN))
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
# ---- РЕЖИМ 1: Создание промта (сохраняем в _ai_response) ----
|
|
|
|
print(line)
|
|
|
|
print("\nСбор информации о проекте для отправки в DeepSeek.")
|
|
|
|
|
|
|
|
main_prompt = input("\nВведите ваш промт (одна строка, Enter для завершения): ").strip()
|
|
|
|
|
|
|
|
if not main_prompt:
|
|
|
|
|
|
|
|
print("Промт не может быть пустым. Завершение.")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print(f"\nСканируем папку: {project_root}")
|
|
|
|
if errors:
|
|
|
|
files_data = collect_project_files(project_root)
|
|
|
|
print(color_text("\n❌ Были ошибки при откате.", COLOR_RED))
|
|
|
|
full_prompt = build_prompt(main_prompt, files_data)
|
|
|
|
else:
|
|
|
|
|
|
|
|
print(color_text("\n✅ Откат выполнен успешно.", COLOR_GREEN))
|
|
|
|
|
|
|
|
print(color_text(f"\n📄 Лог отката записан в: {log_file}", COLOR_GRAY))
|
|
|
|
|
|
|
|
|
|
|
|
output_file = os.path.join(ai_response_dir, "ai_promt.txt")
|
|
|
|
elif mode == '5':
|
|
|
|
|
|
|
|
backups = list_full_backups(ai_response_dir)
|
|
|
|
|
|
|
|
if not backups:
|
|
|
|
|
|
|
|
print(color_text("❌ Нет доступных полных бэкапов.", COLOR_RED))
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print(color_text("\nДоступные полные бэкапы:", COLOR_LIGHT_GRAY))
|
|
|
|
|
|
|
|
for idx, (path, dt, name) in enumerate(backups, 1):
|
|
|
|
|
|
|
|
print(color_text(f" {idx}. {name} ({dt.strftime('%Y-%m-%d %H:%M:%S')})", COLOR_GRAY))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
choice = input(color_text("Выберите номер бэкапа для восстановления (или 0 для отмены): ", COLOR_YELLOW)).strip()
|
|
|
|
|
|
|
|
if choice == '0':
|
|
|
|
|
|
|
|
continue
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
|
|
idx = int(choice) - 1
|
|
|
|
f.write(full_prompt)
|
|
|
|
if idx < 0 or idx >= len(backups):
|
|
|
|
print(f"\n✅ Готово! Промт сохранён в файл: {output_file}")
|
|
|
|
print(color_text("❌ Неверный номер.", COLOR_RED))
|
|
|
|
print(f"Всего собрано файлов: {len(files_data)}")
|
|
|
|
continue
|
|
|
|
print("\nТеперь скопируйте содержимое этого файла и отправьте в DeepSeek.")
|
|
|
|
backup_path = backups[idx][0]
|
|
|
|
print("DeepSeek вернёт ответ с описанием и JSON-блоком.")
|
|
|
|
except ValueError:
|
|
|
|
print("Скопируйте весь ответ и сохраните в файл с именем deepseek_json_YYYYMMDD_HHMMSS.json в папке Downloads.")
|
|
|
|
print(color_text("❌ Неверный ввод.", COLOR_RED))
|
|
|
|
print("Затем запустите этот скрипт в режиме 2 для автоматического применения.")
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print(color_text(f"\n⚠️ Восстановление из бэкапа {os.path.basename(backup_path)} полностью заменит текущий проект.", COLOR_YELLOW))
|
|
|
|
|
|
|
|
confirm = input(color_text("Вы уверены? (введите 'да' для подтверждения): ", COLOR_YELLOW)).strip().lower()
|
|
|
|
|
|
|
|
if confirm != 'да':
|
|
|
|
|
|
|
|
print(color_text("Восстановление отменено.", COLOR_GRAY))
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
restore_full_backup(project_root, backup_path)
|
|
|
|
|
|
|
|
print(color_text(f"\n✅ Проект восстановлен из бэкапа: {os.path.basename(backup_path)}", COLOR_GREEN))
|
|
|
|
|
|
|
|
today = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
|
|
|
|
log_file = os.path.join(ai_response_dir, f'ai_log_{today}.txt')
|
|
|
|
|
|
|
|
write_log_entry(log_file, 'RESTORE_FULL', [f"Восстановлен проект из {os.path.basename(backup_path)}"], extra_info="Полное восстановление")
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
print(f"❌ Ошибка при сохранении: {e}")
|
|
|
|
print(color_text(f"❌ Ошибка при восстановлении: {e}", COLOR_RED))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
print(color_text("❌ Неверный выбор. Пожалуйста, введите число от 0 до 5.", COLOR_RED))
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|
|
|
|
main()
|
|
|
|
|