fix: 修复 rtsp 和 RustDesk 扩展启停错误;修改部分参数描述文本

This commit is contained in:
mofeng-git
2026-05-23 15:16:39 +00:00
parent 3de72677e6
commit dc6475776e
10 changed files with 271 additions and 457 deletions

View File

@@ -1,10 +1,3 @@
/**
* 配置管理 API - 域分离架构
*
* 每个配置域video, stream, hid, msd, atx, audio有独立的 GET/PATCH 端点,
* 避免配置项之间的相互干扰。
*/
import type {
AppConfig,
AuthConfig,
@@ -38,22 +31,12 @@ import type {
import { request } from './request'
export const configApi = {
/**
* 获取完整配置
*/
getAll: () => request<AppConfig>('/config'),
}
export const authConfigApi = {
/**
* 获取认证配置
*/
get: () => request<AuthConfig>('/config/auth'),
/**
* 更新认证配置
* @param config 要更新的字段
*/
update: (config: AuthConfigUpdate) =>
request<AuthConfig>('/config/auth', {
method: 'PATCH',
@@ -62,15 +45,8 @@ export const authConfigApi = {
}
export const videoConfigApi = {
/**
* 获取视频配置
*/
get: () => request<VideoConfig>('/config/video'),
/**
* 更新视频配置
* @param config 要更新的字段(仅发送需要修改的字段)
*/
update: (config: VideoConfigUpdate) =>
request<VideoConfig>('/config/video', {
method: 'PATCH',
@@ -79,15 +55,8 @@ export const videoConfigApi = {
}
export const streamConfigApi = {
/**
* 获取流配置
*/
get: () => request<StreamConfigResponse>('/config/stream'),
/**
* 更新流配置
* @param config 要更新的字段
*/
update: (config: StreamConfigUpdate) =>
request<StreamConfigResponse>('/config/stream', {
method: 'PATCH',
@@ -96,15 +65,8 @@ export const streamConfigApi = {
}
export const hidConfigApi = {
/**
* 获取 HID 配置
*/
get: () => request<HidConfig>('/config/hid'),
/**
* 更新 HID 配置
* @param config 要更新的字段
*/
update: (config: HidConfigUpdate) =>
request<HidConfig>('/config/hid', {
method: 'PATCH',
@@ -113,15 +75,8 @@ export const hidConfigApi = {
}
export const msdConfigApi = {
/**
* 获取 MSD 配置
*/
get: () => request<MsdConfig>('/config/msd'),
/**
* 更新 MSD 配置
* @param config 要更新的字段
*/
update: (config: MsdConfigUpdate) =>
request<MsdConfig>('/config/msd', {
method: 'PATCH',
@@ -139,54 +94,29 @@ export interface WolHistoryResponse {
}
export const atxConfigApi = {
/**
* 获取 ATX 配置
*/
get: () => request<AtxConfig>('/config/atx'),
/**
* 更新 ATX 配置
* @param config 要更新的字段
*/
update: (config: AtxConfigUpdate) =>
request<AtxConfig>('/config/atx', {
method: 'PATCH',
body: JSON.stringify(config),
}),
/**
* 获取可用的 ATX 设备GPIO chips, USB relays
*/
listDevices: () => request<AtxDevices>('/devices/atx'),
/**
* 发送 Wake-on-LAN 魔术包
* @param macAddress 目标 MAC 地址
*/
sendWol: (macAddress: string) =>
request<{ success: boolean; message?: string }>('/atx/wol', {
method: 'POST',
body: JSON.stringify({ mac_address: macAddress }),
}),
/**
* 获取 WOL 历史记录(服务端持久化)
* @param limit 返回条数1-50
*/
getWolHistory: (limit = 5) =>
request<WolHistoryResponse>(`/atx/wol/history?limit=${Math.max(1, Math.min(50, limit))}`),
}
export const audioConfigApi = {
/**
* 获取音频配置
*/
get: () => request<AudioConfig>('/config/audio'),
/**
* 更新音频配置
* @param config 要更新的字段
*/
update: (config: AudioConfigUpdate) =>
request<AudioConfig>('/config/audio', {
method: 'PATCH',
@@ -195,59 +125,35 @@ export const audioConfigApi = {
}
export const extensionsApi = {
/**
* 获取所有扩展状态
*/
getAll: () => request<ExtensionsStatus>('/extensions'),
/**
* 获取单个扩展状态
*/
get: (id: string) => request<ExtensionInfo>(`/extensions/${id}`),
/**
* 启动扩展
*/
start: (id: string) =>
request<ExtensionInfo>(`/extensions/${id}/start`, {
method: 'POST',
}),
/**
* 停止扩展
*/
stop: (id: string) =>
request<ExtensionInfo>(`/extensions/${id}/stop`, {
method: 'POST',
}),
/**
* 获取扩展日志
*/
logs: (id: string, lines = 100) =>
request<ExtensionLogs>(`/extensions/${id}/logs?lines=${lines}`),
/**
* 更新 ttyd 配置
*/
updateTtyd: (config: TtydConfigUpdate) =>
request<TtydConfig>('/extensions/ttyd/config', {
method: 'PATCH',
body: JSON.stringify(config),
}),
/**
* 更新 gostc 配置
*/
updateGostc: (config: GostcConfigUpdate) =>
request<GostcConfig>('/extensions/gostc/config', {
method: 'PATCH',
body: JSON.stringify(config),
}),
/**
* 更新 easytier 配置
*/
updateEasytier: (config: EasytierConfigUpdate) =>
request<EasytierConfig>('/extensions/easytier/config', {
method: 'PATCH',
@@ -255,7 +161,6 @@ export const extensionsApi = {
}),
}
/** RustDesk 配置响应 */
export interface RustDeskConfigResponse {
enabled: boolean
rendezvous_server: string
@@ -263,17 +168,15 @@ export interface RustDeskConfigResponse {
device_id: string
has_password: boolean
has_keypair: boolean
has_relay_key: boolean
relay_key: string | null
}
/** RustDesk 状态响应 */
export interface RustDeskStatusResponse {
config: RustDeskConfigResponse
service_status: string
rendezvous_status: string | null
}
/** RustDesk 配置更新 */
export interface RustDeskConfigUpdate {
enabled?: boolean
rendezvous_server?: string
@@ -282,52 +185,37 @@ export interface RustDeskConfigUpdate {
device_password?: string
}
/** RustDesk 密码响应 */
export interface RustDeskPasswordResponse {
device_id: string
device_password: string
}
export const rustdeskConfigApi = {
/**
* 获取 RustDesk 配置
*/
get: () => request<RustDeskConfigResponse>('/config/rustdesk'),
/**
* 更新 RustDesk 配置
*/
update: (config: RustDeskConfigUpdate) =>
request<RustDeskConfigResponse>('/config/rustdesk', {
method: 'PATCH',
body: JSON.stringify(config),
}),
/**
* 获取 RustDesk 完整状态
*/
getStatus: () => request<RustDeskStatusResponse>('/config/rustdesk/status'),
/**
* 获取设备密码(管理员专用)
*/
getPassword: () => request<RustDeskPasswordResponse>('/config/rustdesk/password'),
/**
* 重新生成设备 ID
*/
regenerateId: () =>
request<RustDeskConfigResponse>('/config/rustdesk/regenerate-id', {
method: 'POST',
}),
/**
* 重新生成设备密码
*/
regeneratePassword: () =>
request<RustDeskConfigResponse>('/config/rustdesk/regenerate-password', {
method: 'POST',
}),
start: () => request<RustDeskStatusResponse>('/config/rustdesk/start', { method: 'POST' }),
stop: () => request<RustDeskStatusResponse>('/config/rustdesk/stop', { method: 'POST' }),
}
export type RtspCodec = 'h264' | 'h265'
@@ -340,7 +228,7 @@ export interface RtspConfigResponse {
allow_one_client: boolean
codec: RtspCodec
username?: string | null
has_password: boolean
password: string | null
}
export interface RtspConfigUpdate {
@@ -369,22 +257,19 @@ export const rtspConfigApi = {
}),
getStatus: () => request<RtspStatusResponse>('/config/rtsp/status'),
start: () => request<RtspStatusResponse>('/config/rtsp/start', { method: 'POST' }),
stop: () => request<RtspStatusResponse>('/config/rtsp/stop', { method: 'POST' }),
}
/** REST `/config/web` 响应(`WebConfigResponse` 别名,兼容旧命名) */
export type WebConfig = WebConfigResponse
export type { WebConfigUpdate }
export const webConfigApi = {
/**
* 获取 Web 服务器配置
*/
get: () => request<WebConfigResponse>('/config/web'),
/**
* 更新 Web 服务器配置(含可选的证书上传)
*/
update: (config: WebConfigUpdate) =>
request<WebConfigResponse>('/config/web', {
method: 'PATCH',
@@ -411,9 +296,6 @@ export const redfishConfigApi = {
}
export const systemApi = {
/**
* 重启系统
*/
restart: () =>
request<{ success: boolean; message?: string }>('/system/restart', {
method: 'POST',

View File

@@ -873,8 +873,7 @@ export default {
turnServerHint: 'TURN relay server. Strongly recommended for public deployments or strict NAT environments.',
turnUsername: 'TURN Username',
turnPassword: 'TURN Password',
turnPasswordConfigured: 'A password is already saved. Leave empty to keep the current password.',
turnCredentialsHint: 'Credentials used for TURN server authentication',
turnCredentialsHint: 'Credentials used for TURN server authentication',
iceConfigNote: 'Changes apply to the next WebRTC session',
},
virtualKeyboard: {
@@ -994,15 +993,10 @@ export default {
serverSettings: 'Server Settings',
rendezvousServer: 'ID Server',
rendezvousServerPlaceholder: 'hbbs.example.com:21116',
rendezvousServerHint: 'Configure your RustDesk server address (port optional, defaults to 21116)',
rendezvousServerRequired: 'Enter the RustDesk ID server',
relayServer: 'Relay Server',
relayServerPlaceholder: 'hbbr.example.com:21117',
relayServerHint: 'Relay server address (port optional, defaults to 21117). Auto-derived if empty',
relayKey: 'Relay Key',
relayKeyPlaceholder: 'e.g. pLU0pEj2IZnNVKzrIO1pIdwGA3dOVJJLkFIYGOCGH1E=',
relayKeySet: 'Saved (32-byte Base64, usually 44 chars; leave empty and save to keep)',
relayKeyHint: 'Same as hbbs/hbbr -k: standard Base64 decoding to exactly 32 bytes (typically 44 characters including trailing =)',
deviceInfo: 'Device Info',
deviceId: 'Device ID',
deviceIdHint: 'Use this ID in RustDesk client to connect',
@@ -1042,8 +1036,6 @@ export default {
usernamePlaceholder: 'Empty means no authentication',
password: 'Password',
passwordPlaceholder: 'Enter new password',
passwordSet: '••••••••',
passwordHint: 'Leave empty to keep current password; enter a new value to update it.',
urlPreview: 'RTSP URL Preview',
},
},

View File

@@ -719,7 +719,7 @@ export default {
autoRecommended: '自动(推荐)',
software: '软件',
supportedFormats: '支持的编码格式',
encoderHint: '硬件编码器延迟低、CPU 占用小;软件编码器兼容性更广但占用资源更多。',
encoderHint: '硬件编码器延迟CPU 占用软件编码低,画质预设更好',
hidSettings: 'HID 设置',
hidSettingsDesc: '配置键盘和鼠标控制',
hidBackend: 'HID 后端',
@@ -869,11 +869,10 @@ export default {
stunServerHint: '留空将使用 Google 公共 STUN 服务器',
turnServer: 'TURN 服务器',
turnServerPlaceholder: 'turn:turn.example.com:3478',
turnServerHint: 'TURN 中继服务器;公网部署或严格 NAT 环境强烈建议配置',
turnServerHint: 'P2P 连接失败时进行流量中继',
turnUsername: 'TURN 用户名',
turnPassword: 'TURN 密码',
turnPasswordConfigured: '密码已保存。留空则保持当前密码不变。',
turnCredentialsHint: '用于 TURN 服务器身份验证的凭据',
turnCredentialsHint: '用于 TURN 服务器身份验证的凭据',
iceConfigNote: '更改后将在下一次 WebRTC 会话建立时生效',
},
virtualKeyboard: {
@@ -993,15 +992,10 @@ export default {
serverSettings: '服务器设置',
rendezvousServer: 'ID 服务器',
rendezvousServerPlaceholder: 'hbbs.example.com:21116',
rendezvousServerHint: '请配置您的 RustDesk 服务器地址(端口可省略,默认 21116',
rendezvousServerRequired: '请填写 RustDesk ID 服务器',
relayServer: '中继服务器',
relayServerPlaceholder: 'hbbr.example.com:21117',
relayServerHint: '中继服务器地址(端口可省略,默认 21117留空则自动从 ID 服务器推导',
relayKey: '中继密钥',
relayKeyPlaceholder: '例如 pLU0pEj2IZnNVKzrIO1pIdwGA3dOVJJLkFIYGOCGH1E=',
relayKeySet: '已保存32 字节 Base64通常 44 字符;留空保存则保留)',
relayKeyHint: '与 hbbs/hbbr 的 -k 一致:标准 Base64解码后固定 32 字节(一般为 44 个字符,含末尾 =',
deviceInfo: '设备信息',
deviceId: '设备 ID',
deviceIdHint: '此 ID 用于 RustDesk 客户端连接',
@@ -1041,8 +1035,6 @@ export default {
usernamePlaceholder: '留空表示无需认证',
password: '密码',
passwordPlaceholder: '输入新密码',
passwordSet: '••••••••',
passwordHint: '留空表示保持当前密码;如需修改请输入新密码。',
urlPreview: 'RTSP 地址预览',
},
},

View File

@@ -2,85 +2,51 @@
Generated by typeshare 1.13.4
*/
/** Authentication configuration */
export interface AuthConfig {
/** Session timeout in seconds */
session_timeout_secs: number;
/** Allow multiple concurrent web sessions (single-user mode) */
single_user_allow_multiple_sessions: boolean;
/** Enable 2FA */
totp_enabled: boolean;
/** TOTP secret (encrypted) */
totp_secret?: string;
}
/** Video capture configuration */
export interface VideoConfig {
/** Video device path (e.g., /dev/video0) */
device?: string;
/** Video pixel format (e.g., "MJPEG", "YUYV", "NV12") */
format?: string;
/** Resolution width */
width: number;
/** Resolution height */
height: number;
/** Frame rate */
fps: number;
/** JPEG quality (1-100) */
quality: number;
}
/** HID backend type */
export enum HidBackend {
/** USB OTG HID gadget */
Otg = "otg",
/** CH9329 serial HID controller */
Ch9329 = "ch9329",
/** Disabled */
None = "none",
}
/** OTG USB device descriptor configuration */
export interface OtgDescriptorConfig {
/** USB Vendor ID (e.g., 0x1d6b) */
vendor_id: number;
/** USB Product ID (e.g., 0x0104) */
product_id: number;
/** Manufacturer string */
manufacturer: string;
/** Product string */
product: string;
/** Serial number (optional, auto-generated if not set) */
serial_number?: string;
}
/** OTG HID function profile */
export enum OtgHidProfile {
/** Full HID device set (keyboard + relative mouse + absolute mouse + consumer control) */
Full = "full",
/** Full HID device set without consumer control */
FullNoConsumer = "full_no_consumer",
/** Legacy profile: only keyboard */
LegacyKeyboard = "legacy_keyboard",
/** Legacy profile: only relative mouse */
LegacyMouseRelative = "legacy_mouse_relative",
/** Custom function selection */
Custom = "custom",
}
/** OTG endpoint budget policy. */
export enum OtgEndpointBudget {
/** Derive a safe default from the selected UDC. */
Auto = "auto",
/** Limit OTG gadget functions to 5 endpoints. */
Five = "five",
/** Limit OTG gadget functions to 6 endpoints. */
Six = "six",
/** Do not impose a software endpoint budget. */
Unlimited = "unlimited",
}
/** OTG HID function selection (used when profile is Custom) */
export interface OtgHidFunctions {
keyboard: boolean;
mouse_relative: boolean;
@@ -88,226 +54,104 @@ export interface OtgHidFunctions {
consumer: boolean;
}
/** HID configuration */
export interface HidConfig {
/** HID backend type */
backend: HidBackend;
/** OTG UDC (USB Device Controller) name */
otg_udc?: string;
/** OTG USB device descriptor configuration */
otg_descriptor?: OtgDescriptorConfig;
/** OTG HID function profile */
otg_profile?: OtgHidProfile;
/** OTG endpoint budget policy */
otg_endpoint_budget?: OtgEndpointBudget;
/** OTG HID function selection (used when profile is Custom) */
otg_functions?: OtgHidFunctions;
/** Enable keyboard LED/status feedback for OTG keyboard */
otg_keyboard_leds?: boolean;
/** CH9329 serial port */
ch9329_port: string;
/** CH9329 baud rate */
ch9329_baudrate: number;
/** Mouse mode: absolute or relative */
mouse_absolute: boolean;
}
/** MSD configuration */
export interface MsdConfig {
/** Enable MSD functionality */
enabled: boolean;
/** MSD base directory (absolute path) */
msd_dir: string;
}
/** Driver type for ATX key operations */
export enum AtxDriverType {
/** GPIO control via Linux character device */
Gpio = "gpio",
/** USB HID relay module */
UsbRelay = "usbrelay",
/** Serial/COM port relay (taobao LCUS type) */
Serial = "serial",
/** Disabled / Not configured */
None = "none",
}
/** Active level for GPIO pins */
export enum ActiveLevel {
/** Active high (default for most cases) */
High = "high",
/** Active low (inverted) */
Low = "low",
}
/**
* Configuration for a single ATX key (power or reset)
* This is the "four-tuple" configuration: (driver, device, pin/channel, level)
*/
export interface AtxKeyConfig {
/** Driver type (GPIO or USB Relay) */
driver: AtxDriverType;
/**
* Device path:
* - For GPIO: /dev/gpiochipX
* - For USB Relay: /dev/hidrawX
*/
device: string;
/**
* Pin or channel number:
* - For GPIO: GPIO pin number
* - For USB Relay: relay channel (0-based)
* - For Serial Relay (LCUS): relay channel (1-based)
*/
pin: number;
/** Active level (only applicable to GPIO, ignored for USB Relay) */
active_level: ActiveLevel;
/** Baud rate for serial relay (start with 9600) */
baud_rate: number;
}
/** LED sensing configuration (optional) */
export interface AtxLedConfig {
/** Whether LED sensing is enabled */
enabled: boolean;
/** GPIO chip for LED sensing */
gpio_chip: string;
/** GPIO pin for LED input */
gpio_pin: number;
/** Whether LED is active low (inverted logic) */
inverted: boolean;
}
/**
* ATX power control configuration
*
* Each ATX action (power, reset) can be independently configured with its own
* hardware binding using the four-tuple: (driver, device, pin, active_level).
*/
export interface AtxConfig {
/** Enable ATX functionality */
enabled: boolean;
/** Power button configuration (used for both short and long press) */
power: AtxKeyConfig;
/** Reset button configuration */
reset: AtxKeyConfig;
/** LED sensing configuration (optional) */
led: AtxLedConfig;
/** Network interface for WOL packets (empty = auto) */
wol_interface: string;
}
/**
* Audio configuration
*
* Note: Sample rate is fixed at 48000Hz and channels at 2 (stereo).
* These are optimal for Opus encoding and match WebRTC requirements.
*/
export interface AudioConfig {
/** Enable audio capture */
enabled: boolean;
/** ALSA device name */
device: string;
/** Audio quality preset: "voice", "balanced", "high" */
quality: string;
}
/** Stream mode */
export enum StreamMode {
/** WebRTC with H264/H265 */
WebRTC = "webrtc",
/** MJPEG over HTTP */
Mjpeg = "mjpeg",
}
/** Encoder type */
export enum EncoderType {
/** Auto-detect best encoder */
Auto = "auto",
/** Software encoder (libx264) */
Software = "software",
/** VAAPI hardware encoder */
Vaapi = "vaapi",
/** NVIDIA NVENC hardware encoder */
Nvenc = "nvenc",
/** Intel Quick Sync hardware encoder */
Qsv = "qsv",
/** AMD AMF hardware encoder */
Amf = "amf",
/** Rockchip MPP hardware encoder */
Rkmpp = "rkmpp",
/** V4L2 M2M hardware encoder */
V4l2m2m = "v4l2m2m",
}
/**
* Bitrate preset for video encoding
*
* Simplifies bitrate configuration by providing three intuitive presets
* plus a custom option for advanced users.
*/
export type BitratePreset =
/**
* Speed priority: 1 Mbps, lowest latency, smaller GOP
* Best for: slow networks, remote management, low-bandwidth scenarios
*/
| { type: "Speed", value?: undefined }
/**
* Balanced: 4 Mbps, good quality/latency tradeoff
* Best for: typical usage, recommended default
*/
| { type: "Balanced", value?: undefined }
/**
* Quality priority: 8 Mbps, best visual quality
* Best for: local network, high-bandwidth scenarios, detailed work
*/
| { type: "Quality", value?: undefined }
/** Custom bitrate in kbps (for advanced users) */
| { type: "Custom", value: number };
/** Streaming configuration */
export interface StreamConfig {
/** Stream mode */
mode: StreamMode;
/** Encoder type for H264/H265 */
encoder: EncoderType;
/** Bitrate preset (Speed/Balanced/Quality) */
bitrate_preset: BitratePreset;
/**
* Custom STUN server (e.g., "stun:stun.l.google.com:19302")
* If empty, uses public ICE servers from secrets.toml
*/
stun_server?: string;
/**
* Custom TURN server (e.g., "turn:turn.example.com:3478")
* If empty, uses public ICE servers from secrets.toml
*/
turn_server?: string;
/** TURN username */
turn_username?: string;
/** TURN password (stored encrypted in DB, not exposed via API) */
turn_password?: string;
}
/**
* Web server configuration persisted in the database (includes on-disk TLS paths).
*
* The HTTP API for `/api/config/web` uses `WebConfigResponse` instead: no path fields, includes `has_custom_cert`.
*/
export interface WebConfig {
/** HTTP port */
http_port: number;
/** HTTPS port */
https_port: number;
/** Bind addresses (preferred) */
bind_addresses: string[];
/** Bind address (legacy) */
bind_address: string;
/** Enable HTTPS */
https_enabled: boolean;
/** Custom SSL certificate path */
ssl_cert_path?: string;
/** Custom SSL key path */
ssl_key_path?: string;
}
@@ -337,74 +181,46 @@ export interface ExtensionsConfig {
easytier: EasytierConfig;
}
/** RustDesk configuration */
export interface RustDeskConfig {
/** Enable RustDesk protocol */
enabled: boolean;
/**
* Rendezvous server address (hbbs), e.g., "rs.example.com" or "192.168.1.100:21116"
* Required for RustDesk to function
*/
rendezvous_server: string;
/**
* Relay server address (hbbr), if different from rendezvous server
* Usually the same host as rendezvous server but different port (21117)
*/
relay_server?: string;
/** Device ID (9-digit number), auto-generated if empty */
device_id: string;
}
/** RTSP output codec */
export enum RtspCodec {
H264 = "h264",
H265 = "h265",
}
/** RTSP configuration */
export interface RtspConfig {
/** Enable RTSP output */
enabled: boolean;
/** Bind IP address */
bind: string;
/** RTSP TCP listen port */
port: number;
/** Stream path (without leading slash) */
path: string;
/** Allow only one client connection at a time */
allow_one_client: boolean;
/** Output codec (H264/H265) */
codec: RtspCodec;
/** Optional username for authentication */
username?: string;
}
/** Main application configuration */
export interface RedfishConfig {
enabled: boolean;
}
export interface AppConfig {
/** Whether initial setup has been completed */
initialized: boolean;
/** Authentication settings */
auth: AuthConfig;
/** Video capture settings */
video: VideoConfig;
/** HID (keyboard/mouse) settings */
hid: HidConfig;
/** Mass Storage Device settings */
msd: MsdConfig;
/** ATX power control settings */
atx: AtxConfig;
/** Audio settings */
audio: AudioConfig;
/** Streaming settings */
stream: StreamConfig;
/** Web server settings */
web: WebConfig;
/** Extensions settings (ttyd, gostc, easytier) */
extensions: ExtensionsConfig;
/** RustDesk remote access settings */
rustdesk: RustDeskConfig;
/** RTSP streaming settings */
rtsp: RtspConfig;
redfish: RedfishConfig;
}
/** Update for a single ATX key configuration */
@@ -437,13 +253,9 @@ export interface AtxConfigUpdate {
wol_interface?: string;
}
/** Available ATX devices for discovery */
export interface AtxDevices {
/** Available GPIO chips (/dev/gpiochip*) */
gpio_chips: string[];
/** Available USB HID relay devices (/dev/hidraw*) */
usb_relays: string[];
/** Available Serial ports (/dev/ttyUSB*) */
serial_ports: string[];
}
@@ -457,7 +269,6 @@ export interface AuthConfigUpdate {
single_user_allow_multiple_sessions?: boolean;
}
/** Update easytier config */
export interface EasytierConfigUpdate {
enabled?: boolean;
network_name?: string;
@@ -471,9 +282,6 @@ export type ExtensionStatus =
| { state: "stopped", data?: undefined }
| { state: "running", data: {
pid: number;
}}
| { state: "failed", data: {
error: string;
}};
export interface EasytierInfo {
@@ -516,7 +324,6 @@ export interface ExtensionsStatus {
easytier: EasytierInfo;
}
/** Update gostc config */
export interface GostcConfigUpdate {
enabled?: boolean;
addr?: string;
@@ -566,7 +373,7 @@ export interface RtspConfigResponse {
allow_one_client: boolean;
codec: RtspCodec;
username?: string;
has_password: boolean;
password?: string;
}
export interface RtspConfigUpdate {
@@ -593,7 +400,7 @@ export interface RustDeskConfigUpdate {
device_password?: string;
}
/** Stream configuration response (includes has_turn_password) */
/** Stream configuration response */
export interface StreamConfigResponse {
mode: StreamMode;
encoder: EncoderType;
@@ -605,8 +412,7 @@ export interface StreamConfigResponse {
stun_server?: string;
turn_server?: string;
turn_username?: string;
/** Indicates whether TURN password has been configured (password is not returned) */
has_turn_password: boolean;
turn_password?: string;
}
export interface StreamConfigUpdate {
@@ -629,7 +435,6 @@ export interface StreamConfigUpdate {
turn_password?: string;
}
/** Update ttyd config */
export interface TtydConfigUpdate {
enabled?: boolean;
shell?: string;
@@ -673,12 +478,6 @@ export interface WebConfigUpdate {
clear_custom_cert?: boolean;
}
/**
* Shared canonical keyboard key identifiers used across frontend and backend.
*
* The enum names intentionally mirror `KeyboardEvent.code` style values so the
* browser, virtual keyboard, and HID backend can all speak the same language.
*/
export enum CanonicalKey {
KeyA = "KeyA",
KeyB = "KeyB",

View File

@@ -14,6 +14,8 @@ import {
atxConfigApi,
extensionsApi,
redfishConfigApi,
rtspConfigApi,
rustdeskConfigApi,
systemApi,
updateApi,
usbApi,
@@ -295,7 +297,6 @@ const passwordSaving = ref(false)
const passwordSaved = ref(false)
const passwordError = ref('')
const showPasswords = ref(false)
const authConfig = ref<AuthConfig>({
session_timeout_secs: 3600 * 24,
single_user_allow_multiple_sessions: false,
@@ -526,8 +527,6 @@ const config = ref({
turn_password: '',
})
const hasTurnPassword = ref(false)
type OtgSelfCheckLevel = 'info' | 'warn' | 'error'
type OtgCheckGroupStatus = 'ok' | 'warn' | 'error' | 'skipped'
@@ -1206,16 +1205,12 @@ async function saveConfig() {
try {
if (activeSection.value === 'video') {
const turnUrl = config.value.turn_server.trim()
await configStore.updateStream({
encoder: config.value.encoder_backend as any,
stun_server: config.value.stun_server.trim(),
turn_server: turnUrl,
turn_server: config.value.turn_server.trim(),
turn_username: config.value.turn_username.trim(),
turn_password:
turnUrl === ''
? ''
: config.value.turn_password || undefined,
turn_password: config.value.turn_password.trim(),
})
await configStore.updateVideo({
device: config.value.video_device || undefined,
@@ -1303,11 +1298,9 @@ async function loadConfig() {
stun_server: stream.stun_server || '',
turn_server: stream.turn_server || '',
turn_username: stream.turn_username || '',
turn_password: '', // Password is never returned from server; set-only field
turn_password: stream.turn_password || '',
}
hasTurnPassword.value = stream.has_turn_password || false
if (hid.otg_descriptor) {
otgVendorIdHex.value = hid.otg_descriptor.vendor_id?.toString(16).padStart(4, '0') || '1d6b'
otgProductIdHex.value = hid.otg_descriptor.product_id?.toString(16).padStart(4, '0') || '0104'
@@ -1448,7 +1441,6 @@ function getExtStatusText(status: ExtensionStatus | undefined): string {
case 'unavailable': return t('extensions.unavailable')
case 'stopped': return t('extensions.stopped')
case 'running': return t('extensions.running')
case 'failed': return t('extensions.failed')
default: return t('extensions.stopped')
}
}
@@ -1459,7 +1451,6 @@ function getExtStatusClass(status: ExtensionStatus | undefined): string {
case 'unavailable': return 'bg-gray-400'
case 'stopped': return 'bg-gray-400'
case 'running': return 'bg-green-500'
case 'failed': return 'bg-red-500'
default: return 'bg-gray-400'
}
}
@@ -1616,19 +1607,23 @@ watch(
},
)
function applyRustdeskStatus(status: RustDeskStatusResponse) {
const config = status.config
rustdeskConfig.value = config
rustdeskStatus.value = status
rustdeskLocalConfig.value = {
enabled: config.enabled,
rendezvous_server: config.rendezvous_server,
relay_server: config.relay_server || '',
relay_key: config.relay_key || '',
}
}
async function loadRustdeskConfig() {
rustdeskLoading.value = true
try {
const status = await configStore.refreshRustdeskStatus()
const config = status.config
rustdeskConfig.value = config
rustdeskStatus.value = status
rustdeskLocalConfig.value = {
enabled: config.enabled,
rendezvous_server: config.rendezvous_server,
relay_server: config.relay_server || '',
relay_key: '',
}
applyRustdeskStatus(status)
} catch {
} finally {
rustdeskLoading.value = false
@@ -1642,17 +1637,17 @@ async function loadRustdeskPassword() {
}
}
function normalizeRustdeskServer(value: string, defaultPort: number): string | undefined {
function normalizeRustdeskServer(value: string, defaultPort: number): string {
const trimmed = value.trim()
if (!trimmed) return undefined
if (!trimmed) return ''
if (trimmed.includes(':')) return trimmed
return `${trimmed}:${defaultPort}`
}
/** Strip line breaks from pasted keys; empty means “do not change” on PATCH. */
function normalizeRustdeskRelayKey(value: string): string | undefined {
/** Strip line breaks from pasted keys. */
function normalizeRustdeskRelayKey(value: string): string {
const cleaned = value.replace(/\r?\n/g, '').trim()
return cleaned || undefined
return cleaned
}
function showValidationError(message: string): boolean {
@@ -2002,7 +1997,6 @@ async function saveRustdeskConfig() {
relay_key: normalizeRustdeskRelayKey(rustdeskLocalConfig.value.relay_key),
})
await loadRustdeskConfig()
rustdeskLocalConfig.value.relay_key = ''
saved.value = true
setTimeout(() => (saved.value = false), 2000)
} catch {
@@ -2042,9 +2036,8 @@ async function startRustdesk() {
rustdeskLoading.value = true
try {
await configStore.updateRustdesk({ enabled: true })
rustdeskLocalConfig.value.enabled = true
await loadRustdeskConfig()
const status = await rustdeskConfigApi.start()
applyRustdeskStatus(status)
} catch {
} finally {
rustdeskLoading.value = false
@@ -2054,9 +2047,8 @@ async function startRustdesk() {
async function stopRustdesk() {
rustdeskLoading.value = true
try {
await configStore.updateRustdesk({ enabled: false })
rustdeskLocalConfig.value.enabled = false
await loadRustdeskConfig()
const status = await rustdeskConfigApi.stop()
applyRustdeskStatus(status)
} catch {
} finally {
rustdeskLoading.value = false
@@ -2113,21 +2105,25 @@ function getRustdeskStatusClass(status: string | null | undefined): string {
}
}
function applyRtspStatus(status: RtspStatusResponse) {
rtspStatus.value = status
rtspLocalConfig.value = {
enabled: status.config.enabled,
bind: status.config.bind,
port: status.config.port,
path: status.config.path,
allow_one_client: status.config.allow_one_client,
codec: status.config.codec,
username: status.config.username || '',
password: status.config.password || '',
}
}
async function loadRtspConfig() {
rtspLoading.value = true
try {
const status = await configStore.refreshRtspStatus()
rtspStatus.value = status
rtspLocalConfig.value = {
enabled: status.config.enabled,
bind: status.config.bind,
port: status.config.port,
path: status.config.path,
allow_one_client: status.config.allow_one_client,
codec: status.config.codec,
username: status.config.username || '',
password: '',
}
applyRtspStatus(status)
} catch {
} finally {
rtspLoading.value = false
@@ -2148,14 +2144,10 @@ async function saveRtspConfig() {
username: (rtspLocalConfig.value.username || '').trim(),
}
const nextPassword = (rtspLocalConfig.value.password || '').trim()
if (nextPassword) {
update.password = nextPassword
}
update.password = (rtspLocalConfig.value.password || '').trim()
await configStore.updateRtsp(update)
await loadRtspConfig()
rtspLocalConfig.value.password = ''
saved.value = true
setTimeout(() => (saved.value = false), 2000)
} catch {
@@ -2167,9 +2159,8 @@ async function saveRtspConfig() {
async function startRtsp() {
rtspLoading.value = true
try {
await configStore.updateRtsp({ enabled: true })
rtspLocalConfig.value.enabled = true
await loadRtspConfig()
const status = await rtspConfigApi.start()
applyRtspStatus(status)
} catch {
} finally {
rtspLoading.value = false
@@ -2179,9 +2170,8 @@ async function startRtsp() {
async function stopRtsp() {
rtspLoading.value = true
try {
await configStore.updateRtsp({ enabled: false })
rtspLocalConfig.value.enabled = false
await loadRtspConfig()
const status = await rtspConfigApi.stop()
applyRtspStatus(status)
} catch {
} finally {
rtspLoading.value = false
@@ -2573,7 +2563,7 @@ watch(isWindows, () => {
<Input
id="turn-username"
v-model="config.turn_username"
:disabled="!config.turn_server"
:disabled="!config.stun_server && !config.turn_server"
/>
</div>
<div class="space-y-2">
@@ -2583,8 +2573,7 @@ watch(isWindows, () => {
id="turn-password"
v-model="config.turn_password"
:type="showPasswords ? 'text' : 'password'"
:disabled="!config.turn_server"
:placeholder="hasTurnPassword ? '••••••••' : ''"
:disabled="!config.stun_server && !config.turn_server"
/>
<button
type="button"
@@ -2596,7 +2585,6 @@ watch(isWindows, () => {
<EyeOff v-else class="h-4 w-4" />
</button>
</div>
<p v-if="hasTurnPassword && !config.turn_password" class="text-xs text-muted-foreground">{{ t('settings.turnPasswordConfigured') }}</p>
</div>
</div>
<p class="text-xs text-muted-foreground">{{ t('settings.turnCredentialsHint') }}</p>
@@ -4007,10 +3995,10 @@ watch(isWindows, () => {
</div>
<div class="flex items-center gap-2">
<Button
v-if="rtspStatus?.service_status !== 'running'"
v-if="rtspStatus?.service_status !== 'running' && rtspStatus?.service_status !== 'starting'"
size="sm"
@click="startRtsp"
:disabled="rtspLoading"
:disabled="rtspLoading || rtspStatus?.service_status === 'starting'"
>
<Play class="h-4 w-4 mr-1" />
{{ t('extensions.start') }}
@@ -4032,27 +4020,27 @@ watch(isWindows, () => {
<div class="grid gap-4">
<div class="flex items-center justify-between">
<Label>{{ t('extensions.autoStart') }}</Label>
<Switch v-model="rtspLocalConfig.enabled" />
<Switch v-model="rtspLocalConfig.enabled" :disabled="rtspStatus?.service_status === 'running'" />
</div>
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
<Label class="sm:text-right">{{ t('extensions.rtsp.bind') }}</Label>
<Input v-model="rtspLocalConfig.bind" class="sm:col-span-3" placeholder="0.0.0.0" />
<Input v-model="rtspLocalConfig.bind" class="sm:col-span-3" placeholder="0.0.0.0" :disabled="rtspStatus?.service_status === 'running'" />
</div>
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
<Label class="sm:text-right">{{ t('extensions.rtsp.port') }}</Label>
<Input v-model.number="rtspLocalConfig.port" class="sm:col-span-3" type="number" min="1" max="65535" />
<Input v-model.number="rtspLocalConfig.port" class="sm:col-span-3" type="number" min="1" max="65535" :disabled="rtspStatus?.service_status === 'running'" />
</div>
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
<Label class="sm:text-right">{{ t('extensions.rtsp.path') }}</Label>
<div class="sm:col-span-3 space-y-1">
<Input v-model="rtspLocalConfig.path" :placeholder="t('extensions.rtsp.pathPlaceholder')" />
<Input v-model="rtspLocalConfig.path" :placeholder="t('extensions.rtsp.pathPlaceholder')" :disabled="rtspStatus?.service_status === 'running'" />
<p class="text-xs text-muted-foreground">{{ t('extensions.rtsp.pathHint') }}</p>
</div>
</div>
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
<Label class="sm:text-right">{{ t('extensions.rtsp.codec') }}</Label>
<div class="sm:col-span-3 space-y-1">
<select v-model="rtspLocalConfig.codec" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
<select v-model="rtspLocalConfig.codec" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm" :disabled="rtspStatus?.service_status === 'running'">
<option value="h264">H.264</option>
<option value="h265">H.265</option>
</select>
@@ -4061,21 +4049,32 @@ watch(isWindows, () => {
</div>
<div class="flex items-center justify-between">
<Label>{{ t('extensions.rtsp.allowOneClient') }}</Label>
<Switch v-model="rtspLocalConfig.allow_one_client" />
<Switch v-model="rtspLocalConfig.allow_one_client" :disabled="rtspStatus?.service_status === 'running'" />
</div>
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
<Label class="sm:text-right">{{ t('extensions.rtsp.username') }}</Label>
<Input v-model="rtspLocalConfig.username" class="sm:col-span-3" :placeholder="t('extensions.rtsp.usernamePlaceholder')" />
<Input v-model="rtspLocalConfig.username" class="sm:col-span-3" :placeholder="t('extensions.rtsp.usernamePlaceholder')" :disabled="rtspStatus?.service_status === 'running'" />
</div>
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
<Label class="sm:text-right">{{ t('extensions.rtsp.password') }}</Label>
<div class="sm:col-span-3 space-y-1">
<Input
v-model="rtspLocalConfig.password"
type="password"
:placeholder="rtspStatus?.config?.has_password ? t('extensions.rtsp.passwordSet') : t('extensions.rtsp.passwordPlaceholder')"
/>
<p class="text-xs text-muted-foreground">{{ t('extensions.rtsp.passwordHint') }}</p>
<div class="relative">
<Input
v-model="rtspLocalConfig.password"
:type="showPasswords ? 'text' : 'password'"
:placeholder="t('extensions.rtsp.passwordPlaceholder')"
:disabled="rtspStatus?.service_status === 'running'"
/>
<button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground"
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
@click="showPasswords = !showPasswords"
>
<Eye v-if="!showPasswords" class="h-4 w-4" />
<EyeOff v-else class="h-4 w-4" />
</button>
</div>
</div>
</div>
</div>
@@ -4089,7 +4088,7 @@ watch(isWindows, () => {
</CardContent>
</Card>
<div class="flex justify-end">
<Button :disabled="loading || rtspLoading" @click="saveRtspConfig">
<Button :disabled="loading || rtspLoading || rtspStatus?.service_status === 'running'" @click="saveRtspConfig">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button>
</div>
@@ -4154,7 +4153,7 @@ watch(isWindows, () => {
<div class="grid gap-4">
<div class="flex items-center justify-between">
<Label>{{ t('extensions.autoStart') }}</Label>
<Switch v-model="rustdeskLocalConfig.enabled" />
<Switch v-model="rustdeskLocalConfig.enabled" :disabled="rustdeskStatus?.service_status === 'running'" />
</div>
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
<Label class="sm:text-right">{{ t('extensions.rustdesk.rendezvousServer') }}</Label>
@@ -4162,8 +4161,8 @@ watch(isWindows, () => {
<Input
v-model="rustdeskLocalConfig.rendezvous_server"
:placeholder="t('extensions.rustdesk.rendezvousServerPlaceholder')"
:disabled="rustdeskStatus?.service_status === 'running'"
/>
<p class="text-xs text-muted-foreground">{{ t('extensions.rustdesk.rendezvousServerHint') }}</p>
<p v-if="rustdeskLocalConfig.enabled && rustdeskValidationMessage" class="text-xs text-destructive">{{ rustdeskValidationMessage }}</p>
</div>
</div>
@@ -4173,23 +4172,33 @@ watch(isWindows, () => {
<Input
v-model="rustdeskLocalConfig.relay_server"
:placeholder="t('extensions.rustdesk.relayServerPlaceholder')"
:disabled="!rustdeskLocalConfig.rendezvous_server || rustdeskStatus?.service_status === 'running'"
/>
<p class="text-xs text-muted-foreground">{{ t('extensions.rustdesk.relayServerHint') }}</p>
</div>
</div>
<div class="grid gap-2 sm:grid-cols-4 sm:items-center">
<Label class="sm:text-right">{{ t('extensions.rustdesk.relayKey') }}</Label>
<div class="sm:col-span-3 space-y-1">
<Input
v-model="rustdeskLocalConfig.relay_key"
type="text"
maxlength="44"
autocomplete="off"
spellcheck="false"
class="font-mono"
:placeholder="rustdeskStatus?.config?.has_relay_key ? t('extensions.rustdesk.relayKeySet') : t('extensions.rustdesk.relayKeyPlaceholder')"
/>
<p class="text-xs text-muted-foreground">{{ t('extensions.rustdesk.relayKeyHint') }}</p>
<div class="relative">
<Input
v-model="rustdeskLocalConfig.relay_key"
:type="showPasswords ? 'text' : 'password'"
:disabled="!rustdeskLocalConfig.rendezvous_server || rustdeskStatus?.service_status === 'running'"
maxlength="44"
autocomplete="off"
spellcheck="false"
class="font-mono"
/>
<button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground"
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
@click="showPasswords = !showPasswords"
>
<Eye v-if="!showPasswords" class="h-4 w-4" />
<EyeOff v-else class="h-4 w-4" />
</button>
</div>
</div>
</div>
</div>
@@ -4259,7 +4268,7 @@ watch(isWindows, () => {
</Card>
<!-- Save button -->
<div class="flex justify-end">
<Button :disabled="loading" @click="saveRustdeskConfig">
<Button :disabled="loading || rustdeskStatus?.service_status === 'running'" @click="saveRustdeskConfig">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button>
</div>