diff --git a/setting/config/config.go b/setting/config/config.go new file mode 100644 index 00000000..3af51b14 --- /dev/null +++ b/setting/config/config.go @@ -0,0 +1,259 @@ +package config + +import ( + "encoding/json" + "one-api/common" + "reflect" + "strconv" + "strings" + "sync" +) + +// ConfigManager 统一管理所有配置 +type ConfigManager struct { + configs map[string]interface{} + mutex sync.RWMutex +} + +var GlobalConfig = NewConfigManager() + +func NewConfigManager() *ConfigManager { + return &ConfigManager{ + configs: make(map[string]interface{}), + } +} + +// Register 注册一个配置模块 +func (cm *ConfigManager) Register(name string, config interface{}) { + cm.mutex.Lock() + defer cm.mutex.Unlock() + cm.configs[name] = config +} + +// Get 获取指定配置模块 +func (cm *ConfigManager) Get(name string) interface{} { + cm.mutex.RLock() + defer cm.mutex.RUnlock() + return cm.configs[name] +} + +// LoadFromDB 从数据库加载配置 +func (cm *ConfigManager) LoadFromDB(options map[string]string) error { + cm.mutex.Lock() + defer cm.mutex.Unlock() + + for name, config := range cm.configs { + prefix := name + "." + configMap := make(map[string]string) + + // 收集属于此配置的所有选项 + for key, value := range options { + if strings.HasPrefix(key, prefix) { + configKey := strings.TrimPrefix(key, prefix) + configMap[configKey] = value + } + } + + // 如果找到配置项,则更新配置 + if len(configMap) > 0 { + if err := updateConfigFromMap(config, configMap); err != nil { + common.SysError("failed to update config " + name + ": " + err.Error()) + continue + } + } + } + + return nil +} + +// SaveToDB 将配置保存到数据库 +func (cm *ConfigManager) SaveToDB(updateFunc func(key, value string) error) error { + cm.mutex.RLock() + defer cm.mutex.RUnlock() + + for name, config := range cm.configs { + configMap, err := configToMap(config) + if err != nil { + return err + } + + for key, value := range configMap { + dbKey := name + "." + key + if err := updateFunc(dbKey, value); err != nil { + return err + } + } + } + + return nil +} + +// 辅助函数:将配置对象转换为map +func configToMap(config interface{}) (map[string]string, error) { + result := make(map[string]string) + + val := reflect.ValueOf(config) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + if val.Kind() != reflect.Struct { + return nil, nil + } + + typ := val.Type() + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + + // 跳过未导出字段 + if !fieldType.IsExported() { + continue + } + + // 获取json标签作为键名 + key := fieldType.Tag.Get("json") + if key == "" || key == "-" { + key = fieldType.Name + } + + // 处理不同类型的字段 + var strValue string + switch field.Kind() { + case reflect.String: + strValue = field.String() + case reflect.Bool: + strValue = strconv.FormatBool(field.Bool()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + strValue = strconv.FormatInt(field.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + strValue = strconv.FormatUint(field.Uint(), 10) + case reflect.Float32, reflect.Float64: + strValue = strconv.FormatFloat(field.Float(), 'f', -1, 64) + case reflect.Map, reflect.Slice, reflect.Struct: + // 复杂类型使用JSON序列化 + bytes, err := json.Marshal(field.Interface()) + if err != nil { + return nil, err + } + strValue = string(bytes) + default: + // 跳过不支持的类型 + continue + } + + result[key] = strValue + } + + return result, nil +} + +// 辅助函数:从map更新配置对象 +func updateConfigFromMap(config interface{}, configMap map[string]string) error { + val := reflect.ValueOf(config) + if val.Kind() != reflect.Ptr { + return nil + } + val = val.Elem() + + if val.Kind() != reflect.Struct { + return nil + } + + typ := val.Type() + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) + + // 跳过未导出字段 + if !fieldType.IsExported() { + continue + } + + // 获取json标签作为键名 + key := fieldType.Tag.Get("json") + if key == "" || key == "-" { + key = fieldType.Name + } + + // 检查map中是否有对应的值 + strValue, ok := configMap[key] + if !ok { + continue + } + + // 根据字段类型设置值 + if !field.CanSet() { + continue + } + + switch field.Kind() { + case reflect.String: + field.SetString(strValue) + case reflect.Bool: + boolValue, err := strconv.ParseBool(strValue) + if err != nil { + continue + } + field.SetBool(boolValue) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + intValue, err := strconv.ParseInt(strValue, 10, 64) + if err != nil { + continue + } + field.SetInt(intValue) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + uintValue, err := strconv.ParseUint(strValue, 10, 64) + if err != nil { + continue + } + field.SetUint(uintValue) + case reflect.Float32, reflect.Float64: + floatValue, err := strconv.ParseFloat(strValue, 64) + if err != nil { + continue + } + field.SetFloat(floatValue) + case reflect.Map, reflect.Slice, reflect.Struct: + // 复杂类型使用JSON反序列化 + err := json.Unmarshal([]byte(strValue), field.Addr().Interface()) + if err != nil { + continue + } + } + } + + return nil +} + +// ConfigToMap 将配置对象转换为map(导出函数) +func ConfigToMap(config interface{}) (map[string]string, error) { + return configToMap(config) +} + +// UpdateConfigFromMap 从map更新配置对象(导出函数) +func UpdateConfigFromMap(config interface{}, configMap map[string]string) error { + return updateConfigFromMap(config, configMap) +} + +// ExportAllConfigs 导出所有已注册的配置为扁平结构 +func (cm *ConfigManager) ExportAllConfigs() map[string]string { + cm.mutex.RLock() + defer cm.mutex.RUnlock() + + result := make(map[string]string) + + for name, cfg := range cm.configs { + configMap, err := ConfigToMap(cfg) + if err != nil { + continue + } + + // 使用 "模块名.配置项" 的格式添加到结果中 + for key, value := range configMap { + result[name+"."+key] = value + } + } + + return result +} diff --git a/setting/model_setting/claude.go b/setting/model_setting/claude.go index 419d09c4..b2252440 100644 --- a/setting/model_setting/claude.go +++ b/setting/model_setting/claude.go @@ -1 +1,49 @@ package model_setting + +import ( + "net/http" + "one-api/setting/config" +) + +//var claudeHeadersSettings = map[string][]string{} +// +//var ClaudeThinkingAdapterEnabled = true +//var ClaudeThinkingAdapterMaxTokens = 8192 +//var ClaudeThinkingAdapterBudgetTokensPercentage = 0.8 + +// ClaudeSettings 定义Claude模型的配置 +type ClaudeSettings struct { + HeadersSettings map[string][]string `json:"headers_settings"` + ThinkingAdapterEnabled bool `json:"thinking_adapter_enabled"` + ThinkingAdapterMaxTokens int `json:"thinking_adapter_max_tokens"` + ThinkingAdapterBudgetTokensPercentage float64 `json:"thinking_adapter_budget_tokens_percentage"` +} + +// 默认配置 +var defaultClaudeSettings = ClaudeSettings{ + HeadersSettings: map[string][]string{}, + ThinkingAdapterEnabled: true, + ThinkingAdapterMaxTokens: 8192, + ThinkingAdapterBudgetTokensPercentage: 0.8, +} + +// 全局实例 +var claudeSettings = defaultClaudeSettings + +func init() { + // 注册到全局配置管理器 + config.GlobalConfig.Register("claude", &claudeSettings) +} + +// GetClaudeSettings 获取Claude配置 +func GetClaudeSettings() *ClaudeSettings { + return &claudeSettings +} + +func (c *ClaudeSettings) WriteHeaders(headers *http.Header) { + for key, values := range c.HeadersSettings { + for _, value := range values { + headers.Add(key, value) + } + } +} diff --git a/web/src/components/ModelSetting.js b/web/src/components/ModelSetting.js index f7516157..219caee1 100644 --- a/web/src/components/ModelSetting.js +++ b/web/src/components/ModelSetting.js @@ -6,12 +6,17 @@ import { API, showError, showSuccess } from '../helpers'; import SettingsChats from '../pages/Setting/Operation/SettingsChats.js'; import { useTranslation } from 'react-i18next'; import SettingGeminiModel from '../pages/Setting/Model/SettingGeminiModel.js'; +import SettingClaudeModel from '../pages/Setting/Model/SettingClaudeModel.js'; const ModelSetting = () => { const { t } = useTranslation(); let [inputs, setInputs] = useState({ - GeminiSafetySettings: '', - GeminiVersionSettings: '', + 'gemini.safety_settings': '', + 'gemini.version_settings': '', + 'claude.headers_settings': '', + 'claude.thinking_adapter_enabled': true, + 'claude.thinking_adapter_max_tokens': 8192, + 'claude.thinking_adapter_budget_tokens_percentage': 0.8, }); let [loading, setLoading] = useState(false); @@ -23,8 +28,9 @@ const ModelSetting = () => { let newInputs = {}; data.forEach((item) => { if ( - item.key === 'GeminiSafetySettings' || - item.key === 'GeminiVersionSettings' + item.key === 'gemini.safety_settings' || + item.key === 'gemini.version_settings' || + item.key === 'claude.headers_settings' ) { item.value = JSON.stringify(JSON.parse(item.value), null, 2); } @@ -65,6 +71,10 @@ const ModelSetting = () => { + {/* Claude */} + + + ); diff --git a/web/src/pages/Setting/Model/SettingClaudeModel.js b/web/src/pages/Setting/Model/SettingClaudeModel.js new file mode 100644 index 00000000..98c6b49e --- /dev/null +++ b/web/src/pages/Setting/Model/SettingClaudeModel.js @@ -0,0 +1,152 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui'; +import { + compareObjects, + API, + showError, + showSuccess, + showWarning, verifyJSON +} from '../../../helpers'; +import { useTranslation } from 'react-i18next'; +import Text from '@douyinfe/semi-ui/lib/es/typography/text'; + +const CLAUDE_HEADER = { + 'anthropic-beta': ['output-128k-2025-02-19', 'token-efficient-tools-2025-02-19'], +}; + +export default function SettingClaudeModel(props) { + const { t } = useTranslation(); + + const [loading, setLoading] = useState(false); + const [inputs, setInputs] = useState({ + 'claude.headers_settings': '', + 'claude.thinking_adapter_enabled': true, + 'claude.thinking_adapter_max_tokens': 8192, + 'claude.thinking_adapter_budget_tokens_percentage': 0.8, + }); + const refForm = useRef(); + const [inputsRow, setInputsRow] = useState(inputs); + + function onSubmit() { + const updateArray = compareObjects(inputs, inputsRow); + if (!updateArray.length) return showWarning(t('你似乎并没有修改什么')); + const requestQueue = updateArray.map((item) => { + let value = ''; + if (typeof inputs[item.key] === 'boolean') { + value = String(inputs[item.key]); + } else { + value = inputs[item.key]; + } + return API.put('/api/option/', { + key: item.key, + value, + }); + }); + setLoading(true); + Promise.all(requestQueue) + .then((res) => { + if (requestQueue.length === 1) { + if (res.includes(undefined)) return; + } else if (requestQueue.length > 1) { + if (res.includes(undefined)) return showError(t('部分保存失败,请重试')); + } + showSuccess(t('保存成功')); + props.refresh(); + }) + .catch(() => { + showError(t('保存失败,请重试')); + }) + .finally(() => { + setLoading(false); + }); + } + + useEffect(() => { + const currentInputs = {}; + for (let key in props.options) { + if (Object.keys(inputs).includes(key)) { + currentInputs[key] = props.options[key]; + } + } + setInputs(currentInputs); + setInputsRow(structuredClone(currentInputs)); + refForm.current.setValues(currentInputs); + }, [props.options]); + + return ( + <> + +
(refForm.current = formAPI)} + style={{ marginBottom: 15 }} + > + + + + verifyJSON(value), + message: t('不是合法的 JSON 字符串') + } + ]} + onChange={(value) => setInputs({ ...inputs, 'claude.headers_settings': value })} + /> + + + + + setInputs({ ...inputs, 'claude.thinking_adapter_enabled': value })} + /> + + + + + {/*//展示MaxTokens和BudgetTokens的计算公式, 并展示实际数字*/} + + {t('Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比')} + + + + + + setInputs({ ...inputs, 'claude.thinking_adapter_max_tokens': value })} + /> + + + setInputs({ ...inputs, 'claude.thinking_adapter_budget_tokens_percentage': value })} + /> + + + + + + + +
+
+ + ); +}