diff --git a/src-tauri/src/services/provider.rs b/src-tauri/src/services/provider.rs index 399bd23..623b81e 100644 --- a/src-tauri/src/services/provider.rs +++ b/src-tauri/src/services/provider.rs @@ -1562,6 +1562,33 @@ impl ProviderService { } } + // 🔧 验证并清理 UsageScript 配置(所有应用类型通用) + if let Some(meta) = &provider.meta { + if let Some(usage_script) = &meta.usage_script { + Self::validate_usage_script(usage_script)?; + } + } + + Ok(()) + } + + /// 验证 UsageScript 配置(边界检查) + fn validate_usage_script(script: &crate::provider::UsageScript) -> Result<(), AppError> { + // 验证自动查询间隔 (0-1440 分钟,即最大24小时) + if let Some(interval) = script.auto_query_interval { + if interval > 1440 { + return Err(AppError::localized( + "usage_script.interval_too_large", + format!( + "自动查询间隔不能超过 1440 分钟(24小时),当前值: {interval}" + ), + format!( + "Auto query interval cannot exceed 1440 minutes (24 hours), current: {interval}" + ), + )); + } + } + Ok(()) } diff --git a/src/components/UsageScriptModal.tsx b/src/components/UsageScriptModal.tsx index 82b6235..24250a2 100644 --- a/src/components/UsageScriptModal.tsx +++ b/src/components/UsageScriptModal.tsx @@ -131,6 +131,86 @@ const UsageScriptModal: React.FC = ({ const [testing, setTesting] = useState(false); + // 🔧 输入时的格式化(宽松)- 只清理格式,不约束范围 + const sanitizeNumberInput = (value: string): string => { + // 移除所有非数字字符 + let cleaned = value.replace(/[^\d]/g, ""); + + // 移除前导零(除非输入的就是 "0") + if (cleaned.length > 1 && cleaned.startsWith("0")) { + cleaned = cleaned.replace(/^0+/, ""); + } + + return cleaned; + }; + + // 🔧 失焦时的验证(严格)- 仅确保有效整数 + const validateTimeout = (value: string): number => { + // 转换为数字 + const num = Number(value); + + // 检查是否为有效数字 + if (isNaN(num) || value.trim() === "") { + return 10; // 默认值 + } + + // 检查是否为整数 + if (!Number.isInteger(num)) { + toast.warning( + t("usageScript.timeoutMustBeInteger") || "超时时间必须为整数", + ); + } + + // 检查负数 + if (num < 0) { + toast.error( + t("usageScript.timeoutCannotBeNegative") || "超时时间不能为负数", + ); + return 10; + } + + return Math.floor(num); + }; + + // 🔧 失焦时的验证(严格)- 自动查询间隔 + const validateAndClampInterval = (value: string): number => { + // 转换为数字 + const num = Number(value); + + // 检查是否为有效数字 + if (isNaN(num) || value.trim() === "") { + return 0; // 禁用自动查询 + } + + // 检查是否为整数 + if (!Number.isInteger(num)) { + toast.warning( + t("usageScript.intervalMustBeInteger") || "自动查询间隔必须为整数", + ); + } + + // 检查负数 + if (num < 0) { + toast.error( + t("usageScript.intervalCannotBeNegative") || "自动查询间隔不能为负数", + ); + return 0; + } + + // 约束到 [0, 1440] 范围(最大24小时) + const clamped = Math.max(0, Math.min(1440, Math.floor(num))); + + // 如果值被调整,显示提示 + if (clamped !== num && num > 0) { + toast.info( + t("usageScript.intervalAdjusted", { value: clamped }) || + `自动查询间隔已调整为 ${clamped} 分钟`, + ); + } + + return clamped; + }; + // 跟踪当前选择的模板类型(用于控制高级配置的显示) // 初始化:如果已有 accessToken 或 userId,说明是 NewAPI 模板 const [selectedTemplate, setSelectedTemplate] = useState( @@ -490,16 +570,25 @@ const UsageScriptModal: React.FC = ({ - setScript({ - ...script, - timeout: parseInt(e.target.value), - }) - } + value={script.timeout ?? ""} + onChange={(e) => { + // 输入时:只清理格式,允许临时为空,避免强制回填默认值 + const cleaned = sanitizeNumberInput(e.target.value); + setScript((prev) => ({ + ...prev, + timeout: + cleaned === "" ? undefined : parseInt(cleaned, 10), + })); + }} + onBlur={(e) => { + // 失焦时:严格验证并约束范围 + const validated = validateTimeout(e.target.value); + setScript({ ...script, timeout: validated }); + }} /> +

+ {t("usageScript.timeoutHint") || "范围: 2-30 秒"} +

{/* 🆕 自动查询间隔 */} @@ -513,13 +602,23 @@ const UsageScriptModal: React.FC = ({ min={0} max={1440} step={1} - value={script.autoQueryInterval || 0} - onChange={(e) => - setScript({ - ...script, - autoQueryInterval: parseInt(e.target.value) || 0, - }) - } + value={script.autoQueryInterval ?? ""} + onChange={(e) => { + // 输入时:只清理格式,允许临时为空 + const cleaned = sanitizeNumberInput(e.target.value); + setScript((prev) => ({ + ...prev, + autoQueryInterval: + cleaned === "" ? undefined : parseInt(cleaned, 10), + })); + }} + onBlur={(e) => { + // 失焦时:严格验证并约束范围 + const validated = validateAndClampInterval( + e.target.value, + ); + setScript({ ...script, autoQueryInterval: validated }); + }} />

{t("usageScript.autoQueryIntervalHint")} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f73d9d0..46d8adc 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -378,8 +378,14 @@ "queryFailedMessage": "Query failed", "queryScript": "Query script (JavaScript)", "timeoutSeconds": "Timeout (seconds)", + "timeoutHint": "Range: 2-30 seconds", + "timeoutMustBeInteger": "Timeout must be an integer, decimal part ignored", + "timeoutCannotBeNegative": "Timeout cannot be negative", "autoQueryInterval": "Auto Query Interval (minutes)", "autoQueryIntervalHint": "0 to disable, recommend 5-60 minutes", + "intervalMustBeInteger": "Interval must be an integer, decimal part ignored", + "intervalCannotBeNegative": "Interval cannot be negative", + "intervalAdjusted": "Interval adjusted to {{value}} minutes", "scriptHelp": "Script writing instructions:", "configFormat": "Configuration format:", "commentOptional": "optional", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 967234b..177609d 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -378,8 +378,14 @@ "queryFailedMessage": "查询失败", "queryScript": "查询脚本(JavaScript)", "timeoutSeconds": "超时时间(秒)", + "timeoutHint": "范围: 2-30 秒", + "timeoutMustBeInteger": "超时时间必须为整数,小数部分已忽略", + "timeoutCannotBeNegative": "超时时间不能为负数", "autoQueryInterval": "自动查询间隔(分钟)", "autoQueryIntervalHint": "0 表示不自动查询,建议设置 5-60 分钟", + "intervalMustBeInteger": "自动查询间隔必须为整数,小数部分已忽略", + "intervalCannotBeNegative": "自动查询间隔不能为负数", + "intervalAdjusted": "自动查询间隔已调整为 {{value}} 分钟", "scriptHelp": "脚本编写说明:", "configFormat": "配置格式:", "commentOptional": "可选",