fix:数字配置读取有问题的问题
This commit is contained in:
@@ -31,21 +31,25 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
value,
|
||||
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)) {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
if (typeof rawValue === 'string') {
|
||||
const parsedValue = schema.type === 'integer'
|
||||
? parseInt(rawValue, 10)
|
||||
: parseFloat(rawValue)
|
||||
const parsedValue = parseFloat(rawValue)
|
||||
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 = () => {
|
||||
@@ -145,16 +149,17 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
const renderInputComponent = () => {
|
||||
const widget = schema['x-widget']
|
||||
const type = schema.type
|
||||
const resolvedWidget =
|
||||
isNumericField && (widget === 'input' || widget === 'number' || !widget)
|
||||
? 'number'
|
||||
: widget
|
||||
|
||||
// x-widget 优先
|
||||
if (widget) {
|
||||
switch (widget) {
|
||||
if (resolvedWidget) {
|
||||
switch (resolvedWidget) {
|
||||
case 'slider':
|
||||
return renderSlider()
|
||||
case 'input':
|
||||
if (type === 'integer' || type === 'number') {
|
||||
return renderNumberInput()
|
||||
}
|
||||
return renderTextInput()
|
||||
case 'number':
|
||||
return renderNumberInput()
|
||||
@@ -240,7 +245,7 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
* 渲染 Slider 组件(用于 number 类型 + x-widget: slider)
|
||||
*/
|
||||
const renderSlider = () => {
|
||||
const numValue = parseNumericValue(value, schema.default as number ?? 0)
|
||||
const numValue = parseNumericValue(value, schema.default)
|
||||
const min = schema.minValue ?? 0
|
||||
const max = schema.maxValue ?? 100
|
||||
const step = schema.step ?? 1
|
||||
@@ -267,7 +272,7 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
* 渲染 Input[type="number"] 组件(用于 number/integer 类型)
|
||||
*/
|
||||
const renderNumberInput = () => {
|
||||
const numValue = parseNumericValue(value, schema.default as number ?? 0)
|
||||
const numValue = parseNumericValue(value, schema.default)
|
||||
const min = schema.minValue
|
||||
const max = schema.maxValue
|
||||
const step = schema.step ?? (schema.type === 'integer' ? 1 : 0.1)
|
||||
|
||||
@@ -239,6 +239,8 @@ class ChatConfig(ConfigBase):
|
||||
json_schema_extra={
|
||||
"x-widget": "input",
|
||||
"x-icon": "layers",
|
||||
"x-layout": "inline-right",
|
||||
"x-input-width": "12rem",
|
||||
},
|
||||
)
|
||||
"""上下文长度"""
|
||||
@@ -248,6 +250,8 @@ class ChatConfig(ConfigBase):
|
||||
json_schema_extra={
|
||||
"x-widget": "input",
|
||||
"x-icon": "layers",
|
||||
"x-layout": "inline-right",
|
||||
"x-input-width": "12rem",
|
||||
},
|
||||
)
|
||||
"""私聊上下文长度"""
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
配置管理API路由
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Dict, List, Tuple, Union, get_args, get_origin
|
||||
import copy
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Dict, List, Tuple
|
||||
import types
|
||||
|
||||
import tomlkit
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel, Field
|
||||
import tomlkit
|
||||
|
||||
from src.common.logger import get_logger
|
||||
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 (
|
||||
APIProvider,
|
||||
ModelInfo,
|
||||
@@ -129,6 +130,66 @@ def _toml_to_plain_dict(obj: Any) -> Any:
|
||||
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):
|
||||
"""更新麦麦主程序配置"""
|
||||
try:
|
||||
config_data = _coerce_config_numeric_values(config_data, Config)
|
||||
|
||||
# 验证配置数据
|
||||
try:
|
||||
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):
|
||||
"""更新模型配置"""
|
||||
try:
|
||||
config_data = _coerce_config_numeric_values(config_data, ModelConfig)
|
||||
|
||||
# 验证配置数据
|
||||
try:
|
||||
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:
|
||||
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:
|
||||
raise HTTPException(status_code=400, detail=f"配置数据验证失败: {str(e)}") from e
|
||||
|
||||
config_data = plain_config_data
|
||||
|
||||
# 保存配置(格式化数组为多行,保留注释)
|
||||
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:
|
||||
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:
|
||||
logger.error(f"配置数据验证失败,详细错误: {str(e)}")
|
||||
# 特殊处理:如果是更新 api_providers,检查是否有模型引用了已删除的provider
|
||||
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)}
|
||||
models = config_data.get("models", [])
|
||||
models = plain_config_data.get("models", [])
|
||||
orphaned_models: List[str] = [
|
||||
str(model_name)
|
||||
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=f"配置数据验证失败: {str(e)}") from e
|
||||
|
||||
config_data = plain_config_data
|
||||
|
||||
# 保存配置(格式化数组为多行,保留注释)
|
||||
save_toml_with_format(config_data, config_path)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user