fix(usage-script): add input validation and boundary checks (#208)

- Backend: validate auto-query interval ≤ 1440 minutes (24 hours)
- Frontend: add number input sanitization and blur validation
- Add user-friendly error messages for invalid inputs
- Support auto-clamping to valid ranges with toast notifications
This commit is contained in:
YoVinchen
2025-11-13 11:28:48 +08:00
committed by GitHub
parent a85f24f616
commit 34f7139fda
4 changed files with 154 additions and 16 deletions

View File

@@ -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(()) Ok(())
} }

View File

@@ -131,6 +131,86 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
const [testing, setTesting] = useState(false); 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 模板 // 初始化:如果已有 accessToken 或 userId说明是 NewAPI 模板
const [selectedTemplate, setSelectedTemplate] = useState<string | null>( const [selectedTemplate, setSelectedTemplate] = useState<string | null>(
@@ -490,16 +570,25 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
<Input <Input
id="usage-timeout" id="usage-timeout"
type="number" type="number"
min={2} value={script.timeout ?? ""}
max={30} onChange={(e) => {
value={script.timeout || 10} // 输入时:只清理格式,允许临时为空,避免强制回填默认值
onChange={(e) => const cleaned = sanitizeNumberInput(e.target.value);
setScript({ setScript((prev) => ({
...script, ...prev,
timeout: parseInt(e.target.value), timeout:
}) cleaned === "" ? undefined : parseInt(cleaned, 10),
} }));
}}
onBlur={(e) => {
// 失焦时:严格验证并约束范围
const validated = validateTimeout(e.target.value);
setScript({ ...script, timeout: validated });
}}
/> />
<p className="text-xs text-muted-foreground">
{t("usageScript.timeoutHint") || "范围: 2-30 秒"}
</p>
</div> </div>
{/* 🆕 自动查询间隔 */} {/* 🆕 自动查询间隔 */}
@@ -513,13 +602,23 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
min={0} min={0}
max={1440} max={1440}
step={1} step={1}
value={script.autoQueryInterval || 0} value={script.autoQueryInterval ?? ""}
onChange={(e) => onChange={(e) => {
setScript({ // 输入时:只清理格式,允许临时为空
...script, const cleaned = sanitizeNumberInput(e.target.value);
autoQueryInterval: parseInt(e.target.value) || 0, setScript((prev) => ({
}) ...prev,
} autoQueryInterval:
cleaned === "" ? undefined : parseInt(cleaned, 10),
}));
}}
onBlur={(e) => {
// 失焦时:严格验证并约束范围
const validated = validateAndClampInterval(
e.target.value,
);
setScript({ ...script, autoQueryInterval: validated });
}}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{t("usageScript.autoQueryIntervalHint")} {t("usageScript.autoQueryIntervalHint")}

View File

@@ -378,8 +378,14 @@
"queryFailedMessage": "Query failed", "queryFailedMessage": "Query failed",
"queryScript": "Query script (JavaScript)", "queryScript": "Query script (JavaScript)",
"timeoutSeconds": "Timeout (seconds)", "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)", "autoQueryInterval": "Auto Query Interval (minutes)",
"autoQueryIntervalHint": "0 to disable, recommend 5-60 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:", "scriptHelp": "Script writing instructions:",
"configFormat": "Configuration format:", "configFormat": "Configuration format:",
"commentOptional": "optional", "commentOptional": "optional",

View File

@@ -378,8 +378,14 @@
"queryFailedMessage": "查询失败", "queryFailedMessage": "查询失败",
"queryScript": "查询脚本JavaScript", "queryScript": "查询脚本JavaScript",
"timeoutSeconds": "超时时间(秒)", "timeoutSeconds": "超时时间(秒)",
"timeoutHint": "范围: 2-30 秒",
"timeoutMustBeInteger": "超时时间必须为整数,小数部分已忽略",
"timeoutCannotBeNegative": "超时时间不能为负数",
"autoQueryInterval": "自动查询间隔(分钟)", "autoQueryInterval": "自动查询间隔(分钟)",
"autoQueryIntervalHint": "0 表示不自动查询,建议设置 5-60 分钟", "autoQueryIntervalHint": "0 表示不自动查询,建议设置 5-60 分钟",
"intervalMustBeInteger": "自动查询间隔必须为整数,小数部分已忽略",
"intervalCannotBeNegative": "自动查询间隔不能为负数",
"intervalAdjusted": "自动查询间隔已调整为 {{value}} 分钟",
"scriptHelp": "脚本编写说明:", "scriptHelp": "脚本编写说明:",
"configFormat": "配置格式:", "configFormat": "配置格式:",
"commentOptional": "可选", "commentOptional": "可选",