fix:数字配置读取有问题的问题

This commit is contained in:
SengokuCola
2026-05-04 23:09:46 +08:00
parent eea95c1961
commit 94a0cb3a62
3 changed files with 100 additions and 20 deletions

View File

@@ -31,21 +31,25 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
value, value,
onChange, onChange,
}) => { }) => {
const parseNumericValue = (rawValue: unknown, fallback: number) => { const isNumericField = schema.type === 'integer' || schema.type === 'number'
const parseNumericValue = (rawValue: unknown, fallbackValue: unknown = 0) => {
if (typeof rawValue === 'number' && Number.isFinite(rawValue)) { if (typeof rawValue === 'number' && Number.isFinite(rawValue)) {
return rawValue return rawValue
} }
if (typeof rawValue === 'string') { if (typeof rawValue === 'string') {
const parsedValue = schema.type === 'integer' const parsedValue = parseFloat(rawValue)
? parseInt(rawValue, 10)
: parseFloat(rawValue)
if (Number.isFinite(parsedValue)) { if (Number.isFinite(parsedValue)) {
return parsedValue return schema.type === 'integer' ? Math.trunc(parsedValue) : parsedValue
} }
} }
return fallback if (fallbackValue !== rawValue) {
return parseNumericValue(fallbackValue, 0)
}
return 0
} }
const renderPrimitiveArrayEditor = () => { const renderPrimitiveArrayEditor = () => {
@@ -145,16 +149,17 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
const renderInputComponent = () => { const renderInputComponent = () => {
const widget = schema['x-widget'] const widget = schema['x-widget']
const type = schema.type const type = schema.type
const resolvedWidget =
isNumericField && (widget === 'input' || widget === 'number' || !widget)
? 'number'
: widget
// x-widget 优先 // x-widget 优先
if (widget) { if (resolvedWidget) {
switch (widget) { switch (resolvedWidget) {
case 'slider': case 'slider':
return renderSlider() return renderSlider()
case 'input': case 'input':
if (type === 'integer' || type === 'number') {
return renderNumberInput()
}
return renderTextInput() return renderTextInput()
case 'number': case 'number':
return renderNumberInput() return renderNumberInput()
@@ -240,7 +245,7 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
* 渲染 Slider 组件(用于 number 类型 + x-widget: slider * 渲染 Slider 组件(用于 number 类型 + x-widget: slider
*/ */
const renderSlider = () => { const renderSlider = () => {
const numValue = parseNumericValue(value, schema.default as number ?? 0) const numValue = parseNumericValue(value, schema.default)
const min = schema.minValue ?? 0 const min = schema.minValue ?? 0
const max = schema.maxValue ?? 100 const max = schema.maxValue ?? 100
const step = schema.step ?? 1 const step = schema.step ?? 1
@@ -267,7 +272,7 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
* 渲染 Input[type="number"] 组件(用于 number/integer 类型) * 渲染 Input[type="number"] 组件(用于 number/integer 类型)
*/ */
const renderNumberInput = () => { const renderNumberInput = () => {
const numValue = parseNumericValue(value, schema.default as number ?? 0) const numValue = parseNumericValue(value, schema.default)
const min = schema.minValue const min = schema.minValue
const max = schema.maxValue const max = schema.maxValue
const step = schema.step ?? (schema.type === 'integer' ? 1 : 0.1) const step = schema.step ?? (schema.type === 'integer' ? 1 : 0.1)

View File

@@ -239,6 +239,8 @@ class ChatConfig(ConfigBase):
json_schema_extra={ json_schema_extra={
"x-widget": "input", "x-widget": "input",
"x-icon": "layers", "x-icon": "layers",
"x-layout": "inline-right",
"x-input-width": "12rem",
}, },
) )
"""上下文长度""" """上下文长度"""
@@ -248,6 +250,8 @@ class ChatConfig(ConfigBase):
json_schema_extra={ json_schema_extra={
"x-widget": "input", "x-widget": "input",
"x-icon": "layers", "x-icon": "layers",
"x-layout": "inline-right",
"x-input-width": "12rem",
}, },
) )
"""私聊上下文长度""" """私聊上下文长度"""

View File

@@ -2,19 +2,20 @@
配置管理API路由 配置管理API路由
""" """
from pathlib import Path
from typing import Annotated, Any, Dict, List, Tuple, Union, get_args, get_origin
import copy import copy
import os import os
from pathlib import Path import types
from typing import Annotated, Any, Dict, List, Tuple
import tomlkit
from fastapi import APIRouter, Body, Depends, HTTPException, Query from fastapi import APIRouter, Body, Depends, HTTPException, Query
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
import tomlkit
from src.common.logger import get_logger from src.common.logger import get_logger
from src.config.config import CONFIG_DIR, PROJECT_ROOT, Config, ModelConfig from src.config.config import CONFIG_DIR, PROJECT_ROOT, Config, ModelConfig
from src.config.config_base import AttributeData from src.config.config_base import AttributeData, ConfigBase
from src.config.model_configs import ( from src.config.model_configs import (
APIProvider, APIProvider,
ModelInfo, ModelInfo,
@@ -129,6 +130,66 @@ def _toml_to_plain_dict(obj: Any) -> Any:
return obj return obj
def _coerce_numeric_value(value: Any, target_type: Any) -> Any:
"""根据配置字段类型,把旧 WebUI 可能写入的数字字符串还原为数字。"""
if target_type is int:
if isinstance(value, str):
try:
parsed_value = float(value.strip())
except ValueError:
return value
if parsed_value.is_integer():
return int(parsed_value)
return value
if target_type is float:
if isinstance(value, str):
try:
return float(value.strip())
except ValueError:
return value
return value
return value
def _coerce_value_by_annotation(value: Any, annotation: Any) -> Any:
"""递归按 ConfigBase 字段注解修正数据类型,避免保存时把数字写成字符串。"""
value = _coerce_numeric_value(value, annotation)
origin = get_origin(annotation)
args = get_args(annotation)
if origin in {Union, types.UnionType}:
for candidate_type in args:
if candidate_type is type(None):
continue
coerced_value = _coerce_value_by_annotation(value, candidate_type)
if coerced_value != value or type(coerced_value) is not type(value):
return coerced_value
return value
if origin in {list, List} and isinstance(value, list) and args:
item_type = args[0]
return [_coerce_value_by_annotation(item, item_type) for item in value]
if origin in {dict, Dict} and isinstance(value, dict) and len(args) >= 2:
value_type = args[1]
return {key: _coerce_value_by_annotation(item, value_type) for key, item in value.items()}
if isinstance(value, dict) and isinstance(annotation, type) and issubclass(annotation, ConfigBase):
return _coerce_config_numeric_values(value, annotation)
return value
def _coerce_config_numeric_values(data: Dict[str, Any], config_type: type[ConfigBase]) -> Dict[str, Any]:
"""按配置类 schema 统一修正所有数字字段类型。"""
for field_name, field_info in config_type.model_fields.items():
if field_name in data:
data[field_name] = _coerce_value_by_annotation(data[field_name], field_info.annotation)
return data
# ===== 架构获取接口 ===== # ===== 架构获取接口 =====
@@ -347,6 +408,8 @@ async def get_model_config():
async def update_bot_config(config_data: ConfigBody): async def update_bot_config(config_data: ConfigBody):
"""更新麦麦主程序配置""" """更新麦麦主程序配置"""
try: try:
config_data = _coerce_config_numeric_values(config_data, Config)
# 验证配置数据 # 验证配置数据
try: try:
Config.from_dict(AttributeData(), copy.deepcopy(config_data)) Config.from_dict(AttributeData(), copy.deepcopy(config_data))
@@ -370,6 +433,8 @@ async def update_bot_config(config_data: ConfigBody):
async def update_model_config(config_data: ConfigBody): async def update_model_config(config_data: ConfigBody):
"""更新模型配置""" """更新模型配置"""
try: try:
config_data = _coerce_config_numeric_values(config_data, ModelConfig)
# 验证配置数据 # 验证配置数据
try: try:
ModelConfig.from_dict(AttributeData(), copy.deepcopy(config_data)) ModelConfig.from_dict(AttributeData(), copy.deepcopy(config_data))
@@ -422,10 +487,13 @@ async def update_bot_config_section(section_name: str, section_data: SectionBody
# 验证完整配置 # 验证完整配置
try: try:
Config.from_dict(AttributeData(), _toml_to_plain_dict(config_data)) plain_config_data = _coerce_config_numeric_values(_toml_to_plain_dict(config_data), Config)
Config.from_dict(AttributeData(), copy.deepcopy(plain_config_data))
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"配置数据验证失败: {str(e)}") from e raise HTTPException(status_code=400, detail=f"配置数据验证失败: {str(e)}") from e
config_data = plain_config_data
# 保存配置(格式化数组为多行,保留注释) # 保存配置(格式化数组为多行,保留注释)
save_toml_with_format(config_data, config_path) save_toml_with_format(config_data, config_path)
@@ -520,13 +588,14 @@ async def update_model_config_section(section_name: str, section_data: SectionBo
# 验证完整配置 # 验证完整配置
try: try:
ModelConfig.from_dict(AttributeData(), _toml_to_plain_dict(config_data)) plain_config_data = _coerce_config_numeric_values(_toml_to_plain_dict(config_data), ModelConfig)
ModelConfig.from_dict(AttributeData(), copy.deepcopy(plain_config_data))
except Exception as e: except Exception as e:
logger.error(f"配置数据验证失败,详细错误: {str(e)}") logger.error(f"配置数据验证失败,详细错误: {str(e)}")
# 特殊处理:如果是更新 api_providers检查是否有模型引用了已删除的provider # 特殊处理:如果是更新 api_providers检查是否有模型引用了已删除的provider
if section_name == "api_providers" and "api_provider" in str(e): if section_name == "api_providers" and "api_provider" in str(e):
provider_names = {p.get("name") for p in section_data if isinstance(p, dict)} provider_names = {p.get("name") for p in section_data if isinstance(p, dict)}
models = config_data.get("models", []) models = plain_config_data.get("models", [])
orphaned_models: List[str] = [ orphaned_models: List[str] = [
str(model_name) str(model_name)
for m in models for m in models
@@ -539,6 +608,8 @@ async def update_model_config_section(section_name: str, section_data: SectionBo
raise HTTPException(status_code=400, detail=error_msg) from e raise HTTPException(status_code=400, detail=error_msg) from e
raise HTTPException(status_code=400, detail=f"配置数据验证失败: {str(e)}") from e raise HTTPException(status_code=400, detail=f"配置数据验证失败: {str(e)}") from e
config_data = plain_config_data
# 保存配置(格式化数组为多行,保留注释) # 保存配置(格式化数组为多行,保留注释)
save_toml_with_format(config_data, config_path) save_toml_with_format(config_data, config_path)