Files
rdgen/rdgenerator/forms.py
T
naeeo 6e5754fb59 停止/删除改为纯本地操作,页面不再查询 Gitea
停止只在本平台标记已取消、删除只删本地记录,均不再调用 GitHub/Gitea 的查询或取消接口,操作秒响应;构建状态完全以工作流结束回调为准,未收到回调的在途任务由纯本地 6 小时超时兜底。

移除上一版的 Gitea 网页登录取消、网页账密配置项、runs 批量状态查询及两后端 get_run/cancel_run 接口,消除页面加载时的远程等待。
2026-09-30 09:20:28 +08:00

441 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import re
from django import forms
from django.contrib.auth.models import User
from PIL import Image
from . import versions as rdeskVersions
# 应用名/公司名会被插入到单/双引号包裹的 bash sed 脚本中:& \ | 会导致替换
# 静默失败,' " $ ` 会破坏 shell 引号,CR/LF 会破坏 sed 寻址。
UNSAFE_NAME_CHARS = re.compile(r'[&\\|\'"$`\r\n]')
UNSAFE_NAME_MESSAGE = "包含构建脚本不支持的字符(& \\ | ' \" $ ` 或换行符)。"
# 统一的控件样式:文本/数字/下拉/文件输入框 .form-control,复选/单选 .form-check-input
# 注意:Django 5.x 中 NumberInput 直接继承 Input(而非 TextInput),必须单独列出
_CONTROL_WIDGETS = (
forms.TextInput, forms.NumberInput, forms.PasswordInput, forms.EmailInput,
forms.Select, forms.Textarea, forms.FileInput,
)
def apply_control_classes(form):
for field in form.fields.values():
widget = field.widget
if isinstance(widget, _CONTROL_WIDGETS):
widget.attrs.setdefault('class', 'form-control')
elif isinstance(widget, forms.CheckboxInput):
widget.attrs.setdefault('class', 'form-check-input')
class GenerateForm(forms.Form):
sh_secret_field = forms.CharField(required=False, widget=forms.HiddenInput)
# 平台与版本
platform = forms.ChoiceField(
label="目标平台",
choices=[
('windows', 'Windows 64 位'),
('windows-x86', 'Windows 32 位'),
('linux', 'Linux'),
('android', 'Android'),
('macos', 'macOS'),
],
initial='windows',
)
version = forms.ChoiceField(
label="RustDesk 版本",
choices=[('master', '开发版(nightly 每夜构建)')] + [(v, v) for v in rdeskVersions.FALLBACK_VERSIONS],
initial='1.4.9',
help_text=(
"'master' 为开发版(每夜构建),包含最新特性,但稳定性可能不如正式版本。"
"正式版列表自动取自 GitHub 官方仓库 rustdesk/rustdesk,跟随官方发版更新。"
),
)
delayFix = forms.BooleanField(label="修复使用第三方 API 时的连接延迟问题", initial=True, required=False)
# 常规设置
exename = forms.CharField(
label="配置名称",
required=True,
help_text="仅限英文、数字与下划线,不含空格;将同时作为生成文件的名称。",
)
appname = forms.CharField(
label="自定义应用名称",
required=False,
help_text="留空则使用默认名称 rustdesk。",
)
direction = forms.ChoiceField(
label="连接方向",
widget=forms.RadioSelect,
choices=[
('incoming', '仅受控(只能被他人远程连接)'),
('outgoing', '仅主控(只能主动连接他人)'),
('both', '主控 / 受控双向(默认)'),
],
initial='both',
)
installation = forms.ChoiceField(
label="是否允许安装",
choices=[
('installationY', '允许安装(默认)'),
('installationN', '禁止安装(绿色单文件运行)'),
],
initial='installationY',
)
settings = forms.ChoiceField(
label="是否允许修改设置",
choices=[
('settingsY', '允许修改设置(默认)'),
('settingsN', '禁止修改设置'),
],
initial='settingsY',
)
androidappid = forms.CharField(
label="自定义 Android 应用 ID",
required=False,
help_text="替换默认 com.carriez.flutter_hbb,留空使用默认值。",
)
# 自定义服务器
serverIP = forms.CharField(
label="ID / 中继服务器地址",
required=False,
help_text="填写自建服务器的主机名或 IP。",
)
serverPort = forms.CharField(label="服务器端口", required=False)
apiServer = forms.CharField(
label="API 服务器地址",
required=False,
help_text="留空时默认使用「服务器地址:21114」。",
)
key = forms.CharField(label="公钥(Key)", required=False)
urlLink = forms.CharField(
label="自定义官网链接",
required=False,
help_text="替换默认的 https://rustdesk.com。",
)
downloadLink = forms.CharField(
label="更新下载链接",
required=False,
help_text="替换默认的 https://rustdesk.com/download。",
)
compname = forms.CharField(
label="版权公司名称",
required=False,
help_text="替换默认的 Purslane Tech Pte. Ltd.。",
)
# 外观
iconfile = forms.FileField(
label="应用图标",
required=False,
widget=forms.FileInput(attrs={'accept': 'image/png'}),
help_text="PNG 格式,建议 256×256 正方形。",
)
logofile = forms.FileField(
label="应用 Logo",
required=False,
widget=forms.FileInput(attrs={'accept': 'image/png'}),
help_text="PNG 格式。",
)
privacyfile = forms.FileField(
label="隐私屏图片",
required=False,
widget=forms.FileInput(attrs={'accept': 'image/png'}),
help_text="PNG 格式。",
)
iconbase64 = forms.CharField(required=False, widget=forms.HiddenInput)
logobase64 = forms.CharField(required=False, widget=forms.HiddenInput)
privacybase64 = forms.CharField(required=False, widget=forms.HiddenInput)
theme = forms.ChoiceField(
label="主题",
choices=[('light', '浅色'), ('dark', '深色'), ('system', '跟随系统')],
initial='system',
)
themeDorO = forms.ChoiceField(
label="主题生效方式",
choices=[('default', '默认(允许客户端更改)'), ('override', '强制锁定(客户端无法更改)')],
initial='default',
)
# 安全
passApproveMode = forms.ChoiceField(
label="连接确认方式",
choices=[
('password', '仅通过密码接受会话'),
('click', '仅通过点击接受会话'),
('password-click', '密码或点击均可(默认)'),
],
initial='password-click',
)
permanentPassword = forms.CharField(
label="设置固定访问密码",
widget=forms.PasswordInput(),
required=False,
help_text="该密码为客户端默认密码,用户仍可在客户端自行修改。",
)
denyLan = forms.BooleanField(label="禁止局域网发现", initial=False, required=False)
enableDirectIP = forms.BooleanField(label="允许直接 IP 直连", initial=False, required=False)
autoClose = forms.BooleanField(label="用户无操作时自动断开入站会话", initial=False, required=False)
hidecm = forms.BooleanField(label="允许在远程画面中隐藏连接窗口", initial=False, required=False)
# 权限
permissionsDorO = forms.ChoiceField(
label="权限生效方式",
choices=[('default', '默认(用户可更改)'), ('override', '强制锁定(用户无法更改)')],
initial='default',
)
permissionsType = forms.ChoiceField(
label="权限预设",
choices=[('custom', '自定义'), ('full', '完全控制'), ('view', '仅查看(屏幕共享)')],
initial='custom',
)
enableKeyboard = forms.BooleanField(label="键盘 / 鼠标控制", initial=True, required=False)
enableClipboard = forms.BooleanField(label="剪贴板同步", initial=True, required=False)
enableFileTransfer = forms.BooleanField(label="文件传输", initial=True, required=False)
enableAudio = forms.BooleanField(label="音频传输", initial=True, required=False)
enableTCP = forms.BooleanField(label="TCP 隧道", initial=True, required=False)
enableRemoteRestart = forms.BooleanField(label="远程重启", initial=True, required=False)
enableRecording = forms.BooleanField(label="会话录制", initial=True, required=False)
enableBlockingInput = forms.BooleanField(label="屏蔽被控端键鼠输入", initial=True, required=False)
enableRemoteModi = forms.BooleanField(label="允许修改远程配置", initial=False, required=False)
enablePrinter = forms.BooleanField(label="远程打印机", initial=True, required=False)
enableCamera = forms.BooleanField(label="远程摄像头", initial=True, required=False)
enableTerminal = forms.BooleanField(label="远程终端", initial=True, required=False)
# 其他
removeWallpaper = forms.BooleanField(label="入站会话期间移除桌面壁纸", initial=True, required=False)
defaultManual = forms.CharField(
label="默认高级设置",
widget=forms.Textarea(attrs={'rows': 4}),
required=False,
help_text="每行一项,格式:键=值。",
)
overrideManual = forms.CharField(
label="强制高级设置",
widget=forms.Textarea(attrs={'rows': 4}),
required=False,
help_text="每行一项,格式:键=值;设置后客户端用户无法修改。",
)
# 定制功能
xOffline = forms.BooleanField(label="通讯录中离线设备显示 X 标记", initial=False, required=False)
removeNewVersionNotif = forms.BooleanField(label="移除新版本更新通知", initial=False, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
apply_control_classes(self)
def clean_version(self):
# 视图会在绑定前把 choices 刷新为官方最新列表;这里再做宽松校验,
# 兼容「打开页面后列表刷新 / 官方最新 tag 尚未进列表」等窗口期。
value = self.cleaned_data.get('version', '')
if not rdeskVersions.is_valid_version(value):
raise forms.ValidationError("版本号无效,请从列表中选择或填写形如 1.4.9 的正式版本号。")
return value
def clean_iconfile(self):
image = self.cleaned_data['iconfile']
if image:
try:
img = Image.open(image)
if img.format != 'PNG':
raise forms.ValidationError("仅支持 PNG 格式的图标。")
width, height = img.size
if width != height:
raise forms.ValidationError("应用图标必须为正方形图片。")
return image
except forms.ValidationError:
raise
except OSError:
raise forms.ValidationError("图标文件无效或已损坏。")
except Exception as e:
raise forms.ValidationError(f"处理图标时出错:{e}")
def _reject_unsafe_name_chars(self, field):
value = self.cleaned_data.get(field, '')
if value and UNSAFE_NAME_CHARS.search(value):
raise forms.ValidationError(UNSAFE_NAME_MESSAGE)
return value
def clean_appname(self):
return self._reject_unsafe_name_chars('appname')
def clean_compname(self):
return self._reject_unsafe_name_chars('compname')
class ApiTokenForm(forms.Form):
name = forms.CharField(label="令牌名称", max_length=100, required=True,
help_text="建议按用途命名,例如:办公网构建机、CI 流水线。")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
apply_control_classes(self)
class GitHubSettingsForm(forms.Form):
"""后台「构建平台配置」表单(GitHub / Gitea)。
非密钥项直接显示生效值;密钥项不回显明文,留空表示不修改,
勾选「清除」则删除后台覆盖、回退到环境变量/默认值。
"""
# 构建平台
build_platform = forms.ChoiceField(
label="构建平台",
choices=[('github', 'GitHub Actions'), ('gitea', 'Gitea Actions(自建)')],
widget=forms.RadioSelect,
required=False,
)
# Gitea 连接
gitea_server_url = forms.CharField(
label="Gitea 服务器地址",
max_length=255,
required=False,
widget=forms.TextInput(attrs={'placeholder': 'https://gitea.example.com'}),
)
gitea_owner = forms.CharField(label="Gitea 仓库属主", max_length=100, required=False)
gitea_repo = forms.CharField(label="Gitea 仓库名称", max_length=100, required=False)
gitea_branch = forms.CharField(label="Gitea 工作流分支", max_length=100, required=False)
gitea_token = forms.CharField(
label="Gitea 访问令牌",
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}),
required=False,
)
clear_gitea_token = forms.BooleanField(label="清除后台保存的令牌", required=False)
gitea_proxy = forms.CharField(
label="Gitea 访问代理",
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '内网直连通常留空,如 http://host:port'}),
required=False,
)
clear_gitea_proxy = forms.BooleanField(label="清除后台保存的代理", required=False)
# GitHub 基础连接
ghuser = forms.CharField(label="GitHub 用户名", max_length=100, required=False)
reponame = forms.CharField(label="仓库名称", max_length=100, required=False)
ghbranch = forms.CharField(label="工作流分支", max_length=100, required=False)
ghbearer = forms.CharField(
label="GitHub 访问令牌",
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}),
required=False,
)
clear_ghbearer = forms.BooleanField(label="清除后台保存的令牌", required=False)
# 回调与加密
genurl = forms.CharField(
label="本平台外部访问地址",
max_length=255,
required=False,
widget=forms.TextInput(attrs={'placeholder': 'https://rdgen.example.com'}),
)
protocol = forms.ChoiceField(
label="平台访问协议",
choices=[('https', 'https'), ('http', 'http')],
required=False,
)
zip_password = forms.CharField(
label="配置压缩包密码",
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}),
required=False,
)
clear_zip_password = forms.BooleanField(label="清除后台保存的密码", required=False)
# 高级
sh_secret = forms.CharField(
label="自托管 Runner 密钥",
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}),
required=False,
)
clear_sh_secret = forms.BooleanField(label="清除后台保存的密钥", required=False)
webhook_secret = forms.CharField(
label="回调校验密钥",
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}),
required=False,
)
clear_webhook_secret = forms.BooleanField(label="清除后台保存的密钥", required=False)
# 网络代理
gh_proxy = forms.CharField(
label="GitHub 访问代理",
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '留空直连,如 http://127.0.0.1:7890'}),
required=False,
)
clear_gh_proxy = forms.BooleanField(label="清除后台保存的代理", required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
apply_control_classes(self)
class MaintenanceSettingsForm(forms.Form):
"""后台「维护设置」:构建任务定时清理与数据库定时备份。"""
build_retention_days = forms.IntegerField(
label="构建任务保留天数",
min_value=0,
max_value=3650,
initial=30,
help_text="超过该天数的已结束任务(成功 / 失败 / 取消)及安装包产物会被自动清理;0 表示永不自动清理。进行中的任务不会被清理。",
)
db_backup_enabled = forms.BooleanField(
label="启用数据库定时备份(每天自动备份一次)",
required=False,
initial=True,
)
db_backup_keep = forms.IntegerField(
label="数据库备份保留份数",
min_value=1,
max_value=100,
initial=7,
help_text="超出份数的最旧备份会被自动删除。备份文件保存在持久化数据卷中,可在下方列表下载。",
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
apply_control_classes(self)
class AdminUserCreateForm(forms.Form):
username = forms.CharField(label="用户名", max_length=150, required=True)
email = forms.EmailField(label="邮箱", required=False)
password = forms.CharField(label="登录密码", widget=forms.PasswordInput, required=True, min_length=8,
help_text="至少 8 位。")
password_confirm = forms.CharField(label="确认密码", widget=forms.PasswordInput, required=True)
is_staff = forms.BooleanField(label="设为管理员(可访问后台管理、查看所有任务)", required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
apply_control_classes(self)
def clean_username(self):
username = self.cleaned_data['username'].strip()
if User.objects.filter(username=username).exists():
raise forms.ValidationError("该用户名已存在。")
return username
def clean(self):
cleaned = super().clean()
if cleaned.get('password') and cleaned.get('password') != cleaned.get('password_confirm'):
self.add_error('password_confirm', "两次输入的密码不一致。")
return cleaned
class AdminUserEditForm(forms.Form):
email = forms.EmailField(label="邮箱", required=False)
is_active = forms.BooleanField(label="账号启用", required=False)
is_staff = forms.BooleanField(label="管理员权限(可访问后台管理)", required=False)
new_password = forms.CharField(
label="重置密码(留空表示不修改)",
widget=forms.PasswordInput,
required=False,
min_length=8,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
apply_control_classes(self)