feat: 添加动态 Tab 分组支持,优化配置界面 UI 元数据

This commit is contained in:
DrSmoothl
2026-03-07 23:13:04 +08:00
parent d3a4e3f3e7
commit b056ad2c34
5 changed files with 321 additions and 124 deletions

View File

@@ -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 { parse as parseToml } from 'smol-toml'
import { AlertDescription, Alert } from '@/components/ui/alert' import { AlertDescription, Alert } from '@/components/ui/alert'
@@ -20,12 +20,13 @@ import { CodeEditor } from '@/components'
import { DynamicConfigForm } from '@/components/dynamic-form' import { DynamicConfigForm } from '@/components/dynamic-form'
import { RestartOverlay } from '@/components/restart-overlay' import { RestartOverlay } from '@/components/restart-overlay'
import { useToast } from '@/hooks/use-toast' 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 { fieldHooks } from '@/lib/field-hooks'
import { RestartProvider, useRestart } from '@/lib/restart-context' import { RestartProvider, useRestart } from '@/lib/restart-context'
import { Code2, Info, Layout, Power, Save } from 'lucide-react' import { Code2, Info, Layout, Power, Save } from 'lucide-react'
import type { ConfigSchema } from '@/types/config-schema'
import type { import type {
BotConfig, BotConfig,
ChatConfig, ChatConfig,
@@ -71,6 +72,58 @@ import {
/** Toast 显示前的延迟时间 (毫秒) */ /** Toast 显示前的延迟时间 (毫秒) */
const TOAST_DISPLAY_DELAY = 500 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<string, TabGroup>()
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 // 主导出组件:包装 RestartProvider
export function BotConfigPage() { export function BotConfigPage() {
return ( return (
@@ -116,6 +169,9 @@ function BotConfigPageContent() {
const [telemetryConfig, setTelemetryConfig] = useState<TelemetryConfig | null>(null) const [telemetryConfig, setTelemetryConfig] = useState<TelemetryConfig | null>(null)
const [webuiConfig, setWebuiConfig] = useState<WebUIConfig | null>(null) const [webuiConfig, setWebuiConfig] = useState<WebUIConfig | null>(null)
// Schema 状态(用于动态 tab 分组)
const [configSchema, setConfigSchema] = useState<ConfigSchema | null>(null)
// 用于标记初始加载和配置缓存 // 用于标记初始加载和配置缓存
const initialLoadRef = useRef(true) const initialLoadRef = useRef(true)
const configRef = useRef<Record<string, unknown>>({}) const configRef = useRef<Record<string, unknown>>({})
@@ -292,7 +348,7 @@ function BotConfigPageContent() {
const loadConfig = useCallback(async () => { const loadConfig = useCallback(async () => {
try { try {
setLoading(true) setLoading(true)
const result = await getBotConfig() const [result, schemaResult] = await Promise.all([getBotConfig(), getBotConfigSchema()])
if (!result.success) { if (!result.success) {
toast({ toast({
title: '加载失败', title: '加载失败',
@@ -303,6 +359,9 @@ function BotConfigPageContent() {
return return
} }
parseAndSetConfig((result.data as Record<string, unknown>).config as Record<string, unknown>) parseAndSetConfig((result.data as Record<string, unknown>).config as Record<string, unknown>)
if (schemaResult.success && schemaResult.data) {
setConfigSchema((schemaResult.data as unknown as Record<string, unknown>).schema as ConfigSchema)
}
setHasUnsavedChanges(false) setHasUnsavedChanges(false)
initialLoadRef.current = false initialLoadRef.current = false
@@ -544,6 +603,12 @@ function BotConfigPageContent() {
} }
} }
// 根据 schema 构建 tab 分组
const tabGroups = useMemo(() => {
if (!configSchema) return []
return buildTabGroupsFromSchema(configSchema)
}, [configSchema])
if (loading) { if (loading) {
return ( return (
<ScrollArea className="h-full"> <ScrollArea className="h-full">
@@ -682,125 +747,31 @@ function BotConfigPageContent() {
{/* 可视化模式 */} {/* 可视化模式 */}
{editMode === 'visual' && ( {editMode === 'visual' && (
<> <DynamicConfigTabs
{/* 标签页 */} tabGroups={tabGroups}
<Tabs defaultValue="bot" className="w-full"> botConfig={botConfig} setBotConfig={setBotConfig}
<TabsList className="flex flex-wrap h-auto gap-1 p-1 sm:grid sm:grid-cols-5 lg:grid-cols-10"> personalityConfig={personalityConfig} setPersonalityConfig={setPersonalityConfig}
<TabsTrigger value="bot" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger> chatConfig={chatConfig} setChatConfig={setChatConfig}
<TabsTrigger value="personality" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger> expressionConfig={expressionConfig} setExpressionConfig={setExpressionConfig}
<TabsTrigger value="chat" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger> emojiConfig={emojiConfig} setEmojiConfig={setEmojiConfig}
<TabsTrigger value="expression" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger> memoryConfig={memoryConfig} setMemoryConfig={setMemoryConfig}
<TabsTrigger value="features" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger> toolConfig={toolConfig} setToolConfig={setToolConfig}
<TabsTrigger value="processing" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger> voiceConfig={voiceConfig} setVoiceConfig={setVoiceConfig}
<TabsTrigger value="dream" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger> messageReceiveConfig={messageReceiveConfig} setMessageReceiveConfig={setMessageReceiveConfig}
<TabsTrigger value="lpmm" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger> dreamConfig={dreamConfig} setDreamConfig={setDreamConfig}
<TabsTrigger value="webui" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm">WebUI</TabsTrigger> lpmmConfig={lpmmConfig} setLpmmConfig={setLpmmConfig}
<TabsTrigger value="other" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger> keywordReactionConfig={keywordReactionConfig} setKeywordReactionConfig={setKeywordReactionConfig}
</TabsList> responsePostProcessConfig={responsePostProcessConfig} setResponsePostProcessConfig={setResponsePostProcessConfig}
{/* 基本信息 */} chineseTypoConfig={chineseTypoConfig} setChineseTypoConfig={setChineseTypoConfig}
<TabsContent value="bot" className="space-y-4"> responseSplitterConfig={responseSplitterConfig} setResponseSplitterConfig={setResponseSplitterConfig}
{botConfig && <BotInfoSection config={botConfig} onChange={setBotConfig} />} logConfig={logConfig} setLogConfig={setLogConfig}
</TabsContent> debugConfig={debugConfig} setDebugConfig={setDebugConfig}
experimentalConfig={experimentalConfig} setExperimentalConfig={setExperimentalConfig}
{/* 人格配置 */} maimMessageConfig={maimMessageConfig} setMaimMessageConfig={setMaimMessageConfig}
<TabsContent value="personality" className="space-y-4"> telemetryConfig={telemetryConfig} setTelemetryConfig={setTelemetryConfig}
{personalityConfig && ( webuiConfig={webuiConfig} setWebuiConfig={setWebuiConfig}
<PersonalitySection config={personalityConfig} onChange={setPersonalityConfig} /> setHasUnsavedChanges={setHasUnsavedChanges}
)} />
</TabsContent>
{/* 聊天配置 */}
<TabsContent value="chat" className="space-y-4">
{chatConfig && (
<DynamicConfigForm
schema={{
className: 'ChatConfig',
classDoc: '聊天配置',
fields: [],
nested: {},
}}
values={{ chat: chatConfig }}
onChange={(field, value) => {
if (field === 'chat') {
setChatConfig(value as ChatConfig)
setHasUnsavedChanges(true)
}
}}
hooks={fieldHooks}
/>
)}
</TabsContent>
{/* 表达配置 */}
<TabsContent value="expression" className="space-y-4">
{expressionConfig && (
<ExpressionSection config={expressionConfig} onChange={setExpressionConfig} />
)}
</TabsContent>
{/* 功能配置(合并表情、记忆、工具) */}
<TabsContent value="features" className="space-y-4">
{emojiConfig && memoryConfig && toolConfig && voiceConfig && (
<FeaturesSection
emojiConfig={emojiConfig}
memoryConfig={memoryConfig}
toolConfig={toolConfig}
voiceConfig={voiceConfig}
onEmojiChange={setEmojiConfig}
onMemoryChange={setMemoryConfig}
onToolChange={setToolConfig}
onVoiceChange={setVoiceConfig}
/>
)}
</TabsContent>
{/* 处理配置(关键词反应和回复后处理) */}
<TabsContent value="processing" className="space-y-4">
{keywordReactionConfig && responsePostProcessConfig && chineseTypoConfig && responseSplitterConfig && (
<ProcessingSection
keywordReactionConfig={keywordReactionConfig}
responsePostProcessConfig={responsePostProcessConfig}
chineseTypoConfig={chineseTypoConfig}
responseSplitterConfig={responseSplitterConfig}
onKeywordReactionChange={setKeywordReactionConfig}
onResponsePostProcessChange={setResponsePostProcessConfig}
onChineseTypoChange={setChineseTypoConfig}
onResponseSplitterChange={setResponseSplitterConfig}
/>
)}
{messageReceiveConfig && (
<MessageReceiveSection
config={messageReceiveConfig}
onChange={setMessageReceiveConfig}
/>
)}
</TabsContent>
{/* 做梦配置 */}
<TabsContent value="dream" className="space-y-4">
{dreamConfig && <DreamSection config={dreamConfig} onChange={setDreamConfig} />}
</TabsContent>
{/* 知识库配置 */}
<TabsContent value="lpmm" className="space-y-4">
{lpmmConfig && <LPMMSection config={lpmmConfig} onChange={setLpmmConfig} />}
</TabsContent>
{/* WebUI 配置 */}
<TabsContent value="webui" className="space-y-4">
{webuiConfig && <WebUISection config={webuiConfig} onChange={setWebuiConfig} />}
</TabsContent>
{/* 其他配置 */}
<TabsContent value="other" className="space-y-4">
{logConfig && <LogSection config={logConfig} onChange={setLogConfig} />}
{debugConfig && <DebugSection config={debugConfig} onChange={setDebugConfig} />}
{experimentalConfig && <ExperimentalSection config={experimentalConfig} onChange={setExperimentalConfig} />}
{maimMessageConfig && <MaimMessageSection config={maimMessageConfig} onChange={setMaimMessageConfig} />}
{telemetryConfig && <TelemetrySection config={telemetryConfig} onChange={setTelemetryConfig} />}
</TabsContent>
</Tabs>
</>
)} )}
{/* 重启遮罩层 */} {/* 重启遮罩层 */}
@@ -809,3 +780,154 @@ function BotConfigPageContent() {
</ScrollArea> </ScrollArea>
) )
} }
// ==================== 动态 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<string, React.ReactNode> = {
bot: props.botConfig && (
<BotInfoSection config={props.botConfig} onChange={props.setBotConfig} />
),
personality: props.personalityConfig && (
<PersonalitySection config={props.personalityConfig} onChange={props.setPersonalityConfig} />
),
chat: props.chatConfig && (
<DynamicConfigForm
schema={{ className: 'ChatConfig', classDoc: '聊天配置', fields: [], nested: {} }}
values={{ chat: props.chatConfig }}
onChange={(field, value) => {
if (field === 'chat') {
props.setChatConfig(value as ChatConfig)
props.setHasUnsavedChanges(true)
}
}}
hooks={fieldHooks}
/>
),
expression: props.expressionConfig && (
<ExpressionSection config={props.expressionConfig} onChange={props.setExpressionConfig} />
),
emoji: props.emojiConfig && props.memoryConfig && props.toolConfig && props.voiceConfig && (
<FeaturesSection
emojiConfig={props.emojiConfig}
memoryConfig={props.memoryConfig}
toolConfig={props.toolConfig}
voiceConfig={props.voiceConfig}
onEmojiChange={props.setEmojiConfig}
onMemoryChange={props.setMemoryConfig}
onToolChange={props.setToolConfig}
onVoiceChange={props.setVoiceConfig}
/>
),
response_post_process: (
<>
{props.keywordReactionConfig && props.responsePostProcessConfig && props.chineseTypoConfig && props.responseSplitterConfig && (
<ProcessingSection
keywordReactionConfig={props.keywordReactionConfig}
responsePostProcessConfig={props.responsePostProcessConfig}
chineseTypoConfig={props.chineseTypoConfig}
responseSplitterConfig={props.responseSplitterConfig}
onKeywordReactionChange={props.setKeywordReactionConfig}
onResponsePostProcessChange={props.setResponsePostProcessConfig}
onChineseTypoChange={props.setChineseTypoConfig}
onResponseSplitterChange={props.setResponseSplitterConfig}
/>
)}
{props.messageReceiveConfig && (
<MessageReceiveSection config={props.messageReceiveConfig} onChange={props.setMessageReceiveConfig} />
)}
</>
),
dream: props.dreamConfig && (
<DreamSection config={props.dreamConfig} onChange={props.setDreamConfig} />
),
lpmm_knowledge: props.lpmmConfig && (
<LPMMSection config={props.lpmmConfig} onChange={props.setLpmmConfig} />
),
webui: props.webuiConfig && (
<WebUISection config={props.webuiConfig} onChange={props.setWebuiConfig} />
),
debug: (
<>
{props.logConfig && <LogSection config={props.logConfig} onChange={props.setLogConfig} />}
{props.debugConfig && <DebugSection config={props.debugConfig} onChange={props.setDebugConfig} />}
{props.experimentalConfig && <ExperimentalSection config={props.experimentalConfig} onChange={props.setExperimentalConfig} />}
{props.maimMessageConfig && <MaimMessageSection config={props.maimMessageConfig} onChange={props.setMaimMessageConfig} />}
{props.telemetryConfig && <TelemetrySection config={props.telemetryConfig} onChange={props.setTelemetryConfig} />}
</>
),
}
if (tabGroups.length === 0) return null
return (
<Tabs defaultValue={tabGroups[0].id} className="w-full">
<TabsList className="flex flex-wrap h-auto gap-1 p-1">
{tabGroups.map((tab) => (
<TabsTrigger
key={tab.id}
value={tab.id}
className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"
>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{tabGroups.map((tab) => (
<TabsContent key={tab.id} value={tab.id} className="space-y-4">
{tabContentMap[tab.id]}
</TabsContent>
))}
</Tabs>
)
}

View File

@@ -38,6 +38,9 @@ export interface ConfigSchema {
classDoc: string classDoc: string
fields: FieldSchema[] fields: FieldSchema[]
nested?: Record<string, ConfigSchema> nested?: Record<string, ConfigSchema>
uiParent?: string
uiLabel?: string
uiIcon?: string
} }
export interface ConfigSchemaResponse { export interface ConfigSchemaResponse {

View File

@@ -5,7 +5,7 @@ import types
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from pydantic import BaseModel, ConfigDict, Field 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"] __all__ = ["ConfigBase", "Field", "AttributeData"]
@@ -131,6 +131,11 @@ class ConfigBase(BaseModel, AttrDocBase):
_validate_any: bool = True # 是否验证 Any 类型的使用,默认为 True _validate_any: bool = True # 是否验证 Any 类型的使用,默认为 True
suppress_any_warning: bool = False # 是否抑制 Any 类型使用的警告,默认为 False仅仅在_validate_any 为 False 时生效 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 @classmethod
def from_dict(cls, attribute_data: AttributeData, data: dict[str, Any]): def from_dict(cls, attribute_data: AttributeData, data: dict[str, Any]):
"""从字典创建配置对象,并收集缺失和多余的属性信息""" """从字典创建配置对象,并收集缺失和多余的属性信息"""

View File

@@ -19,6 +19,9 @@ class ExampleConfig(ConfigBase):
class BotConfig(ConfigBase): class BotConfig(ConfigBase):
"""机器人配置类""" """机器人配置类"""
__ui_label__ = "基本信息"
__ui_icon__ = "bot"
platform: str = Field( platform: str = Field(
default="", default="",
json_schema_extra={ json_schema_extra={
@@ -68,6 +71,9 @@ class BotConfig(ConfigBase):
class PersonalityConfig(ConfigBase): class PersonalityConfig(ConfigBase):
"""人格配置类""" """人格配置类"""
__ui_label__ = "人格"
__ui_icon__ = "user-circle"
personality: str = Field( personality: str = Field(
default="是一个大二在读女大学生,现在正在上网和群友聊天,有时有点攻击性,有时比较温柔", default="是一个大二在读女大学生,现在正在上网和群友聊天,有时有点攻击性,有时比较温柔",
json_schema_extra={ json_schema_extra={
@@ -160,6 +166,8 @@ class PersonalityConfig(ConfigBase):
class RelationshipConfig(ConfigBase): class RelationshipConfig(ConfigBase):
"""关系配置类""" """关系配置类"""
__ui_parent__ = "debug"
enable_relationship: bool = Field( enable_relationship: bool = Field(
default=True, default=True,
json_schema_extra={ json_schema_extra={
@@ -190,6 +198,9 @@ class TalkRulesItem(ConfigBase):
class ChatConfig(ConfigBase): class ChatConfig(ConfigBase):
"""聊天配置类""" """聊天配置类"""
__ui_label__ = "聊天"
__ui_icon__ = "message-square"
talk_value: float = Field( talk_value: float = Field(
default=1, default=1,
ge=0, ge=0,
@@ -293,6 +304,8 @@ class ChatConfig(ConfigBase):
class MessageReceiveConfig(ConfigBase): class MessageReceiveConfig(ConfigBase):
"""消息接收配置类""" """消息接收配置类"""
__ui_parent__ = "response_post_process"
image_parse_threshold: int = Field( image_parse_threshold: int = Field(
default=5, default=5,
json_schema_extra={ json_schema_extra={
@@ -364,6 +377,8 @@ class TargetItem(ConfigBase):
class MemoryConfig(ConfigBase): class MemoryConfig(ConfigBase):
"""记忆配置类""" """记忆配置类"""
__ui_parent__ = "emoji"
max_agent_iterations: int = Field( max_agent_iterations: int = Field(
default=5, default=5,
ge=1, ge=1,
@@ -551,6 +566,9 @@ class ExpressionGroup(ConfigBase):
class ExpressionConfig(ConfigBase): class ExpressionConfig(ConfigBase):
"""表达配置类""" """表达配置类"""
__ui_label__ = "表达"
__ui_icon__ = "pen-tool"
learning_list: list[LearningItem] = Field( learning_list: list[LearningItem] = Field(
default_factory=lambda: [ default_factory=lambda: [
LearningItem( LearningItem(
@@ -687,6 +705,8 @@ class ExpressionConfig(ConfigBase):
class ToolConfig(ConfigBase): class ToolConfig(ConfigBase):
"""工具配置类""" """工具配置类"""
__ui_parent__ = "emoji"
enable_tool: bool = Field( enable_tool: bool = Field(
default=False, default=False,
json_schema_extra={ json_schema_extra={
@@ -700,6 +720,8 @@ class ToolConfig(ConfigBase):
class VoiceConfig(ConfigBase): class VoiceConfig(ConfigBase):
"""语音识别配置类""" """语音识别配置类"""
__ui_parent__ = "emoji"
enable_asr: bool = Field( enable_asr: bool = Field(
default=False, default=False,
json_schema_extra={ json_schema_extra={
@@ -713,6 +735,9 @@ class VoiceConfig(ConfigBase):
class EmojiConfig(ConfigBase): class EmojiConfig(ConfigBase):
"""表情包配置类""" """表情包配置类"""
__ui_label__ = "功能"
__ui_icon__ = "puzzle"
emoji_chance: float = Field( emoji_chance: float = Field(
default=0.4, default=0.4,
ge=0, ge=0,
@@ -829,6 +854,8 @@ class KeywordRuleConfig(ConfigBase):
class KeywordReactionConfig(ConfigBase): class KeywordReactionConfig(ConfigBase):
"""关键词配置类""" """关键词配置类"""
__ui_parent__ = "response_post_process"
keyword_rules: list[KeywordRuleConfig] = Field( keyword_rules: list[KeywordRuleConfig] = Field(
default_factory=lambda: [], default_factory=lambda: [],
json_schema_extra={ json_schema_extra={
@@ -858,6 +885,9 @@ class KeywordReactionConfig(ConfigBase):
class ResponsePostProcessConfig(ConfigBase): class ResponsePostProcessConfig(ConfigBase):
"""回复后处理配置类""" """回复后处理配置类"""
__ui_label__ = "处理"
__ui_icon__ = "settings"
enable_response_post_process: bool = Field( enable_response_post_process: bool = Field(
default=True, default=True,
json_schema_extra={ json_schema_extra={
@@ -871,6 +901,8 @@ class ResponsePostProcessConfig(ConfigBase):
class ChineseTypoConfig(ConfigBase): class ChineseTypoConfig(ConfigBase):
"""中文错别字配置类""" """中文错别字配置类"""
__ui_parent__ = "response_post_process"
enable: bool = Field( enable: bool = Field(
default=True, default=True,
json_schema_extra={ json_schema_extra={
@@ -929,6 +961,8 @@ class ChineseTypoConfig(ConfigBase):
class ResponseSplitterConfig(ConfigBase): class ResponseSplitterConfig(ConfigBase):
"""回复分割器配置类""" """回复分割器配置类"""
__ui_parent__ = "response_post_process"
enable: bool = Field( enable: bool = Field(
default=True, default=True,
json_schema_extra={ json_schema_extra={
@@ -978,6 +1012,8 @@ class ResponseSplitterConfig(ConfigBase):
class TelemetryConfig(ConfigBase): class TelemetryConfig(ConfigBase):
"""遥测配置类""" """遥测配置类"""
__ui_parent__ = "debug"
enable: bool = Field( enable: bool = Field(
default=True, default=True,
json_schema_extra={ json_schema_extra={
@@ -991,6 +1027,9 @@ class TelemetryConfig(ConfigBase):
class DebugConfig(ConfigBase): class DebugConfig(ConfigBase):
"""调试配置类""" """调试配置类"""
__ui_label__ = "其他"
__ui_icon__ = "more-horizontal"
show_prompt: bool = Field( show_prompt: bool = Field(
default=False, default=False,
json_schema_extra={ json_schema_extra={
@@ -1101,6 +1140,8 @@ class ExtraPromptItem(ConfigBase):
class ExperimentalConfig(ConfigBase): class ExperimentalConfig(ConfigBase):
"""实验功能配置类""" """实验功能配置类"""
__ui_parent__ = "debug"
private_plan_style: str = Field( private_plan_style: str = Field(
default=( default=(
"1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用" "1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用"
@@ -1136,6 +1177,8 @@ class ExperimentalConfig(ConfigBase):
class MaimMessageConfig(ConfigBase): class MaimMessageConfig(ConfigBase):
"""maim_message配置类""" """maim_message配置类"""
__ui_parent__ = "debug"
ws_server_host: str = Field( ws_server_host: str = Field(
default="127.0.0.1", default="127.0.0.1",
json_schema_extra={ json_schema_extra={
@@ -1230,6 +1273,9 @@ class MaimMessageConfig(ConfigBase):
class LPMMKnowledgeConfig(ConfigBase): class LPMMKnowledgeConfig(ConfigBase):
"""LPMM知识库配置类""" """LPMM知识库配置类"""
__ui_label__ = "知识库"
__ui_icon__ = "book-open"
enable: bool = Field( enable: bool = Field(
default=True, default=True,
json_schema_extra={ json_schema_extra={
@@ -1397,6 +1443,9 @@ class LPMMKnowledgeConfig(ConfigBase):
class DreamConfig(ConfigBase): class DreamConfig(ConfigBase):
"""Dream配置类""" """Dream配置类"""
__ui_label__ = "做梦"
__ui_icon__ = "moon"
interval_minutes: int = Field( interval_minutes: int = Field(
default=30, default=30,
ge=1, ge=1,
@@ -1467,6 +1516,9 @@ class DreamConfig(ConfigBase):
class WebUIConfig(ConfigBase): class WebUIConfig(ConfigBase):
"""WebUI配置类""" """WebUI配置类"""
__ui_label__ = "WebUI"
__ui_icon__ = "layout"
enabled: bool = Field( enabled: bool = Field(
default=True, default=True,
json_schema_extra={ json_schema_extra={
@@ -1543,6 +1595,8 @@ class WebUIConfig(ConfigBase):
class DatabaseConfig(ConfigBase): class DatabaseConfig(ConfigBase):
"""数据库配置类""" """数据库配置类"""
__ui_parent__ = "debug"
save_binary_data: bool = Field( save_binary_data: bool = Field(
default=False, default=False,
json_schema_extra={ json_schema_extra={

View File

@@ -28,13 +28,26 @@ class ConfigSchemaGenerator:
if nested_schema is not None: if nested_schema is not None:
nested[field_name] = nested_schema nested[field_name] = nested_schema
return { schema: dict[str, Any] = {
"className": config_class.__name__, "className": config_class.__name__,
"classDoc": (config_class.__doc__ or "").strip(), "classDoc": (config_class.__doc__ or "").strip(),
"fields": fields, "fields": fields,
"nested": nested, "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 @classmethod
def _build_nested_schema(cls, annotation: Any) -> dict[str, Any] | None: def _build_nested_schema(cls, annotation: Any) -> dict[str, Any] | None:
origin = get_origin(annotation) origin = get_origin(annotation)