1. 状态同步加 20 秒节流、超时缩短到 8 秒;Gitea 改用 runs 列表接口一次批量取回全部任务状态,在途任务并发补查作业,消除逐任务串行 API 请求导致的后台/我的构建/详情页慢。 2. 停止时先区分 404:运行已删除则本地标记取消;Gitea 1.27 无取消 API 时,若后台配置了 Gitea 网页账号密码则模拟网页登录取消,否则回退手动链接提示。 3. 后台 Gitea 连接设置新增网页登录账号/密码两个可选项。
453 lines
19 KiB
Python
453 lines
19 KiB
Python
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_web_username = forms.CharField(
|
||
label="Gitea 网页登录账号(可选)",
|
||
max_length=100,
|
||
required=False,
|
||
widget=forms.TextInput(attrs={'placeholder': '仅 Gitea 1.27 及更早版本一键停止时需要'}),
|
||
)
|
||
gitea_web_password = forms.CharField(
|
||
label="Gitea 网页登录密码(可选)",
|
||
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}),
|
||
required=False,
|
||
)
|
||
clear_gitea_web_password = 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)
|