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

@@ -88,9 +88,10 @@ impl RtspService {
let status = self.status.clone(); let status = self.status.clone();
let client_handles = self.client_handles.clone(); let client_handles = self.client_handles.clone();
*self.status.write().await = RtspServiceStatus::Running;
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
tracing::info!("RTSP service listening on {}", bind_addr); tracing::info!("RTSP service listening on {}", bind_addr);
*status.write().await = RtspServiceStatus::Running;
loop { loop {
tokio::select! { tokio::select! {

View File

@@ -53,3 +53,59 @@ pub async fn update_rtsp_config(
Ok(Json(RtspConfigResponse::from(&new_config))) Ok(Json(RtspConfigResponse::from(&new_config)))
} }
pub async fn start_rtsp_service(
State(state): State<Arc<AppState>>,
) -> Result<Json<RtspStatusResponse>> {
let _apply_guard = try_apply_lock(&state.config_apply_locks.rtsp, "rtsp")?;
let current_config = state.config.get().rtsp.clone();
let mut start_config = current_config.clone();
start_config.enabled = true;
apply_rtsp_config(
&state,
&current_config,
&start_config,
ConfigApplyOptions::forced(),
)
.await?;
let status = {
let guard = state.rtsp.read().await;
if let Some(ref service) = *guard {
service.status().await
} else {
crate::rtsp::RtspServiceStatus::Stopped
}
};
Ok(Json(RtspStatusResponse::new(&current_config, status)))
}
pub async fn stop_rtsp_service(
State(state): State<Arc<AppState>>,
) -> Result<Json<RtspStatusResponse>> {
let _apply_guard = try_apply_lock(&state.config_apply_locks.rtsp, "rtsp")?;
let current_config = state.config.get().rtsp.clone();
let mut stop_config = current_config.clone();
stop_config.enabled = false;
apply_rtsp_config(
&state,
&current_config,
&stop_config,
ConfigApplyOptions::forced(),
)
.await?;
let status = {
let guard = state.rtsp.read().await;
if let Some(ref service) = *guard {
service.status().await
} else {
crate::rtsp::RtspServiceStatus::Stopped
}
};
Ok(Json(RtspStatusResponse::new(&current_config, status)))
}

View File

@@ -16,7 +16,7 @@ pub struct RustDeskConfigResponse {
pub device_id: String, pub device_id: String,
pub has_password: bool, pub has_password: bool,
pub has_keypair: bool, pub has_keypair: bool,
pub has_relay_key: bool, pub relay_key: Option<String>,
} }
impl From<&RustDeskConfig> for RustDeskConfigResponse { impl From<&RustDeskConfig> for RustDeskConfigResponse {
@@ -28,7 +28,7 @@ impl From<&RustDeskConfig> for RustDeskConfigResponse {
device_id: config.device_id.clone(), device_id: config.device_id.clone(),
has_password: !config.device_password.is_empty(), has_password: !config.device_password.is_empty(),
has_keypair: config.public_key.is_some() && config.private_key.is_some(), has_keypair: config.public_key.is_some() && config.private_key.is_some(),
has_relay_key: config.relay_key.is_some(), relay_key: config.relay_key.clone(),
} }
} }
} }
@@ -144,3 +144,71 @@ pub async fn get_device_password(State(state): State<Arc<AppState>>) -> Json<ser
"device_password": config.device_password "device_password": config.device_password
})) }))
} }
pub async fn start_rustdesk_service(
State(state): State<Arc<AppState>>,
) -> Result<Json<RustDeskStatusResponse>> {
let _apply_guard = try_apply_lock(&state.config_apply_locks.rustdesk, "rustdesk")?;
let current_config = state.config.get().rustdesk.clone();
let mut start_config = current_config.clone();
start_config.enabled = true;
apply_rustdesk_config(
&state,
&current_config,
&start_config,
ConfigApplyOptions::forced(),
)
.await?;
let (service_status, rendezvous_status) = {
let guard = state.rustdesk.read().await;
if let Some(ref service) = *guard {
let status = format!("{}", service.status());
let rv_status = service.rendezvous_status().map(|s| format!("{}", s));
(status, rv_status)
} else {
("not_initialized".to_string(), None)
}
};
Ok(Json(RustDeskStatusResponse {
config: RustDeskConfigResponse::from(&current_config),
service_status,
rendezvous_status,
}))
}
pub async fn stop_rustdesk_service(
State(state): State<Arc<AppState>>,
) -> Result<Json<RustDeskStatusResponse>> {
let _apply_guard = try_apply_lock(&state.config_apply_locks.rustdesk, "rustdesk")?;
let current_config = state.config.get().rustdesk.clone();
let mut stop_config = current_config.clone();
stop_config.enabled = false;
apply_rustdesk_config(
&state,
&current_config,
&stop_config,
ConfigApplyOptions::forced(),
)
.await?;
let (service_status, rendezvous_status) = {
let guard = state.rustdesk.read().await;
if let Some(ref service) = *guard {
let status = format!("{}", service.status());
let rv_status = service.rendezvous_status().map(|s| format!("{}", s));
(status, rv_status)
} else {
("not_initialized".to_string(), None)
}
};
Ok(Json(RustDeskStatusResponse {
config: RustDeskConfigResponse::from(&current_config),
service_status,
rendezvous_status,
}))
}

View File

@@ -90,7 +90,7 @@ impl VideoConfigUpdate {
} }
} }
/// Stream configuration response (includes has_turn_password) /// Stream configuration response
#[typeshare] #[typeshare]
#[derive(Debug, serde::Serialize)] #[derive(Debug, serde::Serialize)]
pub struct StreamConfigResponse { pub struct StreamConfigResponse {
@@ -104,8 +104,7 @@ pub struct StreamConfigResponse {
pub stun_server: Option<String>, pub stun_server: Option<String>,
pub turn_server: Option<String>, pub turn_server: Option<String>,
pub turn_username: Option<String>, pub turn_username: Option<String>,
/// Indicates whether TURN password has been configured (password is not returned) pub turn_password: Option<String>,
pub has_turn_password: bool,
} }
impl From<&StreamConfig> for StreamConfigResponse { impl From<&StreamConfig> for StreamConfigResponse {
@@ -120,7 +119,7 @@ impl From<&StreamConfig> for StreamConfigResponse {
stun_server: config.stun_server.clone(), stun_server: config.stun_server.clone(),
turn_server: config.turn_server.clone(), turn_server: config.turn_server.clone(),
turn_username: config.turn_username.clone(), turn_username: config.turn_username.clone(),
has_turn_password: config.turn_password.is_some(), turn_password: config.turn_password.clone(),
} }
} }
} }
@@ -798,7 +797,7 @@ pub struct RtspConfigResponse {
pub allow_one_client: bool, pub allow_one_client: bool,
pub codec: RtspCodec, pub codec: RtspCodec,
pub username: Option<String>, pub username: Option<String>,
pub has_password: bool, pub password: Option<String>,
} }
impl From<&RtspConfig> for RtspConfigResponse { impl From<&RtspConfig> for RtspConfigResponse {
@@ -811,7 +810,7 @@ impl From<&RtspConfig> for RtspConfigResponse {
allow_one_client: config.allow_one_client, allow_one_client: config.allow_one_client,
codec: config.codec.clone(), codec: config.codec.clone(),
username: config.username.clone(), username: config.username.clone(),
has_password: config.password.is_some(), password: config.password.clone(),
} }
} }
} }

View File

@@ -131,6 +131,14 @@ pub fn create_router(state: Arc<AppState>) -> Router {
"/config/rustdesk/regenerate-password", "/config/rustdesk/regenerate-password",
post(handlers::config::regenerate_device_password), post(handlers::config::regenerate_device_password),
) )
.route(
"/config/rustdesk/start",
post(handlers::config::start_rustdesk_service),
)
.route(
"/config/rustdesk/stop",
post(handlers::config::stop_rustdesk_service),
)
// RTSP configuration endpoints // RTSP configuration endpoints
.route("/config/rtsp", get(handlers::config::get_rtsp_config)) .route("/config/rtsp", get(handlers::config::get_rtsp_config))
.route("/config/rtsp", patch(handlers::config::update_rtsp_config)) .route("/config/rtsp", patch(handlers::config::update_rtsp_config))
@@ -138,6 +146,14 @@ pub fn create_router(state: Arc<AppState>) -> Router {
"/config/rtsp/status", "/config/rtsp/status",
get(handlers::config::get_rtsp_status), get(handlers::config::get_rtsp_status),
) )
.route(
"/config/rtsp/start",
post(handlers::config::start_rtsp_service),
)
.route(
"/config/rtsp/stop",
post(handlers::config::stop_rtsp_service),
)
// Web server configuration // Web server configuration
.route("/config/web", get(handlers::config::get_web_config)) .route("/config/web", get(handlers::config::get_web_config))
.route("/config/web", patch(handlers::config::update_web_config)) .route("/config/web", patch(handlers::config::update_web_config))

View File

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

View File

@@ -873,8 +873,7 @@ export default {
turnServerHint: 'TURN relay server. Strongly recommended for public deployments or strict NAT environments.', turnServerHint: 'TURN relay server. Strongly recommended for public deployments or strict NAT environments.',
turnUsername: 'TURN Username', turnUsername: 'TURN Username',
turnPassword: 'TURN Password', 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', iceConfigNote: 'Changes apply to the next WebRTC session',
}, },
virtualKeyboard: { virtualKeyboard: {
@@ -994,15 +993,10 @@ export default {
serverSettings: 'Server Settings', serverSettings: 'Server Settings',
rendezvousServer: 'ID Server', rendezvousServer: 'ID Server',
rendezvousServerPlaceholder: 'hbbs.example.com:21116', rendezvousServerPlaceholder: 'hbbs.example.com:21116',
rendezvousServerHint: 'Configure your RustDesk server address (port optional, defaults to 21116)',
rendezvousServerRequired: 'Enter the RustDesk ID server', rendezvousServerRequired: 'Enter the RustDesk ID server',
relayServer: 'Relay Server', relayServer: 'Relay Server',
relayServerPlaceholder: 'hbbr.example.com:21117', relayServerPlaceholder: 'hbbr.example.com:21117',
relayServerHint: 'Relay server address (port optional, defaults to 21117). Auto-derived if empty',
relayKey: 'Relay Key', 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', deviceInfo: 'Device Info',
deviceId: 'Device ID', deviceId: 'Device ID',
deviceIdHint: 'Use this ID in RustDesk client to connect', deviceIdHint: 'Use this ID in RustDesk client to connect',
@@ -1042,8 +1036,6 @@ export default {
usernamePlaceholder: 'Empty means no authentication', usernamePlaceholder: 'Empty means no authentication',
password: 'Password', password: 'Password',
passwordPlaceholder: 'Enter new password', passwordPlaceholder: 'Enter new password',
passwordSet: '••••••••',
passwordHint: 'Leave empty to keep current password; enter a new value to update it.',
urlPreview: 'RTSP URL Preview', urlPreview: 'RTSP URL Preview',
}, },
}, },

View File

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

View File

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

View File

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