Kgshop commited on
Commit
72c3aff
·
verified ·
1 Parent(s): 484ce69

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +397 -393
app.py CHANGED
@@ -1,503 +1,507 @@
1
  import os
2
  import uuid
3
- import sys
4
- import subprocess
5
- import time
6
  import json
7
  import logging
8
  import threading
9
- from datetime import datetime
10
- import signal
 
11
  import atexit
12
-
13
- from flask import Flask, request, jsonify, render_template_string, redirect, url_for, flash
14
  import google.generativeai as genai
15
  from huggingface_hub import HfApi, hf_hub_download
16
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
17
- from dotenv import load_dotenv
18
  import requests
 
 
 
19
 
20
- # --- 1. Конфигурация и Инициализация ---
21
  load_dotenv()
22
  app = Flask(__name__)
23
- app.secret_key = 'super_secret_key_for_flask_site_generator_12345'
24
 
25
- # Конфигурация для генератора сайтов
26
- API_KEY_INTERNAL = "AIzaSyArKidc4o0MwbaCFKStlb2q2AwNg6Pnqns" # Ваш ключ для Google AI
27
- GENERATED_SITES_DIR = 'generated_sites'
28
 
29
- # Конфигурация для Hugging Face Sync
30
- REPO_ID = "Kgshop/testsynk" # !!! ИЗМЕНИТЕ НА ВАШ РЕПОЗИТОРИЙ !!!
31
- HF_TOKEN = os.getenv("HF_TOKEN")
32
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") or HF_TOKEN
33
- DATA_FILE = 'sites_db.json'
34
  SYNC_FILES = [DATA_FILE]
 
 
 
 
 
35
 
36
- # Управление процессами
37
- running_sites = {} # { "site_id": subprocess_object }
38
- BASE_PORT = 7861
39
 
40
- # Настройка логирования
41
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
42
 
43
- if not os.path.exists(GENERATED_SITES_DIR):
44
- os.makedirs(GENERATED_SITES_DIR)
 
45
 
46
 
47
- # --- 2. Функции для работы с Hugging Face (из Кода 2) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- def download_db_from_hf(specific_file=None, retries=3, delay=5):
50
- if not HF_TOKEN_READ:
51
- logging.warning("HF_TOKEN_READ not set. Download might fail for private repos.")
 
 
 
 
52
  files_to_download = [specific_file] if specific_file else SYNC_FILES
53
  logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
54
- all_successful = True
55
-
56
  for file_name in files_to_download:
57
- success = False
58
  for attempt in range(retries + 1):
59
  try:
60
- logging.info(f"Downloading {file_name} (Attempt {attempt + 1})...")
61
  hf_hub_download(
62
- repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=HF_TOKEN_READ,
63
  local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False
64
  )
65
  logging.info(f"Successfully downloaded {file_name}.")
66
- success = True
67
- break
68
  except HfHubHTTPError as e:
69
  if e.response.status_code == 404:
70
- logging.warning(f"File {file_name} not found in repo {REPO_ID}. Creating empty local file.")
71
- if not os.path.exists(file_name):
72
- if file_name == DATA_FILE:
73
- with open(file_name, 'w', encoding='utf-8') as f:
74
- json.dump({'sites': {}}, f)
75
- success = True # Считаем успехом, так как создали пустой файл
76
- break
77
- logging.error(f"HTTP error downloading {file_name}: {e}. Retrying...")
78
  except Exception as e:
79
- logging.error(f"Unexpected error downloading {file_name}: {e}. Retrying...", exc_info=True)
80
  if attempt < retries: time.sleep(delay)
81
- if not success:
82
- all_successful = False
83
- logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
84
- return all_successful
85
 
86
  def upload_db_to_hf(specific_file=None):
87
- if not HF_TOKEN:
88
  logging.warning("HF_TOKEN (for writing) not set. Skipping upload.")
89
  return
90
  try:
91
  api = HfApi()
92
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
93
- logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
94
  for file_name in files_to_upload:
95
  if os.path.exists(file_name):
96
  api.upload_file(
97
- path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN,
 
98
  commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
99
  )
100
- logging.info(f"File {file_name} successfully uploaded.")
101
- else:
102
- logging.warning(f"File {file_name} not found locally, skipping upload.")
103
  except Exception as e:
104
- logging.error(f"Error during Hugging Face upload: {e}", exc_info=True)
105
 
106
  def periodic_backup():
107
  while True:
108
  time.sleep(1800) # 30 минут
109
  logging.info("Starting periodic backup...")
110
- upload_db_to_hf()
 
 
 
111
 
112
  def load_data():
113
- if not os.path.exists(DATA_FILE):
114
- logging.warning(f"{DATA_FILE} not found locally. Attempting to download.")
115
- download_db_from_hf(DATA_FILE)
116
- try:
117
- with open(DATA_FILE, 'r', encoding='utf-8') as f:
118
- data = json.load(f)
119
- if 'sites' not in data: data['sites'] = {}
120
- return data
121
- except (FileNotFoundError, json.JSONDecodeError):
122
- logging.error(f"Failed to load or parse {DATA_FILE}. Using empty structure.")
123
- return {'sites': {}}
124
 
125
  def save_data(data):
 
 
 
 
 
 
 
 
 
126
  try:
127
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
128
- json.dump(data, f, ensure_ascii=False, indent=4)
129
- logging.info(f"Data saved to {DATA_FILE}. Uploading to HF.")
130
- upload_db_to_hf(specific_file=DATA_FILE)
 
131
  except Exception as e:
132
- logging.error(f"Error saving data: {e}", exc_info=True)
133
 
134
 
135
- # --- 3. Функции Управления Сайтами и Процессами ---
136
 
137
- def find_available_port(start_port):
138
- data = load_data()
139
- used_ports = {site['port'] for site in data['sites'].values() if 'port' in site and site.get('status') == 'running'}
140
- port = start_port
141
- while port in used_ports:
142
- port += 1
143
- return port
144
-
145
- def start_site_process(site_id, filepath):
146
- """Запускает сайт в отдельном процессе и возвращает объект процесса и порт."""
147
- port = find_available_port(BASE_PORT)
148
- env = os.environ.copy()
149
- env['PORT'] = str(port)
150
-
151
- try:
152
- process = subprocess.Popen(
153
- [sys.executable, filepath],
154
- env=env,
155
- stdout=subprocess.PIPE, # Можно перенаправить в лог-файлы
156
- stderr=subprocess.PIPE
157
- )
158
- time.sleep(2) # Даем время на запуск
159
-
160
- # Проверяем, жив ли процесс
161
- if process.poll() is None:
162
- running_sites[site_id] = process
163
- return process, port
164
- else:
165
- stdout, stderr = process.communicate()
166
- logging.error(f"Failed to start site {site_id}. Exit code: {process.returncode}")
167
- logging.error(f"Stderr: {stderr.decode('utf-8', errors='ignore')}")
168
- return None, None
169
- except Exception as e:
170
- logging.error(f"Exception while starting site {site_id}: {e}", exc_info=True)
171
- return None, None
172
-
173
- def stop_site_process(site_id):
174
- """Останавливает процесс сайта."""
175
- data = load_data()
176
- site_info = data['sites'].get(site_id)
177
-
178
- if site_id in running_sites:
179
- process = running_sites[site_id]
180
- logging.info(f"Stopping running process for site {site_id} with PID {process.pid}")
181
- process.terminate()
182
- try:
183
- process.wait(timeout=5)
184
- except subprocess.TimeoutExpired:
185
- process.kill()
186
- del running_sites[site_id]
187
- return True
188
-
189
- # Если процесс не в памяти, но в БД есть PID (после перезапуска)
190
- if site_info and 'pid' in site_info and site_info.get('status') == 'running':
191
- pid = site_info['pid']
192
- logging.warning(f"Process for site {site_id} not in memory. Attempting to kill by PID {pid}")
193
- try:
194
- os.kill(pid, signal.SIGTERM)
195
- return True
196
- except ProcessLookupError:
197
- logging.warning(f"Process with PID {pid} not found.")
198
- return True # Процесса и так нет
199
- except Exception as e:
200
- logging.error(f"Failed to kill process with PID {pid}: {e}")
201
- return False
202
- return True
203
-
204
- def cleanup_processes():
205
- """Останавливает все дочерние процессы при выходе."""
206
- logging.info("Shutting down. Terminating all running site processes...")
207
- for site_id, process in list(running_sites.items()):
208
- logging.info(f"Terminating site {site_id} (PID: {process.pid})")
209
- process.terminate()
210
- logging.info("Cleanup complete.")
211
-
212
- atexit.register(cleanup_processes)
213
-
214
- # --- 4. Генерация Кода с Помощью ИИ (доработано) ---
215
-
216
- def generate_website_code_from_prompt(user_prompt):
217
  try:
218
  genai.configure(api_key=API_KEY_INTERNAL)
219
  except Exception as e:
220
- raise ValueError(f"Ошибка конфигурации Google AI: {e}")
 
221
 
222
  if not user_prompt or not user_prompt.strip():
223
- raise ValueError("Промпт не может быть пустым.")
224
 
 
225
  system_instruction = (
226
- "You are an expert Python Flask web developer. Your mission is to generate a complete, single-file Python Flask web application based on the user's request.\n"
227
- "The generated script MUST adhere to the following strict rules:\n"
228
- "1. **Single File:** The entire application, including Flask routes and HTML templates, must be in one single `.py` file.\n"
229
- "2. **Flask Framework:** The application must use the Flask framework. It should start with `from flask import Flask, render_template_string, ...`.\n"
230
- "3. **Inline Templates:** All HTML templates must be embedded directly into the Python script as multiline string variables. For example: `HOME_PAGE_TEMPLATE = '''<!DOCTYPE html>...'''`. Use `render_template_string()` to render them.\n"
231
- "4. **No External Files:** The script must NOT attempt to read from or write to any local files (like .css, .js, .json). All data should be pre-populated in-memory (e.g., as a list of dictionaries). All CSS and JS must be inside the HTML templates within `<style>` and `<script>` tags.\n"
232
- "5. **Port from Environment:** The application must get its port number from an environment variable named `PORT`. The code to do this is crucial: `port = int(os.environ.get('PORT', 5000))`.\n"
233
- "6. **Startup Command:** The final executable line of the script MUST be `if __name__ == '__main__': app.run(host='0.0.0.0', port=port)`. This is non-negotiable.\n"
234
- "7. **Dependencies:** Assume only Flask is installed (`pip install Flask`). Do not use any other external libraries that are not part of the Python standard library.\n"
235
- "8. **Output Format:** Your output must be ONLY the raw Python code. Do not wrap it in markdown like ```python or add any explanatory text before or after the code block.\n"
236
- "\n"
237
- "Example of a valid structure:\n"
238
- "```python\n"
239
- "import os\n"
240
- "from flask import Flask, render_template_string\n\n"
241
- "app = Flask(__name__)\n\n"
242
- "PRODUCTS_DATA = [{'id': 1, 'name': 'Toy Car'}, {'id': 2, 'name': 'Plush Bear'}]\n\n"
243
- "HOME_TEMPLATE = '''\n"
244
- "<!DOCTYPE html><html>...<body>{% for product in products %}...{% endfor %}</body></html>'''\n\n"
245
- "@app.route('/')\n"
246
- "def home():\n"
247
- " return render_template_string(HOME_TEMPLATE, products=PRODUCTS_DATA)\n\n"
248
- "if __name__ == '__main__':\n"
249
- " port = int(os.environ.get('PORT', 5000))\n"
250
- " app.run(host='0.0.0.0', port=port)\n"
251
- "```"
252
  )
253
 
254
  full_prompt = f"{system_instruction}\n\nUser request: \"{user_prompt}\""
255
 
256
  try:
257
- model = genai.GenerativeModel('gemini-1.5-flash-latest') # Используем современную модель
258
  response = model.generate_content(full_prompt)
259
- generated_code = response.text
260
-
261
- # Очистка от markdown, если модель его добавила
262
- if generated_code.strip().startswith("```python"):
263
- generated_code = generated_code.strip()[9:]
264
- if generated_code.strip().endswith("```"):
265
- generated_code = generated_code.strip()[:-3]
266
 
267
- if not generated_code.strip():
268
- raise ValueError("Модель вернула пустой результат.")
269
-
270
- return generated_code.strip()
 
 
 
 
 
 
 
271
 
272
  except Exception as e:
273
- logging.error(f"Error generating content with GenAI: {e}", exc_info=True)
274
- raise ValueError(f"Ошибка при генерации кода сайта: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
 
276
- # --- 5. Flask Routes ---
 
 
 
 
 
 
 
277
 
278
- ADMIN_TEMPLATE = """
279
- <!DOCTYPE html>
280
- <html lang="ru">
281
- <head>
282
- <meta charset="UTF-8">
283
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
284
- <title>Панель Управления Сайтами</title>
285
- <style>
286
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: #f0f2f5; color: #1c1c1e; margin: 0; padding: 20px; }
287
- .container { max-width: 900px; margin: 20px auto; background: #fff; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
288
- .header { padding: 20px 25px; border-bottom: 1px solid #e5e5e5; }
289
- .header h1 { margin: 0; font-size: 24px; }
290
- .content { padding: 25px; }
291
- .form-section { margin-bottom: 30px; padding: 20px; background: #f7f7f7; border-radius: 8px; }
292
- textarea { width: 100%; padding: 12px; border-radius: 6px; border: 1px solid #ccc; font-size: 16px; min-height: 80px; box-sizing: border-box; }
293
- button { background-color: #007aff; color: white; border: none; padding: 12px 20px; font-size: 16px; font-weight: 500; border-radius: 8px; cursor: pointer; transition: background-color 0.2s; }
294
- button:hover { background-color: #005ecf; }
295
- button:disabled { background-color: #aaa; cursor: not-allowed; }
296
- .site-list { margin-top: 20px; }
297
- .site-item { display: grid; grid-template-columns: 1fr auto; gap: 15px; align-items: center; padding: 15px; border: 1px solid #e5e5e5; border-radius: 8px; margin-bottom: 10px; background: #fff; }
298
- .site-info p { margin: 0 0 8px; }
299
- .site-info p:last-child { margin-bottom: 0; }
300
- .site-info .prompt { font-style: italic; color: #666; font-size: 14px; }
301
- .site-actions { display: flex; gap: 10px; flex-wrap: wrap; }
302
- .site-actions .btn { padding: 8px 12px; font-size: 14px; text-decoration: none; border-radius: 6px; color: white; display: inline-block; }
303
- .btn-start { background-color: #34c759; } .btn-start:hover { background-color: #2ea34a; }
304
- .btn-stop { background-color: #ff3b30; } .btn-stop:hover { background-color: #d9332a; }
305
- .btn-delete { background-color: #8e8e93; } .btn-delete:hover { background-color: #6c6c70; }
306
- .status { padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: 500; }
307
- .status.running { background-color: #d4edda; color: #155724; }
308
- .status.stopped { background-color: #f8d7da; color: #721c24; }
309
- .flash { padding: 15px; margin-bottom: 20px; border-radius: 8px; }
310
- .flash.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
311
- .flash.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
312
- </style>
313
- </head>
314
- <body>
315
- <div class="container">
316
- <div class="header"><h1>Панель Управления Сайтами</h1></div>
317
- <div class="content">
318
- {% with messages = get_flashed_messages(with_categories=true) %}
319
- {% if messages %}
320
- {% for category, message in messages %}
321
- <div class="flash {{ category }}">{{ message }}</div>
322
- {% endfor %}
323
- {% endif %}
324
- {% endwith %}
325
-
326
- <div class="form-section">
327
- <h2>Создать новый сайт</h2>
328
- <form action="{{ url_for('handle_generate') }}" method="POST">
329
- <textarea name="prompt" required placeholder="Например: создай сайт-визитку для фотографа Анны. На сайте должны быть разделы 'Главная', 'Портфолио' с 6 примерами работ и 'Контакты' с формой обратной связи."></textarea><br><br>
330
- <button type="submit">Сгенерировать и запустить</button>
331
- </form>
332
- </div>
333
 
334
- <h2>Управление Сайтами</h2>
335
- <div class="site-list">
336
- {% if sites %}
337
- {% for site_id, site in sites.items()|sort(attribute='1.created_at', reverse=True) %}
338
- <div class="site-item">
339
- <div class="site-info">
340
- <p><strong>ID:</strong> {{ site_id }}</p>
341
- <p class="prompt"><strong>Промпт:</strong> {{ site.prompt }}</p>
342
- <p><strong>Статус:</strong> <span class="status {{ 'running' if site.status == 'running' else 'stopped' }}">{{ site.status }}</span>
343
- {% if site.status == 'running' and site.port %}
344
- | <strong>Порт:</strong> {{ site.port }} | <a href="http://{{ request.host.split(':')[0] }}:{{ site.port }}" target="_blank">Открыть сайт →</a>
345
- {% endif %}
346
- </p>
347
- </div>
348
- <div class="site-actions">
349
- {% if site.status == 'stopped' %}
350
- <a href="{{ url_for('start_site', site_id=site_id) }}" class="btn btn-start">Запустить</a>
351
- {% else %}
352
- <a href="{{ url_for('stop_site', site_id=site_id) }}" class="btn btn-stop">Остановить</a>
353
- {% endif %}
354
- <a href="{{ url_for('delete_site', site_id=site_id) }}" class="btn btn-delete" onclick="return confirm('Вы уверены, что хотите удалить этот сайт и все его файлы?');">Удалить</a>
355
- </div>
356
- </div>
357
- {% endfor %}
358
- {% else %}
359
- <p>Пока не создано ни одного сайта.</p>
360
- {% endif %}
361
- </div>
362
- </div>
363
- </div>
364
- </body>
365
- </html>
366
- """
367
 
368
  @app.route('/')
369
  def index():
370
- data = load_data()
371
- return render_template_string(ADMIN_TEMPLATE, sites=data.get('sites', {}))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
 
373
  @app.route('/generate', methods=['POST'])
374
  def handle_generate():
375
- user_prompt = request.form.get('prompt', '').strip()
376
  if not user_prompt:
377
- flash("Промпт не может быть пустым.", 'error')
378
- return redirect(url_for('index'))
379
 
380
  try:
381
- python_code = generate_website_code_from_prompt(user_prompt)
382
 
383
- site_id = uuid.uuid4().hex[:8]
384
- filename = f"{site_id}.py"
385
- filepath = os.path.join(GENERATED_SITES_DIR, filename)
386
 
387
  with open(filepath, "w", encoding="utf-8") as f:
388
  f.write(python_code)
389
 
390
- process, port = start_site_process(site_id, filepath)
391
- if process and port:
392
- data = load_data()
393
- data['sites'][site_id] = {
394
- 'prompt': user_prompt,
395
- 'filename': filename,
396
- 'status': 'running',
397
- 'port': port,
398
- 'pid': process.pid,
399
- 'created_at': datetime.now().isoformat()
400
- }
401
- save_data(data)
402
- flash(f"Сайт '{site_id}' успешно сгенерирован и запущен на порту {port}.", 'success')
403
- else:
404
- os.remove(filepath) # Удаляем неудачный файл
405
- flash(f"Не удалось запустить сгенерированный сайт '{site_id}'. Проверьте логи.", 'error')
406
 
407
- except (ValueError, Exception) as e:
408
- flash(f"Ошибка при генерации сайта: {e}", 'error')
409
-
410
- return redirect(url_for('index'))
411
-
412
- @app.route('/start/<site_id>')
413
- def start_site(site_id):
414
- data = load_data()
415
- site_info = data['sites'].get(site_id)
416
- if not site_info:
417
- flash(f"Сайт с ID {site_id} не найден.", 'error')
418
- return redirect(url_for('index'))
419
-
420
- if site_info['status'] == 'running':
421
- flash(f"Сайт {site_id} уже запущен.", 'error')
422
- return redirect(url_for('index'))
423
-
424
- filepath = os.path.join(GENERATED_SITES_DIR, site_info['filename'])
425
- if not os.path.exists(filepath):
426
- flash(f"Файл для сайта {site_id} не найден. Возможно, он был удален.", 'error')
427
- return redirect(url_for('index'))
428
-
429
- process, port = start_site_process(site_id, filepath)
430
- if process and port:
431
- site_info.update({'status': 'running', 'port': port, 'pid': process.pid})
432
- data['sites'][site_id] = site_info
433
- save_data(data)
434
- flash(f"Сайт {site_id} успешно запущен на порту {port}.", 'success')
435
- else:
436
- flash(f"Не удалось запустить сайт {site_id}.", 'error')
437
-
438
- return redirect(url_for('index'))
439
-
440
- @app.route('/stop/<site_id>')
441
- def stop_site(site_id):
442
- if stop_site_process(site_id):
443
- data = load_data()
444
- if site_id in data['sites']:
445
- data['sites'][site_id]['status'] = 'stopped'
446
- data['sites'][site_id].pop('port', None)
447
- data['sites'][site_id].pop('pid', None)
448
- save_data(data)
449
- flash(f"Сайт {site_id} успешно остановлен.", 'success')
450
- else:
451
- flash(f"Сайт {site_id} не найден в базе данных, но процесс (если был) остановлен.", 'error')
452
- else:
453
- flash(f"Не удалось остановить сайт {site_id}.", 'error')
454
-
455
- return redirect(url_for('index'))
456
-
457
- @app.route('/delete/<site_id>')
458
- def delete_site(site_id):
459
- data = load_data()
460
- site_info = data['sites'].get(site_id)
461
- if not site_info:
462
- flash(f"Сайт {site_id} не найден.", 'error')
463
- return redirect(url_for('index'))
464
-
465
- # Сначала остановить, если запущен
466
- if site_info['status'] == 'running':
467
- stop_site_process(site_id)
468
-
469
- # Удалить файл
470
- filepath = os.path.join(GENERATED_SITES_DIR, site_info['filename'])
471
- if os.path.exists(filepath):
472
- os.remove(filepath)
473
-
474
- # Удалить из БД
475
- del data['sites'][site_id]
476
- save_data(data)
477
-
478
- flash(f"Сайт {site_id} и его файлы были успешно удалены.", 'success')
479
- return redirect(url_for('index'))
480
 
 
481
 
482
- # --- 6. Запуск Приложения ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
 
484
  if __name__ == '__main__':
 
 
485
  logging.info("Application starting up...")
486
- download_db_from_hf()
487
 
488
- # При старте синхронизируем состояние (все сайты считаются остановленными)
489
- db_data = load_data()
490
- for site_id in db_data['sites']:
491
- db_data['sites'][site_id]['status'] = 'stopped'
492
- db_data['sites'][site_id].pop('port', None)
493
- db_data['sites'][site_id].pop('pid', None)
494
- save_data(db_data)
495
-
496
- if HF_TOKEN:
497
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
498
  backup_thread.start()
499
  logging.info("Periodic backup thread started.")
500
  else:
501
  logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).")
502
 
503
- app.run(host='0.0.0.0', port=7860, debug=False)
 
 
 
 
1
  import os
2
  import uuid
 
 
 
3
  import json
4
  import logging
5
  import threading
6
+ import time
7
+ import socket
8
+ import subprocess
9
  import atexit
10
+ from datetime import datetime
11
+ from flask import Flask, request, jsonify, Response, render_template_string
12
  import google.generativeai as genai
13
  from huggingface_hub import HfApi, hf_hub_download
14
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
 
15
  import requests
16
+ from dotenv import load_dotenv
17
+
18
+ # --- Конфигурация и инициализация ---
19
 
 
20
  load_dotenv()
21
  app = Flask(__name__)
 
22
 
23
+ # --- Константы из Кода 1 ---
24
+ API_KEY_INTERNAL = "AIzaSyArKidc4o0MwbaCFKStlb2q2AwNg6Pnqns" # Ваш ключ Google AI
25
+ GENERATED_APPS_DIR = 'generated_apps'
26
 
27
+ # --- Константы из Кода 2 (адаптированные) ---
28
+ DATA_FILE = 'running_apps.json'
 
 
 
29
  SYNC_FILES = [DATA_FILE]
30
+ REPO_ID = "Kgshop/testsynk" # <-- ВАШ НОВЫЙ РЕПОЗИТОРИЙ
31
+ HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
32
+ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
33
+ DOWNLOAD_RETRIES = 3
34
+ DOWNLOAD_DELAY = 5
35
 
36
+ # --- Глобальные переменные для управления состоянием ---
37
+ RUNNING_APPS = {} # { "uuid": {"process": Popen_object, "port": 12345, "prompt": "..."} }
 
38
 
39
+ # --- Настройка логирования ---
40
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
41
 
42
+ # --- Создание необходимых директорий ---
43
+ if not os.path.exists(GENERATED_APPS_DIR):
44
+ os.makedirs(GENERATED_APPS_DIR)
45
 
46
 
47
+ # --- HTML-шаблон для главной страницы (из Кода 1) ---
48
+ html_template = """
49
+ <!DOCTYPE html>
50
+ <html lang="ru">
51
+ <head>
52
+ <meta charset="UTF-8">
53
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
54
+ <title>EVA - Генератор Сайтов</title>
55
+ <style>
56
+ :root {
57
+ --system-gray-100-light: #f2f2f7; --system-gray-75-light: #f8f8fa; --system-gray-50-light: #ffffff;
58
+ --system-gray-dark-100-light: #000000; --system-gray-dark-75-light: #1c1c1e; --system-gray-dark-50-light: #3a3a3c;
59
+ --system-gray-light-75-light: #8e8e93; --system-gray-light-50-light: #aeaeb2; --system-blue-light: #007aff;
60
+ --system-blue-light-hover: #005ecf; --system-red-light: #ff3b30; --system-separator-light: rgba(60, 60, 67, 0.29);
61
+ --system-separator-opaque-light: #d1d1d6; --system-gray-100-dark: #1c1c1e; --system-gray-75-dark: #2c2c2e;
62
+ --system-gray-50-dark: #000000; --system-gray-dark-100-dark: #ffffff; --system-gray-dark-75-dark: #f2f2f7;
63
+ --system-gray-dark-50-dark: #e5e5ea; --system-gray-light-75-dark: #8e8e93; --system-gray-light-50-dark: #636366;
64
+ --system-blue-dark: #0a84ff; --system-blue-dark-hover: #3b9eff; --system-red-dark: #ff453a;
65
+ --system-separator-dark: rgba(84, 84, 88, 0.65); --system-separator-opaque-dark: #38383a;
66
+ }
67
+ @media (prefers-color-scheme: dark) {
68
+ :root { --bg-color: var(--system-gray-50-dark); --content-bg: var(--system-gray-100-dark); --text-color: var(--system-gray-dark-100-dark);
69
+ --secondary-text-color: var(--system-gray-light-75-dark); --tertiary-text-color: var(--system-gray-light-50-dark); --border-color: var(--system-separator-dark);
70
+ --border-color-opaque: var(--system-separator-opaque-dark); --input-bg: var(--system-gray-75-dark); --primary-color: var(--system-blue-dark);
71
+ --primary-color-hover: var(--system-blue-dark-hover); --error-color: var(--system-red-dark); } }
72
+ @media (prefers-color-scheme: light) {
73
+ :root { --bg-color: var(--system-gray-100-light); --content-bg: var(--system-gray-50-light); --text-color: var(--system-gray-dark-100-light);
74
+ --secondary-text-color: var(--system-gray-light-75-light); --tertiary-text-color: var(--system-gray-light-50-light); --border-color: var(--system-separator-light);
75
+ --border-color-opaque: var(--system-separator-opaque-light); --input-bg: var(--system-gray-75-light); --primary-color: var(--system-blue-light);
76
+ --primary-color-hover: var(--system-blue-light-hover); --error-color: var(--system-red-light); } }
77
+ html { height: -webkit-fill-available; }
78
+ body { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0;
79
+ padding: 20px env(safe-area-inset-top) 20px env(safe-area-inset-bottom); background-color: var(--bg-color); color: var(--text-color); display: flex;
80
+ justify-content: center; align-items: flex-start; min-height: 100vh; min-height: -webkit-fill-available; line-height: 1.45;
81
+ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
82
+ .container { background-color: var(--content-bg); padding: 25px 30px 30px 30px; border-radius: 24px; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05);
83
+ max-width: 580px; width: calc(100% - 40px); box-sizing: border-box; margin-top: 30px; }
84
+ h1 { font-size: 32px; font-weight: 700; text-align: center; margin-bottom: 8px; color: var(--text-color); letter-spacing: -0.5px; }
85
+ p.subtitle { font-size: 17px; color: var(--secondary-text-color); text-align: center; margin-bottom: 35px; font-weight: 400; }
86
+ .form-group { margin-bottom: 28px; }
87
+ label.input-label { display: block; font-weight: 500; margin-bottom: 10px; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
88
+ textarea#prompt-input { width: 100%; padding: 14px 18px; border: 1px solid var(--border-color-opaque); border-radius: 12px; font-size: 16px;
89
+ background-color: var(--input-bg); color: var(--text-color); box-sizing: border-box; transition: border-color 0.2s ease, box-shadow 0.2s ease;
90
+ font-family: inherit; resize: vertical; min-height: 120px; }
91
+ textarea#prompt-input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent); outline: none; }
92
+ button#generate-button { width: 100%; padding: 16px; background-color: var(--primary-color); color: white; border: none; border-radius: 12px; font-size: 17px;
93
+ font-weight: 600; cursor: pointer; transition: background-color 0.2s ease, transform 0.1s ease; margin-top: 15px; }
94
+ button#generate-button:hover { background-color: var(--primary-color-hover); }
95
+ button#generate-button:active { transform: scale(0.98); }
96
+ button#generate-button:disabled { background-color: var(--tertiary-text-color); cursor: not-allowed; }
97
+ .output-section { margin-top: 35px; }
98
+ .output-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
99
+ label#output-label { font-weight: 500; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
100
+ button#copy-button { background-color: transparent; border: none; color: var(--primary-color); font-size: 14px; font-weight: 500; cursor: pointer;
101
+ padding: 5px 8px; border-radius: 6px; transition: background-color 0.2s ease, color 0.2s ease; display: none; }
102
+ button#copy-button:hover { background-color: color-mix(in srgb, var(--primary-color) 15%, transparent); }
103
+ button#copy-button:active { background-color: color-mix(in srgb, var(--primary-color) 25%, transparent); }
104
+ button#copy-button.copied { color: #34c759; }
105
+ @media (prefers-color-scheme: dark) { button#copy-button.copied { color: #30d158; } }
106
+ #output-container { background-color: var(--input-bg); padding: 18px 20px; border-radius: 12px; min-height: 60px; border: 1px solid var(--border-color);
107
+ word-wrap: break-word; font-size: 15px; color: var(--text-color); line-height: 1.5; transition: border-color 0.2s ease, background-color 0.2s ease;
108
+ display: flex; align-items: center; justify-content: center; }
109
+ #output-container a { color: var(--primary-color); text-decoration: none; font-weight: 500; }
110
+ #output-container a:hover { text-decoration: underline; }
111
+ #output-container.loading::before { content: "Генерация приложения..."; display: block; text-align: center; font-style: italic; color: var(--secondary-text-color); animation: fadePulse 1.8s infinite ease-in-out; }
112
+ #output-container.error { color: var(--error-color); font-weight: 500; border-color: color-mix(in srgb, var(--error-color) 50%, transparent);
113
+ background-color: color-mix(in srgb, var(--error-color) 10%, var(--input-bg)); justify-content: flex-start; }
114
+ @keyframes fadePulse { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }
115
+ @media (max-width: 620px) {
116
+ body { padding: 15px env(safe-area-inset-top) 15px env(safe-area-inset-bottom); align-items: flex-start; }
117
+ .container { padding: 20px 20px 25px 20px; margin-top: 15px; border-radius: 20px; width: calc(100% - 30px); }
118
+ h1 { font-size: 28px; } p.subtitle { font-size: 16px; margin-bottom: 25px; } .form-group { margin-bottom: 22px; }
119
+ textarea#prompt-input { padding: 12px 15px; min-height: 100px; } button#generate-button { padding: 15px; font-size: 16px; }
120
+ #output-container { padding: 15px 18px; font-size: 14px; min-height: 50px; } .output-section { margin-top: 30px; } }
121
+ </style>
122
+ </head>
123
+ <body>
124
+ <div class="container">
125
+ <h1>EVA</h1>
126
+ <p class="subtitle">Генератор Python сайтов на базе ИИ</p>
127
+ <form id="generate-form">
128
+ <div class="form-group">
129
+ <label for="prompt-input" class="input-label">Опишите сайт, который вы хотите создать</label>
130
+ <textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай одностраничный сайт-портфолио для веб-дизайнера Алии. Нужны секции 'Обо мне', 'Работы' и 'Контакты'. Используй современный минималистичный дизайн с темной темой."></textarea>
131
+ </div>
132
+ <button type="submit" id="generate-button">Создать сайт</button>
133
+ </form>
134
+ <div class="output-section">
135
+ <div class="output-header">
136
+ <label id="output-label">Ссылка на сайт</label>
137
+ <button id="copy-button">Копировать</button>
138
+ </div>
139
+ <div id="output-container" aria-live="polite"></div>
140
+ </div>
141
+ </div>
142
+ <script>
143
+ const form = document.getElementById('generate-form');
144
+ const promptInput = document.getElementById('prompt-input');
145
+ const outputContainer = document.getElementById('output-container');
146
+ const generateButton = document.getElementById('generate-button');
147
+ const copyButton = document.getElementById('copy-button');
148
+
149
+ form.addEventListener('submit', async (event) => {
150
+ event.preventDefault();
151
+ if (!promptInput.value.trim()) { showError("Пожалуйста, опишите сайт, который вы хотите создать."); return; }
152
+ const formData = new FormData(form);
153
+ generateButton.disabled = true; generateButton.textContent = 'Генерация...';
154
+ outputContainer.innerHTML = ''; outputContainer.classList.add('loading'); outputContainer.classList.remove('error');
155
+ copyButton.style.display = 'none'; copyButton.textContent = 'Копировать'; copyButton.classList.remove('copied');
156
+
157
+ try {
158
+ const response = await fetch('/generate', { method: 'POST', body: formData });
159
+ const result = await response.json();
160
+ if (!response.ok) { throw new Error(result.error || `Ошибка сервера: ${response.status}`); }
161
+
162
+ if (result.site_url) {
163
+ const link = document.createElement('a');
164
+ link.href = result.site_url;
165
+ link.textContent = "Открыть сгенерированное приложение";
166
+ link.target = "_blank";
167
+ outputContainer.innerHTML = '';
168
+ outputContainer.appendChild(link);
169
+ copyButton.style.display = 'block';
170
+ copyButton.dataset.copyText = window.location.origin + result.site_url;
171
+ } else {
172
+ showError(result.error || "Не удалось получить ссылку на сайт. Ответ сервера не содержит URL.");
173
+ }
174
+ } catch (error) {
175
+ console.error("Fetch Error:", error);
176
+ showError(`Ошибка: ${error.message}`);
177
+ copyButton.style.display = 'none';
178
+ } finally {
179
+ generateButton.disabled = false;
180
+ generateButton.textContent = 'Создать сайт';
181
+ outputContainer.classList.remove('loading');
182
+ }
183
+ });
184
+ copyButton.addEventListener('click', () => {
185
+ const textToCopy = copyButton.dataset.copyText;
186
+ if (!textToCopy) return;
187
+ navigator.clipboard.writeText(textToCopy).then(() => {
188
+ copyButton.textContent = 'Скопировано!'; copyButton.classList.add('copied');
189
+ setTimeout(() => { copyButton.textContent = 'Копировать'; copyButton.classList.remove('copied'); }, 1500);
190
+ }).catch(err => {
191
+ console.error('Ошибка копирования: ', err);
192
+ copyButton.textContent = 'Ошибка';
193
+ setTimeout(() => { copyButton.textContent = 'Копировать'; }, 1500);
194
+ });
195
+ });
196
+ function showError(message) {
197
+ outputContainer.innerHTML = '';
198
+ const errorMessageElement = document.createElement('span');
199
+ errorMessageElement.textContent = message;
200
+ outputContainer.appendChild(errorMessageElement);
201
+ outputContainer.classList.add('error');
202
+ outputContainer.classList.remove('loading');
203
+ copyButton.style.display = 'none';
204
+ }
205
+ </script>
206
+ </body>
207
+ </html>
208
+ """
209
 
210
+ # --- Функции для работы с Hugging Face (из Кода 2) ---
211
+
212
+ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
213
+ token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
214
+ if not token_to_use:
215
+ logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
216
+
217
  files_to_download = [specific_file] if specific_file else SYNC_FILES
218
  logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
219
+
 
220
  for file_name in files_to_download:
 
221
  for attempt in range(retries + 1):
222
  try:
 
223
  hf_hub_download(
224
+ repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use,
225
  local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False
226
  )
227
  logging.info(f"Successfully downloaded {file_name}.")
228
+ return True
 
229
  except HfHubHTTPError as e:
230
  if e.response.status_code == 404:
231
+ logging.warning(f"File {file_name} not found in repo {REPO_ID}. Skipping.")
232
+ return False
233
+ logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying...")
 
 
 
 
 
234
  except Exception as e:
235
+ logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying...", exc_info=True)
236
  if attempt < retries: time.sleep(delay)
237
+ logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
238
+ return False
 
 
239
 
240
  def upload_db_to_hf(specific_file=None):
241
+ if not HF_TOKEN_WRITE:
242
  logging.warning("HF_TOKEN (for writing) not set. Skipping upload.")
243
  return
244
  try:
245
  api = HfApi()
246
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
 
247
  for file_name in files_to_upload:
248
  if os.path.exists(file_name):
249
  api.upload_file(
250
+ path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID,
251
+ repo_type="dataset", token=HF_TOKEN_WRITE,
252
  commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
253
  )
254
+ logging.info(f"File {file_name} successfully uploaded to Hugging Face.")
 
 
255
  except Exception as e:
256
+ logging.error(f"Error uploading file to Hugging Face: {e}", exc_info=True)
257
 
258
  def periodic_backup():
259
  while True:
260
  time.sleep(1800) # 30 минут
261
  logging.info("Starting periodic backup...")
262
+ save_data(RUNNING_APPS)
263
+ logging.info("Periodic backup finished.")
264
+
265
+ # --- Функции управления данными (адаптированные из Кода 2) ---
266
 
267
  def load_data():
268
+ if os.path.exists(DATA_FILE):
269
+ try:
270
+ with open(DATA_FILE, 'r', encoding='utf-8') as f:
271
+ return json.load(f)
272
+ except json.JSONDecodeError:
273
+ logging.error(f"Could not decode {DATA_FILE}. It might be corrupted.")
274
+ return {}
275
+ return {}
 
 
 
276
 
277
  def save_data(data):
278
+ # Сохраняем только метаданные, а не объекты Popen
279
+ data_to_save = {}
280
+ for site_uuid, details in data.items():
281
+ data_to_save[site_uuid] = {
282
+ "port": details.get("port"),
283
+ "prompt": details.get("prompt"),
284
+ "pid": details.get("process").pid if details.get("process") else None,
285
+ "filepath": details.get("filepath")
286
+ }
287
  try:
288
  with open(DATA_FILE, 'w', encoding='utf-8') as f:
289
+ json.dump(data_to_save, f, ensure_ascii=False, indent=4)
290
+ logging.info(f"Data successfully saved to {DATA_FILE}")
291
+ # Запускаем загрузку в HF в отдельном потоке, чтобы не блокировать основной
292
+ threading.Thread(target=upload_db_to_hf, args=(DATA_FILE,)).start()
293
  except Exception as e:
294
+ logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
295
 
296
 
297
+ # --- Основная логика генерации и управления приложениями ---
298
 
299
+ def generate_app_code_from_prompt(user_prompt):
300
+ """
301
+ Генерирует код Python Flask приложения на основе запроса пользователя.
302
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  try:
304
  genai.configure(api_key=API_KEY_INTERNAL)
305
  except Exception as e:
306
+ logging.error(f"Error configuring GenAI: {e}")
307
+ raise ValueError(f"Не удалось настроить Google AI: {e}")
308
 
309
  if not user_prompt or not user_prompt.strip():
310
+ raise ValueError("Текстовый запрос (промпт) не может быть пустым.")
311
 
312
+ # !ВАЖНО! Новый системный промпт для генерации Python кода
313
  system_instruction = (
314
+ "You are an expert Python Flask developer. Your task is to generate a complete, single-file Flask application "
315
+ "based on the user's request. The entire application must be contained in a single Python script.\n"
316
+ "Follow these rules strictly:\n"
317
+ "1. The script must import all necessary libraries, like `Flask`, `render_template_string`, and `os`.\n"
318
+ "2. All HTML, CSS, and JavaScript must be embedded within Python multiline strings.\n"
319
+ "3. Use `render_template_string` to serve the HTML content.\n"
320
+ "4. The application MUST get its port from an environment variable named 'PORT'. Use `os.environ.get('PORT', 5000)`.\n"
321
+ "5. The application must run on host '0.0.0.0' to be accessible within a containerized environment.\n"
322
+ "6. The script must be directly runnable using `if __name__ == '__main__':`.\n"
323
+ "7. Do NOT use any external files, databases, or libraries that are not standard (like flask, os, json).\n"
324
+ "8. The generated code must be visually appealing and functional according to the user's request.\n"
325
+ "9. CRITICAL: Directly output ONLY the raw Python code. Do not include any explanatory text, markdown formatting (like ```python), or anything else before `import` or after the last line of code."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  )
327
 
328
  full_prompt = f"{system_instruction}\n\nUser request: \"{user_prompt}\""
329
 
330
  try:
331
+ model = genai.GenerativeModel('gemini-1.5-flash-latest')
332
  response = model.generate_content(full_prompt)
 
 
 
 
 
 
 
333
 
334
+ generated_text = response.text
335
+ if not generated_text or not generated_text.strip().startswith("import"):
336
+ raise ValueError("Модель сгенерировала некорректный или пустой код.")
337
+
338
+ # Очистка от возможных артефактов
339
+ if generated_text.strip().startswith("```python"):
340
+ generated_text = generated_text.strip()[9:]
341
+ if generated_text.strip().endswith("```"):
342
+ generated_text = generated_text.strip()[:-3]
343
+
344
+ return generated_text.strip()
345
 
346
  except Exception as e:
347
+ logging.error(f"Error generating content with GenAI: {e}")
348
+ if "API key not valid" in str(e):
349
+ raise ValueError("Внутренняя ошибка конфигурации API.")
350
+ raise ValueError(f"Ошибка при генерации Python-кода сайта: {e}")
351
+
352
+ def find_free_port():
353
+ """Находит свободный TCP порт."""
354
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
355
+ s.bind(('', 0))
356
+ return s.getsockname()[1]
357
+
358
+ def start_app_process(site_uuid, filepath, prompt):
359
+ """Запускает сгенерированное приложение в отдельном процессе."""
360
+ port = find_free_port()
361
+ env = os.environ.copy()
362
+ env["PORT"] = str(port)
363
 
364
+ try:
365
+ process = subprocess.Popen(
366
+ ['python', filepath],
367
+ env=env,
368
+ stdout=open(f'{GENERATED_APPS_DIR}/{site_uuid}.log', 'w'),
369
+ stderr=subprocess.STDOUT
370
+ )
371
+ time.sleep(2) # Даем время на запуск
372
 
373
+ if process.poll() is not None: # Проверяем, не завершился ли процесс с ошибкой
374
+ raise RuntimeError(f"Процесс для сайта {site_uuid} не запустился. Смотрите лог {site_uuid}.log")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
+ RUNNING_APPS[site_uuid] = {
377
+ "process": process,
378
+ "port": port,
379
+ "prompt": prompt,
380
+ "filepath": filepath,
381
+ }
382
+ logging.info(f"Successfully started app {site_uuid} on port {port} with PID {process.pid}")
383
+ return True
384
+ except Exception as e:
385
+ logging.error(f"Failed to start app {site_uuid}: {e}", exc_info=True)
386
+ return False
387
+
388
+ # --- Маршруты Flask ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
 
390
  @app.route('/')
391
  def index():
392
+ return Response(html_template, mimetype='text/html')
393
+
394
+ @app.route('/app/<site_uuid>/', defaults={'path': ''})
395
+ @app.route('/app/<site_uuid>/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
396
+ def proxy_to_generated_app(site_uuid, path):
397
+ """Проксирует запросы к сгенерированным приложениям."""
398
+ if site_uuid not in RUNNING_APPS:
399
+ return "Приложение не найдено или не запущено.", 404
400
+
401
+ port = RUNNING_APPS[site_uuid]['port']
402
+ target_url = f"http://127.0.0.1:{port}/{path}"
403
+
404
+ try:
405
+ resp = requests.request(
406
+ method=request.method,
407
+ url=target_url,
408
+ headers={key: value for (key, value) in request.headers if key != 'Host'},
409
+ data=request.get_data(),
410
+ cookies=request.cookies,
411
+ allow_redirects=False,
412
+ stream=True,
413
+ params=request.args
414
+ )
415
+
416
+ headers = [(name, value) for (name, value) in resp.raw.headers.items()]
417
+
418
+ return Response(resp.iter_content(chunk_size=1024), resp.status_code, headers)
419
+
420
+ except requests.exceptions.RequestException as e:
421
+ logging.error(f"Proxy error for app {site_uuid}: {e}")
422
+ return f"Ошибка при подключении к приложению {site_uuid}.", 502
423
+
424
 
425
  @app.route('/generate', methods=['POST'])
426
  def handle_generate():
427
+ user_prompt = request.form.get('prompt')
428
  if not user_prompt:
429
+ return jsonify({"error": "Текстовый запрос (промпт) не найден."}), 400
 
430
 
431
  try:
432
+ python_code = generate_app_code_from_prompt(user_prompt)
433
 
434
+ site_uuid = str(uuid.uuid4())
435
+ filename = f"{site_uuid}.py"
436
+ filepath = os.path.join(GENERATED_APPS_DIR, filename)
437
 
438
  with open(filepath, "w", encoding="utf-8") as f:
439
  f.write(python_code)
440
 
441
+ logging.info(f"Generated Python code saved to {filepath}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
 
443
+ if not start_app_process(site_uuid, filepath, user_prompt):
444
+ return jsonify({"error": f"Сгенерированный код не удалось запустить. Проверьте лог {site_uuid}.log"}), 500
445
+
446
+ save_data(RUNNING_APPS) # Сохраняем новое состояние
447
+
448
+ site_url = f"/app/{site_uuid}/"
449
+ return jsonify({"site_url": site_url})
450
+
451
+ except ValueError as ve:
452
+ return jsonify({"error": str(ve)}), 400
453
+ except Exception as e:
454
+ logging.error(f"Unexpected error during site generation: {e}", exc_info=True)
455
+ return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
 
457
+ # --- Функции запуска и очистки ---
458
 
459
+ def cleanup_processes():
460
+ """Останавливает все дочерние процессы при выходе."""
461
+ logging.info("Shutting down... Terminating all running app processes.")
462
+ for site_uuid, details in RUNNING_APPS.items():
463
+ if details.get("process"):
464
+ logging.info(f"Terminating process for site {site_uuid} (PID: {details['process'].pid})")
465
+ details["process"].terminate()
466
+ details["process"].wait() # Ждем завершения
467
+ logging.info("All child processes terminated.")
468
+
469
+ def restart_running_apps(saved_apps):
470
+ """Перезапускает приложения, которые были активны до перезагрузки."""
471
+ logging.info("Restarting previously running applications...")
472
+ for site_uuid, details in saved_apps.items():
473
+ filepath = details.get("filepath")
474
+ prompt = details.get("prompt")
475
+ if filepath and os.path.exists(filepath):
476
+ logging.info(f"Attempting to restart app {site_uuid} from {filepath}")
477
+ start_app_process(site_uuid, filepath, prompt)
478
+ else:
479
+ logging.warning(f"Could not restart app {site_uuid}, file {filepath} not found.")
480
+
481
+ if saved_apps:
482
+ save_data(RUNNING_APPS) # Сохраняем обновленные PIDы и порты
483
 
484
  if __name__ == '__main__':
485
+ atexit.register(cleanup_processes)
486
+
487
  logging.info("Application starting up...")
 
488
 
489
+ # 1. Скачиваем последнее состояние с HF
490
+ if download_db_from_hf(DATA_FILE):
491
+ saved_state = load_data()
492
+ if saved_state:
493
+ # 2. Перезапускаем ранее активные приложения
494
+ restart_running_apps(saved_state)
495
+
496
+ # 3. Запускаем фоновый бэкап, если есть токен
497
+ if HF_TOKEN_WRITE:
498
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
499
  backup_thread.start()
500
  logging.info("Periodic backup thread started.")
501
  else:
502
  logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).")
503
 
504
+ # 4. Запускаем основное приложение
505
+ port = int(os.environ.get('PORT', 7860))
506
+ logging.info(f"Starting main Flask app on host 0.0.0.0 and port {port}")
507
+ app.run(host='0.0.0.0', port=port, debug=False)