├── 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/jiduoduo/static/logout.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
8 | -
9 |
17 |
18 | {% for testing in testing_list %}
19 | {{ utils.render_testing_list_item(testing) }}
20 | {% endfor %}
21 |
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 |
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 |
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 |
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 |
12 | -
13 |
21 |
22 | {% for vps in vps_list %}
23 | -
24 |
29 |
30 | {% endfor %}
31 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
7 |
8 | {% if current_user.is_authenticated %}
9 | -
10 |
18 |
19 |
20 | -
21 |
29 |
30 |
31 |
32 | {% else %}
33 | -
34 |
40 |
41 | {% endif %}
42 |
43 | -
44 |
45 | 已公开的最近运行的测试:
46 |
47 |
48 | {% for testing in testing_list %}
49 | {{ utils.render_testing_list_item(testing) }}
50 | {% endfor %}
51 |
52 |
53 |
54 |
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 |
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":"",
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":"[](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 |
--------------------------------------------------------------------------------
/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 |
94 | {{ message }}
95 |
96 |
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 | 
20 |
21 | ### 目前支持 20+ 种常见测试(正在陆续增加中...)
22 |
23 | 
24 |
25 | ### GB5测试截图
26 |
27 | 
28 |
29 | ### 融合怪测试截图
30 |
31 | 
32 |
33 | ### IP质量体检报告截图
34 |
35 | 
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 |
--------------------------------------------------------------------------------
/src/jiduoduo/static/chickens-chick-svgrepo-com.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------