Initial commit: rdgen with GitHub/Gitea dual build backend

This commit is contained in:
naeeo
2026-09-29 19:56:02 +08:00
commit a3686a49d4
103 changed files with 20007 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
import json
import re
from django.http import JsonResponse
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from . import app_settings
from .models import ApiToken
from .versions import is_valid_version
from .views import generate_custom_client, _get_run_status
# 字段取值约束(与 GenerateForm 保持一致)
PLATFORM_CHOICES = ['windows', 'windows-x86', 'linux', 'android', 'macos']
# 版本跟随 rustdesk/rustdesk 官方 tags 动态更新,这里只做格式校验
DIRECTION_CHOICES = ['incoming', 'outgoing', 'both']
INSTALLATION_CHOICES = ['installationY', 'installationN']
SETTINGS_CHOICES = ['settingsY', 'settingsN']
THEME_CHOICES = ['light', 'dark', 'system']
THEME_DORO_CHOICES = ['default', 'override']
PASS_APPROVE_MODE_CHOICES = ['password', 'click', 'password-click']
PERMISSIONS_DORO_CHOICES = ['default', 'override']
PERMISSIONS_TYPE_CHOICES = ['custom', 'full', 'view']
# 布尔字段
BOOL_FIELDS = [
'delayFix', 'xOffline', 'hidecm', 'removeNewVersionNotif',
'denyLan', 'enableDirectIP', 'autoClose',
'enableKeyboard', 'enableClipboard', 'enableFileTransfer', 'enableAudio',
'enableTCP', 'enableRemoteRestart', 'enableRecording', 'enableBlockingInput',
'enableRemoteModi', 'removeWallpaper', 'enablePrinter', 'enableCamera', 'enableTerminal',
]
# 可选字符串字段
OPTIONAL_STR_FIELDS = [
'sh_secret_field', 'serverIP', 'serverPort', 'key', 'apiServer', 'urlLink', 'downloadLink',
'appname', 'compname', 'androidappid', 'permanentPassword',
'defaultManual', 'overrideManual',
'iconbase64', 'logobase64', 'privacybase64',
]
def _authenticate(request):
"""通过 Authorization: Token <key> 请求头认证,返回 User 或 None。"""
auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Token '):
return None
key = auth_header[len('Token '):].strip()
token = (
ApiToken.objects
.filter(key=key, is_active=True)
.select_related('user')
.first()
)
if token and token.user.is_active:
token.last_used_at = timezone.now()
token.save(update_fields=['last_used_at'])
return token.user
return None
def _unauthorized():
return JsonResponse(
{"success": False, "error": "未认证或令牌无效,请在请求头中携带有效的 Token(Authorization: Token <key>)。"},
status=401,
headers={'WWW-Authenticate': 'Token'},
)
def validate_generate_params(data):
"""
按与 GenerateForm 相同的约束校验 JSON API 参数。
Returns:
(cleaned_data, errors)
"""
errors = {}
cleaned = {}
# 必填字符串字段
exename = data.get('exename', '')
if not exename:
errors['exename'] = '该字段为必填项。'
else:
cleaned['exename'] = exename
# 枚举字段
choice_validations = {
'platform': (PLATFORM_CHOICES, 'windows'),
'direction': (DIRECTION_CHOICES, 'both'),
'installation': (INSTALLATION_CHOICES, 'installationY'),
'settings': (SETTINGS_CHOICES, 'settingsY'),
'theme': (THEME_CHOICES, 'system'),
'themeDorO': (THEME_DORO_CHOICES, 'default'),
'passApproveMode': (PASS_APPROVE_MODE_CHOICES, 'password-click'),
'permissionsDorO': (PERMISSIONS_DORO_CHOICES, 'default'),
'permissionsType': (PERMISSIONS_TYPE_CHOICES, 'custom'),
}
for field, (choices, default) in choice_validations.items():
value = data.get(field, default)
if value not in choices:
errors[field] = f'取值无效,必须为以下之一:{choices}'
else:
cleaned[field] = value
# 版本:master 或形如 1.4.9 / 1.5.0-rc.1 的官方 tag(列表动态获取,不做硬枚举)
version = data.get('version', '1.4.9')
if not is_valid_version(version):
errors['version'] = "版本号无效,应为 'master' 或形如 1.4.9 的官方发布 tag。"
else:
cleaned['version'] = version
# 布尔字段
for field in BOOL_FIELDS:
value = data.get(field, False)
if not isinstance(value, bool):
errors[field] = '必须为布尔值(true/false)。'
else:
cleaned[field] = value
# 可选字符串字段
for field in OPTIONAL_STR_FIELDS:
cleaned[field] = data.get(field, '')
# 自由文本名称会进入 shell sed 脚本(与表单校验规则一致)
for field in ('appname', 'compname'):
value = cleaned.get(field, '')
if isinstance(value, str) and re.search(r'[&\\|\'"$`\r\n]', value):
errors[field] = '包含构建脚本不支持的字符(& \\ | \' " $ ` 或换行符)。'
# API 模式不使用文件上传,改用 base64 字段
cleaned['iconfile'] = None
cleaned['logofile'] = None
cleaned['privacyfile'] = None
return cleaned, errors
@csrf_exempt
def api_generate(request):
"""
POST /api/generate
接收客户端配置 JSON,触发 GitHub Actions 自定义构建。
需在请求头携带:Authorization: Token <key>
"""
if request.method != 'POST':
return JsonResponse({"success": False, "error": "仅支持 POST 请求。"}, status=405)
user = _authenticate(request)
if user is None:
return _unauthorized()
try:
data = json.loads(request.body)
except (json.JSONDecodeError, ValueError) as e:
return JsonResponse({"success": False, "error": f"JSON 格式无效:{str(e)}"}, status=400)
cleaned, errors = validate_generate_params(data)
if errors:
return JsonResponse({
"success": False,
"error": "参数校验未通过",
"details": errors
}, status=400)
full_url = f"{app_settings.get_value('PROTOCOL')}://{request.get_host()}"
result = generate_custom_client(cleaned, full_url, user=user)
if result['success']:
result['status_url'] = f"/api/status?uuid={result['uuid']}&platform={result['platform']}&filename={result['filename']}"
return JsonResponse(result)
else:
return JsonResponse({"success": False, "error": result['error']}, status=result.get('status_code', 500))
@csrf_exempt
def api_status(request):
"""
GET /api/status?uuid=<uuid>
查询指定构建任务的状态。需携带 API Token。
"""
if request.method != 'GET':
return JsonResponse({"success": False, "error": "仅支持 GET 请求。"}, status=405)
if _authenticate(request) is None:
return _unauthorized()
uuid_val = request.GET.get('uuid')
if not uuid_val:
return JsonResponse({"success": False, "error": "缺少必填参数:uuid"}, status=400)
filename = request.GET.get('filename', '')
platform = request.GET.get('platform', '')
result = _get_run_status(uuid_val)
if not result['found']:
return JsonResponse({"success": False, "error": "未找到对应的构建任务"}, status=404)
response_data = {
"success": True,
"status": result['status'],
"status_label": result['gh_run'].status_label(),
"uuid": uuid_val,
"log_url": result['github_log_url'],
}
if filename:
response_data['filename'] = filename
if platform:
response_data['platform'] = platform
return JsonResponse(response_data)