fix:数字配置读取有问题的问题
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
"""私聊上下文长度"""
|
"""私聊上下文长度"""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user