├── src └── jiduoduo │ ├── forms │ ├── __init__.py │ ├── base.py │ ├── testing.py │ ├── user.py │ └── vps.py │ ├── services │ ├── __init__.py │ ├── image_bed │ │ ├── __init__.py │ │ ├── base.py │ │ ├── lv_se.py │ │ ├── image_hosting_16.py │ │ ├── gwwc.py │ │ └── mjj_today.py │ └── testing │ │ ├── df_h.py │ │ ├── login.py │ │ ├── free_h.py │ │ ├── ip_sb.py │ │ ├── ip_info_io.py │ │ ├── dd.py │ │ ├── memory_check.py │ │ ├── yabs_basic_sys_info.py │ │ ├── yabs_disk.py │ │ ├── yabs_gb5.py │ │ ├── nws_global.py │ │ ├── yabs_default.py │ │ ├── bash_icu_gb5.py │ │ ├── ip_check_place.py │ │ ├── check_unlock_media.py │ │ ├── backtrace.py │ │ ├── next_trace.py │ │ ├── bash_icu_speed_test.py │ │ ├── hyper_speed.py │ │ ├── media_unlock_test.py │ │ ├── spiritlhls_ecs.py │ │ ├── spiritlhls_ecs_speed.py │ │ ├── spiritlhls_ecs_basic_sys_info.py │ │ ├── oneclickvirt_ecs.py │ │ ├── region_restriction_check.py │ │ ├── base.py │ │ └── __init__.py │ ├── utils │ ├── __init__.py │ └── fabric_utils.py │ ├── __init__.py │ ├── tasks │ ├── base.py │ ├── __init__.py │ └── testing.py │ ├── static │ ├── lightning.svg │ ├── correct.svg │ ├── error.svg │ ├── edit.svg │ ├── download.svg │ ├── edit-two.svg │ ├── new-computer.svg │ ├── time.svg │ ├── add.svg │ ├── logout.svg │ ├── redo.svg │ ├── mail.svg │ ├── replay-music.svg │ ├── data-sheet.svg │ ├── success.svg │ ├── upload.svg │ ├── doc-success.svg │ ├── pin.svg │ ├── delete.svg │ ├── analysis.svg │ ├── hourglass-full.svg │ ├── date-comes-back.svg │ ├── click.svg │ ├── people-delete-one.svg │ ├── calendar.svg │ ├── loading-three.svg │ ├── loading-one.svg │ ├── setting-two.svg │ ├── chick-hatched-from-egg-svgrepo-com.svg │ ├── chicken-svgrepo-com.svg │ └── chickens-chick-svgrepo-com.svg │ ├── models │ ├── __init__.py │ ├── image.py │ ├── user.py │ ├── vps.py │ ├── base.py │ └── testing.py │ ├── blueprints │ ├── __init__.py │ ├── main.py │ ├── user.py │ ├── vps.py │ └── testing.py │ ├── config.py │ ├── templates │ ├── utils.html │ ├── testing │ │ ├── list.html │ │ └── create.html │ ├── user │ │ ├── register.html │ │ ├── login.html │ │ └── setting.html │ ├── template │ │ └── bootstrap.html │ ├── vps │ │ ├── list.html │ │ ├── create.html │ │ └── detail.html │ ├── main │ │ └── index.html │ └── frame │ │ └── base.html │ ├── cli.py │ └── app.py ├── .dockerignore ├── debug ├── debug_redis.py ├── debug_worker.py ├── debug_postgresql.py └── debug_webserver.py ├── login-password-less.php ├── .env.example ├── nezha_agent_demo ├── README.md ├── main.py └── proto │ ├── nezha.proto │ ├── nezha_pb2.py │ └── nezha_pb2.pyi ├── Dockerfile ├── pyproject.toml ├── docker-compose.yml ├── docker-compose.all.yml ├── .gitignore └── README.md /src/jiduoduo/forms/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/services/image_bed/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !src 3 | !pyproject.toml 4 | -------------------------------------------------------------------------------- /src/jiduoduo/__init__.py: -------------------------------------------------------------------------------- 1 | from jiduoduo.app import app 2 | -------------------------------------------------------------------------------- /src/jiduoduo/tasks/base.py: -------------------------------------------------------------------------------- 1 | from flask_celeryext import FlaskCeleryExt 2 | 3 | flask_celery_ext = FlaskCeleryExt() 4 | 5 | celery = flask_celery_ext.celery 6 | -------------------------------------------------------------------------------- /debug/debug_redis.py: -------------------------------------------------------------------------------- 1 | import redis 2 | 3 | if __name__ == '__main__': 4 | host = f'redis' 5 | password = None 6 | client = redis.Redis(host=host, port=6379, db=0, password=password) 7 | print(client.info()) 8 | -------------------------------------------------------------------------------- /src/jiduoduo/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from jiduoduo.tasks.base import flask_celery_ext 4 | from jiduoduo.tasks.testing import run_testing 5 | 6 | 7 | def register_tasks(app: Flask): 8 | flask_celery_ext.init_app(app) 9 | -------------------------------------------------------------------------------- /login-password-less.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/jiduoduo/forms/base.py: -------------------------------------------------------------------------------- 1 | from flask import flash 2 | from flask_wtf import FlaskForm 3 | 4 | 5 | class BaseForm(FlaskForm): 6 | def flash_errors(self): 7 | for field, errors in self.errors.items(): 8 | for error in errors: 9 | flash(f"{getattr(self, field).label.text}: {error}", 'error') 10 | -------------------------------------------------------------------------------- /src/jiduoduo/static/correct.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/static/error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/static/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/models/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from jiduoduo.models.base import db 4 | from jiduoduo.models.testing import Testing 5 | from jiduoduo.models.testing import TestingState 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.models.user import User 8 | from jiduoduo.models.vps import VPS 9 | 10 | 11 | def register_models(app: Flask): 12 | db.init_app(app) 13 | -------------------------------------------------------------------------------- /debug/debug_worker.py: -------------------------------------------------------------------------------- 1 | from celery import Celery 2 | 3 | from jiduoduo.tasks import flask_celery_ext 4 | 5 | if __name__ == '__main__': 6 | celery: Celery = flask_celery_ext.celery 7 | 8 | celery.start([ 9 | 'worker', 10 | '-l', 'info', 11 | '-c', '2', 12 | # window系统调试运行需要解除 -P solo 的注释,否则可能无法执行任务 13 | # https://stackoverflow.com/questions/37255548/how-to-run-celery-on-windows 14 | # '-P', 'solo', 15 | ]) 16 | -------------------------------------------------------------------------------- /src/jiduoduo/blueprints/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from jiduoduo.blueprints import main 4 | from jiduoduo.blueprints import testing 5 | from jiduoduo.blueprints import user 6 | from jiduoduo.blueprints import vps 7 | 8 | 9 | def register_blueprints(app: Flask): 10 | app.register_blueprint(main.blueprint) 11 | app.register_blueprint(user.blueprint) 12 | app.register_blueprint(vps.blueprint) 13 | app.register_blueprint(testing.blueprint) 14 | -------------------------------------------------------------------------------- /src/jiduoduo/static/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/static/edit-two.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/static/new-computer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/static/time.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/static/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/jiduoduo/static/logout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/jiduoduo/static/redo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/static/mail.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debug/debug_postgresql.py: -------------------------------------------------------------------------------- 1 | # pip install -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com psycopg2-binary==2.9.9 2 | 3 | import psycopg2 4 | 5 | 6 | def main(): 7 | host = '' 8 | database = '' 9 | user = '' 10 | password = '' 11 | port = 5432 12 | conn = psycopg2.connect( 13 | database=database, 14 | host=host, 15 | user=user, 16 | password=password, 17 | port=port, 18 | ) 19 | 20 | print(f'server_version={conn.info.server_version}') 21 | 22 | 23 | if __name__ == "__main__": 24 | main() 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim-bookworm AS python311 2 | 3 | WORKDIR /jiduoduo_data 4 | 5 | COPY ./src /jiduoduo_data/src 6 | 7 | COPY ./pyproject.toml /jiduoduo_data/pyproject.toml 8 | 9 | RUN python -m pip install --upgrade build && python -m build 10 | 11 | 12 | FROM python:3.11-slim-bookworm 13 | 14 | WORKDIR /jiduoduo_data 15 | 16 | COPY --from=python311 /jiduoduo_data/dist /jiduoduo_data/dist 17 | 18 | RUN python -m pip install --no-cache-dir /jiduoduo_data/dist/*.whl && rm -rf /jiduoduo_data/dist 19 | 20 | 21 | # docker build -f Dockerfile -t jiduoduo . 22 | 23 | # docker run --rm -it jiduoduo bash 24 | -------------------------------------------------------------------------------- /src/jiduoduo/tasks/testing.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from celery import shared_task 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | @shared_task 9 | def run_testing(testing_id): 10 | logger.info(f'task run_testing start!!! testing_id={testing_id}') 11 | from jiduoduo.app import app 12 | from jiduoduo.services.testing import run_testing as _run_testing 13 | 14 | try: 15 | with app.app_context(): 16 | _run_testing(testing_id) 17 | 18 | except Exception as e: 19 | logger.error(f'{e}') 20 | 21 | logger.info(f'task run_testing done!!! testing_id={testing_id}') 22 | -------------------------------------------------------------------------------- /src/jiduoduo/static/replay-music.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/static/data-sheet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/jiduoduo/static/success.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/static/upload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/static/doc-success.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/static/pin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/static/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/jiduoduo/forms/testing.py: -------------------------------------------------------------------------------- 1 | from wtforms import SelectField 2 | from wtforms.validators import DataRequired 3 | 4 | from jiduoduo.forms.base import BaseForm 5 | from jiduoduo.models.testing import TESTING_TYPE_ZH 6 | 7 | 8 | class TestingForm(BaseForm): 9 | type = SelectField( 10 | '测试类型:', 11 | choices=[ 12 | (value, label) 13 | for value, label in TESTING_TYPE_ZH.items() 14 | ], 15 | validators=[ 16 | DataRequired(message='测试类型必须选'), 17 | ], 18 | ) 19 | 20 | vps_id = SelectField( 21 | 'VPS:', 22 | choices=[ 23 | ], 24 | validators=[ 25 | DataRequired(message='VPS必填'), 26 | ], 27 | render_kw={}, 28 | ) 29 | -------------------------------------------------------------------------------- /src/jiduoduo/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pydantic_settings import BaseSettings 4 | from pydantic_settings import SettingsConfigDict 5 | 6 | 7 | class Config(BaseSettings): 8 | model_config = SettingsConfigDict(extra='ignore', env_file=os.getenv('ENV_FILE', '.env')) 9 | 10 | LOGGING_LEVEL: str = 'INFO' 11 | LOGGING_FORMAT: str = '[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s' 12 | 13 | SECRET_KEY: str = 'jiduoduo' 14 | PERMANENT_SESSION_LIFETIME_MINUTES: int = 10 15 | 16 | SQLALCHEMY_DATABASE_URI: str = 'sqlite:///db.sqlite3' 17 | SQLALCHEMY_ECHO: bool = False 18 | 19 | CELERY_ALWAYS_EAGER: bool = False 20 | CELERY_BROKER_URL: str = 'redis://redis:6379/0' 21 | CELERY_RESULT_BACKEND: str = 'redis://redis:6379/0' 22 | 23 | 24 | config = Config() 25 | -------------------------------------------------------------------------------- /src/jiduoduo/static/analysis.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/templates/utils.html: -------------------------------------------------------------------------------- 1 | {% macro render_testing_list_item(testing) %} 2 |
  • 3 |
    4 |
    7 | {{ testing.display_state_emoji }} 8 |
    9 |
    10 | 11 |
    12 | 17 |
    18 | {{ testing.vps_name }} 19 |
    20 |
    21 |
  • 22 | {% endmacro %} -------------------------------------------------------------------------------- /src/jiduoduo/forms/user.py: -------------------------------------------------------------------------------- 1 | from wtforms import BooleanField 2 | from wtforms import StringField 3 | from wtforms import SubmitField 4 | from wtforms.validators import DataRequired 5 | from wtforms.validators import Email 6 | 7 | from jiduoduo.forms.base import BaseForm 8 | 9 | 10 | # https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-iii-web-forms 11 | 12 | class LoginForm(BaseForm): 13 | email = StringField('邮箱:', validators=[DataRequired(), Email()]) 14 | password = StringField('密码:', validators=[DataRequired()]) 15 | remember_me = BooleanField('保持登录') 16 | submit = SubmitField('登录') 17 | 18 | 19 | class RegisterForm(BaseForm): 20 | email = StringField('邮箱:', validators=[DataRequired(), Email()]) 21 | password = StringField('密码:', validators=[DataRequired()]) 22 | submit = SubmitField('注册') 23 | -------------------------------------------------------------------------------- /src/jiduoduo/static/hourglass-full.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/static/date-comes-back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/jiduoduo/static/click.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import click 5 | 6 | 7 | @click.group() 8 | @click.option('--dry_run', type=bool, default=True) 9 | @click.pass_context 10 | def cli(ctx, dry_run: bool): 11 | ctx.ensure_object(dict) 12 | ctx.obj['dry_run'] = dry_run 13 | 14 | click.echo(f'os.getcwd()={os.getcwd()}') 15 | click.echo(f'sys.executable={sys.executable}') 16 | click.echo(f'dry_run={dry_run}') 17 | 18 | 19 | @cli.command() 20 | @click.argument('name', required=True) 21 | @click.pass_context 22 | def test(ctx, name: str): 23 | from jiduoduo.app import app 24 | from jiduoduo.models import VPS 25 | with app.app_context(): 26 | vps = VPS.get_by_name(name) 27 | if not vps: 28 | click.echo(f'No such VPS: {name}') 29 | return 30 | vps.run('echo test') 31 | 32 | 33 | if __name__ == '__main__': 34 | cli() 35 | -------------------------------------------------------------------------------- /src/jiduoduo/templates/testing/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'frame/base.html' %} 2 | {% import 'utils.html' as utils %} 3 | 4 | 5 | {% block container %} 6 |
    7 | 22 |
    23 | {% endblock %} 24 | 25 | 26 | {% block scripts %} 27 | {{ super() }} 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /src/jiduoduo/static/people-delete-one.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/templates/user/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'frame/base.html' %} 2 | 3 | 4 | {% block container %} 5 |
    6 |

    用户注册

    7 |
    8 |
    9 |
    10 | {{ form.hidden_tag() }} 11 |
    12 | 13 |
    14 | {{ form.email.label(class="form-label") }} 15 | {{ form.email(class="form-control") }} 16 |
    17 | 18 |
    19 | {{ form.password.label(class="form-label") }} 20 | {{ form.password(class="form-control") }} 21 |
    22 | 23 |
    24 | {{ form.submit(class='btn btn-primary btn-block') }} 25 |
    26 | 27 |
    28 |
    29 |
    30 | {% endblock %} 31 | 32 | 33 | {% block scripts %} 34 | {{ super() }} 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /src/jiduoduo/templates/testing/create.html: -------------------------------------------------------------------------------- 1 | {% extends 'frame/base.html' %} 2 | 3 | 4 | {% block container %} 5 |
    6 |
    7 |
    8 |
    9 | {{ form.hidden_tag() }} 10 |
    11 | 12 |
    13 | {{ form.type.label(class="form-label") }} 14 | {{ form.type(required='', class="form-control") }} 15 |
    16 | 17 |
    18 | {{ form.vps_id.label(class="form-label") }} 19 | {{ form.vps_id(class="form-control") }} 20 |
    21 | 22 |
    23 | 26 |
    27 | 28 |
    29 |
    30 |
    31 | {% endblock %} 32 | 33 | 34 | {% block scripts %} 35 | {{ super() }} 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /src/jiduoduo/static/calendar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/utils/fabric_utils.py: -------------------------------------------------------------------------------- 1 | import io 2 | from typing import Callable 3 | 4 | from fabric2 import Connection 5 | from invoke import UnexpectedExit 6 | 7 | 8 | def has_command(conn: Connection, command: str) -> bool: 9 | try: 10 | conn.run(command) 11 | return True 12 | except UnexpectedExit as e: 13 | return False 14 | 15 | 16 | def has_curl(conn: Connection) -> bool: 17 | return has_command(conn=conn, command='curl') 18 | 19 | 20 | def has_wget(conn: Connection) -> bool: 21 | return has_command(conn=conn, command='wget') 22 | 23 | 24 | def has_sudo(conn: Connection) -> bool: 25 | return has_command(conn=conn, command='sudo') 26 | 27 | 28 | class StreamFlusher: 29 | def __init__(self, flush_callback: Callable[[str], None]): 30 | self.flush_callback = flush_callback 31 | self.buffer = io.StringIO() 32 | 33 | def write(self, message): 34 | self.buffer.write(message) 35 | 36 | def flush(self): 37 | self.flush_callback(self.buffer.getvalue()) 38 | -------------------------------------------------------------------------------- /src/jiduoduo/blueprints/main.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from flask import render_template 3 | from flask_login import current_user 4 | 5 | from jiduoduo.models import Testing 6 | from jiduoduo.models import VPS 7 | 8 | blueprint = Blueprint('main', __name__, url_prefix='/') 9 | 10 | 11 | @blueprint.get('/health') 12 | def health(): 13 | return 'ok' 14 | 15 | 16 | @blueprint.get('/') 17 | def index(): 18 | testing_list = Testing.get_list( 19 | Testing.is_public == True, 20 | order_by=[Testing.updated_at.desc()], 21 | limit=20, 22 | ) 23 | if current_user.is_authenticated: 24 | vps_count = VPS.count( 25 | VPS.user_id == current_user.id, 26 | ) 27 | testing_count = Testing.count( 28 | Testing.user_id == current_user.id, 29 | ) 30 | else: 31 | vps_count = 0 32 | testing_count = 0 33 | 34 | return render_template( 35 | 'main/index.html', 36 | vps_count=vps_count, 37 | testing_count=testing_count, 38 | testing_list=testing_list 39 | ) 40 | -------------------------------------------------------------------------------- /src/jiduoduo/templates/user/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'frame/base.html' %} 2 | 3 | 4 | {% block container %} 5 |
    6 |

    用户登录

    7 |
    8 |
    9 |
    10 | {{ form.hidden_tag() }} 11 |
    12 | 13 |
    14 | {{ form.email.label(class="form-label") }} 15 | {{ form.email(class="form-control") }} 16 |
    请放心,我们永远不会泄露您的任何信息
    17 |
    18 | 19 |
    20 | {{ form.password.label(class="form-label") }} 21 | {{ form.password(class="form-control") }} 22 |
    23 | 24 |
    25 | {{ form.remember_me(class="form-check-input", type="checkbox") }} 26 | {{ form.remember_me.label }} 27 |
    28 | 29 |
    30 | {{ form.submit(class='btn btn-primary btn-block') }} 31 |
    32 | 33 |
    34 |
    35 |
    36 | {% endblock %} 37 | 38 | 39 | {% block scripts %} 40 | {{ super() }} 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /src/jiduoduo/static/loading-three.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/df_h.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | 11 | 12 | class DFHTestingParams(TestingParams): 13 | timeout: int = Field(10) # seconds 14 | 15 | 16 | class DFHTestingResult(TestingResult): 17 | pass 18 | 19 | 20 | class DFHTestingService(TestingService): 21 | testing_type: TestingType = TestingType.DF_H 22 | testing_params_cls: type[DFHTestingParams] = DFHTestingParams 23 | testing_result_cls: type[DFHTestingResult] = DFHTestingResult 24 | 25 | def run_on_vps( 26 | self, 27 | vps: VPS, 28 | params: DFHTestingParams, 29 | flush_callback: Callable[[str], None] | None = None, 30 | ) -> DFHTestingResult: 31 | command = 'df -h' 32 | 33 | run_result = vps.run( 34 | command=command, 35 | timeout=params.timeout, 36 | warn=True, 37 | ) 38 | 39 | return DFHTestingResult(result=str(run_result)) 40 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/login.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | 11 | 12 | class LoginTestingParams(TestingParams): 13 | timeout: int = Field(6) # seconds 14 | 15 | 16 | class LoginTestingResult(TestingResult): 17 | pass 18 | 19 | 20 | class LoginTestingService(TestingService): 21 | testing_type: TestingType = TestingType.LOGIN 22 | testing_params_cls: type[LoginTestingParams] = LoginTestingParams 23 | testing_result_cls: type[LoginTestingResult] = LoginTestingResult 24 | 25 | def run_on_vps( 26 | self, 27 | vps: VPS, 28 | params: LoginTestingParams, 29 | flush_callback: Callable[[str], None] | None = None, 30 | ) -> LoginTestingResult: 31 | command = 'echo "登录成功!"' 32 | 33 | run_result = vps.run( 34 | command, 35 | timeout=params.timeout, 36 | warn=True, 37 | ) 38 | 39 | return LoginTestingResult(result=str(run_result)) 40 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/free_h.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | 11 | 12 | class FreeHTestingParams(TestingParams): 13 | timeout: int = Field(10) # seconds 14 | 15 | 16 | class FreeHTestingResult(TestingResult): 17 | pass 18 | 19 | 20 | class FreeHTestingService(TestingService): 21 | testing_type: TestingType = TestingType.FREE_H 22 | testing_params_cls: type[FreeHTestingParams] = FreeHTestingParams 23 | testing_result_cls: type[FreeHTestingResult] = FreeHTestingResult 24 | 25 | def run_on_vps( 26 | self, 27 | vps: VPS, 28 | params: FreeHTestingParams, 29 | flush_callback: Callable[[str], None] | None = None, 30 | ) -> FreeHTestingResult: 31 | command = 'free -h' 32 | 33 | run_result = vps.run( 34 | command=command, 35 | timeout=params.timeout, 36 | warn=True, 37 | ) 38 | 39 | return FreeHTestingResult(result=str(run_result)) 40 | -------------------------------------------------------------------------------- /src/jiduoduo/templates/template/bootstrap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block html %} 4 | 5 | 6 | 7 | 8 | 9 | {% block google_tag %} 10 | {% endblock %} 11 | 12 | 13 | {% block title %}{% endblock %} 14 | 15 | {% block styles %} 16 | 17 | {% endblock styles %} 18 | 19 | 20 | 21 | {% block body %} 22 | {% block header %} 23 | {% endblock header %} 24 | 25 | {% block content %} 26 | {% endblock content %} 27 | 28 | {% block footer %} 29 | {% endblock %} 30 | 31 | {% block scripts %} 32 | 33 | 34 | {% endblock scripts %} 35 | {% endblock body %} 36 | 37 | {% endblock html %} 38 | 39 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/ip_sb.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | 11 | 12 | class IPSBTestingParams(TestingParams): 13 | timeout: int = Field(10) # seconds 14 | 15 | 16 | class IPSBTestingResult(TestingResult): 17 | pass 18 | 19 | 20 | class IPSBTestingService(TestingService): 21 | testing_type: TestingType = TestingType.IP_SB 22 | testing_params_cls: type[IPSBTestingParams] = IPSBTestingParams 23 | testing_result_cls: type[IPSBTestingResult] = IPSBTestingResult 24 | 25 | def run_on_vps( 26 | self, 27 | vps: VPS, 28 | params: IPSBTestingParams, 29 | flush_callback: Callable[[str], None] | None = None, 30 | ) -> IPSBTestingResult: 31 | command = 'echo "IPv4"; curl -4 ip.sb; echo; echo "IPv6:"; curl -6 ip.sb' 32 | 33 | run_result = vps.run( 34 | command=command, 35 | timeout=params.timeout, 36 | warn=True, 37 | ) 38 | 39 | return IPSBTestingResult(result=str(run_result)) 40 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ['setuptools'] 3 | build-backend = 'setuptools.build_meta' 4 | 5 | [project] 6 | name = 'jiduoduo' 7 | version = '0.0.1' 8 | requires-python = '>=3.11,<3.12' 9 | dependencies = [ 10 | 'click==8.1.7', 11 | 'Werkzeug==3.0.3', 12 | 'Jinja2==3.1.4', 13 | 'Flask==3.0.3', 14 | 'psycopg2-binary==2.9.9', 15 | 'SQLAlchemy==2.0.31', 16 | 'Flask-SQLAlchemy==3.1.1', 17 | 'Flask-Login==0.6.3', 18 | 'email_validator==2.2.0', 19 | 'WTForms==3.1.2', 20 | 'Flask-WTF==1.2.1', 21 | 'gevent==24.2.1', 22 | 'gunicorn==22.0.0', 23 | 'invoke==2.2.0', 24 | 'paramiko==3.4.0', 25 | 'fabric2==3.2.2', 26 | 'sshtunnel==0.4.0', 27 | 'redis==5.0.7', 28 | 'celery==5.4.0', 29 | 'Flask-CeleryExt==0.5.0', 30 | 'pendulum==3.0.0', 31 | 'pydantic==2.8.2', 32 | 'pydantic-settings==2.3.4', 33 | 'filelock==3.15.4', 34 | 'pyte==0.8.2', 35 | 'curl_cffi==0.7.1', 36 | 'requests==2.32.3', 37 | 'grpcio==1.65.1', 38 | 'grpcio-tools==1.65.1', 39 | ] 40 | 41 | [project.scripts] 42 | jiduoduo = 'jiduoduo.cli:cli' 43 | 44 | [tool.setuptools.packages.find] # https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html 45 | where = ['src'] 46 | include = ['*'] 47 | 48 | [tool.setuptools.package-data] 49 | '*' = ['*.*'] 50 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/ip_info_io.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | 11 | 12 | class IPInfoIOTestingParams(TestingParams): 13 | timeout: int = Field(10) # seconds 14 | 15 | 16 | class IPInfoIOTestingResult(TestingResult): 17 | pass 18 | 19 | 20 | class IPInfoIOTestingService(TestingService): 21 | testing_type: TestingType = TestingType.IP_INFO_IO 22 | testing_params_cls: type[IPInfoIOTestingParams] = IPInfoIOTestingParams 23 | testing_result_cls: type[IPInfoIOTestingResult] = IPInfoIOTestingResult 24 | 25 | def run_on_vps( 26 | self, 27 | vps: VPS, 28 | params: IPInfoIOTestingParams, 29 | flush_callback: Callable[[str], None] | None = None, 30 | ) -> IPInfoIOTestingResult: 31 | command = 'curl https://ipinfo.io/' 32 | 33 | run_result = vps.run( 34 | command, 35 | timeout=params.timeout, 36 | warn=True, 37 | ) 38 | 39 | return IPInfoIOTestingResult(result=str(run_result)) 40 | -------------------------------------------------------------------------------- /src/jiduoduo/templates/vps/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'frame/base.html' %} 2 | 3 | 4 | {% block container %} 5 |
    6 | 9 | 10 |
    11 | 32 |
    33 |
    34 | {% endblock %} 35 | 36 | 37 | {% block scripts %} 38 | {{ super() }} 39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /src/jiduoduo/models/image.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import uuid 3 | from enum import StrEnum 4 | 5 | from sqlalchemy import String 6 | from sqlalchemy import Text 7 | from sqlalchemy.orm import Mapped 8 | from sqlalchemy.orm import mapped_column 9 | 10 | from jiduoduo.models.base import BaseModel 11 | from jiduoduo.models.base import UUID 12 | from jiduoduo.models.base import UserMixin 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class ImageState(StrEnum): 18 | CREATED = 'created' 19 | UPLOADING = 'uploading' 20 | SUCCESS = 'success' 21 | FAILED = 'failed' 22 | 23 | 24 | class Image(BaseModel, UserMixin): 25 | user_id: Mapped[uuid.UUID] = mapped_column( 26 | UUID(), 27 | nullable=False, 28 | ) 29 | 30 | _type: Mapped[str] = mapped_column( 31 | 'type', 32 | String(32), 33 | nullable=False, 34 | ) 35 | 36 | _state: Mapped[str] = mapped_column( 37 | 'state', 38 | String(32), 39 | nullable=False, 40 | default=ImageState.CREATED, 41 | server_default=ImageState.CREATED, 42 | ) 43 | 44 | url: Mapped[str] = mapped_column( 45 | Text, 46 | nullable=False, 47 | ) 48 | 49 | result: Mapped[str] = mapped_column( 50 | Text, 51 | nullable=False, 52 | default='', 53 | ) 54 | -------------------------------------------------------------------------------- /src/jiduoduo/static/loading-one.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/jiduoduo/static/setting-two.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/dd.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | 11 | 12 | class DDTestingParams(TestingParams): 13 | # 有的NAT鸡速度巨慢,需要227秒以上,速度1.2MB/s 14 | # 所以延长超时时间 15 | timeout: int = Field(60 * 5) # seconds 16 | 17 | 18 | class DDTestingResult(TestingResult): 19 | pass 20 | 21 | 22 | class DDTestingService(TestingService): 23 | testing_type: TestingType = TestingType.DD 24 | testing_params_cls: type[DDTestingParams] = DDTestingParams 25 | testing_result_cls: type[DDTestingResult] = DDTestingResult 26 | 27 | def run_on_vps( 28 | self, 29 | vps: VPS, 30 | params: DDTestingParams, 31 | flush_callback: Callable[[str], None] | None = None, 32 | ) -> DDTestingResult: 33 | # https://pickstar.today/2023/07/%E6%96%B0%E8%B4%ADvps%E5%B8%B8%E7%94%A8%E8%AF%84%E6%B5%8B%E8%84%9A%E6%9C%AC%E9%9B%86%E5%90%88/ 34 | # 参考 硬盘专项测试 35 | 36 | command = 'dd if=/dev/zero of=256 bs=64K count=4K oflag=dsync' 37 | 38 | run_result = vps.run( 39 | command, 40 | timeout=params.timeout, 41 | warn=True, 42 | ) 43 | 44 | return DDTestingResult(result=str(run_result)) 45 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/memory_check.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | 11 | 12 | class MemoryCheckTestingParams(TestingParams): 13 | timeout: int = Field(10) # seconds 14 | 15 | 16 | class MemoryCheckTestingResult(TestingResult): 17 | pass 18 | 19 | 20 | class MemoryCheckTestingService(TestingService): 21 | testing_type: TestingType = TestingType.MEMORY_CHECK 22 | testing_params_cls: type[MemoryCheckTestingParams] = MemoryCheckTestingParams 23 | testing_result_cls: type[MemoryCheckTestingResult] = MemoryCheckTestingResult 24 | 25 | def run_on_vps( 26 | self, 27 | vps: VPS, 28 | params: MemoryCheckTestingParams, 29 | flush_callback: Callable[[str], None] | None = None, 30 | ) -> MemoryCheckTestingResult: 31 | # https://github.com/uselibrary/memoryCheck 32 | 33 | command = 'curl https://raw.githubusercontent.com/uselibrary/memoryCheck/main/memoryCheck.sh | bash' 34 | 35 | run_result = vps.run( 36 | command, 37 | timeout=params.timeout, 38 | warn=True, 39 | pty=True, 40 | ) 41 | 42 | return MemoryCheckTestingResult(result=str(run_result)) 43 | -------------------------------------------------------------------------------- /src/jiduoduo/templates/user/setting.html: -------------------------------------------------------------------------------- 1 | {% extends 'frame/base.html' %} 2 | 3 | 4 | {% block container %} 5 |
    6 |

    用户中心

    7 |
    8 | 9 | 41 | 42 |
    43 |
    44 |
    45 | 46 |
    47 |
    48 | {% endblock %} 49 | 50 | 51 | {% block scripts %} 52 | {{ super() }} 53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/yabs_basic_sys_info.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | 11 | 12 | class YABSBasicSysInfoTestingParams(TestingParams): 13 | timeout: int = Field(60) # seconds 14 | 15 | 16 | class YABSBasicSysInfoTestingResult(TestingResult): 17 | pass 18 | 19 | 20 | class YABSBasicSysInfoTestingService(TestingService): 21 | testing_type: TestingType = TestingType.YABS_BASIC_SYS_INFO 22 | testing_params_cls: type[YABSBasicSysInfoTestingParams] = YABSBasicSysInfoTestingParams 23 | testing_result_cls: type[YABSBasicSysInfoTestingResult] = YABSBasicSysInfoTestingResult 24 | 25 | def run_on_vps( 26 | self, 27 | vps: VPS, 28 | params: YABSBasicSysInfoTestingParams, 29 | flush_callback: Callable[[str], None] | None = None, 30 | ) -> YABSBasicSysInfoTestingResult: 31 | # https://github.com/masonr/yet-another-bench-script 32 | 33 | command = 'curl -sL yabs.sh | bash -s -- -dign' 34 | 35 | run_result = vps.run( 36 | command, 37 | timeout=params.timeout, 38 | warn=True, 39 | pty=True, 40 | ) 41 | 42 | return YABSBasicSysInfoTestingResult(result=str(run_result)) 43 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/yabs_disk.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | from jiduoduo.utils.fabric_utils import StreamFlusher 11 | 12 | 13 | class YABSDiskTestingParams(TestingParams): 14 | timeout: int = Field(60 * 10) # seconds 15 | 16 | 17 | class YABSDiskTestingResult(TestingResult): 18 | pass 19 | 20 | 21 | class YABSDiskTestingService(TestingService): 22 | testing_type: TestingType = TestingType.YABS_DISK 23 | testing_params_cls: type[YABSDiskTestingParams] = YABSDiskTestingParams 24 | testing_result_cls: type[YABSDiskTestingResult] = YABSDiskTestingResult 25 | 26 | def run_on_vps( 27 | self, 28 | vps: VPS, 29 | params: YABSDiskTestingParams, 30 | flush_callback: Callable[[str], None] | None = None, 31 | ) -> YABSDiskTestingResult: 32 | # https://github.com/masonr/yet-another-bench-script 33 | 34 | command = 'curl -sL yabs.sh | bash -s -- -ign' 35 | 36 | run_result = vps.run( 37 | command, 38 | timeout=params.timeout, 39 | warn=True, 40 | pty=True, 41 | out_stream=StreamFlusher(flush_callback=flush_callback), 42 | ) 43 | 44 | return YABSDiskTestingResult(result=str(run_result)) 45 | -------------------------------------------------------------------------------- /src/jiduoduo/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import pathlib 4 | import secrets 5 | import uuid 6 | 7 | from filelock import FileLock 8 | from flask import Flask 9 | from flask_login import LoginManager 10 | 11 | from jiduoduo.blueprints import register_blueprints 12 | from jiduoduo.config import config 13 | from jiduoduo.models import User 14 | from jiduoduo.models import db 15 | from jiduoduo.models import register_models 16 | from jiduoduo.tasks import register_tasks 17 | 18 | logging.basicConfig(format=config.LOGGING_FORMAT, level=config.LOGGING_LEVEL) 19 | logger = logging.getLogger(__name__) 20 | 21 | app = Flask(__name__, instance_path=os.getcwd()) 22 | app.config.update(config.model_dump()) 23 | 24 | login_manager = LoginManager(app) 25 | login_manager.login_view = 'user.login' 26 | login_manager.login_message = '访问该页面需要先登录' 27 | 28 | 29 | @login_manager.user_loader 30 | def load_user(user_id: uuid.UUID): 31 | return User.get(user_id) 32 | 33 | 34 | register_models(app) 35 | register_blueprints(app) 36 | register_tasks(app) 37 | 38 | 39 | @login_manager.user_loader 40 | def load_user(user_id: int): 41 | return db.session.get(User, user_id) 42 | 43 | 44 | with app.app_context(): 45 | db.create_all() 46 | 47 | env_file_path = pathlib.Path('.env') 48 | if not env_file_path.exists(): 49 | with FileLock(".env.lock") as lock: 50 | if not env_file_path.exists(): 51 | with open('.env', 'w+') as f: 52 | secret_key = secrets.token_hex(16) 53 | f.write(f'SECRET_KEY={secret_key}') 54 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/yabs_gb5.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | from jiduoduo.utils.fabric_utils import StreamFlusher 11 | 12 | 13 | class YABSGB5TestingParams(TestingParams): 14 | timeout: int = Field(60 * 10 * 2) # seconds 15 | 16 | 17 | class YABSGB5TestingResult(TestingResult): 18 | pass 19 | 20 | 21 | class YABSGB5TestingService(TestingService): 22 | testing_type: TestingType = TestingType.YABS_GB5 23 | testing_params_cls: type[YABSGB5TestingParams] = YABSGB5TestingParams 24 | testing_result_cls: type[YABSGB5TestingResult] = YABSGB5TestingResult 25 | 26 | def run_on_vps( 27 | self, 28 | vps: VPS, 29 | params: YABSGB5TestingParams, 30 | flush_callback: Callable[[str], None] | None = None, 31 | ) -> YABSGB5TestingResult: 32 | # https://github.com/masonr/yet-another-bench-script 33 | 34 | command = 'curl -sL yabs.sh | bash -s -- -i -5' 35 | 36 | run_result = vps.run( 37 | command, 38 | timeout=params.timeout, 39 | hide=True, 40 | warn=True, 41 | pty=True, 42 | out_stream=StreamFlusher(flush_callback=flush_callback), 43 | ) 44 | 45 | return YABSGB5TestingResult(result=str(run_result)) 46 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/nws_global.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | from jiduoduo.utils.fabric_utils import StreamFlusher 11 | 12 | 13 | class NWSGlobalDefaultTestingParams(TestingParams): 14 | timeout: int = Field(60 * 10 * 2) # seconds 15 | 16 | 17 | class NWSGlobalDefaultTestingResult(TestingResult): 18 | pass 19 | 20 | 21 | class NWSGlobalDefaultTestingService(TestingService): 22 | testing_type: TestingType = TestingType.NWS_GLOBAL 23 | testing_params_cls: type[NWSGlobalDefaultTestingParams] = NWSGlobalDefaultTestingParams 24 | testing_result_cls: type[NWSGlobalDefaultTestingResult] = NWSGlobalDefaultTestingResult 25 | 26 | def run_on_vps( 27 | self, 28 | vps: VPS, 29 | params: NWSGlobalDefaultTestingParams, 30 | flush_callback: Callable[[str], None] | None = None, 31 | ) -> NWSGlobalDefaultTestingResult: 32 | # 暂未开源 33 | 34 | command = 'curl -sL nws.sh | bash' 35 | 36 | run_result = vps.run( 37 | command, 38 | timeout=params.timeout, 39 | warn=True, 40 | pty=True, 41 | out_stream=StreamFlusher(flush_callback=flush_callback), 42 | ) 43 | 44 | return NWSGlobalDefaultTestingResult(result=str(run_result)) 45 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/yabs_default.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | from jiduoduo.utils.fabric_utils import StreamFlusher 11 | 12 | 13 | class YABSDefaultTestingParams(TestingParams): 14 | timeout: int = Field(60 * 10 * 2) # seconds 15 | 16 | 17 | class YABSDefaultTestingResult(TestingResult): 18 | pass 19 | 20 | 21 | class YABSDefaultTestingService(TestingService): 22 | testing_type: TestingType = TestingType.YABS_DEFAULT 23 | testing_params_cls: type[YABSDefaultTestingParams] = YABSDefaultTestingParams 24 | testing_result_cls: type[YABSDefaultTestingResult] = YABSDefaultTestingResult 25 | 26 | def run_on_vps( 27 | self, 28 | vps: VPS, 29 | params: YABSDefaultTestingParams, 30 | flush_callback: Callable[[str], None] | None = None, 31 | ) -> YABSDefaultTestingResult: 32 | # https://github.com/masonr/yet-another-bench-script 33 | 34 | command = 'curl -sL yabs.sh | bash' 35 | 36 | run_result = vps.run( 37 | command, 38 | timeout=params.timeout, 39 | warn=True, 40 | pty=True, 41 | out_stream=StreamFlusher(flush_callback=flush_callback), 42 | ) 43 | 44 | return YABSDefaultTestingResult(result=str(run_result)) 45 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/bash_icu_gb5.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | from jiduoduo.utils.fabric_utils import StreamFlusher 11 | 12 | 13 | class BashIcuGB5TestingParams(TestingParams): 14 | timeout: int = Field(60 * 10 * 2) # seconds 15 | 16 | 17 | class BashIcuGB5TestingResult(TestingResult): 18 | pass 19 | 20 | 21 | class BashIcuGB5TestingService(TestingService): 22 | testing_type: TestingType = TestingType.BASH_ICU_GB5 23 | testing_params_cls: type[BashIcuGB5TestingParams] = BashIcuGB5TestingParams 24 | testing_result_cls: type[BashIcuGB5TestingResult] = BashIcuGB5TestingResult 25 | 26 | def run_on_vps( 27 | self, 28 | vps: VPS, 29 | params: BashIcuGB5TestingParams, 30 | flush_callback: Callable[[str], None] | None = None, 31 | ) -> BashIcuGB5TestingResult: 32 | # https://github.com/i-abc/gb5 33 | 34 | command = 'bash <(curl -sL bash.icu/gb5)' 35 | 36 | run_result = vps.run( 37 | command, 38 | timeout=params.timeout, 39 | hide=True, 40 | warn=True, 41 | pty=True, 42 | out_stream=StreamFlusher(flush_callback=flush_callback), 43 | ) 44 | 45 | return BashIcuGB5TestingResult(result=str(run_result)) 46 | -------------------------------------------------------------------------------- /nezha_agent_demo/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import certifi 3 | import grpc 4 | 5 | from proto import nezha_pb2 6 | from proto import nezha_pb2_grpc 7 | 8 | 9 | class AuthGateway(grpc.AuthMetadataPlugin): 10 | def __init__(self, token): 11 | self.token = token 12 | 13 | def __call__(self, context, callback): 14 | metadata = (('client_secret', self.token),) 15 | callback(metadata, None) 16 | 17 | 18 | def load_credentials(): 19 | with open(certifi.where(), 'rb') as f: 20 | return f.read() 21 | 22 | 23 | def main(): 24 | # 未接入CDN的面板服务器域名/IP 25 | address = os.getenv('NEZHA_SERVER_ADDRESS') 26 | 27 | # 密钥 28 | secret = os.getenv('NEZHA_SERVER_SECRET') 29 | 30 | channel_credentials = grpc.ssl_channel_credentials(load_credentials()) 31 | 32 | call_credentials = grpc.metadata_call_credentials(AuthGateway(token=secret)) 33 | 34 | composite_credentials = grpc.composite_channel_credentials(channel_credentials, call_credentials) 35 | 36 | channel = grpc.secure_channel(address, composite_credentials) 37 | 38 | stub = nezha_pb2_grpc.NezhaServiceStub(channel) 39 | 40 | host = nezha_pb2.Host() 41 | host.platform = 'platform' 42 | host.platform_version = 'platform_version' 43 | host.cpu.append('cpu') 44 | host.mem_total = 1 45 | host.disk_total = 2 46 | host.arch = 'arch' 47 | host.swap_total = 3 48 | host.boot_time = 4 49 | host.version = 'version' 50 | 51 | response = stub.ReportSystemInfo(host) 52 | print(response) 53 | 54 | channel.close() 55 | 56 | 57 | if __name__ == '__main__': 58 | main() 59 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/ip_check_place.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | from jiduoduo.utils.fabric_utils import StreamFlusher 11 | 12 | 13 | class IPCheckPlaceTestingParams(TestingParams): 14 | timeout: int = Field(60 * 10) # seconds 15 | 16 | 17 | class IPCheckPlaceTestingResult(TestingResult): 18 | pass 19 | 20 | 21 | class IPCheckPlaceTestingService(TestingService): 22 | testing_type: TestingType = TestingType.IP_CHECK_PLACE 23 | testing_params_cls: type[IPCheckPlaceTestingParams] = IPCheckPlaceTestingParams 24 | testing_result_cls: type[IPCheckPlaceTestingResult] = IPCheckPlaceTestingResult 25 | 26 | def run_on_vps( 27 | self, 28 | vps: VPS, 29 | params: IPCheckPlaceTestingParams, 30 | flush_callback: Callable[[str], None] | None = None, 31 | ) -> IPCheckPlaceTestingResult: 32 | # https://github.com/xykt/IPQuality 33 | 34 | command = 'bash <(curl -sL IP.Check.Place)' 35 | 36 | run_result = vps.run( 37 | command, 38 | timeout=params.timeout, 39 | hide=True, 40 | warn=True, 41 | pty=True, 42 | out_stream=StreamFlusher(flush_callback=flush_callback), 43 | ) 44 | 45 | return IPCheckPlaceTestingResult(result=str(run_result)) 46 | -------------------------------------------------------------------------------- /src/jiduoduo/templates/vps/create.html: -------------------------------------------------------------------------------- 1 | {% extends 'frame/base.html' %} 2 | 3 | 4 | {% block container %} 5 |
    6 |
    7 |
    8 |
    9 | {{ form.hidden_tag() }} 10 |
    11 | 12 |
    13 | {{ form.name.label(class="form-label") }} 14 | {{ form.name(class="form-control") }} 15 |
    16 | 17 |
    18 | {{ form.host.label(class="form-label") }} 19 | {{ form.host(required='', class="form-control") }} 20 |
    21 | 22 |
    23 | {{ form.port.label(class="form-label") }} 24 | {{ form.port(class="form-control") }} 25 |
    26 | 27 |
    28 | {{ form.user.label(class="form-label") }} 29 | {{ form.user(class="form-control") }} 30 |
    31 | 32 |
    33 | {{ form.password.label(class="form-label") }} 34 | {{ form.password(class="form-control") }} 35 |
    36 | 37 |
    38 | {{ form.identify_key.label(class="form-label") }} 39 | {{ form.identify_key(class="form-control") }} 40 |
    41 | 42 |
    43 | 46 |
    47 | 48 |
    49 |
    50 |
    51 | {% endblock %} 52 | 53 | 54 | {% block scripts %} 55 | {{ super() }} 56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/check_unlock_media.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | from jiduoduo.utils.fabric_utils import StreamFlusher 11 | 12 | 13 | class CheckUnlockMediaTestingParams(TestingParams): 14 | timeout: int = Field(60 * 5) # seconds 15 | 16 | 17 | class CheckUnlockMediaTestingResult(TestingResult): 18 | pass 19 | 20 | 21 | class CheckUnlockMediaTestingService(TestingService): 22 | testing_type: TestingType = TestingType.CHECK_UNLOCK_MEDIA 23 | testing_params_cls: type[CheckUnlockMediaTestingParams] = CheckUnlockMediaTestingParams 24 | testing_result_cls: type[CheckUnlockMediaTestingResult] = CheckUnlockMediaTestingResult 25 | 26 | def run_on_vps( 27 | self, 28 | vps: VPS, 29 | params: CheckUnlockMediaTestingParams, 30 | flush_callback: Callable[[str], None] | None = None, 31 | ) -> CheckUnlockMediaTestingResult: 32 | # https://github.com/lmc999/RegionRestrictionCheck 33 | 34 | command = "bash <(curl -L -s check.unlock.media) <<< '4'" 35 | 36 | run_result = vps.run( 37 | command, 38 | timeout=params.timeout, 39 | hide=True, 40 | warn=True, 41 | pty=True, 42 | out_stream=StreamFlusher(flush_callback=flush_callback), 43 | ) 44 | 45 | return CheckUnlockMediaTestingResult(result=str(run_result)) 46 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/backtrace.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from pydantic import Field 4 | 5 | from jiduoduo.models import VPS 6 | from jiduoduo.models.testing import TestingType 7 | from jiduoduo.services.testing.base import TestingParams 8 | from jiduoduo.services.testing.base import TestingResult 9 | from jiduoduo.services.testing.base import TestingService 10 | 11 | 12 | class BacktraceTestingParams(TestingParams): 13 | timeout: int = Field(60) # seconds 14 | 15 | 16 | class BacktraceTestingResult(TestingResult): 17 | pass 18 | 19 | 20 | class BacktraceTestingService(TestingService): 21 | testing_type: TestingType = TestingType.BACKTRACE 22 | testing_params_cls: type[BacktraceTestingParams] = BacktraceTestingParams 23 | testing_result_cls: type[BacktraceTestingResult] = BacktraceTestingResult 24 | 25 | def run_on_vps( 26 | self, 27 | vps: VPS, 28 | params: BacktraceTestingParams, 29 | flush_callback: Callable[[str], None] | None = None, 30 | ) -> BacktraceTestingResult: 31 | # https://github.com/zhanghanyun/backtrace # 更新滞后,暂时去掉 32 | # https://github.com/oneclickvirt/backtrace 33 | 34 | # command = 'curl https://raw.githubusercontent.com/zhanghanyun/backtrace/main/install.sh -sSf | sh' 35 | command = 'curl https://cdn.spiritlhl.net/https://raw.githubusercontent.com/oneclickvirt/backtrace/main/backtrace_install.sh -sSf | bash && backtrace' 36 | 37 | run_result = vps.run( 38 | command, 39 | timeout=params.timeout, 40 | warn=True, 41 | pty=True, 42 | ) 43 | 44 | return BacktraceTestingResult(result=str(run_result)) 45 | -------------------------------------------------------------------------------- /debug/debug_webserver.py: -------------------------------------------------------------------------------- 1 | from jiduoduo.app import app 2 | from jiduoduo.models import User 3 | from jiduoduo.models import VPS 4 | from jiduoduo.models import db 5 | 6 | 7 | def create_test_data(): 8 | with app.app_context(): 9 | db.drop_all() 10 | db.create_all() 11 | 12 | user = User(email='a@a.com') 13 | user.password = 'a' 14 | db.session.add(user) 15 | db.session.flush() 16 | 17 | objs = [ 18 | VPS( 19 | user_id=user.id, 20 | name='🐔老大(纯测试用)', 21 | host='192.168.1.101', 22 | port=10001, 23 | user='jihaoda', 24 | password='jiduoduo1_password', 25 | identify_key='jiduoduo1_pkey', 26 | ), 27 | VPS( 28 | user_id=user.id, 29 | name='🐔老二(纯测试用)', 30 | host='192.168.1.102', 31 | port=10002, 32 | user='jihaoer', 33 | password='jiduoduo2_password', 34 | identify_key='jiduoduo2_pkey', 35 | ), 36 | VPS( 37 | user_id=user.id, 38 | name='🐔老三(纯测试用)', 39 | host='192.168.1.103', 40 | port=10003, 41 | user='jihaosan', 42 | password='jiduoduo3_password', 43 | identify_key='jiduoduo3_pkey', 44 | ), 45 | ] 46 | db.session.add_all(objs) 47 | db.session.flush() 48 | db.session.commit() 49 | 50 | 51 | if __name__ == '__main__': 52 | # 如果需要测试数据,可以解除 create_test_data() 注释 53 | # create_test_data() 54 | app.run(debug=True, threaded=False, host='localhost', port=15000) 55 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/next_trace.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from invoke import Responder 4 | from pydantic import Field 5 | 6 | from jiduoduo.models import VPS 7 | from jiduoduo.models.testing import TestingType 8 | from jiduoduo.services.testing.base import TestingParams 9 | from jiduoduo.services.testing.base import TestingResult 10 | from jiduoduo.services.testing.base import TestingService 11 | from jiduoduo.utils.fabric_utils import StreamFlusher 12 | 13 | 14 | class NextTraceTestingParams(TestingParams): 15 | timeout: int = Field(60 * 10 * 2) # seconds 16 | 17 | 18 | class NextTraceTestingResult(TestingResult): 19 | pass 20 | 21 | 22 | class NextTraceTestingService(TestingService): 23 | testing_type: TestingType = TestingType.NEXT_TRACE 24 | testing_params_cls: type[NextTraceTestingParams] = NextTraceTestingParams 25 | testing_result_cls: type[NextTraceTestingResult] = NextTraceTestingResult 26 | 27 | def run_on_vps( 28 | self, 29 | vps: VPS, 30 | params: NextTraceTestingParams, 31 | flush_callback: Callable[[str], None] | None = None, 32 | ) -> NextTraceTestingResult: 33 | # https://github.com/nxtrace/NTrace-core 34 | 35 | command = 'curl nxtrace.org/nt |bash ; nexttrace 8.8.4.4; nexttrace 2001:4860:4860::64 ; nexttrace youtube.com' 36 | 37 | run_result = vps.run( 38 | command, 39 | timeout=params.timeout, 40 | warn=True, 41 | pty=True, 42 | watchers=[ 43 | Responder(pattern=r'Your Option', response='0\n'), 44 | ], 45 | out_stream=StreamFlusher(flush_callback=flush_callback), 46 | ) 47 | 48 | return NextTraceTestingResult(result=str(run_result)) 49 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/bash_icu_speed_test.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from invoke import Responder 4 | from pydantic import Field 5 | 6 | from jiduoduo.models import VPS 7 | from jiduoduo.models.testing import TestingType 8 | from jiduoduo.services.testing.base import TestingParams 9 | from jiduoduo.services.testing.base import TestingResult 10 | from jiduoduo.services.testing.base import TestingService 11 | from jiduoduo.utils.fabric_utils import StreamFlusher 12 | 13 | 14 | class BashIcuSpeedTestTestingParams(TestingParams): 15 | timeout: int = Field(60 * 10 * 2) # seconds 16 | 17 | 18 | class BashIcuSpeedTestTestingResult(TestingResult): 19 | pass 20 | 21 | 22 | class BashIcuSpeedTestTestingService(TestingService): 23 | testing_type: TestingType = TestingType.BASH_ICU_SPEED_TEST 24 | testing_params_cls: type[BashIcuSpeedTestTestingParams] = BashIcuSpeedTestTestingParams 25 | testing_result_cls: type[BashIcuSpeedTestTestingResult] = BashIcuSpeedTestTestingResult 26 | 27 | def run_on_vps( 28 | self, 29 | vps: VPS, 30 | params: BashIcuSpeedTestTestingParams, 31 | flush_callback: Callable[[str], None] | None = None, 32 | ) -> BashIcuSpeedTestTestingResult: 33 | # https://github.com/i-abc/speedtest 34 | 35 | command = 'bash <(curl -sL bash.icu/speedtest)' 36 | 37 | run_result = vps.run( 38 | command, 39 | timeout=params.timeout, 40 | warn=True, 41 | pty=True, 42 | watchers=[ 43 | Responder(pattern=r'请输入', response='1\n'), 44 | ], 45 | out_stream=StreamFlusher(flush_callback=flush_callback), 46 | ) 47 | 48 | return BashIcuSpeedTestTestingResult(result=str(run_result)) 49 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/hyper_speed.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from invoke import Responder 4 | from pydantic import Field 5 | 6 | from jiduoduo.models import VPS 7 | from jiduoduo.models.testing import TestingType 8 | from jiduoduo.services.testing.base import TestingParams 9 | from jiduoduo.services.testing.base import TestingResult 10 | from jiduoduo.services.testing.base import TestingService 11 | from jiduoduo.utils.fabric_utils import StreamFlusher 12 | 13 | 14 | class HyperSpeedTestingParams(TestingParams): 15 | timeout: int = Field(60 * 10 * 2) # seconds 16 | 17 | 18 | class HyperSpeedTestingResult(TestingResult): 19 | pass 20 | 21 | 22 | class HyperSpeedTestingService(TestingService): 23 | testing_type: TestingType = TestingType.HYPER_SPEED 24 | testing_params_cls: type[HyperSpeedTestingParams] = HyperSpeedTestingParams 25 | testing_result_cls: type[HyperSpeedTestingResult] = HyperSpeedTestingResult 26 | 27 | def run_on_vps( 28 | self, 29 | vps: VPS, 30 | params: HyperSpeedTestingParams, 31 | flush_callback: Callable[[str], None] | None = None, 32 | ) -> HyperSpeedTestingResult: 33 | # https://hostloc.com/thread-1076585-1-1.html 34 | 35 | command = 'bash <(wget -qO- https://bench.im/hyperspeed)' 36 | 37 | run_result = vps.run( 38 | command, 39 | timeout=params.timeout, 40 | warn=True, 41 | pty=True, 42 | watchers=[ 43 | Responder(pattern=r'请选择测速类型', response='1\n'), 44 | Responder(pattern=r'启用八线程测速', response='N\n'), 45 | ], 46 | out_stream=StreamFlusher(flush_callback=flush_callback), 47 | ) 48 | 49 | return HyperSpeedTestingResult(result=str(run_result)) 50 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/media_unlock_test.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from invoke import Responder 4 | from pydantic import Field 5 | 6 | from jiduoduo.models import VPS 7 | from jiduoduo.models.testing import TestingType 8 | from jiduoduo.services.testing.base import TestingParams 9 | from jiduoduo.services.testing.base import TestingResult 10 | from jiduoduo.services.testing.base import TestingService 11 | from jiduoduo.utils.fabric_utils import StreamFlusher 12 | 13 | 14 | class MediaUnlockTestTestingParams(TestingParams): 15 | timeout: int = Field(60 * 10) # seconds 16 | 17 | 18 | class MediaUnlockTestTestingResult(TestingResult): 19 | pass 20 | 21 | 22 | class MediaUnlockTestTestingService(TestingService): 23 | testing_type: TestingType = TestingType.MEDIA_UNLOCK_TEST 24 | testing_params_cls: type[MediaUnlockTestTestingParams] = MediaUnlockTestTestingParams 25 | testing_result_cls: type[MediaUnlockTestTestingResult] = MediaUnlockTestTestingResult 26 | 27 | def run_on_vps( 28 | self, 29 | vps: VPS, 30 | params: MediaUnlockTestTestingParams, 31 | flush_callback: Callable[[str], None] | None = None, 32 | ) -> MediaUnlockTestTestingResult: 33 | # https://github.com/HsukqiLee/MediaUnlockTest 34 | 35 | command = 'bash <(curl -Ls unlock.icmp.ing/test.sh)' 36 | 37 | run_result = vps.run( 38 | command, 39 | timeout=params.timeout, 40 | hide=True, 41 | warn=True, 42 | pty=True, 43 | watchers=[ 44 | Responder(pattern=r'回车确认', response='\n'), 45 | ], 46 | out_stream=StreamFlusher(flush_callback=flush_callback), 47 | ) 48 | 49 | return MediaUnlockTestTestingResult(result=str(run_result)) 50 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/spiritlhls_ecs.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from invoke import Responder 4 | from pydantic import Field 5 | 6 | from jiduoduo.models import VPS 7 | from jiduoduo.models.testing import TestingType 8 | from jiduoduo.services.testing.base import TestingParams 9 | from jiduoduo.services.testing.base import TestingResult 10 | from jiduoduo.services.testing.base import TestingService 11 | from jiduoduo.utils.fabric_utils import StreamFlusher 12 | 13 | 14 | class SpiritLHLSECSTestingParams(TestingParams): 15 | timeout: int = Field(60 * 10 * 3) # seconds 16 | 17 | 18 | class SpiritLHLSECSTestingResult(TestingResult): 19 | pass 20 | 21 | 22 | class SpiritLHLSECSTestingService(TestingService): 23 | testing_type: TestingType = TestingType.SPIRITLHLS_ECS 24 | testing_params_cls: type[SpiritLHLSECSTestingParams] = SpiritLHLSECSTestingParams 25 | testing_result_cls: type[SpiritLHLSECSTestingResult] = SpiritLHLSECSTestingResult 26 | 27 | def run_on_vps( 28 | self, 29 | vps: VPS, 30 | params: SpiritLHLSECSTestingParams, 31 | flush_callback: Callable[[str], None] | None = None, 32 | ) -> SpiritLHLSECSTestingResult: 33 | # https://github.com/spiritLHLS/ecs 34 | 35 | command = 'curl -L https://gitlab.com/spiritysdx/za/-/raw/main/ecs.sh -o ecs.sh && chmod +x ecs.sh && bash ecs.sh -m 1' 36 | 37 | run_result = vps.run( 38 | command, 39 | timeout=params.timeout, 40 | warn=True, 41 | pty=True, 42 | watchers=[ 43 | Responder(pattern=r'[y]/n', response='y\n'), 44 | Responder(pattern=r'Y/n', response='Y\n'), 45 | ], 46 | out_stream=StreamFlusher(flush_callback=flush_callback), 47 | ) 48 | 49 | return SpiritLHLSECSTestingResult(result=str(run_result)) 50 | -------------------------------------------------------------------------------- /src/jiduoduo/templates/main/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'frame/base.html' %} 2 | {% import 'utils.html' as utils %} 3 | 4 | 5 | {% block container %} 6 | 55 | 56 | {% endblock %} 57 | 58 | 59 | {% block scripts %} 60 | {{ super() }} 61 | {% endblock %} 62 | -------------------------------------------------------------------------------- /src/jiduoduo/services/image_bed/lv_se.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import requests 4 | 5 | from jiduoduo.services.image_bed.base import ImageBedService 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class LvSeImageBedService(ImageBedService): 11 | base_url: str = 'https://lvse.eu.org' 12 | user_agent: str = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0' 13 | 14 | def __init__(self): 15 | self.session = requests.Session() 16 | 17 | def get_url(self, resource: str = '') -> str: 18 | return f'{self.base_url}{resource}' 19 | 20 | def get_headers(self, headers: dict | None = None) -> dict: 21 | default_headers = { 22 | 'User-Agent': self.user_agent, 23 | 'Origin': self.base_url, 24 | 'sec-fetch-site': "same-origin", 25 | 'sec-fetch-mode': "cors", 26 | 'sec-fetch-dest': "empty", 27 | 'accept-language': "en-US,en;q=0.5", 28 | 'Referer': self.get_url('/'), 29 | } 30 | return default_headers | (headers or {}) 31 | 32 | def upload(self, image) -> dict: 33 | url = self.get_url('/upload/localhost') 34 | headers = self.get_headers({'Referer': self.get_url('/')}) 35 | 36 | files = {'file': image} 37 | 38 | response = self.session.post( 39 | url, files=files, headers=headers, 40 | ) 41 | logger.info(f'response.text={response.text}') 42 | 43 | return response.json() 44 | 45 | 46 | r = ''' 47 | {"code":200, 48 | "id":"10009", 49 | "imgid":"28d3df6c56451571", 50 | "relative_path":"\/imgs\/2024\/07\/28d3df6c56451571.jpg", 51 | "url":"https:\/\/img.erpweb.eu.org\/imgs\/2024\/07\/28d3df6c56451571.jpg", 52 | "thumbnail_url":"https:\/\/img.erpweb.eu.org\/imgs\/2024\/07\/28d3df6c56451571_thumb.jpg", 53 | "width":640, 54 | "height":640, 55 | "delete":"https:\/\/img.0112233.xyz\/delete\/82f50a3e3825eef9"} 56 | ''' 57 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/spiritlhls_ecs_speed.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from invoke import Responder 4 | from pydantic import Field 5 | 6 | from jiduoduo.models import VPS 7 | from jiduoduo.models.testing import TestingType 8 | from jiduoduo.services.testing.base import TestingParams 9 | from jiduoduo.services.testing.base import TestingResult 10 | from jiduoduo.services.testing.base import TestingService 11 | from jiduoduo.utils.fabric_utils import StreamFlusher 12 | 13 | 14 | class SpiritLHLSECSSpeedTestingParams(TestingParams): 15 | timeout: int = Field(60 * 10) # seconds 16 | 17 | 18 | class SpiritLHLSECSSpeedTestingResult(TestingResult): 19 | pass 20 | 21 | 22 | class SpiritLHLSECSSpeedTestingService(TestingService): 23 | testing_type: TestingType = TestingType.SPIRITLHLS_ECS_SPEED 24 | testing_params_cls: type[SpiritLHLSECSSpeedTestingParams] = SpiritLHLSECSSpeedTestingParams 25 | testing_result_cls: type[SpiritLHLSECSSpeedTestingResult] = SpiritLHLSECSSpeedTestingResult 26 | 27 | def run_on_vps( 28 | self, 29 | vps: VPS, 30 | params: SpiritLHLSECSSpeedTestingParams, 31 | flush_callback: Callable[[str], None] | None = None, 32 | ) -> SpiritLHLSECSSpeedTestingResult: 33 | # https://github.com/spiritLHLS/ecsspeed 34 | 35 | command = 'bash <(wget -qO- bash.spiritlhl.net/ecs-net)' 36 | 37 | run_result = vps.run( 38 | command, 39 | timeout=params.timeout, 40 | warn=True, 41 | pty=True, 42 | watchers=[ 43 | Responder(pattern=r'[y]/n', response='y\n'), 44 | Responder(pattern=r'Y/n', response='Y\n'), 45 | Responder(pattern=r'请输入数字', response='1\n'), 46 | ], 47 | out_stream=StreamFlusher(flush_callback=flush_callback), 48 | ) 49 | 50 | return SpiritLHLSECSSpeedTestingResult(result=str(run_result)) 51 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/spiritlhls_ecs_basic_sys_info.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from invoke import Responder 4 | from pydantic import Field 5 | 6 | from jiduoduo.models import VPS 7 | from jiduoduo.models.testing import TestingType 8 | from jiduoduo.services.testing.base import TestingParams 9 | from jiduoduo.services.testing.base import TestingResult 10 | from jiduoduo.services.testing.base import TestingService 11 | from jiduoduo.utils.fabric_utils import StreamFlusher 12 | 13 | 14 | class SpiritLHLSECSBasicSysInfoTestingParams(TestingParams): 15 | timeout: int = Field(60 * 10) # seconds 16 | 17 | 18 | class SpiritLHLSECSBasicSysInfoTestingResult(TestingResult): 19 | pass 20 | 21 | 22 | class SpiritLHLSECSBasicSysInfoTestingService(TestingService): 23 | testing_type: TestingType = TestingType.SPIRITLHLS_ECS_BASIC_SYS_INFO 24 | testing_params_cls: type[SpiritLHLSECSBasicSysInfoTestingParams] = SpiritLHLSECSBasicSysInfoTestingParams 25 | testing_result_cls: type[SpiritLHLSECSBasicSysInfoTestingResult] = SpiritLHLSECSBasicSysInfoTestingResult 26 | 27 | def run_on_vps( 28 | self, 29 | vps: VPS, 30 | params: SpiritLHLSECSBasicSysInfoTestingParams, 31 | flush_callback: Callable[[str], None] | None = None, 32 | ) -> SpiritLHLSECSBasicSysInfoTestingResult: 33 | # https://github.com/spiritLHLS/ecs 34 | 35 | command = 'curl -L https://gitlab.com/spiritysdx/za/-/raw/main/ecs.sh -o ecs.sh && chmod +x ecs.sh && bash ecs.sh -base' 36 | 37 | run_result = vps.run( 38 | command, 39 | timeout=params.timeout, 40 | warn=True, 41 | pty=True, 42 | watchers=[ 43 | Responder(pattern=r'[y]/n', response='y\n'), 44 | Responder(pattern=r'Y/n', response='Y\n'), 45 | ], 46 | out_stream=StreamFlusher(flush_callback=flush_callback), 47 | ) 48 | 49 | return SpiritLHLSECSBasicSysInfoTestingResult(result=str(run_result)) 50 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/oneclickvirt_ecs.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from invoke import Responder 4 | from pydantic import Field 5 | 6 | from jiduoduo.models import VPS 7 | from jiduoduo.models.testing import TestingType 8 | from jiduoduo.services.testing.base import TestingParams 9 | from jiduoduo.services.testing.base import TestingResult 10 | from jiduoduo.services.testing.base import TestingService 11 | from jiduoduo.utils.fabric_utils import StreamFlusher 12 | 13 | 14 | class OneClickVirtECSTestingParams(TestingParams): 15 | timeout: int = Field(60 * 10 * 3) # seconds 16 | 17 | 18 | class OneClickVirtECSTestingResult(TestingResult): 19 | pass 20 | 21 | 22 | class OneClickVirtECSTestingService(TestingService): 23 | testing_type: TestingType = TestingType.ONECLICKVIRT_ECS 24 | testing_params_cls: type[OneClickVirtECSTestingParams] = OneClickVirtECSTestingParams 25 | testing_result_cls: type[OneClickVirtECSTestingResult] = OneClickVirtECSTestingResult 26 | 27 | def run_on_vps( 28 | self, 29 | vps: VPS, 30 | params: OneClickVirtECSTestingParams, 31 | flush_callback: Callable[[str], None] | None = None, 32 | ) -> OneClickVirtECSTestingResult: 33 | # https://github.com/oneclickvirt/ecs 34 | 35 | command = 'curl -L https://cdn.spiritlhl.net/https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh && bash goecs.sh env && bash goecs.sh install && goecs' 36 | 37 | run_result = vps.run( 38 | command, 39 | timeout=params.timeout, 40 | warn=True, 41 | pty=True, 42 | watchers=[ 43 | Responder(pattern=r'[y]/n', response='y\n'), 44 | Responder(pattern=r'Y/n', response='Y\n'), 45 | Responder(pattern=r'your choice', response='1\n'), 46 | ], 47 | out_stream=StreamFlusher(flush_callback=flush_callback), 48 | ) 49 | 50 | return OneClickVirtECSTestingResult(result=str(run_result)) 51 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/region_restriction_check.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from invoke import Responder 4 | from pydantic import Field 5 | 6 | from jiduoduo.models import VPS 7 | from jiduoduo.models.testing import TestingType 8 | from jiduoduo.services.testing.base import TestingParams 9 | from jiduoduo.services.testing.base import TestingResult 10 | from jiduoduo.services.testing.base import TestingService 11 | from jiduoduo.utils.fabric_utils import StreamFlusher 12 | 13 | 14 | class RegionRestrictionCheckTestingParams(TestingParams): 15 | timeout: int = Field(60 * 5) # seconds 16 | 17 | 18 | class RegionRestrictionCheckTestingResult(TestingResult): 19 | pass 20 | 21 | 22 | class RegionRestrictionCheckTestingService(TestingService): 23 | testing_type: TestingType = TestingType.REGION_RESTRICTION_CHECK 24 | testing_params_cls: type[RegionRestrictionCheckTestingParams] = RegionRestrictionCheckTestingParams 25 | testing_result_cls: type[RegionRestrictionCheckTestingResult] = RegionRestrictionCheckTestingResult 26 | 27 | def run_on_vps( 28 | self, 29 | vps: VPS, 30 | params: RegionRestrictionCheckTestingParams, 31 | flush_callback: Callable[[str], None] | None = None, 32 | ) -> RegionRestrictionCheckTestingResult: 33 | # https://github.com/1-stream/RegionRestrictionCheck # 更新滞后,暂时去掉 34 | # https://github.com/xykt/RegionRestrictionCheck 35 | 36 | # command = 'bash <(curl -L -s https://github.com/1-stream/RegionRestrictionCheck/raw/main/check.sh)' 37 | command = 'bash <(curl -L -s media.ispvps.com)' 38 | 39 | run_result = vps.run( 40 | command, 41 | timeout=params.timeout, 42 | hide=True, 43 | warn=True, 44 | pty=True, 45 | watchers=[ 46 | Responder(pattern=r'或直接按回车', response='\n'), 47 | ], 48 | out_stream=StreamFlusher(flush_callback=flush_callback), 49 | ) 50 | return RegionRestrictionCheckTestingResult(result=str(run_result)) 51 | -------------------------------------------------------------------------------- /nezha_agent_demo/proto/nezha.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "./proto"; 3 | 4 | package proto; 5 | 6 | service NezhaService { 7 | rpc ReportSystemState(State)returns(Receipt){} 8 | rpc ReportSystemInfo(Host)returns(Receipt){} 9 | rpc ReportTask(TaskResult)returns(Receipt){} 10 | rpc RequestTask(Host)returns(stream Task){} 11 | rpc IOStream(stream IOStreamData)returns(stream IOStreamData){} 12 | rpc LookupGeoIP(GeoIP)returns(GeoIP){} 13 | } 14 | 15 | message Host { 16 | string platform = 1; 17 | string platform_version = 2; 18 | repeated string cpu = 3; 19 | uint64 mem_total = 4; 20 | uint64 disk_total = 5; 21 | uint64 swap_total = 6; 22 | string arch = 7; 23 | string virtualization = 8; 24 | uint64 boot_time = 9; 25 | string ip = 10; 26 | string country_code = 11; // deprecated 27 | string version = 12; 28 | repeated string gpu = 13; 29 | } 30 | 31 | message State { 32 | double cpu = 1; 33 | uint64 mem_used = 3; 34 | uint64 swap_used = 4; 35 | uint64 disk_used = 5; 36 | uint64 net_in_transfer = 6; 37 | uint64 net_out_transfer = 7; 38 | uint64 net_in_speed = 8; 39 | uint64 net_out_speed = 9; 40 | uint64 uptime = 10; 41 | double load1 = 11; 42 | double load5 = 12; 43 | double load15 = 13; 44 | uint64 tcp_conn_count = 14; 45 | uint64 udp_conn_count = 15; 46 | uint64 process_count = 16; 47 | repeated State_SensorTemperature temperatures = 17; 48 | double gpu = 18; 49 | } 50 | 51 | message State_SensorTemperature { 52 | string name = 1; 53 | double temperature = 2; 54 | } 55 | 56 | message Task { 57 | uint64 id = 1; 58 | uint64 type = 2; 59 | string data = 3; 60 | } 61 | 62 | message TaskResult { 63 | uint64 id = 1; 64 | uint64 type = 2; 65 | float delay = 3; 66 | string data = 4; 67 | bool successful = 5; 68 | } 69 | 70 | message Receipt{ 71 | bool proced = 1; 72 | } 73 | 74 | message IOStreamData { 75 | bytes data = 1; 76 | } 77 | 78 | message GeoIP { 79 | string ip = 1; 80 | string country_code = 2; 81 | } -------------------------------------------------------------------------------- /src/jiduoduo/services/image_bed/image_hosting_16.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | from string import ascii_letters 4 | from string import digits 5 | 6 | import requests 7 | 8 | from jiduoduo.services.image_bed.base import ImageBedService 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | TOKEN_LENGTH = 32 13 | 14 | 15 | class ImageHosting16ImageBedService(ImageBedService): 16 | base_url: str = 'https://i.111666.best' 17 | user_agent: str = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0' 18 | 19 | def __init__(self, auth_token: str | None = None): 20 | self.auth_token = auth_token 21 | 22 | self.session = requests.Session() 23 | 24 | def get_url(self, resource: str = '') -> str: 25 | return f'{self.base_url}{resource}' 26 | 27 | def get_headers(self, headers: dict | None = None) -> dict: 28 | auth_token = self.get_auth_token() 29 | logger.info(f'auth_token={auth_token}') 30 | 31 | default_headers = { 32 | 'User-Agent': self.user_agent, 33 | 'Origin': self.base_url, 34 | 'sec-fetch-site': "same-origin", 35 | 'sec-fetch-mode': "cors", 36 | 'sec-fetch-dest': "empty", 37 | 'accept-language': "en-US,en;q=0.5", 38 | 'Referer': self.get_url('/'), 39 | 'Auth-Token': auth_token, 40 | } 41 | return default_headers | (headers or {}) 42 | 43 | def get_auth_token(self) -> str: 44 | if self.auth_token: 45 | return self.auth_token 46 | 47 | return ''.join([random.choice(ascii_letters + digits) for _ in range(TOKEN_LENGTH)]) 48 | 49 | def upload(self, image) -> dict: 50 | url = self.get_url('/image') 51 | headers = self.get_headers({'Referer': url}) 52 | 53 | files = {'abc': image} 54 | 55 | response = self.session.post( 56 | url, files=files, headers=headers, 57 | ) 58 | logger.info(f'response.text={response.text}') 59 | 60 | return response.json() 61 | 62 | 63 | r = ''' 64 | {"ok":true, "src":"/image/0hljT6c3OCidEZVKSxnEvF.png"} 65 | ''' 66 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | x-jiduoduo-common: 2 | &jiduoduo-common 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | image: jiduoduo 7 | working_dir: /jiduoduo_data 8 | volumes: 9 | - ./jiduoduo_data:/jiduoduo_data 10 | environment: 11 | &jiduoduo-common-env 12 | SECRET_KEY: ${SECRET_KEY:-jiduoduo} 13 | PERMANENT_SESSION_LIFETIME_MINUTES: ${PERMANENT_SESSION_LIFETIME_MINUTES:-120} 14 | SQLALCHEMY_DATABASE_URI: ${SQLALCHEMY_DATABASE_URI:-sqlite:///db.sqlite3} 15 | CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis:6379/0} 16 | CELERY_RESULT_BACKEND: ${CELERY_RESULT_BACKEND:-redis://redis:6379/0} 17 | logging: 18 | driver: "json-file" 19 | options: 20 | max-size: "10M" 21 | max-file: "10" 22 | networks: 23 | - jiduoduo-network 24 | 25 | services: 26 | webserver: 27 | <<: *jiduoduo-common 28 | container_name: 'jiduoduo-webserver' 29 | restart: always 30 | ports: 31 | - ${WEBSERVER_PORT:-15000}:5000 32 | command: [ 33 | 'gunicorn', 34 | '--log-level=INFO', 35 | '--capture-output', 36 | '--access-logfile','-', 37 | '--error-logfile', '-', 38 | '-b', '0.0.0.0:5000', 39 | '-k', 'gevent', 40 | '-w', '2', 41 | '-t', '30', 42 | 'jiduoduo.app:app' 43 | ] 44 | 45 | worker: 46 | <<: *jiduoduo-common 47 | container_name: 'jiduoduo-worker' 48 | restart: always 49 | command: celery --app jiduoduo.tasks.base.celery worker --loglevel=info 50 | 51 | redis: 52 | image: redis:7.2.4 53 | container_name: jiduoduo-redis 54 | ports: 55 | - ${REDIS_PORT:-15010}:6379 56 | command: redis-server 57 | privileged: true 58 | restart: always 59 | networks: 60 | - jiduoduo-network 61 | 62 | networks: 63 | jiduoduo-network: 64 | name: jiduoduo-network 65 | enable_ipv6: true 66 | driver: bridge 67 | ipam: 68 | driver: default 69 | config: 70 | - subnet: fd00:db8:1234::/64 71 | gateway: fd00:db8:1234::1 72 | 73 | # docker-compose pull 74 | 75 | # docker-compose build 76 | 77 | # docker-compose down && docker-compose up -d 78 | 79 | # 启动命令 80 | # git pull && docker-compose build && docker-compose down && docker-compose up -d 81 | -------------------------------------------------------------------------------- /src/jiduoduo/blueprints/user.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from flask import flash 3 | from flask import redirect 4 | from flask import render_template 5 | from flask import request 6 | from flask import url_for 7 | from flask_login import current_user 8 | from flask_login import login_required 9 | from flask_login import login_user 10 | from flask_login import logout_user 11 | 12 | from jiduoduo.forms.user import LoginForm 13 | from jiduoduo.forms.user import RegisterForm 14 | from jiduoduo.models import User 15 | 16 | blueprint = Blueprint('user', __name__, url_prefix='/') 17 | 18 | 19 | @blueprint.route('/login', methods=['GET', 'POST']) 20 | def login(): 21 | form = LoginForm() 22 | if request.method == 'POST': 23 | if form.validate_on_submit(): 24 | try: 25 | user = User.login(email=form.email.data, password=form.password.data) 26 | login_user(user, remember=form.remember_me.data) 27 | return redirect(url_for('main.index')) 28 | except ValueError as e: 29 | flash(f'{e}', 'error') 30 | else: 31 | form.flash_errors() 32 | return render_template('user/login.html', form=form) 33 | 34 | 35 | @blueprint.route('/register', methods=['GET', 'POST']) 36 | def register(): 37 | form = RegisterForm() 38 | if request.method == 'POST': 39 | if form.validate_on_submit(): 40 | try: 41 | User.register(email=form.email.data, password=form.password.data) 42 | flash('注册成功!', 'success') 43 | return redirect(url_for('user.login')) 44 | except ValueError as e: 45 | flash(f'{e}', 'error') 46 | else: 47 | form.flash_errors() 48 | return render_template('user/register.html', form=form) 49 | 50 | 51 | @blueprint.route('/logout', methods=['GET', 'POST']) 52 | @login_required 53 | def logout(): 54 | logout_user() 55 | return redirect(url_for('main.index')) 56 | 57 | 58 | @blueprint.route('/delete', methods=['GET', 'POST']) 59 | @login_required 60 | def delete(): 61 | current_user.delete() 62 | flash('账户已删除', 'success') 63 | return redirect(url_for('main.index')) 64 | 65 | 66 | @blueprint.route('/setting', methods=['GET', 'POST']) 67 | @login_required 68 | def setting(): 69 | return render_template('user/setting.html') 70 | -------------------------------------------------------------------------------- /src/jiduoduo/forms/vps.py: -------------------------------------------------------------------------------- 1 | from wtforms import IntegerField 2 | from wtforms import StringField 3 | from wtforms import TextAreaField 4 | from wtforms.validators import DataRequired 5 | 6 | from jiduoduo.forms.base import BaseForm 7 | 8 | 9 | class VPSForm(BaseForm): 10 | host = StringField( 11 | 'VPS 地址:', 12 | validators=[ 13 | DataRequired(message='VPS地址必填'), 14 | ], 15 | render_kw={ 16 | 'placeholder': '如:231.3.43.85 或者 2a04:bdc7:100:3ce2::df63:1234', 17 | } 18 | ) 19 | 20 | name = StringField( 21 | 'VPS 名称:', 22 | render_kw={ 23 | 'placeholder': '如:斯巴达24刀建站鸡;可以不填,默认为VPS地址', 24 | } 25 | ) 26 | 27 | port = IntegerField( 28 | 'SSH 端口:', 29 | default=22, 30 | validators=[ 31 | DataRequired(message='SSH端口必填'), 32 | ], 33 | render_kw={ 34 | 'placeholder': '如:22 或者 其他自定义端口', 35 | } 36 | ) 37 | 38 | user = StringField( 39 | 'SSH 用户:', 40 | default='root', 41 | validators=[ 42 | DataRequired(message='SSH用户必填'), 43 | ], 44 | render_kw={ 45 | 'placeholder': '如:root 或者 ubuntu 或者 别的', 46 | } 47 | ) 48 | 49 | password = StringField( 50 | 'SSH 密码:', 51 | render_kw={ 52 | 'placeholder': '和登录Key二选一,填一个就行', 53 | } 54 | ) 55 | 56 | identify_key = TextAreaField( 57 | 'SSH Key:', 58 | render_kw={ 59 | 'rows': 5, 60 | 'placeholder': '和登录密码二选一,填一个就行', 61 | } 62 | 63 | ) 64 | 65 | 66 | class VPSDetailForm(VPSForm): 67 | password = StringField( 68 | 'SSH 密码:', 69 | render_kw={ 70 | 'placeholder': '已隐藏', 71 | } 72 | ) 73 | 74 | identify_key = TextAreaField( 75 | 'SSH Key:', 76 | render_kw={ 77 | 'rows': 5, 78 | 'placeholder': '已隐藏', 79 | } 80 | ) 81 | 82 | 83 | class VPSCreateForm(VPSForm): 84 | 85 | def validate(self, extra_validators=None) -> bool: 86 | result = super().validate(extra_validators=extra_validators) 87 | 88 | if not self.password.data and not self.identify_key.data: 89 | self.password.errors.append('和登录Key二选一') 90 | self.identify_key.errors.append('和登录密码二选一') 91 | result = False 92 | 93 | return result 94 | 95 | 96 | class VPSUpdateForm(VPSForm): 97 | pass 98 | -------------------------------------------------------------------------------- /src/jiduoduo/models/user.py: -------------------------------------------------------------------------------- 1 | from typing import Self 2 | 3 | from flask_login import UserMixin 4 | from sqlalchemy import Boolean 5 | from sqlalchemy import String 6 | from sqlalchemy.orm import Mapped 7 | from sqlalchemy.orm import mapped_column 8 | from werkzeug.security import check_password_hash 9 | from werkzeug.security import generate_password_hash 10 | 11 | from jiduoduo.models.base import BaseModel 12 | 13 | 14 | class User(BaseModel, UserMixin): 15 | email: Mapped[str] = mapped_column( 16 | String(64), 17 | unique=True, 18 | index=True, 19 | nullable=False, 20 | ) 21 | 22 | password_hash: Mapped[str] = mapped_column( 23 | String(256), 24 | nullable=False, 25 | ) 26 | 27 | is_active: Mapped[bool] = mapped_column( 28 | Boolean, 29 | nullable=False, 30 | default=True, 31 | server_default='1', 32 | ) 33 | 34 | @property 35 | def username(self) -> str: 36 | return self.email.split('@')[0] 37 | 38 | @property 39 | def password(self): 40 | raise ValueError('can not read password') 41 | 42 | @password.setter 43 | def password(self, password: str): 44 | self.password_hash = generate_password_hash(password) 45 | 46 | def verify_password(self, password) -> bool: 47 | return check_password_hash(self.password_hash, password) 48 | 49 | @classmethod 50 | def get_by_email(cls, email: str) -> Self | None: 51 | email = email.strip() 52 | if not email: 53 | return 54 | return cls.get_one(cls.email == email) 55 | 56 | @classmethod 57 | def login(cls, email: str, password: str) -> Self: 58 | email = (email or '').strip() 59 | if not email: 60 | raise ValueError('邮箱不能为空') 61 | 62 | password = (password or '').strip() 63 | if not password: 64 | raise ValueError('密码不能为空') 65 | 66 | user = cls.get_by_email(email) 67 | if user is None or not user.is_active or not user.verify_password(password): 68 | raise ValueError('用户不存在或密码不对') 69 | 70 | return user 71 | 72 | @classmethod 73 | def register(cls, email: str, password: str, commit: bool = True) -> Self: 74 | email = (email or '').strip() 75 | if not email: 76 | raise ValueError('邮箱不能为空') 77 | 78 | password = (password or '').strip() 79 | if not password: 80 | raise ValueError('密码不能为空') 81 | 82 | user = cls.get_by_email(email) 83 | if user: 84 | raise ValueError('用户已存在') 85 | 86 | user = cls(email=email) 87 | user.password = password 88 | user.save(commit=commit) 89 | return user 90 | -------------------------------------------------------------------------------- /src/jiduoduo/templates/vps/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'frame/base.html' %} 2 | 3 | 4 | {% block container %} 5 |
    6 |
    7 |
    8 |
    9 | {{ form.hidden_tag() }} 10 |
    11 | 12 |
    13 | {{ form.name.label(class="form-label") }} 14 | {{ form.name(class="form-control") }} 15 |
    16 | 17 |
    18 | {{ form.host.label(class="form-label") }} 19 | {{ form.host(required='', class="form-control") }} 20 |
    21 | 22 |
    23 | {{ form.port.label(class="form-label") }} 24 | {{ form.port(class="form-control") }} 25 |
    26 | 27 |
    28 | {{ form.user.label(class="form-label") }} 29 | {{ form.user(class="form-control") }} 30 |
    31 | 32 |
    33 | {{ form.password.label(class="form-label") }} 34 | {{ form.password(class="form-control") }} 35 |
    36 | 37 |
    38 | {{ form.identify_key.label(class="form-label") }} 39 | {{ form.identify_key(class="form-control") }} 40 |
    41 | 42 |
    43 | 46 | delete vps 47 | 删除VPS 48 | 49 | 50 | 57 | 58 | 61 | login testing 62 | 登录测试 63 | 64 | 65 | 68 | add testing 69 | 新建测试 70 | 71 | 72 |
    73 | 74 |
    75 | 76 |
    77 |
    78 | {% endblock %} 79 | 80 | 81 | {% block scripts %} 82 | {{ super() }} 83 | {% endblock %} 84 | -------------------------------------------------------------------------------- /src/jiduoduo/services/image_bed/gwwc.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | import requests 5 | 6 | from jiduoduo.services.image_bed.base import ImageBedService 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class GwwcImageBedService(ImageBedService): 12 | base_url: str = 'https://img.gwwc.net' 13 | user_agent: str = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0' 14 | 15 | def __init__(self): 16 | self.session = requests.Session() 17 | 18 | def get_url(self, resource: str = '') -> str: 19 | return f'{self.base_url}{resource}' 20 | 21 | def get_headers(self, headers: dict | None = None) -> dict: 22 | default_headers = { 23 | 'User-Agent': self.user_agent, 24 | 'Origin': self.base_url, 25 | 'sec-fetch-site': "same-origin", 26 | 'sec-fetch-mode': "cors", 27 | 'sec-fetch-dest': "empty", 28 | 'accept-language': "en-US,en;q=0.5", 29 | 'Referer': self.get_url('/'), 30 | } 31 | return default_headers | (headers or {}) 32 | 33 | def upload(self, image) -> dict: 34 | #  https://img.gwwc.net/ 35 | 36 | response = self.session.get(self.get_url('/')) 37 | logger.info(response.text) 38 | 39 | pattern = 'csrf-token" content="(?P.*)"' 40 | csrf_token = re.search(pattern, response.text).group('csrf_token') 41 | 42 | url = self.get_url('/upload') 43 | headers = self.get_headers({ 44 | 'Referer': self.get_url('/'), 45 | 'X-Csrf-Token': csrf_token, 46 | }) 47 | 48 | data = {'strategy_id': 15} 49 | files = {'file': image} 50 | 51 | response = self.session.post( 52 | url, data=data, files=files, headers=headers, 53 | ) 54 | logger.info(f'response.text={response.text}') 55 | 56 | return response.json() 57 | 58 | 59 | r = ''' 60 | {"status":true, 61 | "message":"\u4e0a\u4f20\u6210\u529f", 62 | "data":{"id":1090, 63 | "pathname":"2024\/07\/9QC4kQ.png", 64 | "origin_name":"result.png", 65 | "size":49.8642578125, 66 | "mimetype":"image\/png", 67 | "md5":"8bb47f25e9ed65121cbaf4c3ccca0225", 68 | "sha1":"b136539a03f28b2ae45059678252a337f5b3723a", 69 | "links":{"url":"https:\/\/img.gwwc.net\/mjj\/2024\/07\/9QC4kQ.png", 70 | "markdown":"![null](https:\/\/img.gwwc.net\/mjj\/2024\/07\/9QC4kQ.png)", 71 | "html":"<img src=\"https:\/\/img.gwwc.net\/mjj\/2024\/07\/9QC4kQ.png\" alt=\"result.png\" title=\"result.png\" \/>", 72 | "bbcode":"[null]https:\/\/img.gwwc.net\/mjj\/2024\/07\/9QC4kQ.png[\/img]", 73 | "markdown_with_link":"[![null](https:\/\/img.gwwc.net\/mjj\/2024\/07\/9QC4kQ.png)](https:\/\/img.gwwc.net\/mjj\/2024\/07\/9QC4kQ.png)", 74 | "thumbnail_url":"https:\/\/img.gwwc.net\/thumbnails\/8bb47f25e9ed65121cbaf4c3ccca0225.png"}}} 75 | ''' 76 | -------------------------------------------------------------------------------- /src/jiduoduo/static/chick-hatched-from-egg-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 10 | 11 | 30 | 32 | 33 | -------------------------------------------------------------------------------- /docker-compose.all.yml: -------------------------------------------------------------------------------- 1 | x-jiduoduo-common: 2 | &jiduoduo-common 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | image: jiduoduo 7 | working_dir: /jiduoduo_data 8 | volumes: 9 | - ./jiduoduo_data:/jiduoduo_data 10 | environment: 11 | &jiduoduo-common-env 12 | SECRET_KEY: ${SECRET_KEY:-jiduoduo} 13 | PERMANENT_SESSION_LIFETIME_MINUTES: ${PERMANENT_SESSION_LIFETIME_MINUTES:-120} 14 | SQLALCHEMY_DATABASE_URI: ${SQLALCHEMY_DATABASE_URI:-sqlite:///db.sqlite3} 15 | CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis:6379/0} 16 | CELERY_RESULT_BACKEND: ${CELERY_RESULT_BACKEND:-redis://redis:6379/0} 17 | logging: 18 | driver: "json-file" 19 | options: 20 | max-size: "10M" 21 | max-file: "10" 22 | networks: 23 | - jiduoduo-network 24 | 25 | services: 26 | webserver: 27 | <<: *jiduoduo-common 28 | container_name: 'jiduoduo-webserver' 29 | restart: always 30 | ports: 31 | - ${WEBSERVER_PORT:-15000}:5000 32 | command: [ 33 | 'gunicorn', 34 | '--log-level=INFO', 35 | '--capture-output', 36 | '--access-logfile','-', 37 | '--error-logfile', '-', 38 | '-b', '0.0.0.0:5000', 39 | '-k', 'gevent', 40 | '-w', '2', 41 | '-t', '30', 42 | 'jiduoduo.app:app' 43 | ] 44 | 45 | worker: 46 | <<: *jiduoduo-common 47 | container_name: 'jiduoduo-worker' 48 | restart: always 49 | command: celery --app jiduoduo.tasks.base.celery worker --loglevel=info 50 | 51 | redis: 52 | image: redis:7.2.4 53 | container_name: jiduoduo-redis 54 | ports: 55 | - ${REDIS_PORT:-15010}:6379 56 | command: redis-server 57 | privileged: true 58 | restart: always 59 | networks: 60 | - jiduoduo-network 61 | 62 | redis-commander: 63 | image: ghcr.io/joeferner/redis-commander:latest 64 | container_name: jiduoduo-redis-commander 65 | environment: 66 | # https://github.com/joeferner/redis-commander 67 | - REDIS_HOSTS=local:redis:6379:0 68 | - HTTP_USER=${COMMANDER_ADMIN:-jiduoduo} 69 | - HTTP_PASSWORD=${COMMANDER_PASSWORD:-jiduoduo} 70 | hostname: redis-commander 71 | ports: 72 | - ${REDIS_COMMANDER_PORT:-15011}:8081 73 | restart: always 74 | networks: 75 | - jiduoduo-network 76 | 77 | adminer: 78 | image: adminer:4.8.1-standalone 79 | container_name: jiduoduo-adminer 80 | working_dir: /jiduoduo_data 81 | environment: 82 | - ADMINER_DEFAULT_SERVER=db.sqlite3 83 | volumes: 84 | # https://github.com/TimWolla/docker-adminer/issues/123 85 | - ./jiduoduo_data:/jiduoduo_data 86 | - ./login-password-less.php:/var/www/html/plugins-enabled/login-password-less.php 87 | restart: always 88 | ports: 89 | - ${ADMINER_PORT:-15012}:8080 90 | networks: 91 | - jiduoduo-network 92 | 93 | 94 | networks: 95 | jiduoduo-network: 96 | name: jiduoduo-network 97 | enable_ipv6: true 98 | driver: bridge 99 | ipam: 100 | driver: default 101 | config: 102 | - subnet: fd00:db8:1234::/64 103 | gateway: fd00:db8:1234::1 104 | 105 | # docker-compose pull 106 | 107 | # docker-compose build 108 | 109 | # docker-compose down && docker-compose up -d 110 | 111 | # 启动命令 112 | # git pull && docker-compose build && docker-compose down && docker-compose up -d 113 | -------------------------------------------------------------------------------- /src/jiduoduo/blueprints/vps.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from flask import Blueprint 4 | from flask import flash 5 | from flask import redirect 6 | from flask import render_template 7 | from flask import request 8 | from flask import url_for 9 | from flask_login import current_user 10 | from flask_login import login_required 11 | 12 | from jiduoduo.forms.vps import VPSCreateForm 13 | from jiduoduo.forms.vps import VPSDetailForm 14 | from jiduoduo.forms.vps import VPSUpdateForm 15 | from jiduoduo.models import VPS 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | blueprint = Blueprint('vps', __name__) 20 | 21 | 22 | @blueprint.get('/vps') 23 | @login_required 24 | def list(): 25 | vps_list = VPS.get_list( 26 | VPS.user_id == current_user.id, 27 | order_by=[VPS.updated_at.desc()], 28 | ) 29 | return render_template( 30 | 'vps/list.html', 31 | vps_list=vps_list, 32 | ) 33 | 34 | 35 | @blueprint.route('/vps/create', methods=['GET', 'POST']) 36 | @login_required 37 | def create(): 38 | form = VPSCreateForm() 39 | if request.method == 'POST': 40 | if not form.validate_on_submit(): 41 | form.flash_errors() 42 | 43 | else: 44 | vps = VPS( 45 | user_id=current_user.id, 46 | name=form.name.data or form.host.data, 47 | host=form.host.data, 48 | port=form.port.data, 49 | user=form.user.data, 50 | password=form.password.data, 51 | identify_key=form.identify_key.data, 52 | ) 53 | vps.save() 54 | 55 | return redirect(url_for('vps.list')) 56 | 57 | return render_template('vps/create.html', form=form) 58 | 59 | 60 | @blueprint.get('/vps/') 61 | @login_required 62 | def detail(id: str): 63 | vps = VPS.get(id) 64 | if not vps or vps.user_id != current_user.id: 65 | return redirect(url_for('vps.list')) 66 | 67 | form = VPSDetailForm( 68 | name=vps.name, 69 | host=vps.host, 70 | port=vps.port, 71 | user=vps.user, 72 | password='', 73 | identify_key='', 74 | ) 75 | 76 | return render_template( 77 | 'vps/detail.html', 78 | vps=vps, 79 | form=form, 80 | ) 81 | 82 | 83 | @blueprint.post('/vps/') 84 | @login_required 85 | def update(id: str): 86 | vps = VPS.get(id) 87 | if not vps or vps.user_id != current_user.id: 88 | return redirect(url_for('vps.list')) 89 | 90 | form = VPSUpdateForm() 91 | if not form.validate_on_submit(): 92 | form.flash_errors() 93 | return redirect(url_for('vps.list')) 94 | 95 | vps.name = form.name.data 96 | vps.host = form.host.data 97 | vps.port = form.port.data 98 | vps.user = form.user.data 99 | vps.password = form.password.data or vps.password 100 | vps.identify_key = form.identify_key.data or vps.identify_key 101 | vps.save() 102 | 103 | flash('更新成功', 'success') 104 | return redirect(url_for('vps.detail', id=id)) 105 | 106 | 107 | @blueprint.route('/vps//delete', methods=['GET', 'POST']) 108 | @login_required 109 | def delete(id: str): 110 | vps = VPS.get(id) 111 | if not vps or vps.user_id != current_user.id: 112 | return redirect(url_for('vps.list')) 113 | 114 | vps.delete() 115 | flash('VPS已删除', 'success') 116 | return redirect(url_for('vps.list')) 117 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/base.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import random 4 | import time 5 | from abc import ABC 6 | from abc import abstractmethod 7 | from typing import Callable 8 | 9 | from pydantic import BaseModel 10 | from pydantic import ConfigDict 11 | from pydantic import Field 12 | 13 | from jiduoduo.models import Testing 14 | from jiduoduo.models import TestingType 15 | from jiduoduo.models import VPS 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class TestingParams(BaseModel): 21 | model_config = ConfigDict(from_attributes=True) 22 | 23 | 24 | class TestingResult(BaseModel): 25 | is_success: bool = Field(True) 26 | result: str = Field('success') 27 | 28 | 29 | def remove_x00(obj): 30 | if isinstance(obj, list): 31 | return [remove_x00(item) for item in obj] 32 | if isinstance(obj, dict): 33 | return {k: remove_x00(v) for k, v in obj.items()} 34 | if isinstance(obj, str): 35 | return obj.replace('\x00', '') 36 | return obj 37 | 38 | 39 | class TestingService(ABC): 40 | testing_type: TestingType 41 | testing_params_cls: type[TestingParams] = TestingParams 42 | testing_result_cls: type[TestingResult] = TestingResult 43 | 44 | def __init__(self, dry_run: bool = False): 45 | self.dry_run = dry_run 46 | 47 | def run( 48 | self, 49 | testing: Testing, 50 | params: TestingParams | dict | None = None, 51 | ) -> Testing: 52 | if not testing.vps: 53 | raise ValueError(f'not found vps, testing_id={testing.id}') 54 | 55 | if params is None: 56 | params = testing.params 57 | 58 | if params is None or isinstance(params, self.testing_params_cls): 59 | pass 60 | 61 | if isinstance(params, str): 62 | params = json.loads(params) 63 | 64 | if isinstance(params, dict): 65 | params = self.testing_params_cls(**params) 66 | 67 | else: 68 | params = self.testing_params_cls.from_orm(params) 69 | 70 | testing.set_state_running(commit=not self.dry_run) 71 | 72 | def flush_result(r): 73 | from jiduoduo.app import app 74 | from jiduoduo.models import db 75 | with app.app_context(): 76 | session = db.session.object_session(testing) 77 | testing.result = remove_x00(r) 78 | session.add(testing) 79 | session.commit() 80 | 81 | try: 82 | result = self.run_on_vps( 83 | vps=testing.vps, 84 | params=params, 85 | flush_callback=flush_result, 86 | ) 87 | time.sleep(random.randint(1, 2)) 88 | if result.is_success: 89 | testing.set_state_success(result=remove_x00(result.result), commit=not self.dry_run) 90 | 91 | else: 92 | testing.set_state_failed(result=remove_x00(result.result), commit=not self.dry_run) 93 | 94 | except Exception as e: 95 | result = f'error: {e}' 96 | logger.error(result) 97 | testing.set_state_failed(result=result, commit=not self.dry_run) 98 | 99 | return testing 100 | 101 | @abstractmethod 102 | def run_on_vps( 103 | self, vps: VPS, params: TestingParams, flush_callback: Callable[[str], None] | None = None 104 | ) -> TestingResult: 105 | pass 106 | -------------------------------------------------------------------------------- /src/jiduoduo/models/vps.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import uuid 3 | from io import StringIO 4 | from typing import Self 5 | 6 | from fabric2 import Connection 7 | from fabric2.runners import Result 8 | from paramiko import DSSKey 9 | from paramiko import ECDSAKey 10 | from paramiko import Ed25519Key 11 | from paramiko import RSAKey 12 | from paramiko import SSHException 13 | from sqlalchemy import Integer 14 | from sqlalchemy import String 15 | from sqlalchemy import Text 16 | from sqlalchemy.orm import Mapped 17 | from sqlalchemy.orm import mapped_column 18 | 19 | from jiduoduo.models.base import BaseModel 20 | from jiduoduo.models.base import UUID 21 | from jiduoduo.models.base import UserMixin 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | PKEY_CLS_LIST = [RSAKey, Ed25519Key, DSSKey, ECDSAKey] 26 | 27 | 28 | class VPS(BaseModel, UserMixin): 29 | user_id: Mapped[uuid.UUID] = mapped_column( 30 | UUID(), 31 | nullable=False, 32 | ) 33 | 34 | name: Mapped[str] = mapped_column( 35 | String(128), 36 | nullable=False, 37 | unique=True, 38 | ) 39 | 40 | host: Mapped[str] = mapped_column( 41 | String(128), 42 | nullable=False, 43 | ) 44 | 45 | port: Mapped[int] = mapped_column( 46 | Integer, 47 | nullable=False, 48 | default=22, 49 | ) 50 | 51 | user: Mapped[str] = mapped_column( 52 | String(128), 53 | nullable=False, 54 | default='root', 55 | ) 56 | 57 | password: Mapped[str] = mapped_column( 58 | String(128), 59 | nullable=False, 60 | default='', 61 | server_default='', 62 | ) 63 | 64 | identify_key: Mapped[str] = mapped_column( 65 | Text, 66 | nullable=False, 67 | default='', 68 | ) 69 | 70 | @classmethod 71 | def get_by_name(cls, name: str) -> Self | None: 72 | return cls.get_one(cls.name == name) 73 | 74 | def make_new_connection(self) -> Connection: 75 | connect_kwargs = {} 76 | if self.password: 77 | connect_kwargs['password'] = self.password 78 | 79 | elif self.identify_key: 80 | pkey = None 81 | for pk_cls in PKEY_CLS_LIST: 82 | try: 83 | pkey = pk_cls.from_private_key(StringIO(self.identify_key)) 84 | break 85 | except SSHException as e: 86 | logger.error(f'{e}') 87 | continue 88 | 89 | if pkey is None: 90 | raise ValueError(f'identify_key无效') 91 | 92 | connect_kwargs['pkey'] = pkey 93 | 94 | return Connection( 95 | host=self.host, 96 | port=self.port, 97 | user=self.user, 98 | connect_kwargs=connect_kwargs, 99 | ) 100 | 101 | @property 102 | def connection(self) -> Connection: 103 | attr = '_connection' 104 | if getattr(self, attr, None) is None: 105 | setattr(self, attr, self.make_new_connection()) 106 | return getattr(self, attr) 107 | 108 | def close_connection(self): 109 | self.connection.close() 110 | setattr(self, '_connection', None) 111 | 112 | def run( 113 | self, 114 | command: str, 115 | hide: bool | str | None = None, 116 | timeout: float | None = None, 117 | warn: bool = False, 118 | **kwargs, 119 | ) -> Result | None: 120 | result = self.connection.run( 121 | command=command, 122 | hide=hide, 123 | timeout=timeout, 124 | warn=warn, 125 | **kwargs, 126 | ) 127 | self.close_connection() 128 | return result 129 | -------------------------------------------------------------------------------- /src/jiduoduo/templates/frame/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'template/bootstrap.html' %} 2 | 3 | {% block icon %} 4 | {# 5 | 图标: 6 | https://www.svgrepo.com/ 7 | https://iconpark.oceanengine.com/official 8 | #} 9 | {{ url_for('static', filename='chickens-chick-svgrepo-com.svg') }} 10 | {% endblock %} 11 | 12 | {% block title %} 13 | 鸡多多 | 开源VPS自动化测试平台 14 | {% endblock %} 15 | 16 | {% block description %} 17 | 鸡多多;开源VPS自动化测试平台;VPS自动化测试工具 18 | {% endblock %} 19 | 20 | {% block styles %} 21 | {{ super() }} 22 | 23 | 48 | {% endblock %} 49 | 50 | {% block content %} 51 |
    52 | 53 | {% block app %} 54 |
    55 | 78 | 79 | {% with messages = get_flashed_messages(with_categories=true) %} 80 | {% if messages %} 81 | {% for category, message in messages %} 82 | {% if category == 'warning' %} 83 | {% set alert_class = 'alert-warning' %} 84 | {% elif category == 'error' %} 85 | {% set alert_class = 'alert-danger' %} 86 | {% elif category == 'info' %} 87 | {% set alert_class = 'alert-primary' %} 88 | {% elif category =='success' %} 89 | {% set alert_class = 'alert-success' %} 90 | {% else %} 91 | {% set alert_class = 'alert-secondary' %} 92 | {% endif %} 93 | 97 | {% endfor %} 98 | {% endif %} 99 | {% endwith %} 100 | 101 | {% block container %} 102 | {% endblock %} 103 |
    104 | 105 | {% block terminal %} 106 | {% endblock %} 107 | 108 |
    109 | 110 | {% endblock %} 111 | 112 |
    113 | {% endblock content %} 114 | 115 | 116 | {% block scripts %} 117 | {{ super() }} 118 | {% endblock %} 119 | -------------------------------------------------------------------------------- /.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 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | scripts/ 162 | browser/ 163 | jiduoduo_data/ 164 | *.lock 165 | .adminer-init 166 | *.sqlite3 167 | *.sqlite3-journal@ 168 | *.db 169 | .DS_Store 170 | src/*.png 171 | src/*.jpg 172 | -------------------------------------------------------------------------------- /nezha_agent_demo/proto/nezha_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: nezha.proto 4 | # Protobuf Python Version: 5.26.1 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bnezha.proto\x12\x05proto\"\xf3\x01\n\x04Host\x12\x10\n\x08platform\x18\x01 \x01(\t\x12\x18\n\x10platform_version\x18\x02 \x01(\t\x12\x0b\n\x03\x63pu\x18\x03 \x03(\t\x12\x11\n\tmem_total\x18\x04 \x01(\x04\x12\x12\n\ndisk_total\x18\x05 \x01(\x04\x12\x12\n\nswap_total\x18\x06 \x01(\x04\x12\x0c\n\x04\x61rch\x18\x07 \x01(\t\x12\x16\n\x0evirtualization\x18\x08 \x01(\t\x12\x11\n\tboot_time\x18\t \x01(\x04\x12\n\n\x02ip\x18\n \x01(\t\x12\x14\n\x0c\x63ountry_code\x18\x0b \x01(\t\x12\x0f\n\x07version\x18\x0c \x01(\t\x12\x0b\n\x03gpu\x18\r \x03(\t\"\xf4\x02\n\x05State\x12\x0b\n\x03\x63pu\x18\x01 \x01(\x01\x12\x10\n\x08mem_used\x18\x03 \x01(\x04\x12\x11\n\tswap_used\x18\x04 \x01(\x04\x12\x11\n\tdisk_used\x18\x05 \x01(\x04\x12\x17\n\x0fnet_in_transfer\x18\x06 \x01(\x04\x12\x18\n\x10net_out_transfer\x18\x07 \x01(\x04\x12\x14\n\x0cnet_in_speed\x18\x08 \x01(\x04\x12\x15\n\rnet_out_speed\x18\t \x01(\x04\x12\x0e\n\x06uptime\x18\n \x01(\x04\x12\r\n\x05load1\x18\x0b \x01(\x01\x12\r\n\x05load5\x18\x0c \x01(\x01\x12\x0e\n\x06load15\x18\r \x01(\x01\x12\x16\n\x0etcp_conn_count\x18\x0e \x01(\x04\x12\x16\n\x0eudp_conn_count\x18\x0f \x01(\x04\x12\x15\n\rprocess_count\x18\x10 \x01(\x04\x12\x34\n\x0ctemperatures\x18\x11 \x03(\x0b\x32\x1e.proto.State_SensorTemperature\x12\x0b\n\x03gpu\x18\x12 \x01(\x01\"<\n\x17State_SensorTemperature\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0btemperature\x18\x02 \x01(\x01\".\n\x04Task\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0c\n\x04type\x18\x02 \x01(\x04\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\t\"W\n\nTaskResult\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0c\n\x04type\x18\x02 \x01(\x04\x12\r\n\x05\x64\x65lay\x18\x03 \x01(\x02\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\t\x12\x12\n\nsuccessful\x18\x05 \x01(\x08\"\x19\n\x07Receipt\x12\x0e\n\x06proced\x18\x01 \x01(\x08\"\x1c\n\x0cIOStreamData\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\")\n\x05GeoIP\x12\n\n\x02ip\x18\x01 \x01(\t\x12\x14\n\x0c\x63ountry_code\x18\x02 \x01(\t2\xbf\x02\n\x0cNezhaService\x12\x33\n\x11ReportSystemState\x12\x0c.proto.State\x1a\x0e.proto.Receipt\"\x00\x12\x31\n\x10ReportSystemInfo\x12\x0b.proto.Host\x1a\x0e.proto.Receipt\"\x00\x12\x31\n\nReportTask\x12\x11.proto.TaskResult\x1a\x0e.proto.Receipt\"\x00\x12+\n\x0bRequestTask\x12\x0b.proto.Host\x1a\x0b.proto.Task\"\x00\x30\x01\x12:\n\x08IOStream\x12\x13.proto.IOStreamData\x1a\x13.proto.IOStreamData\"\x00(\x01\x30\x01\x12+\n\x0bLookupGeoIP\x12\x0c.proto.GeoIP\x1a\x0c.proto.GeoIP\"\x00\x42\tZ\x07./protob\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'nezha_pb2', _globals) 22 | if not _descriptor._USE_C_DESCRIPTORS: 23 | _globals['DESCRIPTOR']._loaded_options = None 24 | _globals['DESCRIPTOR']._serialized_options = b'Z\007./proto' 25 | _globals['_HOST']._serialized_start=23 26 | _globals['_HOST']._serialized_end=266 27 | _globals['_STATE']._serialized_start=269 28 | _globals['_STATE']._serialized_end=641 29 | _globals['_STATE_SENSORTEMPERATURE']._serialized_start=643 30 | _globals['_STATE_SENSORTEMPERATURE']._serialized_end=703 31 | _globals['_TASK']._serialized_start=705 32 | _globals['_TASK']._serialized_end=751 33 | _globals['_TASKRESULT']._serialized_start=753 34 | _globals['_TASKRESULT']._serialized_end=840 35 | _globals['_RECEIPT']._serialized_start=842 36 | _globals['_RECEIPT']._serialized_end=867 37 | _globals['_IOSTREAMDATA']._serialized_start=869 38 | _globals['_IOSTREAMDATA']._serialized_end=897 39 | _globals['_GEOIP']._serialized_start=899 40 | _globals['_GEOIP']._serialized_end=940 41 | _globals['_NEZHASERVICE']._serialized_start=943 42 | _globals['_NEZHASERVICE']._serialized_end=1262 43 | # @@protoc_insertion_point(module_scope) 44 | -------------------------------------------------------------------------------- /src/jiduoduo/blueprints/testing.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from flask import Blueprint 4 | from flask import flash 5 | from flask import redirect 6 | from flask import render_template 7 | from flask import request 8 | from flask import url_for 9 | from flask_login import current_user 10 | from flask_login import login_required 11 | 12 | from jiduoduo.forms.testing import TestingForm 13 | from jiduoduo.models import Testing 14 | from jiduoduo.models import VPS 15 | from jiduoduo.tasks import run_testing 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | blueprint = Blueprint('testing', __name__) 20 | 21 | 22 | @blueprint.get('/testing') 23 | @login_required 24 | def list(): 25 | testing_list = Testing.get_list( 26 | Testing.user_id == current_user.id, 27 | order_by=[Testing.updated_at.desc()], 28 | ) 29 | return render_template( 30 | 'testing/list.html', 31 | testing_list=testing_list, 32 | ) 33 | 34 | 35 | @blueprint.route('/testing/create', methods=['GET', 'POST']) 36 | @login_required 37 | def create(): 38 | type = request.args.get('type') or request.form.get('type') 39 | vps_id = request.args.get('vps_id') or request.form.get('vps_id') 40 | 41 | form = TestingForm( 42 | type=type, 43 | vps_id=vps_id, 44 | ) 45 | form.vps_id.choices = [ 46 | (str(id), name) 47 | for id, name in VPS.get_id_name_list( 48 | VPS.user_id == current_user.id 49 | ) 50 | ] 51 | 52 | if request.method == 'POST': 53 | vps = VPS.get(vps_id) 54 | if not vps or vps.user_id != current_user.id: 55 | flash(f'VPS不存在, id={vps_id}', 'error') 56 | 57 | if not form.validate_on_submit(): 58 | form.flash_errors() 59 | 60 | else: 61 | Testing.check_precreate() 62 | testing = Testing.create(type=type, vps_or_id=vps, user_or_id=current_user) 63 | run_testing.delay(testing_id=testing.id) 64 | return redirect(url_for('testing.detail', id=testing.id)) 65 | 66 | return render_template('testing/create.html', form=form) 67 | 68 | 69 | @blueprint.route('/testing//rerun', methods=['GET', 'POST']) 70 | @login_required 71 | def rerun(id: str): 72 | testing = Testing.get(id) 73 | if not testing: 74 | return redirect(url_for('testing.list')) 75 | if testing.user_id != current_user.id: 76 | return redirect(url_for('testing.list')) 77 | 78 | Testing.check_precreate() 79 | testing.set_state_created() 80 | run_testing.delay(testing_id=testing.id) 81 | 82 | return redirect(url_for('testing.detail', id=id)) 83 | 84 | 85 | @blueprint.route('/testing//delete', methods=['GET', 'POST']) 86 | @login_required 87 | def delete(id: str): 88 | testing = Testing.get(id) 89 | if not testing or testing.user_id != current_user.id: 90 | return redirect(url_for('testing.list')) 91 | 92 | testing.delete() 93 | flash('测试已删除', 'success') 94 | return redirect(url_for('testing.list')) 95 | 96 | 97 | @blueprint.get('/testing/') 98 | def detail(id: str): 99 | testing = Testing.get(id) 100 | if not testing: 101 | return redirect(url_for('testing.list')) 102 | 103 | return render_template( 104 | 'testing/detail.html', 105 | testing=testing, 106 | ) 107 | 108 | 109 | @blueprint.get('/api/testing/') 110 | def api_detail(id: str): 111 | testing = Testing.get(id) 112 | if not testing: 113 | return { 114 | 'err': '测试不存在', 115 | } 116 | return { 117 | 'data': testing.to_dict(), 118 | } 119 | 120 | 121 | @blueprint.post('/api/testing//public') 122 | @login_required 123 | def api_make_public(id: str): 124 | testing = Testing.get(id) 125 | if not testing: 126 | return { 127 | 'err': '不存在', 128 | } 129 | if testing.user_id != current_user.id: 130 | return { 131 | 'err': '无权限', 132 | } 133 | testing.make_public() 134 | return { 135 | 'data': testing.to_dict(), 136 | } 137 | 138 | 139 | @blueprint.post('/api/testing//private') 140 | @login_required 141 | def api_make_private(id: str): 142 | testing = Testing.get(id) 143 | if not testing: 144 | return { 145 | 'err': '不存在', 146 | } 147 | if testing.user_id != current_user.id: 148 | return { 149 | 'err': '无权限', 150 | } 151 | testing.make_private() 152 | return { 153 | 'data': testing.to_dict(), 154 | } 155 | 156 | 157 | @blueprint.post('/api/testing//image') 158 | @login_required 159 | def api_upload_image(id: str): 160 | testing = Testing.get(id) 161 | if not testing: 162 | return { 163 | 'err': '不存在', 164 | } 165 | if testing.user_id != current_user.id: 166 | return { 167 | 'err': '无权限', 168 | } 169 | 170 | # todo upload image 171 | return { 172 | 'data': testing.to_dict(), 173 | } 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jiduoduo(中文名:鸡多多) 2 | 3 | ❗️警❗️告❗.......️前方发现一大波测试脚本........正在向鸡多多袭来!!!!!!!!! 4 | 5 | ## 废话连篇的介绍: 6 | 7 | (送给全宇宙 MJJ 的礼物) 8 | 9 | 基于 Web 的开源 VPS 自动化测试平台,支持IPv4 + IPv6,私有化部署,全自动后台运行,随时随地测! 10 | 11 | 芜湖~ 目前已支持 20+ 种顶级测试脚本!包括但不限于:YABS GB5、IP解锁测试、三网回程、融合怪... 12 | 13 | `也欢迎各位小伙伴参与 jiduoduo 开源项目中来~ XD ` 14 | 15 | ## 下面是 jiduoduo 的部分碎片: 16 | 17 | ### 首页截图 18 | 19 | ![首页截图](https://img.erpweb.eu.org/imgs/2024/07/fd9dc374b31ac895.png) 20 | 21 | ### 目前支持 20+ 种常见测试(正在陆续增加中...) 22 | 23 | ![新建测试](https://img.erpweb.eu.org/imgs/2024/07/fcd552821dffdeec.png) 24 | 25 | ### GB5测试截图 26 | 27 | ![GB5测试](https://img.erpweb.eu.org/imgs/2024/07/ec18842560908b26.png) 28 | 29 | ### 融合怪测试截图 30 | 31 | ![融合怪测试截图](https://img.erpweb.eu.org/imgs/2024/07/f49f44c886971261.png) 32 | 33 | ### IP质量体检报告截图 34 | 35 | ![IP质量体检报告](https://img.erpweb.eu.org/imgs/2024/07/69b49875c81716e4.png) 36 | 37 | ## 免责声明 38 | 39 | 本项目及相关代码文件,仅限于学习用途,不可用于商业,不可用于违法犯罪,否则后果自负。 40 | 41 | ## 如何参与到 jiduoduo 项目中? 42 | 43 | 欢迎贡献代码,欢迎标点符号修改,欢迎错别字修改,欢迎文本文案优化,欢迎变量命名修改,欢迎反馈BUG,欢迎提出改进建议,欢迎吐槽,欢迎拍砖... 44 | `(超!发现README.md文件存在错别字,窝立刻提交!)` 45 | 46 | ## 如何帮助修改错字?如何贡献代码?如何让 jiduoduo 变得更加健壮? 47 | 48 | 向 `develop` 分支发起 `Pull requests` 就可以啦。Review 通过后会进行合并。合并后,提交的内容立刻就会出现在 develop 分支了。 49 | `(好耶!)` 50 | 51 | 提交前,请先让自己疯狂Review一下正在提交的内容,以免造成大家时间流失。 52 | `(好哦!)` 53 | 54 | ## 如何在本机部署? 55 | 56 | 首先,确保本机已经安装 docker 和 docker-compose 软件 57 | 58 | 如果不会安装,这里推荐推荐一个炒鸡好用的办法! 59 | 60 | 先安装 1panel(到下面的链接,找到你当前系统对应的命令行,跟着提示下一步下一步下一步...) 61 | https://1panel.cn/docs/installation/online_installation/ 62 | 63 | 安装好 1panel后,输入下面的命令,一键卸载 1panel(1panel团队当场拍断大腿) 64 | 1pctl uninstall 65 | 66 | 这样就免费得到最新版本的docker 和 docker-compose了!是不是很简单! 67 | 68 | 小技巧:可在命令行输入下面的两条命令,来确认 docker 和 docker-compose 是否已经安装成功) 69 | 70 | 如果已安装,则会输出版本信息;否则会报错 71 | `(诶嘿嘿,又学到一招~` 72 | 73 | docker -v 74 | 75 | docker-compose -v 76 | 77 | 确认已安装过 docker 和 docker-compose 后,可在本机启动服务 78 | (在VPS上也行,只不过目前没文档。。。) 79 | 80 | 最后,按照下面的步骤,依次执行命令: 81 | 82 | ### 1. 拉 Docker 镜像 83 | 84 | docker-compose pull 85 | 86 | ### 2. 构建 Docker 镜像(主要是 jiduoduo-webserver 和 jiduoduo-worker) 87 | 88 | docker-compose build 89 | 90 | ### 3. 干掉旧的 Docker 容器,启动新的 Docker 容器 91 | 92 | docker-compose down && docker-compose up -d 93 | 94 | ## 本机部署后,如何浏览器访问? 95 | 96 | ### jiduoduo Web 页面: 97 | 98 | 默认地址:http://localhost:15000/ 99 | 默认账户 & 密码:请手动注册 100 | 101 | ### jiduoduo Redis 管理页面(需要用docker-compose.all.yaml启动才行): 102 | 103 | 默认地址:http://localhost:15011/ 104 | 默认Username:jiduoduo 105 | 默认Password:jiduoduo 106 | 107 | ### jiduoduo SQLite3 管理页面(需要用docker-compose.all.yaml启动才行): 108 | 109 | 默认地址:http://localhost:15012/ 110 | 默认System:SQLite 3 111 | 默认Username:jiduoduo 112 | 默认Password:jiduoduo 113 | 默认Database:/jiduoduo_data/db.sqlite3 114 | 115 | ### ⚠️注意:不了解数据库结构的情况下,请勿随意修改数据,以免导致服务不可用,或数据永久丢失 116 | 117 | ## FAQ 118 | 119 | ### 如何备份 数据库 和 配置文件? 120 | 121 | `请看下面的 ⚠️注意` 122 | 123 | ### ⚠️注意:启动后会在当前目录新建 jiduoduo_data 文件夹,内容如下: 124 | 125 | 1. db.sqlite3 文件(jiduoduo的sqlite3数据库,有需要可以拷贝出去备份) 126 | 2. .env 文件(Web相关的配置,能够自定义配置 SQL数据库 和 Redis。可参照.env.example文件修改) 127 | 3. 其他文件 128 | 129 | ### 如何单独启动某个 Docker 容器? 130 | 131 | # 命令行执行下面的命令: 132 | 133 | # 单独启动 webserver 134 | docker-compose down webserver && docker-compose up webserver -d 135 | 136 | # 启动 webserver 和 worker 137 | docker-compose down webserver worker && docker-compose up webserver worker -d 138 | 139 | # 启动 redis 和 redis-commander(需要用docker-compose.all.yaml启动才行) 140 | docker-compose down redis redis-commander && docker-compose up redis redis-commander -d 141 | 142 | ### 如何查看当前 Docker 运行了哪些容器? 143 | 144 | # 命令行执行下面的命令: 145 | 146 | docker ps 147 | 148 | ### 如何查看 Docker 容器里,正在运行的进程有哪些? 149 | 150 | # 命令行执行下面的命令: 151 | 152 | # 查看 webserver 容器里运行了哪些进程 153 | docker top jiduoduo-webserver 154 | 155 | # 查看 worker 容器里运行了哪些进程 156 | docker top jiduoduo-worker 157 | 158 | # 查看 redis 容器里运行了哪些进程 159 | docker top jiduoduo-redis 160 | 161 | # 查看 redis-commander 容器里运行了哪些进程(需要用docker-compose.all.yaml启动才行) 162 | docker top jiduoduo-redis-commander 163 | 164 | # 查看 adminer 容器里运行了哪些进程(需要用docker-compose.all.yaml启动才行) 165 | docker top jiduoduo-adminer 166 | 167 | ### 如何进入到某个正在运行的 Docker 容器里? 168 | 169 | # 命令行执行下面的命令: 170 | 171 | # 进入 webserver 容器 172 | docker exec -it jiduoduo-webserver bash 173 | 174 | # 进入 worker 容器 175 | docker exec -it jiduoduo-worker bash 176 | 177 | # 进入 redis 容器 178 | docker exec -it jiduoduo-redis bash 179 | 180 | # 进入 redis-commander 容器(注意这里不是 bash 而是 sh)(需要用docker-compose.all.yaml启动才行) 181 | docker exec -it jiduoduo-redis-commander sh 182 | 183 | # 进入 adminer 容器(需要用docker-compose.all.yaml启动才行) 184 | docker exec -it jiduoduo-adminer bash 185 | 186 | ## 写代码前的准备——入门资料 187 | 188 | ### 如何 Python3 入门? 189 | 190 | 廖雪峰:Python 教程 191 | https://www.liaoxuefeng.com/wiki/1016959663602400 192 | 193 | Python3 官方文档 194 | https://docs.python.org/zh-cn/3/tutorial/index.html 195 | 196 | ### 如何 Flask 入门?(Flask 是开源的 Python3 Web 框架) 197 | 198 | 李辉:Flask 入门教程 199 | https://tutorial.helloflask.com/ 200 | 201 | ### 如何 Docker 入门? 202 | 203 | 阮一峰:Docker 入门教程 204 | https://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html 205 | 206 | ## 如何本地开发和调试? 207 | 208 | ### 1. 确保本机安装了Python3.11,如何确定已经安装?输入下面命令,返回类似 “Python 3.11.9” 则表示成功 209 | 210 | python3 -V 211 | 212 | ### 【可选】安装虚拟环境,安装代码编辑工具(notepad 或者 vscode 或者 pycharm 都可以) 213 | 214 | 啊?这个还不太会?快去问问神奇的ChatGPT叭~ 215 | 216 | ### 2. 使用下面命令安装依赖包 217 | 218 | pip3 install -e . 219 | 220 | ### 芜湖~ 开始魔改代码吧! 221 | 222 | `(淦!时间怎么过得就像花钱一样快啊?` 223 | 224 | 225 | -------------------------------------------------------------------------------- /src/jiduoduo/services/testing/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import uuid 3 | 4 | from jiduoduo.models import Testing 5 | from jiduoduo.models import TestingType 6 | from jiduoduo.models import VPS 7 | from jiduoduo.services.testing.backtrace import BacktraceTestingService 8 | from jiduoduo.services.testing.base import TestingParams 9 | from jiduoduo.services.testing.base import TestingService 10 | from jiduoduo.services.testing.bash_icu_gb5 import BashIcuGB5TestingService 11 | from jiduoduo.services.testing.bash_icu_speed_test import BashIcuSpeedTestTestingService 12 | from jiduoduo.services.testing.check_unlock_media import CheckUnlockMediaTestingService 13 | from jiduoduo.services.testing.dd import DDTestingService 14 | from jiduoduo.services.testing.df_h import DFHTestingService 15 | from jiduoduo.services.testing.free_h import FreeHTestingService 16 | from jiduoduo.services.testing.hyper_speed import HyperSpeedTestingService 17 | from jiduoduo.services.testing.ip_check_place import IPCheckPlaceTestingService 18 | from jiduoduo.services.testing.ip_info_io import IPInfoIOTestingService 19 | from jiduoduo.services.testing.ip_sb import IPSBTestingService 20 | from jiduoduo.services.testing.login import LoginTestingService 21 | from jiduoduo.services.testing.media_unlock_test import MediaUnlockTestTestingService 22 | from jiduoduo.services.testing.memory_check import MemoryCheckTestingService 23 | from jiduoduo.services.testing.next_trace import NextTraceTestingService 24 | from jiduoduo.services.testing.nws_global import NWSGlobalDefaultTestingService 25 | from jiduoduo.services.testing.oneclickvirt_ecs import OneClickVirtECSTestingService 26 | from jiduoduo.services.testing.region_restriction_check import RegionRestrictionCheckTestingService 27 | from jiduoduo.services.testing.spiritlhls_ecs import SpiritLHLSECSTestingService 28 | from jiduoduo.services.testing.spiritlhls_ecs_basic_sys_info import SpiritLHLSECSBasicSysInfoTestingService 29 | from jiduoduo.services.testing.spiritlhls_ecs_speed import SpiritLHLSECSSpeedTestingService 30 | from jiduoduo.services.testing.yabs_basic_sys_info import YABSBasicSysInfoTestingService 31 | from jiduoduo.services.testing.yabs_default import YABSDefaultTestingService 32 | from jiduoduo.services.testing.yabs_disk import YABSDiskTestingService 33 | from jiduoduo.services.testing.yabs_gb5 import YABSGB5TestingService 34 | 35 | logger = logging.getLogger(__name__) 36 | 37 | TESTING_SERVICE_CLS_DICT = { 38 | TestingType.LOGIN: LoginTestingService, 39 | TestingType.MEMORY_CHECK: MemoryCheckTestingService, 40 | TestingType.ONECLICKVIRT_ECS: OneClickVirtECSTestingService, 41 | TestingType.SPIRITLHLS_ECS: SpiritLHLSECSTestingService, 42 | TestingType.SPIRITLHLS_ECS_BASIC_SYS_INFO: SpiritLHLSECSBasicSysInfoTestingService, 43 | TestingType.SPIRITLHLS_ECS_SPEED: SpiritLHLSECSSpeedTestingService, 44 | TestingType.NWS_GLOBAL: NWSGlobalDefaultTestingService, 45 | TestingType.BASH_ICU_GB5: BashIcuGB5TestingService, 46 | TestingType.BASH_ICU_SPEED_TEST: BashIcuSpeedTestTestingService, 47 | TestingType.DF_H: DFHTestingService, 48 | TestingType.DD: DDTestingService, 49 | TestingType.FREE_H: FreeHTestingService, 50 | TestingType.HYPER_SPEED: HyperSpeedTestingService, 51 | TestingType.NEXT_TRACE: NextTraceTestingService, 52 | TestingType.BACKTRACE: BacktraceTestingService, 53 | TestingType.IP_CHECK_PLACE: IPCheckPlaceTestingService, 54 | TestingType.MEDIA_UNLOCK_TEST: MediaUnlockTestTestingService, 55 | TestingType.REGION_RESTRICTION_CHECK: RegionRestrictionCheckTestingService, 56 | TestingType.CHECK_UNLOCK_MEDIA: CheckUnlockMediaTestingService, 57 | TestingType.IP_INFO_IO: IPInfoIOTestingService, 58 | TestingType.YABS_DEFAULT: YABSDefaultTestingService, 59 | TestingType.YABS_BASIC_SYS_INFO: YABSBasicSysInfoTestingService, 60 | TestingType.YABS_DISK: YABSDiskTestingService, 61 | TestingType.YABS_GB5: YABSGB5TestingService, 62 | TestingType.IP_SB: IPSBTestingService, 63 | } 64 | 65 | 66 | def get_testing_service_cls(testing_type: TestingType | str) -> type[TestingService]: 67 | testing_type = TestingType(testing_type) 68 | 69 | testing_service_cls = TESTING_SERVICE_CLS_DICT.get(testing_type) 70 | 71 | if testing_service_cls is None: 72 | raise ValueError(f'不支持 `{testing_type}` 测试类型') 73 | 74 | return testing_service_cls 75 | 76 | 77 | def run_testing( 78 | testing_or_id: Testing | uuid.UUID, 79 | params: TestingParams | dict | None = None, 80 | dry_run: bool = False, 81 | ) -> Testing: 82 | if isinstance(testing_or_id, uuid.UUID): 83 | testing = Testing.get(testing_or_id) 84 | if not testing: 85 | raise ValueError(f'not found testing, testing_id={testing_or_id}') 86 | else: 87 | testing = testing_or_id 88 | 89 | try: 90 | testing_service_cls = get_testing_service_cls(testing.type) 91 | service = testing_service_cls(dry_run=dry_run) 92 | return service.run(testing=testing, params=params) 93 | except Exception as e: 94 | testing.set_state_failed(f'{e}') 95 | 96 | 97 | def run_testing_on_vps( 98 | testing_type: TestingType | str, 99 | vps_or_id: VPS | uuid.UUID, 100 | params: TestingParams | dict | None = None, 101 | dry_run: bool = False, 102 | ): 103 | if isinstance(vps_or_id, uuid.UUID): 104 | vps = VPS.get(vps_or_id) 105 | if not vps: 106 | raise ValueError(f'not found vps, vps_id={vps_or_id}') 107 | 108 | else: 109 | vps = vps_or_id 110 | 111 | testing_service_cls = get_testing_service_cls(testing_type) 112 | service = testing_service_cls(dry_run=dry_run) 113 | return service.run_on_vps(vps=vps, params=params) 114 | -------------------------------------------------------------------------------- /src/jiduoduo/services/image_bed/mjj_today.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | from datetime import datetime 4 | 5 | import requests 6 | 7 | from jiduoduo.services.image_bed.base import ImageBedService 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class MjjTodayImageBedService(ImageBedService): 13 | base_url: str = 'https://mjj.today' # https://img.hmvod.cc/ 14 | user_agent: str = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:126.0) Gecko/20100101 Firefox/126.0' 15 | 16 | def __init__(self, auth_token: str | None = None): 17 | self.auth_token = auth_token 18 | 19 | self.session = requests.Session() 20 | 21 | def get_url(self, resource: str = '') -> str: 22 | return f'{self.base_url}{resource}' 23 | 24 | def get_headers(self, headers: dict | None = None) -> dict: 25 | default_headers = { 26 | 'User-Agent': self.user_agent, 27 | 'Origin': self.base_url, 28 | 'sec-fetch-site': "same-origin", 29 | 'sec-fetch-mode': "cors", 30 | 'sec-fetch-dest': "empty", 31 | 'accept-language': "en-US,en;q=0.5", 32 | 'Referer': self.get_url('/'), 33 | } 34 | return default_headers | (headers or {}) 35 | 36 | def get_auth_token(self) -> str: 37 | if self.auth_token: 38 | return self.auth_token 39 | 40 | url = self.get_url('/') 41 | headers = self.get_headers() 42 | 43 | response = self.session.get(url, headers=headers) 44 | html = response.text 45 | 46 | match = re.search(r'auth_token = "(?P.*)";', html) 47 | if match: 48 | return match.group('auth_token') 49 | return '' 50 | 51 | def upload(self, image) -> dict: 52 | url = self.get_url('/json') 53 | headers = self.get_headers({'Referer': url}) 54 | 55 | auth_token = self.get_auth_token() 56 | logger.info(f'auth_token={auth_token}') 57 | 58 | data = { 59 | 'type': 'file', 60 | 'action': 'upload', 61 | 'timestamp': str(int(datetime.now().timestamp() * 1000)), 62 | 'auth_token': auth_token, 63 | 'expiration': '', 64 | 'nsfw': '0', 65 | } 66 | files = {'source': image} 67 | 68 | response = self.session.post( 69 | url, data=data, files=files, headers=headers, 70 | ) 71 | logger.info(f'response.text={response.text}') 72 | 73 | return response.json() 74 | 75 | 76 | r = ''' 77 | { 78 | "status_code": 200, 79 | "success": { 80 | "message": "image uploaded", 81 | "code": 200 82 | }, 83 | "image": { 84 | "name": "46d0abbecede15e6629b4bd44ed6a940", 85 | "extension": "jpeg", 86 | "size": 11832, 87 | "width": "108", 88 | "height": "120", 89 | "date": "2024-07-22 00:10:48", 90 | "date_gmt": "2024-07-21 16:10:48", 91 | "title": "30 avatar middle", 92 | "description": null, 93 | "nsfw": "0", 94 | "storage_mode": "datefolder", 95 | "md5": "6335651ee356e856810ba6174366e073", 96 | "source_md5": null, 97 | "original_filename": "30_avatar_middle.jpg", 98 | "original_exifdata": "{\"FileName\":\"chvtempSgUEyX\",\"FileDateTime\":\"1721578248\",\"FileSize\":\"11832\",\"FileType\":\"2\",\"MimeType\":\"image\\\/jpeg\",\"SectionsFound\":\"\",\"COMPUTED\":{\"html\":\"width=\\\"108\\\" height=\\\"120\\\"\",\"Height\":\"120\",\"Width\":\"108\",\"IsColor\":\"1\"},\"IPTC\":[],\"width\":\"108\",\"height\":\"120\"}", 99 | "views": "0", 100 | "category_id": null, 101 | "chain": "5", 102 | "thumb_size": "6107", 103 | "medium_size": "0", 104 | "expiration_date_gmt": null, 105 | "likes": "0", 106 | "is_animated": "0", 107 | "is_approved": "1", 108 | "is_360": "0", 109 | "file": { 110 | "resource": { 111 | "type": "url" 112 | } 113 | }, 114 | "id_encoded": "j3BSa8", 115 | "filename": "46d0abbecede15e6629b4bd44ed6a940.jpeg", 116 | "mime": "image\/jpeg", 117 | "url": "https:\/\/ice.frostsky.com\/2024\/07\/22\/46d0abbecede15e6629b4bd44ed6a940.jpeg", 118 | "ratio": 0.9, 119 | "size_formatted": "11.8 KB", 120 | "url_viewer": "https:\/\/mjj.today\/i\/j3BSa8", 121 | "path_viewer": "\/i\/j3BSa8", 122 | "url_short": "https:\/\/mjj.today\/i\/j3BSa8", 123 | "image": { 124 | "filename": "46d0abbecede15e6629b4bd44ed6a940.jpeg", 125 | "name": "46d0abbecede15e6629b4bd44ed6a940", 126 | "mime": "image\/jpeg", 127 | "extension": "jpeg", 128 | "url": "https:\/\/ice.frostsky.com\/2024\/07\/22\/46d0abbecede15e6629b4bd44ed6a940.jpeg", 129 | "size": 11832 130 | }, 131 | "thumb": { 132 | "filename": "46d0abbecede15e6629b4bd44ed6a940.th.jpeg", 133 | "name": "46d0abbecede15e6629b4bd44ed6a940.th", 134 | "mime": "image\/jpeg", 135 | "extension": "jpeg", 136 | "url": "https:\/\/ice.frostsky.com\/2024\/07\/22\/46d0abbecede15e6629b4bd44ed6a940.th.jpeg", 137 | "size": "6107" 138 | }, 139 | "display_url": "https:\/\/ice.frostsky.com\/2024\/07\/22\/46d0abbecede15e6629b4bd44ed6a940.jpeg", 140 | "display_width": "108", 141 | "display_height": "120", 142 | "views_label": "\u6b21\u6d4f\u89c8", 143 | "likes_label": "\u6536\u85cf", 144 | "how_long_ago": "1 \u79d2\u524d", 145 | "date_fixed_peer": "2024-07-21 16:10:48", 146 | "title_truncated": "30 avatar middle", 147 | "title_truncated_html": "30 avatar middle", 148 | "is_use_loader": false, 149 | "delete_url": "https:\/\/mjj.today\/i\/j3BSa8\/delete\/2b23293aa6f770d8807d46498f05bc59c1bc3f8d4f304cff" 150 | }, 151 | "request": { 152 | "type": "file", 153 | "action": "upload", 154 | "timestamp": "1721578247807", 155 | "auth_token": "d0e59297d3231dbaa7866df3ee0d74cd25647d05", 156 | "expiration": "", 157 | "nsfw": "0" 158 | }, 159 | "status_txt": "OK" 160 | } 161 | ''' 162 | -------------------------------------------------------------------------------- /nezha_agent_demo/proto/nezha_pb2.pyi: -------------------------------------------------------------------------------- 1 | from google.protobuf.internal import containers as _containers 2 | from google.protobuf import descriptor as _descriptor 3 | from google.protobuf import message as _message 4 | from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union 5 | 6 | DESCRIPTOR: _descriptor.FileDescriptor 7 | 8 | class Host(_message.Message): 9 | __slots__ = ("platform", "platform_version", "cpu", "mem_total", "disk_total", "swap_total", "arch", "virtualization", "boot_time", "ip", "country_code", "version", "gpu") 10 | PLATFORM_FIELD_NUMBER: _ClassVar[int] 11 | PLATFORM_VERSION_FIELD_NUMBER: _ClassVar[int] 12 | CPU_FIELD_NUMBER: _ClassVar[int] 13 | MEM_TOTAL_FIELD_NUMBER: _ClassVar[int] 14 | DISK_TOTAL_FIELD_NUMBER: _ClassVar[int] 15 | SWAP_TOTAL_FIELD_NUMBER: _ClassVar[int] 16 | ARCH_FIELD_NUMBER: _ClassVar[int] 17 | VIRTUALIZATION_FIELD_NUMBER: _ClassVar[int] 18 | BOOT_TIME_FIELD_NUMBER: _ClassVar[int] 19 | IP_FIELD_NUMBER: _ClassVar[int] 20 | COUNTRY_CODE_FIELD_NUMBER: _ClassVar[int] 21 | VERSION_FIELD_NUMBER: _ClassVar[int] 22 | GPU_FIELD_NUMBER: _ClassVar[int] 23 | platform: str 24 | platform_version: str 25 | cpu: _containers.RepeatedScalarFieldContainer[str] 26 | mem_total: int 27 | disk_total: int 28 | swap_total: int 29 | arch: str 30 | virtualization: str 31 | boot_time: int 32 | ip: str 33 | country_code: str 34 | version: str 35 | gpu: _containers.RepeatedScalarFieldContainer[str] 36 | def __init__(self, platform: _Optional[str] = ..., platform_version: _Optional[str] = ..., cpu: _Optional[_Iterable[str]] = ..., mem_total: _Optional[int] = ..., disk_total: _Optional[int] = ..., swap_total: _Optional[int] = ..., arch: _Optional[str] = ..., virtualization: _Optional[str] = ..., boot_time: _Optional[int] = ..., ip: _Optional[str] = ..., country_code: _Optional[str] = ..., version: _Optional[str] = ..., gpu: _Optional[_Iterable[str]] = ...) -> None: ... 37 | 38 | class State(_message.Message): 39 | __slots__ = ("cpu", "mem_used", "swap_used", "disk_used", "net_in_transfer", "net_out_transfer", "net_in_speed", "net_out_speed", "uptime", "load1", "load5", "load15", "tcp_conn_count", "udp_conn_count", "process_count", "temperatures", "gpu") 40 | CPU_FIELD_NUMBER: _ClassVar[int] 41 | MEM_USED_FIELD_NUMBER: _ClassVar[int] 42 | SWAP_USED_FIELD_NUMBER: _ClassVar[int] 43 | DISK_USED_FIELD_NUMBER: _ClassVar[int] 44 | NET_IN_TRANSFER_FIELD_NUMBER: _ClassVar[int] 45 | NET_OUT_TRANSFER_FIELD_NUMBER: _ClassVar[int] 46 | NET_IN_SPEED_FIELD_NUMBER: _ClassVar[int] 47 | NET_OUT_SPEED_FIELD_NUMBER: _ClassVar[int] 48 | UPTIME_FIELD_NUMBER: _ClassVar[int] 49 | LOAD1_FIELD_NUMBER: _ClassVar[int] 50 | LOAD5_FIELD_NUMBER: _ClassVar[int] 51 | LOAD15_FIELD_NUMBER: _ClassVar[int] 52 | TCP_CONN_COUNT_FIELD_NUMBER: _ClassVar[int] 53 | UDP_CONN_COUNT_FIELD_NUMBER: _ClassVar[int] 54 | PROCESS_COUNT_FIELD_NUMBER: _ClassVar[int] 55 | TEMPERATURES_FIELD_NUMBER: _ClassVar[int] 56 | GPU_FIELD_NUMBER: _ClassVar[int] 57 | cpu: float 58 | mem_used: int 59 | swap_used: int 60 | disk_used: int 61 | net_in_transfer: int 62 | net_out_transfer: int 63 | net_in_speed: int 64 | net_out_speed: int 65 | uptime: int 66 | load1: float 67 | load5: float 68 | load15: float 69 | tcp_conn_count: int 70 | udp_conn_count: int 71 | process_count: int 72 | temperatures: _containers.RepeatedCompositeFieldContainer[State_SensorTemperature] 73 | gpu: float 74 | def __init__(self, cpu: _Optional[float] = ..., mem_used: _Optional[int] = ..., swap_used: _Optional[int] = ..., disk_used: _Optional[int] = ..., net_in_transfer: _Optional[int] = ..., net_out_transfer: _Optional[int] = ..., net_in_speed: _Optional[int] = ..., net_out_speed: _Optional[int] = ..., uptime: _Optional[int] = ..., load1: _Optional[float] = ..., load5: _Optional[float] = ..., load15: _Optional[float] = ..., tcp_conn_count: _Optional[int] = ..., udp_conn_count: _Optional[int] = ..., process_count: _Optional[int] = ..., temperatures: _Optional[_Iterable[_Union[State_SensorTemperature, _Mapping]]] = ..., gpu: _Optional[float] = ...) -> None: ... 75 | 76 | class State_SensorTemperature(_message.Message): 77 | __slots__ = ("name", "temperature") 78 | NAME_FIELD_NUMBER: _ClassVar[int] 79 | TEMPERATURE_FIELD_NUMBER: _ClassVar[int] 80 | name: str 81 | temperature: float 82 | def __init__(self, name: _Optional[str] = ..., temperature: _Optional[float] = ...) -> None: ... 83 | 84 | class Task(_message.Message): 85 | __slots__ = ("id", "type", "data") 86 | ID_FIELD_NUMBER: _ClassVar[int] 87 | TYPE_FIELD_NUMBER: _ClassVar[int] 88 | DATA_FIELD_NUMBER: _ClassVar[int] 89 | id: int 90 | type: int 91 | data: str 92 | def __init__(self, id: _Optional[int] = ..., type: _Optional[int] = ..., data: _Optional[str] = ...) -> None: ... 93 | 94 | class TaskResult(_message.Message): 95 | __slots__ = ("id", "type", "delay", "data", "successful") 96 | ID_FIELD_NUMBER: _ClassVar[int] 97 | TYPE_FIELD_NUMBER: _ClassVar[int] 98 | DELAY_FIELD_NUMBER: _ClassVar[int] 99 | DATA_FIELD_NUMBER: _ClassVar[int] 100 | SUCCESSFUL_FIELD_NUMBER: _ClassVar[int] 101 | id: int 102 | type: int 103 | delay: float 104 | data: str 105 | successful: bool 106 | def __init__(self, id: _Optional[int] = ..., type: _Optional[int] = ..., delay: _Optional[float] = ..., data: _Optional[str] = ..., successful: bool = ...) -> None: ... 107 | 108 | class Receipt(_message.Message): 109 | __slots__ = ("proced",) 110 | PROCED_FIELD_NUMBER: _ClassVar[int] 111 | proced: bool 112 | def __init__(self, proced: bool = ...) -> None: ... 113 | 114 | class IOStreamData(_message.Message): 115 | __slots__ = ("data",) 116 | DATA_FIELD_NUMBER: _ClassVar[int] 117 | data: bytes 118 | def __init__(self, data: _Optional[bytes] = ...) -> None: ... 119 | 120 | class GeoIP(_message.Message): 121 | __slots__ = ("ip", "country_code") 122 | IP_FIELD_NUMBER: _ClassVar[int] 123 | COUNTRY_CODE_FIELD_NUMBER: _ClassVar[int] 124 | ip: str 125 | country_code: str 126 | def __init__(self, ip: _Optional[str] = ..., country_code: _Optional[str] = ...) -> None: ... 127 | -------------------------------------------------------------------------------- /src/jiduoduo/models/base.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import uuid 4 | from datetime import datetime 5 | from typing import Self 6 | 7 | import pendulum 8 | from flask_sqlalchemy import SQLAlchemy 9 | from sqlalchemy import DateTime 10 | from sqlalchemy import Select 11 | from sqlalchemy import func 12 | from sqlalchemy import select 13 | from sqlalchemy.dialects import postgresql 14 | from sqlalchemy.ext.compiler import compiles 15 | from sqlalchemy.orm import DeclarativeBase 16 | from sqlalchemy.orm import Mapped 17 | from sqlalchemy.orm import mapped_column 18 | from sqlalchemy.sql.functions import FunctionElement 19 | from sqlalchemy.types import CHAR 20 | from sqlalchemy.types import TypeDecorator 21 | from sqlalchemy.types import TypeEngine 22 | 23 | ZERO_UUID = uuid.UUID('00000000-0000-0000-0000-000000000000') 24 | 25 | 26 | class GenerateUUID(FunctionElement): 27 | name = "uuid_default" 28 | 29 | 30 | @compiles(GenerateUUID, "sqlite") 31 | def _generate_uuid_sqlite(element, compiler, **kwargs): 32 | return """ 33 | ( 34 | lower(hex(randomblob(4))) 35 | || '-' 36 | || lower(hex(randomblob(2))) 37 | || '-4' 38 | || substr(lower(hex(randomblob(2))),2) 39 | || '-' 40 | || substr('89ab',abs(random()) % 4 + 1, 1) 41 | || substr(lower(hex(randomblob(2))),2) 42 | || '-' 43 | || lower(hex(randomblob(6))) 44 | ) 45 | """ 46 | 47 | 48 | @compiles(GenerateUUID, "postgresql") 49 | @compiles(GenerateUUID) 50 | def _generate_uuid_postgresql(element, compiler, **kwargs): 51 | return "(GEN_RANDOM_UUID())" 52 | 53 | 54 | class UUID(TypeDecorator): 55 | impl = TypeEngine 56 | cache_ok = True 57 | 58 | def load_dialect_impl(self, dialect): 59 | if dialect.name == "postgresql": 60 | return dialect.type_descriptor(postgresql.UUID()) 61 | else: 62 | return dialect.type_descriptor(CHAR(36)) 63 | 64 | def process_bind_param(self, value, dialect): 65 | if value is None: 66 | return None 67 | elif dialect.name == "postgresql": 68 | return str(value) 69 | elif isinstance(value, uuid.UUID): 70 | return str(value) 71 | else: 72 | return str(uuid.UUID(value)) 73 | 74 | def process_result_value(self, value, dialect): 75 | if value is None: 76 | return value 77 | else: 78 | if not isinstance(value, uuid.UUID): 79 | value = uuid.UUID(value) 80 | return value 81 | 82 | 83 | class SqlalchemyBaseModel(DeclarativeBase): 84 | id: Mapped[uuid.UUID] = mapped_column( 85 | UUID(), 86 | primary_key=True, 87 | server_default=GenerateUUID(), 88 | default=uuid.uuid4, 89 | ) 90 | 91 | created_at: Mapped[datetime] = mapped_column( 92 | DateTime, 93 | nullable=False, 94 | default=lambda: datetime.utcnow(), 95 | server_default=func.current_timestamp(), 96 | ) 97 | 98 | updated_at: Mapped[datetime] = mapped_column( 99 | DateTime, 100 | nullable=False, 101 | default=lambda: datetime.utcnow(), 102 | server_default=func.current_timestamp(), 103 | onupdate=lambda: datetime.utcnow(), 104 | server_onupdate=func.current_timestamp(), 105 | ) 106 | 107 | def format_created_at(self, fmt='%Y-%m-%d %H:%M:%S', tz='UTC') -> str: 108 | return pendulum.from_timestamp(self.created_at.timestamp(), tz).strftime(fmt) 109 | 110 | def format_updated_at(self, fmt='%Y-%m-%d %H:%M:%S', tz='UTC') -> str: 111 | return pendulum.from_timestamp(self.updated_at.timestamp(), tz).strftime(fmt) 112 | 113 | def save(self, commit: bool = True): 114 | db.session.add(self) 115 | db.session.flush([self]) 116 | if commit: 117 | db.session.commit() 118 | 119 | def delete(self, commit: bool = True): 120 | db.session.delete(self) 121 | db.session.flush() 122 | if commit: 123 | db.session.commit() 124 | 125 | @classmethod 126 | def get_obj_id(cls, obj_or_id: Self | uuid.UUID) -> uuid.UUID: 127 | if isinstance(obj_or_id, uuid.UUID): 128 | return obj_or_id 129 | 130 | elif isinstance(obj_or_id, cls): 131 | return obj_or_id.id 132 | 133 | else: 134 | id = getattr(obj_or_id, 'id', None) 135 | if id is None: 136 | raise ValueError(f'obj {obj_or_id} 没有ID') 137 | return id 138 | 139 | @classmethod 140 | def get(cls, ident) -> Self | None: 141 | return db.session.get(cls, ident) 142 | 143 | @classmethod 144 | def get_one(cls, *where) -> Self | None: 145 | stmt = select(cls) 146 | if where: 147 | stmt = stmt.where(*where) 148 | result = db.session.execute(stmt) 149 | return result.scalar_one_or_none() 150 | 151 | @classmethod 152 | def build_stmt( 153 | cls, 154 | *where, order_by: list | None = None, 155 | offset: int | None = None, limit: int | None = None, 156 | ) -> Select: 157 | stmt = select(cls) 158 | 159 | if where: 160 | stmt = stmt.where(*where) 161 | if order_by: 162 | stmt = stmt.order_by(*order_by) 163 | if offset is not None: 164 | stmt = stmt.offset(offset) 165 | if limit is not None: 166 | stmt = stmt.limit(limit) 167 | 168 | return stmt 169 | 170 | @classmethod 171 | def count(cls, *where) -> int: 172 | subquery = cls.build_stmt(*where).subquery() 173 | stmt = select(func.count(subquery.c.id).label('count')) 174 | result = db.session.execute(stmt) 175 | for (count,) in result: 176 | return count 177 | 178 | @classmethod 179 | def get_list( 180 | cls, 181 | *where, order_by: list | None = None, 182 | offset: int | None = None, limit: int | None = None, 183 | ) -> list[Self]: 184 | stmt = cls.build_stmt(*where, order_by=order_by, offset=offset, limit=limit) 185 | result = db.session.execute(stmt).scalars() 186 | return list(result) 187 | 188 | @classmethod 189 | def get_list_by_page( 190 | cls, 191 | *where, order_by: list | None = None, 192 | page_num: int = 1, page_size: int = 20, 193 | ) -> list[Self]: 194 | if not order_by: 195 | order_by = [cls.created_at.desc(), cls.id.desc()] 196 | return cls.get_list( 197 | *where, order_by=order_by, 198 | offset=(page_num - 1) * page_size, limit=page_size, 199 | ) 200 | 201 | @classmethod 202 | def get_id_name_list(cls, *where, order_by: list | None = None) -> list[uuid.UUID]: 203 | subquery = cls.build_stmt(*where, order_by=order_by).subquery() 204 | stmt = select(subquery.c.id, subquery.c.name) 205 | result = db.session.execute(stmt) 206 | id_list = [(id, name) for (id, name) in result] 207 | return id_list 208 | 209 | def to_dict(self) -> dict: 210 | return {'id': self.id} 211 | 212 | def to_json(self) -> str: 213 | 214 | class MyJsonEncoder(json.JSONEncoder): 215 | def default(self, field): 216 | if isinstance(field, uuid.UUID): 217 | return str(field) 218 | else: 219 | return super().default(field) 220 | 221 | return json.dumps(self.to_dict(), cls=MyJsonEncoder) 222 | 223 | 224 | db = SQLAlchemy( 225 | model_class=SqlalchemyBaseModel, 226 | engine_options={ 227 | 'pool_size': 5, 228 | 'pool_recycle': 100, 229 | 'pool_pre_ping': True 230 | }, 231 | session_options={ 232 | 'autocommit': False, 233 | 'autoflush': False, 234 | 'expire_on_commit': False, 235 | } 236 | ) 237 | 238 | BaseModel: type[SqlalchemyBaseModel] = db.Model 239 | 240 | 241 | class UserMixin: 242 | user_id: Mapped[uuid.UUID] = mapped_column( 243 | UUID(), 244 | nullable=False, 245 | ) 246 | -------------------------------------------------------------------------------- /src/jiduoduo/static/chicken-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 13 | 14 | 17 | 18 | 20 | 23 | 25 | 28 | 31 | 32 | 33 | 41 | 94 | -------------------------------------------------------------------------------- /src/jiduoduo/static/chickens-chick-svgrepo-com.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 34 | 35 | 37 | 39 | 41 | 42 | 43 | 44 | 46 | 48 | 51 | 54 | 55 | 56 | 57 | 59 | 61 | 62 | 63 | 68 | 69 | 70 | 71 | 74 | 77 | 78 | 79 | 81 | 83 | 84 | 88 | 91 | 94 | 95 | 97 | 99 | 100 | 103 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/jiduoduo/models/testing.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import uuid 4 | from datetime import datetime 5 | from datetime import timedelta 6 | from enum import StrEnum 7 | from functools import cached_property 8 | from typing import Self 9 | 10 | import pendulum 11 | import pyte 12 | from flask_login import current_user 13 | from pydantic import BaseModel as PydanticBaseModel 14 | from sqlalchemy import Boolean 15 | from sqlalchemy import DateTime 16 | from sqlalchemy import String 17 | from sqlalchemy import Text 18 | from sqlalchemy import select 19 | from sqlalchemy.orm import Mapped 20 | from sqlalchemy.orm import mapped_column 21 | from sqlalchemy.orm import relationship 22 | 23 | from jiduoduo.models.base import BaseModel 24 | from jiduoduo.models.base import UUID 25 | from jiduoduo.models.base import UserMixin 26 | from jiduoduo.models.base import db 27 | from jiduoduo.models.user import User 28 | from jiduoduo.models.vps import VPS 29 | 30 | logger = logging.getLogger(__name__) 31 | 32 | 33 | class TestingState(StrEnum): 34 | CREATED = 'created' 35 | RUNNING = 'running' 36 | SUCCESS = 'success' 37 | FAILED = 'failed' 38 | 39 | 40 | UNKNOWN_TESTING_STATE_ZH = '未知状态' 41 | TESTING_STATE_ZH = { 42 | TestingState.CREATED: '刚刚创建,还未运行', 43 | TestingState.RUNNING: '正在运行...', 44 | TestingState.SUCCESS: '测试成功', 45 | TestingState.FAILED: '测试失败', 46 | } 47 | 48 | UNKNOWN_TESTING_STATE_EMOJI = '❔' 49 | TESTING_STATE_EMOJI = { 50 | TestingState.CREATED: '👀', 51 | TestingState.RUNNING: '⏳', 52 | TestingState.SUCCESS: '✅', 53 | TestingState.FAILED: '❌', 54 | } 55 | 56 | 57 | class TestingType(StrEnum): 58 | LOGIN = 'login' 59 | MEMORY_CHECK = 'memory_check' 60 | ONECLICKVIRT_ECS = 'oneclickvirt_ecs' 61 | SPIRITLHLS_ECS = 'spiritlhls_ecs' 62 | SPIRITLHLS_ECS_BASIC_SYS_INFO = 'spiritlhls_ecs_basic_sys_info' 63 | SPIRITLHLS_ECS_SPEED = 'spiritlhls_ecs_speed' 64 | NWS_GLOBAL = 'nws_global' 65 | BASH_ICU_GB5 = 'bash_icu_gb5' 66 | BASH_ICU_SPEED_TEST = 'bash_icu_speed_test' 67 | DF_H = 'df_h' 68 | DD = 'dd' 69 | FREE_H = 'free_h' 70 | HYPER_SPEED = 'hyper_speed' 71 | NEXT_TRACE = 'next_trace' 72 | BACKTRACE = 'backtrace' 73 | IP_CHECK_PLACE = 'ip_check_place' 74 | MEDIA_UNLOCK_TEST = 'unlock_media_test' 75 | REGION_RESTRICTION_CHECK = 'region_restriction_check' 76 | CHECK_UNLOCK_MEDIA = 'check_unlock_media' 77 | IP_SB = 'ip_sb' 78 | IP_INFO_IO = 'ip_info_io' 79 | YABS_DEFAULT = 'yabs_default' 80 | YABS_BASIC_SYS_INFO = 'yabs_basic_sys_info' 81 | YABS_DISK = 'yabs_disk' 82 | YABS_GB5 = 'yabs_gb5' 83 | 84 | 85 | UNKNOWN_TESTING_TYPE_ZH = '未知测试类型' 86 | 87 | TESTING_TYPE_ZH = { 88 | TestingType.YABS_DEFAULT: 'YABS 基准测试', 89 | TestingType.YABS_GB5: 'GB5 YABS 基准测试', 90 | TestingType.BASH_ICU_GB5: 'GB5 bash.icu 基准测试', 91 | TestingType.SPIRITLHLS_ECS: '融合怪 spiritLHLS ecs VPS融合怪服务器测评脚本(Shell版)', 92 | TestingType.ONECLICKVIRT_ECS: '融合怪 oneclickvirt ecs VPS融合怪服务器测评脚本(GO重构版)', 93 | 94 | TestingType.MEDIA_UNLOCK_TEST: 'HsukqiLee MediaUnlockTest 更快的流媒体解锁检测工具', 95 | TestingType.CHECK_UNLOCK_MEDIA: 'lmc999 RegionRestrictionCheck 流媒体平台及游戏区域限制测试', 96 | TestingType.REGION_RESTRICTION_CHECK: 'xykt RegionRestrictionCheck 流媒体解锁检测脚本', 97 | TestingType.IP_CHECK_PLACE: 'xykt IPQuality IP质量体检报告', 98 | TestingType.NEXT_TRACE: 'nxtrace NTrace-core 可视化路由跟踪', 99 | TestingType.BACKTRACE: 'oneclickvirt backtrace 三网回程路由测试', 100 | 101 | TestingType.SPIRITLHLS_ECS_BASIC_SYS_INFO: 'spiritLHLS ecs 融合怪 基础系统信息', 102 | TestingType.YABS_BASIC_SYS_INFO: 'YABS 基础系统信息', 103 | TestingType.YABS_DISK: 'YABS 硬盘测试', 104 | 105 | TestingType.SPIRITLHLS_ECS_SPEED: 'spiritLHLS ecsspeed 自动更新测速服务器节点列表的网络基准测试脚本', 106 | TestingType.NWS_GLOBAL: '网络测试专项(全球);暂未开源', 107 | TestingType.BASH_ICU_SPEED_TEST: 'bash.icu 多功能测速脚本', 108 | TestingType.HYPER_SPEED: 'HyperSpeed 单线程三网测速', 109 | 110 | TestingType.MEMORY_CHECK: 'uselibrary memoryCheck 内存超售检查', 111 | TestingType.IP_SB: 'ip.sb IPv4 + IPv6 查询', 112 | TestingType.IP_INFO_IO: 'ipinfo.io 当前IP信息查询', 113 | TestingType.DD: 'Linux 硬盘测试专项', 114 | TestingType.DF_H: 'Linux 文件系统磁盘空间', 115 | TestingType.FREE_H: 'Linux 系统内存使用情况', 116 | TestingType.LOGIN: 'SSH 登录测试', 117 | } 118 | 119 | 120 | class Testing(BaseModel, UserMixin): 121 | user_id: Mapped[uuid.UUID] = mapped_column( 122 | UUID(), 123 | nullable=False, 124 | ) 125 | 126 | _type: Mapped[str] = mapped_column( 127 | 'type', 128 | String(32), 129 | nullable=False, 130 | ) 131 | 132 | vps_id: Mapped[uuid.UUID] = mapped_column( 133 | UUID(), 134 | nullable=False, 135 | ) 136 | 137 | vps: Mapped[VPS] = relationship( 138 | 'VPS', 139 | uselist=False, 140 | primaryjoin='foreign(Testing.vps_id)==VPS.id', 141 | lazy='subquery', 142 | ) 143 | 144 | _state: Mapped[str] = mapped_column( 145 | 'state', 146 | String(32), 147 | nullable=False, 148 | default=TestingState.CREATED, 149 | server_default=TestingState.CREATED, 150 | ) 151 | 152 | params: Mapped[str] = mapped_column( 153 | Text, 154 | nullable=False, 155 | ) 156 | 157 | result: Mapped[str] = mapped_column( 158 | Text, 159 | nullable=False, 160 | default='', 161 | ) 162 | 163 | started_at: Mapped[datetime] = mapped_column( 164 | DateTime, 165 | nullable=False, 166 | default=lambda: datetime.utcnow(), 167 | ) 168 | 169 | ended_at: Mapped[datetime] = mapped_column( 170 | DateTime, 171 | nullable=False, 172 | default=lambda: datetime.utcnow(), 173 | ) 174 | 175 | is_public: Mapped[bool] = mapped_column( 176 | Boolean, 177 | nullable=False, 178 | default=False, 179 | server_default='0', 180 | ) 181 | 182 | @cached_property 183 | def vps_name(self) -> str: 184 | return self.vps.name if self.vps else 'Not found' 185 | 186 | @cached_property 187 | def display_name(self) -> str: 188 | state = self.display_state_emoji 189 | return f'{state}【{self.display_type_zh}】{self.vps_name}' 190 | 191 | @property 192 | def type(self) -> TestingType: 193 | return TestingType(self._type) 194 | 195 | @type.setter 196 | def type(self, type: TestingType): 197 | self._type = TestingType(type).value 198 | 199 | @cached_property 200 | def display_type_zh(self) -> str: 201 | return TESTING_TYPE_ZH.get(self.type, UNKNOWN_TESTING_STATE_ZH) 202 | 203 | @property 204 | def state(self) -> TestingState: 205 | return TestingState(self._state) 206 | 207 | @state.setter 208 | def state(self, state: TestingState): 209 | self._state = TestingState(state).value 210 | 211 | @cached_property 212 | def display_state_zh(self) -> str: 213 | return TESTING_STATE_ZH.get(self.state, UNKNOWN_TESTING_STATE_ZH) 214 | 215 | @cached_property 216 | def display_state_emoji(self) -> str: 217 | return TESTING_STATE_EMOJI.get(self.state, UNKNOWN_TESTING_STATE_EMOJI) 218 | 219 | @property 220 | def is_done(self) -> bool: 221 | return True if self.state in (TestingState.SUCCESS, TestingState.FAILED) else False 222 | 223 | @property 224 | def is_running(self) -> bool: 225 | return True if self.state == TestingState.RUNNING else False 226 | 227 | def format_started_at(self, fmt='%Y-%m-%d %H:%M:%S', tz='UTC') -> str: 228 | return pendulum.from_timestamp(self.started_at.timestamp(), tz).strftime(fmt) 229 | 230 | def format_ended_at(self, fmt='%Y-%m-%d %H:%M:%S', tz='UTC') -> str: 231 | return pendulum.from_timestamp(self.ended_at.timestamp(), tz).strftime(fmt) 232 | 233 | @property 234 | def duration(self) -> timedelta: 235 | return self.ended_at - self.started_at 236 | 237 | @cached_property 238 | def terminal_cols_rows(self) -> tuple[int, int]: 239 | rows = self.result.count('\n') + 1 240 | screen = pyte.Screen(columns=1024, lines=rows) 241 | stream = pyte.Stream(screen) 242 | stream.feed(self.result) 243 | 244 | for index, line in enumerate(screen.display[::-1]): 245 | if line.rstrip(): 246 | break 247 | else: 248 | rows -= 1 249 | continue 250 | rows += 1 251 | 252 | cols = 0 253 | for index, line in enumerate(screen.display[:rows]): 254 | line = line.rstrip() 255 | cols = max(cols, len(line)) 256 | 257 | return int(cols * 1.15), rows + 1 258 | 259 | def to_dict(self) -> dict: 260 | terminal_cols, terminal_rows = self.terminal_cols_rows 261 | return { 262 | 'id': self.id, 263 | 'state': self.state, 264 | 'is_done': self.is_done, 265 | 'is_public': self.is_public, 266 | 'result': self.result, 267 | 'terminal_cols': terminal_cols, 268 | 'terminal_rows': terminal_rows, 269 | 'display_state_emoji_with_zh': f'{self.display_state_emoji} {self.display_state_zh}' 270 | } 271 | 272 | def set_result(self, result: str | None, commit: bool = True): 273 | if result is not None: 274 | self.result = result 275 | self.save(commit=commit) 276 | 277 | @classmethod 278 | def create( 279 | cls, 280 | type: TestingType, 281 | vps_or_id: VPS | uuid.UUID, 282 | params: str | dict | PydanticBaseModel | None = None, 283 | user_or_id: User | uuid.UUID = current_user, 284 | commit: bool = True, 285 | ) -> Self: 286 | if isinstance(vps_or_id, uuid.UUID): 287 | vps_id = vps_or_id 288 | else: 289 | vps_id = vps_or_id.id 290 | testing = cls(vps_id=vps_id, user_id=User.get_obj_id(user_or_id)) 291 | testing.type = type 292 | testing.set_state_created(params=params, commit=commit) 293 | return testing 294 | 295 | def set_state_created( 296 | self, 297 | params: str | dict | PydanticBaseModel | None = None, 298 | result: str | None = '', 299 | commit: bool = True, 300 | ): 301 | self.state = TestingState.CREATED 302 | if params is None: 303 | params = {} 304 | if isinstance(params, dict): 305 | params = json.dumps(params) 306 | if isinstance(params, PydanticBaseModel): 307 | params = params.model_dump_json() 308 | self.params = params 309 | self.set_result(result=result, commit=commit) 310 | 311 | def set_state_running(self, result: str | None = None, commit: bool = True): 312 | self.started_at = datetime.utcnow() 313 | self.state = TestingState.RUNNING 314 | self.set_result(result=result, commit=commit) 315 | 316 | def set_state_failed(self, result: str | None = None, commit: bool = True): 317 | self.ended_at = datetime.utcnow() 318 | self.state = TestingState.FAILED 319 | self.set_result(result=result, commit=commit) 320 | 321 | def set_state_success(self, result: str | None = None, commit: bool = True): 322 | self.ended_at = datetime.utcnow() 323 | self.state = TestingState.SUCCESS 324 | self.set_result(result=result, commit=commit) 325 | 326 | @classmethod 327 | def get_created_id_list(cls) -> list: 328 | stmt = ( 329 | select(cls.id) 330 | .where(cls._state == TestingState.CREATED.value) 331 | ) 332 | result = db.session.execute(stmt).scalars() 333 | return list(result) 334 | 335 | @classmethod 336 | def check_precreate(cls): 337 | MAX = 10 338 | current = cls.count( 339 | cls._state != TestingState.SUCCESS.value, 340 | cls._state != TestingState.FAILED.value, 341 | ) 342 | if MAX < current: 343 | raise ValueError(f'系统最多支持同时运行{MAX}个测试,当前已有{current}个测试正在运行,请稍后再试') 344 | 345 | def make_public(self, commit: bool = True): 346 | self.is_public = True 347 | self.save(commit=commit) 348 | 349 | def make_private(self, commit: bool = True): 350 | self.is_public = False 351 | self.save(commit=commit) 352 | --------------------------------------------------------------------------------