diff --git a/dashboard/src/routes/config/bot.tsx b/dashboard/src/routes/config/bot.tsx index 84133f92..f62521d9 100644 --- a/dashboard/src/routes/config/bot.tsx +++ b/dashboard/src/routes/config/bot.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { parse as parseToml } from 'smol-toml' import { AlertDescription, Alert } from '@/components/ui/alert' @@ -20,12 +20,13 @@ import { CodeEditor } from '@/components' import { DynamicConfigForm } from '@/components/dynamic-form' import { RestartOverlay } from '@/components/restart-overlay' import { useToast } from '@/hooks/use-toast' -import { getBotConfig, getBotConfigRaw, updateBotConfig, updateBotConfigRaw } from '@/lib/config-api' +import { getBotConfig, getBotConfigRaw, getBotConfigSchema, updateBotConfig, updateBotConfigRaw } from '@/lib/config-api' import { fieldHooks } from '@/lib/field-hooks' import { RestartProvider, useRestart } from '@/lib/restart-context' import { Code2, Info, Layout, Power, Save } from 'lucide-react' +import type { ConfigSchema } from '@/types/config-schema' import type { BotConfig, ChatConfig, @@ -71,6 +72,58 @@ import { /** Toast 显示前的延迟时间 (毫秒) */ const TOAST_DISPLAY_DELAY = 500 +/** Tab 标签页的首选排列顺序 (host field name) */ +const TAB_ORDER = [ + 'bot', 'personality', 'chat', 'expression', 'emoji', + 'response_post_process', 'dream', 'lpmm_knowledge', 'webui', 'debug', +] + +// ==================== Tab 分组类型与构建 ==================== +interface TabGroup { + id: string + label: string + icon: string + sections: string[] +} + +/** + * 从 schema 的 nested 字段解析出 tab 分组信息。 + * - 有 uiLabel 且无 uiParent → 独立 tab (host) + * - 有 uiParent → 归入对应 host tab 的 sections + */ +function buildTabGroupsFromSchema(schema: ConfigSchema): TabGroup[] { + const nested = schema.nested || {} + const hosts = new Map() + const children: Array<{ fieldName: string; parentId: string }> = [] + + for (const [fieldName, fieldSchema] of Object.entries(nested)) { + if (fieldSchema.uiLabel && !fieldSchema.uiParent) { + hosts.set(fieldName, { + id: fieldName, + label: fieldSchema.uiLabel, + icon: fieldSchema.uiIcon || '', + sections: [fieldName], + }) + } else if (fieldSchema.uiParent) { + children.push({ fieldName, parentId: fieldSchema.uiParent }) + } + } + + for (const { fieldName, parentId } of children) { + const parent = hosts.get(parentId) + if (parent) { + parent.sections.push(fieldName) + } + } + + // 按 TAB_ORDER 排序;未列入的 tab 追加到末尾 + return Array.from(hosts.values()).sort((a, b) => { + const ai = TAB_ORDER.indexOf(a.id) + const bi = TAB_ORDER.indexOf(b.id) + return (ai === -1 ? Infinity : ai) - (bi === -1 ? Infinity : bi) + }) +} + // 主导出组件:包装 RestartProvider export function BotConfigPage() { return ( @@ -116,6 +169,9 @@ function BotConfigPageContent() { const [telemetryConfig, setTelemetryConfig] = useState(null) const [webuiConfig, setWebuiConfig] = useState(null) + // Schema 状态(用于动态 tab 分组) + const [configSchema, setConfigSchema] = useState(null) + // 用于标记初始加载和配置缓存 const initialLoadRef = useRef(true) const configRef = useRef>({}) @@ -292,7 +348,7 @@ function BotConfigPageContent() { const loadConfig = useCallback(async () => { try { setLoading(true) - const result = await getBotConfig() + const [result, schemaResult] = await Promise.all([getBotConfig(), getBotConfigSchema()]) if (!result.success) { toast({ title: '加载失败', @@ -303,6 +359,9 @@ function BotConfigPageContent() { return } parseAndSetConfig((result.data as Record).config as Record) + if (schemaResult.success && schemaResult.data) { + setConfigSchema((schemaResult.data as unknown as Record).schema as ConfigSchema) + } setHasUnsavedChanges(false) initialLoadRef.current = false @@ -544,6 +603,12 @@ function BotConfigPageContent() { } } + // 根据 schema 构建 tab 分组 + const tabGroups = useMemo(() => { + if (!configSchema) return [] + return buildTabGroupsFromSchema(configSchema) + }, [configSchema]) + if (loading) { return ( @@ -682,125 +747,31 @@ function BotConfigPageContent() { {/* 可视化模式 */} {editMode === 'visual' && ( - <> - {/* 标签页 */} - - - 基本信息 - 人格 - 聊天 - 表达 - 功能 - 处理 - 做梦 - 知识库 - WebUI - 其他 - - {/* 基本信息 */} - - {botConfig && } - - - {/* 人格配置 */} - - {personalityConfig && ( - - )} - - - {/* 聊天配置 */} - - {chatConfig && ( - { - if (field === 'chat') { - setChatConfig(value as ChatConfig) - setHasUnsavedChanges(true) - } - }} - hooks={fieldHooks} - /> - )} - - - {/* 表达配置 */} - - {expressionConfig && ( - - )} - - - {/* 功能配置(合并表情、记忆、工具) */} - - {emojiConfig && memoryConfig && toolConfig && voiceConfig && ( - - )} - - - {/* 处理配置(关键词反应和回复后处理) */} - - {keywordReactionConfig && responsePostProcessConfig && chineseTypoConfig && responseSplitterConfig && ( - - )} - {messageReceiveConfig && ( - - )} - - - {/* 做梦配置 */} - - {dreamConfig && } - - - {/* 知识库配置 */} - - {lpmmConfig && } - - - {/* WebUI 配置 */} - - {webuiConfig && } - - - {/* 其他配置 */} - - {logConfig && } - {debugConfig && } - {experimentalConfig && } - {maimMessageConfig && } - {telemetryConfig && } - - - + )} {/* 重启遮罩层 */} @@ -809,3 +780,154 @@ function BotConfigPageContent() { ) } + +// ==================== 动态 Tab 渲染组件 ==================== + +interface DynamicConfigTabsProps { + tabGroups: TabGroup[] + botConfig: BotConfig | null + setBotConfig: (c: BotConfig) => void + personalityConfig: PersonalityConfig | null + setPersonalityConfig: (c: PersonalityConfig) => void + chatConfig: ChatConfig | null + setChatConfig: (c: ChatConfig) => void + expressionConfig: ExpressionConfig | null + setExpressionConfig: (c: ExpressionConfig) => void + emojiConfig: EmojiConfig | null + setEmojiConfig: (c: EmojiConfig) => void + memoryConfig: MemoryConfig | null + setMemoryConfig: (c: MemoryConfig) => void + toolConfig: ToolConfig | null + setToolConfig: (c: ToolConfig) => void + voiceConfig: VoiceConfig | null + setVoiceConfig: (c: VoiceConfig) => void + messageReceiveConfig: MessageReceiveConfig | null + setMessageReceiveConfig: (c: MessageReceiveConfig) => void + dreamConfig: DreamConfig | null + setDreamConfig: (c: DreamConfig) => void + lpmmConfig: LPMMKnowledgeConfig | null + setLpmmConfig: (c: LPMMKnowledgeConfig) => void + keywordReactionConfig: KeywordReactionConfig | null + setKeywordReactionConfig: (c: KeywordReactionConfig) => void + responsePostProcessConfig: ResponsePostProcessConfig | null + setResponsePostProcessConfig: (c: ResponsePostProcessConfig) => void + chineseTypoConfig: ChineseTypoConfig | null + setChineseTypoConfig: (c: ChineseTypoConfig) => void + responseSplitterConfig: ResponseSplitterConfig | null + setResponseSplitterConfig: (c: ResponseSplitterConfig) => void + logConfig: LogConfig | null + setLogConfig: (c: LogConfig) => void + debugConfig: DebugConfig | null + setDebugConfig: (c: DebugConfig) => void + experimentalConfig: ExperimentalConfig | null + setExperimentalConfig: (c: ExperimentalConfig) => void + maimMessageConfig: MaimMessageConfig | null + setMaimMessageConfig: (c: MaimMessageConfig) => void + telemetryConfig: TelemetryConfig | null + setTelemetryConfig: (c: TelemetryConfig) => void + webuiConfig: WebUIConfig | null + setWebuiConfig: (c: WebUIConfig) => void + setHasUnsavedChanges: (v: boolean) => void +} + +function DynamicConfigTabs(props: DynamicConfigTabsProps) { + const { tabGroups } = props + + // 每个 tab host field name → 对应的 ReactNode 内容 + const tabContentMap: Record = { + bot: props.botConfig && ( + + ), + personality: props.personalityConfig && ( + + ), + chat: props.chatConfig && ( + { + if (field === 'chat') { + props.setChatConfig(value as ChatConfig) + props.setHasUnsavedChanges(true) + } + }} + hooks={fieldHooks} + /> + ), + expression: props.expressionConfig && ( + + ), + emoji: props.emojiConfig && props.memoryConfig && props.toolConfig && props.voiceConfig && ( + + ), + response_post_process: ( + <> + {props.keywordReactionConfig && props.responsePostProcessConfig && props.chineseTypoConfig && props.responseSplitterConfig && ( + + )} + {props.messageReceiveConfig && ( + + )} + + ), + dream: props.dreamConfig && ( + + ), + lpmm_knowledge: props.lpmmConfig && ( + + ), + webui: props.webuiConfig && ( + + ), + debug: ( + <> + {props.logConfig && } + {props.debugConfig && } + {props.experimentalConfig && } + {props.maimMessageConfig && } + {props.telemetryConfig && } + + ), + } + + if (tabGroups.length === 0) return null + + return ( + + + {tabGroups.map((tab) => ( + + {tab.label} + + ))} + + {tabGroups.map((tab) => ( + + {tabContentMap[tab.id]} + + ))} + + ) +} diff --git a/dashboard/src/types/config-schema.ts b/dashboard/src/types/config-schema.ts index 206c253d..c4a5681f 100644 --- a/dashboard/src/types/config-schema.ts +++ b/dashboard/src/types/config-schema.ts @@ -38,6 +38,9 @@ export interface ConfigSchema { classDoc: string fields: FieldSchema[] nested?: Record + uiParent?: string + uiLabel?: string + uiIcon?: string } export interface ConfigSchemaResponse { diff --git a/src/config/config_base.py b/src/config/config_base.py index 5e2d1827..8661adab 100644 --- a/src/config/config_base.py +++ b/src/config/config_base.py @@ -5,7 +5,7 @@ import types from dataclasses import dataclass, field from pathlib import Path from pydantic import BaseModel, ConfigDict, Field -from typing import Any, Dict, List, Literal, Set, Tuple, Union, cast, get_args, get_origin +from typing import Any, ClassVar, Dict, List, Literal, Set, Tuple, Union, cast, get_args, get_origin __all__ = ["ConfigBase", "Field", "AttributeData"] @@ -131,6 +131,11 @@ class ConfigBase(BaseModel, AttrDocBase): _validate_any: bool = True # 是否验证 Any 类型的使用,默认为 True suppress_any_warning: bool = False # 是否抑制 Any 类型使用的警告,默认为 False,仅仅在_validate_any 为 False 时生效 + # UI 分组元数据:子类可覆盖以声明所属 Tab 分组 + __ui_parent__: ClassVar[str] = "" # 父配置类在 Config 中的字段名,空表示独立 Tab + __ui_label__: ClassVar[str] = "" # Tab 显示名称(仅做 Tab 主人时使用),空则使用 classDoc + __ui_icon__: ClassVar[str] = "" # Tab 图标名称(Lucide 图标名) + @classmethod def from_dict(cls, attribute_data: AttributeData, data: dict[str, Any]): """从字典创建配置对象,并收集缺失和多余的属性信息""" diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 0ef25ca6..f5f3afb5 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -19,6 +19,9 @@ class ExampleConfig(ConfigBase): class BotConfig(ConfigBase): """机器人配置类""" + __ui_label__ = "基本信息" + __ui_icon__ = "bot" + platform: str = Field( default="", json_schema_extra={ @@ -68,6 +71,9 @@ class BotConfig(ConfigBase): class PersonalityConfig(ConfigBase): """人格配置类""" + __ui_label__ = "人格" + __ui_icon__ = "user-circle" + personality: str = Field( default="是一个大二在读女大学生,现在正在上网和群友聊天,有时有点攻击性,有时比较温柔", json_schema_extra={ @@ -160,6 +166,8 @@ class PersonalityConfig(ConfigBase): class RelationshipConfig(ConfigBase): """关系配置类""" + __ui_parent__ = "debug" + enable_relationship: bool = Field( default=True, json_schema_extra={ @@ -190,6 +198,9 @@ class TalkRulesItem(ConfigBase): class ChatConfig(ConfigBase): """聊天配置类""" + __ui_label__ = "聊天" + __ui_icon__ = "message-square" + talk_value: float = Field( default=1, ge=0, @@ -293,6 +304,8 @@ class ChatConfig(ConfigBase): class MessageReceiveConfig(ConfigBase): """消息接收配置类""" + __ui_parent__ = "response_post_process" + image_parse_threshold: int = Field( default=5, json_schema_extra={ @@ -364,6 +377,8 @@ class TargetItem(ConfigBase): class MemoryConfig(ConfigBase): """记忆配置类""" + __ui_parent__ = "emoji" + max_agent_iterations: int = Field( default=5, ge=1, @@ -551,6 +566,9 @@ class ExpressionGroup(ConfigBase): class ExpressionConfig(ConfigBase): """表达配置类""" + __ui_label__ = "表达" + __ui_icon__ = "pen-tool" + learning_list: list[LearningItem] = Field( default_factory=lambda: [ LearningItem( @@ -687,6 +705,8 @@ class ExpressionConfig(ConfigBase): class ToolConfig(ConfigBase): """工具配置类""" + __ui_parent__ = "emoji" + enable_tool: bool = Field( default=False, json_schema_extra={ @@ -700,6 +720,8 @@ class ToolConfig(ConfigBase): class VoiceConfig(ConfigBase): """语音识别配置类""" + __ui_parent__ = "emoji" + enable_asr: bool = Field( default=False, json_schema_extra={ @@ -713,6 +735,9 @@ class VoiceConfig(ConfigBase): class EmojiConfig(ConfigBase): """表情包配置类""" + __ui_label__ = "功能" + __ui_icon__ = "puzzle" + emoji_chance: float = Field( default=0.4, ge=0, @@ -829,6 +854,8 @@ class KeywordRuleConfig(ConfigBase): class KeywordReactionConfig(ConfigBase): """关键词配置类""" + __ui_parent__ = "response_post_process" + keyword_rules: list[KeywordRuleConfig] = Field( default_factory=lambda: [], json_schema_extra={ @@ -858,6 +885,9 @@ class KeywordReactionConfig(ConfigBase): class ResponsePostProcessConfig(ConfigBase): """回复后处理配置类""" + __ui_label__ = "处理" + __ui_icon__ = "settings" + enable_response_post_process: bool = Field( default=True, json_schema_extra={ @@ -871,6 +901,8 @@ class ResponsePostProcessConfig(ConfigBase): class ChineseTypoConfig(ConfigBase): """中文错别字配置类""" + __ui_parent__ = "response_post_process" + enable: bool = Field( default=True, json_schema_extra={ @@ -929,6 +961,8 @@ class ChineseTypoConfig(ConfigBase): class ResponseSplitterConfig(ConfigBase): """回复分割器配置类""" + __ui_parent__ = "response_post_process" + enable: bool = Field( default=True, json_schema_extra={ @@ -978,6 +1012,8 @@ class ResponseSplitterConfig(ConfigBase): class TelemetryConfig(ConfigBase): """遥测配置类""" + __ui_parent__ = "debug" + enable: bool = Field( default=True, json_schema_extra={ @@ -991,6 +1027,9 @@ class TelemetryConfig(ConfigBase): class DebugConfig(ConfigBase): """调试配置类""" + __ui_label__ = "其他" + __ui_icon__ = "more-horizontal" + show_prompt: bool = Field( default=False, json_schema_extra={ @@ -1101,6 +1140,8 @@ class ExtraPromptItem(ConfigBase): class ExperimentalConfig(ConfigBase): """实验功能配置类""" + __ui_parent__ = "debug" + private_plan_style: str = Field( default=( "1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用" @@ -1136,6 +1177,8 @@ class ExperimentalConfig(ConfigBase): class MaimMessageConfig(ConfigBase): """maim_message配置类""" + __ui_parent__ = "debug" + ws_server_host: str = Field( default="127.0.0.1", json_schema_extra={ @@ -1230,6 +1273,9 @@ class MaimMessageConfig(ConfigBase): class LPMMKnowledgeConfig(ConfigBase): """LPMM知识库配置类""" + __ui_label__ = "知识库" + __ui_icon__ = "book-open" + enable: bool = Field( default=True, json_schema_extra={ @@ -1397,6 +1443,9 @@ class LPMMKnowledgeConfig(ConfigBase): class DreamConfig(ConfigBase): """Dream配置类""" + __ui_label__ = "做梦" + __ui_icon__ = "moon" + interval_minutes: int = Field( default=30, ge=1, @@ -1467,6 +1516,9 @@ class DreamConfig(ConfigBase): class WebUIConfig(ConfigBase): """WebUI配置类""" + __ui_label__ = "WebUI" + __ui_icon__ = "layout" + enabled: bool = Field( default=True, json_schema_extra={ @@ -1543,6 +1595,8 @@ class WebUIConfig(ConfigBase): class DatabaseConfig(ConfigBase): """数据库配置类""" + __ui_parent__ = "debug" + save_binary_data: bool = Field( default=False, json_schema_extra={ diff --git a/src/webui/config_schema.py b/src/webui/config_schema.py index 711b18a8..5abd4b1d 100644 --- a/src/webui/config_schema.py +++ b/src/webui/config_schema.py @@ -28,13 +28,26 @@ class ConfigSchemaGenerator: if nested_schema is not None: nested[field_name] = nested_schema - return { + schema: dict[str, Any] = { "className": config_class.__name__, "classDoc": (config_class.__doc__ or "").strip(), "fields": fields, "nested": nested, } + # 将 UI 分组元数据写入 schema + ui_parent = getattr(config_class, "__ui_parent__", "") + ui_label = getattr(config_class, "__ui_label__", "") + ui_icon = getattr(config_class, "__ui_icon__", "") + if ui_parent: + schema["uiParent"] = ui_parent + if ui_label: + schema["uiLabel"] = ui_label + if ui_icon: + schema["uiIcon"] = ui_icon + + return schema + @classmethod def _build_nested_schema(cls, annotation: Any) -> dict[str, Any] | None: origin = get_origin(annotation)