| import os |
| import subprocess |
| import threading |
| import time |
| import sys |
| import queue |
| from flask import Flask, Response, render_template_string |
|
|
| |
| |
| |
|
|
| app = Flask(__name__) |
|
|
| |
| log_queue = queue.Queue() |
| MAX_LOG_LINES = 200 |
| recent_logs = [] |
|
|
| def enqueue_output(out, queue): |
| """Đọc output từ process và đẩy vào hàng đợi""" |
| for line in iter(out.readline, b''): |
| line = line.decode('utf-8').rstrip() |
| if line: |
| print(f"[BOT] {line}") |
| queue.put(line) |
| |
| recent_logs.append(line) |
| if len(recent_logs) > MAX_LOG_LINES: |
| recent_logs.pop(0) |
| out.close() |
|
|
| def prepare_config(): |
| """Inject Token vào config.yml""" |
| token = os.getenv("LICHESS_TOKEN") |
| config_path = "config.yml" |
| |
| if not os.path.exists(config_path): |
| log_queue.put("❌ ERROR: config.yml not found!") |
| return False |
|
|
| if token: |
| try: |
| with open(config_path, "r", encoding="utf-8") as f: |
| content = f.read() |
| if "REPLACE_ME_WITH_LICHESS_TOKEN" in content: |
| log_queue.put("🔑 System: Injecting Lichess Token...") |
| new_content = content.replace("REPLACE_ME_WITH_LICHESS_TOKEN", token) |
| with open(config_path, "w", encoding="utf-8") as f: |
| f.write(new_content) |
| except Exception as e: |
| log_queue.put(f"❌ Config Error: {e}") |
| return False |
| else: |
| log_queue.put("⚠️ Warning: No LICHESS_TOKEN found in env.") |
| return True |
|
|
| def run_bot_process(): |
| """Chạy Lichess-bot trong subprocess""" |
| if not prepare_config(): |
| return |
|
|
| log_queue.put("🚀 System: Starting KiyEngine v6 Assassin...") |
| log_queue.put("Target: Lichess.org | Mode: Hunter") |
| log_queue.put("--------------------------------------------------") |
|
|
| |
| process = subprocess.Popen( |
| ["python3", "-u", "lichess-bot.py"], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| bufsize=1 |
| ) |
|
|
| |
| t = threading.Thread(target=enqueue_output, args=(process.stdout, log_queue)) |
| t.daemon = True |
| t.start() |
| |
| process.wait() |
| log_queue.put("🛑 System: Bot process terminated.") |
|
|
| |
| HTML_TEMPLATE = """ |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>KiyEngine v6 Control Center</title> |
| <style> |
| body { |
| background-color: #0d1117; |
| color: #c9d1d9; |
| font-family: 'Consolas', 'Monaco', 'Courier New', monospace; |
| margin: 0; |
| padding: 20px; |
| display: flex; |
| flex-direction: column; |
| height: 100vh; |
| box-sizing: border_box; |
| } |
| .header { |
| border-bottom: 1px solid #30363d; |
| padding-bottom: 10px; |
| margin-bottom: 10px; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| .title { |
| font-size: 1.2rem; |
| font-weight: bold; |
| color: #58a6ff; |
| } |
| .status { |
| font-size: 0.9rem; |
| color: #2ea043; |
| } |
| .status::before { |
| content: '● '; |
| color: #2ea043; |
| animation: blink 1.5s infinite; |
| } |
| @keyframes blink { 50% { opacity: 0; } } |
| |
| #terminal { |
| flex-grow: 1; |
| background-color: #010409; |
| border: 1px solid #30363d; |
| border-radius: 6px; |
| padding: 15px; |
| overflow-y: auto; |
| white-space: pre-wrap; |
| font-size: 0.9rem; |
| line-height: 1.4; |
| box-shadow: 0 0 15px rgba(0,0,0,0.5); |
| } |
| .log-line { margin: 2px 0; } |
| .log-info { color: #8b949e; } |
| .log-warn { color: #d29922; } |
| .log-err { color: #f85149; } |
| .log-success { color: #2ea043; } |
| .prompt { color: #79c0ff; margin-right: 5px; } |
| |
| /* Scrollbar đẹp */ |
| ::-webkit-scrollbar { width: 10px; } |
| ::-webkit-scrollbar-track { background: #0d1117; } |
| ::-webkit-scrollbar-thumb { background: #30363d; border-radius: 5px; } |
| ::-webkit-scrollbar-thumb:hover { background: #58a6ff; } |
| </style> |
| </head> |
| <body> |
| <div class="header"> |
| <div class="title">KiyEngine v6 Assassin <span style="font-size:0.8em; color:#8b949e;">[BitNet 1.58-bit]</span></div> |
| <div class="status">SYSTEM ONLINE</div> |
| </div> |
| <div id="terminal"></div> |
| |
| <script> |
| const terminal = document.getElementById('terminal'); |
| |
| function appendLog(text) { |
| const div = document.createElement('div'); |
| div.className = 'log-line'; |
| |
| // Tô màu log cơ bản |
| if (text.includes('ERROR') || text.includes('Traceback')) div.classList.add('log-err'); |
| else if (text.includes('WARNING')) div.classList.add('log-warn'); |
| else if (text.includes('Welcome') || text.includes('Connected')) div.classList.add('log-success'); |
| else if (text.includes('INFO')) div.classList.add('log-info'); |
| |
| // Thêm dấu nhắc lệnh giả |
| div.innerHTML = '<span class="prompt">$</span>' + text.replace(/</g, '<').replace(/>/g, '>'); |
| terminal.appendChild(div); |
| |
| // Auto scroll |
| terminal.scrollTop = terminal.scrollHeight; |
| } |
| |
| // Kết nối luồng sự kiện (Server-Sent Events) |
| const evtSource = new EventSource("/stream_logs"); |
| |
| evtSource.onmessage = function(e) { |
| appendLog(e.data); |
| }; |
| |
| evtSource.onerror = function() { |
| console.log("Connection lost, retrying..."); |
| }; |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| @app.route('/') |
| def index(): |
| return render_template_string(HTML_TEMPLATE) |
|
|
| @app.route('/stream_logs') |
| def stream_logs(): |
| def generate(): |
| |
| for line in recent_logs: |
| yield f"data: {line}\n\n" |
| |
| |
| while True: |
| try: |
| |
| line = log_queue.get(timeout=5) |
| yield f"data: {line}\n\n" |
| except queue.Empty: |
| yield ": keep-alive\n\n" |
|
|
| return Response(generate(), mimetype='text/event-stream') |
|
|
| if __name__ == "__main__": |
| |
| threading.Thread(target=run_bot_process, daemon=True).start() |
| |
| |
| port = int(os.environ.get("PORT", 7860)) |
| app.run(host='0.0.0.0', port=port, threaded=True) |