├── interface.jpg ├── README.md ├── LICENSE ├── .gitignore └── main.py /interface.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuinasHistori/BotCommander/HEAD/interface.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![BotCommander интерфейс](interface.jpg) 3 | 4 | 🚀 BotCommander 5 | 6 | BotCommander — это простой и удобный веб-интерфейс для управления множеством Python-ботов. 7 | Каждый бот изолирован в своей собственной директории с виртуальным окружением (venv). Интерфейс показывает статус, загрузку CPU/RAM, аптайм, а также позволяет запускать, останавливать, перезапускать и отключать ботов. 8 | 🛠 Возможности 9 | 10 | Веб-интерфейс на Flask 11 | 12 | Автоматический запуск всех ботов при старте 13 | 14 | Мониторинг загрузки CPU, памяти и аптайма каждого бота 15 | 16 | Управление ботами: запуск, остановка, перезапуск, отключение 17 | 18 | Отображение системной информации (CPU, RAM, аптайм, ОС и т.д.) 19 | 20 | 🧩 Пример добавления нового бота 21 | 22 | mkdir -p bot/mybot 23 | cd bot/mybot 24 | python3 -m venv venv 25 | source venv/bin/activate 26 | pip install <нужные_зависимости> 27 | nano main.py # создайте и напишите код вашего бота 28 | 29 | ⚠️ Важно: у каждого бота обязательно должен быть файл main.py, а также создано виртуальное окружение venv/. 30 | Скрипт запускает бота с помощью source venv/bin/activate && python3 main.py. 31 | 32 | ✅ Запуск BotCommander 33 | 34 | Установите зависимости: 35 | 36 | pip install flask psutil 37 | 38 | Запустите интерфейс: 39 | 40 | python3 main.py 41 | 42 | Откройте в браузере: http://localhost:9999 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2025, RuinasHistori 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # Abstra 171 | # Abstra is an AI-powered process automation framework. 172 | # Ignore directories containing user credentials, local state, and settings. 173 | # Learn more at https://abstra.io/docs 174 | .abstra/ 175 | 176 | # Visual Studio Code 177 | # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 178 | # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore 179 | # and can be added to the global gitignore or merged into this file. However, if you prefer, 180 | # you could uncomment the following to ignore the enitre vscode folder 181 | # .vscode/ 182 | 183 | # Ruff stuff: 184 | .ruff_cache/ 185 | 186 | # PyPI configuration file 187 | .pypirc 188 | 189 | # Cursor 190 | # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to 191 | # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data 192 | # refer to https://docs.cursor.com/context/ignore-files 193 | .cursorignore 194 | .cursorindexingignore -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | import time 5 | import threading 6 | import psutil 7 | import platform 8 | import socket 9 | from flask import Flask, render_template_string, redirect, url_for, jsonify 10 | 11 | app = Flask(__name__) 12 | 13 | BOTS_DIR = './bot' 14 | STATUS = {} 15 | PROCESSES = {} 16 | DISABLED = set() 17 | ERROR_HISTORY = {} 18 | 19 | def add_error(botname, message): 20 | if botname not in ERROR_HISTORY: 21 | ERROR_HISTORY[botname] = [] 22 | ERROR_HISTORY[botname].append(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - {message}") 23 | if len(ERROR_HISTORY[botname]) > 5: 24 | ERROR_HISTORY[botname] = ERROR_HISTORY[botname][-5:] 25 | 26 | def bot_worker(name, path): 27 | while True: 28 | if name in DISABLED: 29 | STATUS[name] = 'OFFLINE' 30 | time.sleep(5) 31 | continue 32 | 33 | try: 34 | STATUS[name] = 'STARTING' 35 | PROCESSES[name] = subprocess.Popen( 36 | ['bash', '-c', f'source {path}/venv/bin/activate && python3 {path}/main.py'], 37 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 38 | STATUS[name] = 'ON' 39 | stdout, stderr = PROCESSES[name].communicate() 40 | if PROCESSES[name].returncode != 0: 41 | err_msg = stderr.decode(errors='ignore').strip() or f"Exit code {PROCESSES[name].returncode}" 42 | add_error(name, err_msg) 43 | STATUS[name] = f'DOWN' 44 | except Exception as e: 45 | add_error(name, str(e)) 46 | STATUS[name] = f'ERROR' 47 | time.sleep(5) 48 | 49 | def start_all_bots(): 50 | for name in os.listdir(BOTS_DIR): 51 | path = os.path.join(BOTS_DIR, name) 52 | if os.path.isdir(path) and os.path.isfile(os.path.join(path, 'main.py')): 53 | if name not in DISABLED: 54 | STATUS[name] = 'STARTING' 55 | t = threading.Thread(target=bot_worker, args=(name, path), daemon=True) 56 | t.start() 57 | else: 58 | STATUS[name] = 'OFFLINE' 59 | 60 | def get_bot_cpu_usage(): 61 | usage = {} 62 | for name, proc in PROCESSES.items(): 63 | if proc.poll() is None: 64 | try: 65 | p = psutil.Process(proc.pid) 66 | cpu_percent = p.cpu_percent(interval=0.1) 67 | mem_percent = p.memory_percent() 68 | create_time = p.create_time() 69 | uptime_sec = time.time() - create_time 70 | usage[name] = { 71 | 'cpu': cpu_percent, 72 | 'mem': mem_percent, 73 | 'uptime': uptime_sec, 74 | } 75 | except Exception: 76 | usage[name] = {'cpu': 0.0, 'mem': 0.0, 'uptime': 0} 77 | else: 78 | usage[name] = {'cpu': 0.0, 'mem': 0.0, 'uptime': 0} 79 | return usage 80 | 81 | def format_uptime(seconds): 82 | m, s = divmod(int(seconds), 60) 83 | h, m = divmod(m, 60) 84 | d, h = divmod(h, 24) 85 | parts = [] 86 | if d > 0: 87 | parts.append(f"{d}д") 88 | if h > 0: 89 | parts.append(f"{h}ч") 90 | if m > 0: 91 | parts.append(f"{m}м") 92 | parts.append(f"{s}с") 93 | return ' '.join(parts) 94 | 95 | def get_system_info(): 96 | cpu = psutil.cpu_percent(interval=0.1) 97 | ram = psutil.virtual_memory().percent 98 | hostname = socket.gethostname() 99 | os_info = platform.system() + " " + platform.release() 100 | uptime_seconds = time.time() - psutil.boot_time() 101 | uptime_str = format_uptime(uptime_seconds) 102 | python_version = platform.python_version() 103 | cpu_arch = platform.machine() 104 | return { 105 | 'cpu': cpu, 106 | 'ram': ram, 107 | 'hostname': hostname, 108 | 'os_info': os_info, 109 | 'uptime': uptime_str, 110 | 'python_version': python_version, 111 | 'cpu_arch': cpu_arch, 112 | } 113 | 114 | @app.route('/') 115 | def index(): 116 | bots = STATUS 117 | return render_template_string(''' 118 | 119 | 120 | 121 | 122 | Управление ботами 123 | 190 | 223 | 224 | 225 |

Управление ботами

226 |
Загрузка...
227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 |
Имя ботаСтатусCPU (%)RAM (%)АптаймДействия
242 | 243 | 244 | ''', bots=bots) 245 | 246 | @app.route('/status') 247 | def status(): 248 | system = get_system_info() 249 | bot_stats = get_bot_cpu_usage() 250 | bots_data = {} 251 | for name, status in STATUS.items(): 252 | errors = ERROR_HISTORY.get(name, []) 253 | usage = bot_stats.get(name, {'cpu': 0.0, 'mem': 0.0, 'uptime': 0}) 254 | bots_data[name] = { 255 | 'status': status, 256 | 'cpu': usage['cpu'], 257 | 'mem': usage['mem'], 258 | 'uptime': format_uptime(usage['uptime']) if usage['uptime'] > 0 else '-', 259 | 'errors': errors, 260 | } 261 | system_info_text = ( 262 | f"{system['hostname']} | {system['os_info']}\n" 263 | f"CPU: {system['cpu']:.1f}% | RAM: {system['ram']:.1f}% | Uptime: {system['uptime']}\n" 264 | f"Python: {system['python_version']} | Arch: {system['cpu_arch']}" 265 | ) 266 | return jsonify({ 267 | 'system_info': system_info_text, 268 | 'bots': bots_data, 269 | }) 270 | 271 | @app.route('/restart/') 272 | def restart_bot(botname): 273 | proc = PROCESSES.get(botname) 274 | if proc and proc.poll() is None: 275 | proc.terminate() 276 | STATUS[botname] = 'STARTING' 277 | return redirect(url_for('index')) 278 | 279 | @app.route('/stop/') 280 | def stop_bot(botname): 281 | proc = PROCESSES.get(botname) 282 | if proc and proc.poll() is None: 283 | proc.terminate() 284 | STATUS[botname] = 'OFFLINE' 285 | return redirect(url_for('index')) 286 | 287 | @app.route('/disable/') 288 | def disable_bot(botname): 289 | proc = PROCESSES.get(botname) 290 | if proc and proc.poll() is None: 291 | proc.terminate() 292 | DISABLED.add(botname) 293 | STATUS[botname] = 'OFFLINE' 294 | return redirect(url_for('index')) 295 | 296 | @app.route('/enable/') 297 | def enable_bot(botname): 298 | if botname in DISABLED: 299 | DISABLED.remove(botname) 300 | STATUS[botname] = 'STARTING' 301 | path = os.path.join(BOTS_DIR, botname) 302 | t = threading.Thread(target=bot_worker, args=(botname, path), daemon=True) 303 | t.start() 304 | return redirect(url_for('index')) 305 | 306 | if __name__ == '__main__': 307 | start_all_bots() 308 | app.run(host='0.0.0.0', port=9999) 309 | --------------------------------------------------------------------------------