From d0840db2a5808053a76ea8a9eb4eba838d1a71c2 Mon Sep 17 00:00:00 2001 From: DrSmoothl <1787882683@qq.com> Date: Sun, 8 Mar 2026 00:12:10 +0800 Subject: [PATCH] refactor: remove RuleEditor, RuleList, RulePreview, TimeRangePicker, and VoiceSection components - Deleted RuleEditor.tsx, RuleList.tsx, RulePreview.tsx, TimeRangePicker.tsx, and VoiceSection.tsx as part of a cleanup. - Updated index.ts to remove imports related to deleted components. - Modified types.ts and model.tsx to accommodate changes in task configuration structure. - Introduced new types for TargetItem and LearningItem to enhance type safety. - Refactored ModelTaskConfig to be dynamic based on backend schema. - Updated ModelProviderConfigPage to unwrap backend config response for better handling. --- dashboard/src/routes/config/bot.tsx | 2 +- .../config/bot/hooks/ChatSectionHook.tsx | 4 +- .../config/bot/sections/ChatSection.tsx | 191 ---------------- .../config/bot/sections/ExpressionSection.tsx | 185 ++++++++------- .../routes/config/bot/sections/RuleEditor.tsx | 213 ------------------ .../routes/config/bot/sections/RuleList.tsx | 70 ------ .../config/bot/sections/RulePreview.tsx | 40 ---- .../config/bot/sections/TimeRangePicker.tsx | 170 -------------- .../config/bot/sections/VoiceSection.tsx | 27 --- .../src/routes/config/bot/sections/index.ts | 1 - dashboard/src/routes/config/bot/types.ts | 27 ++- dashboard/src/routes/config/model.tsx | 181 +++++++++------ dashboard/src/routes/config/model/types.ts | 19 +- .../src/routes/config/modelProvider/index.tsx | 18 +- 14 files changed, 250 insertions(+), 898 deletions(-) delete mode 100644 dashboard/src/routes/config/bot/sections/ChatSection.tsx delete mode 100644 dashboard/src/routes/config/bot/sections/RuleEditor.tsx delete mode 100644 dashboard/src/routes/config/bot/sections/RuleList.tsx delete mode 100644 dashboard/src/routes/config/bot/sections/RulePreview.tsx delete mode 100644 dashboard/src/routes/config/bot/sections/TimeRangePicker.tsx delete mode 100644 dashboard/src/routes/config/bot/sections/VoiceSection.tsx diff --git a/dashboard/src/routes/config/bot.tsx b/dashboard/src/routes/config/bot.tsx index f62521d9..5e02fdae 100644 --- a/dashboard/src/routes/config/bot.tsx +++ b/dashboard/src/routes/config/bot.tsx @@ -843,7 +843,7 @@ function DynamicConfigTabs(props: DynamicConfigTabsProps) { ), chat: props.chatConfig && ( { if (field === 'chat') { diff --git a/dashboard/src/routes/config/bot/hooks/ChatSectionHook.tsx b/dashboard/src/routes/config/bot/hooks/ChatSectionHook.tsx index d4310bd7..96744486 100644 --- a/dashboard/src/routes/config/bot/hooks/ChatSectionHook.tsx +++ b/dashboard/src/routes/config/bot/hooks/ChatSectionHook.tsx @@ -454,7 +454,7 @@ export const ChatSectionHook: FieldHookComponent = ({ value, onChange }) => {
onChange({ ...config, talk_value: parseFloat(e.target.value) })} - /> -

越小越沉默,范围 0-1

-
- -
- - -

- 控制麦麦的思考深度。经典模式回复快但简单;深度模式更深入但较慢;动态模式根据情况自动选择 -

-
- -
- - onChange({ ...config, mentioned_bot_reply: checked }) - } - /> - -
- -
- - - onChange({ ...config, max_context_size: parseInt(e.target.value) }) - } - /> -
- -
- - - onChange({ ...config, planner_smooth: parseFloat(e.target.value) }) - } - /> -

- 增大数值会减小 planner 负荷,推荐 1-5,0 为关闭 -

-
- -
- - - onChange({ ...config, plan_reply_log_max_per_chat: parseInt(e.target.value) }) - } - /> -

- 每个聊天流保存的 Plan/Reply 日志最大数量,超过此数量时会自动删除最老的日志 -

-
- -
- - onChange({ ...config, llm_quote: checked }) - } - /> - -
-

- 启用后,LLM 可以决定是否在回复时引用消息 -

- -
- - onChange({ ...config, enable_talk_value_rules: checked }) - } - /> - -
- - - - {/* 动态发言频率规则配置 */} - {config.enable_talk_value_rules && ( - - )} - - ) -} diff --git a/dashboard/src/routes/config/bot/sections/ExpressionSection.tsx b/dashboard/src/routes/config/bot/sections/ExpressionSection.tsx index b5927b77..fd31e05a 100644 --- a/dashboard/src/routes/config/bot/sections/ExpressionSection.tsx +++ b/dashboard/src/routes/config/bot/sections/ExpressionSection.tsx @@ -27,17 +27,27 @@ import { PopoverTrigger, } from '@/components/ui/popover' import { Plus, Trash2, Eye } from 'lucide-react' -import type { ExpressionConfig } from '../types' +import type { ExpressionConfig, LearningItem, TargetItem, ExpressionGroup } from '../types' interface ExpressionGroupMemberInputProps { - member: string + member: TargetItem groupIndex: number memberIndex: number availableChatIds: string[] - onUpdate: (groupIndex: number, memberIndex: number, value: string) => void + onUpdate: (groupIndex: number, memberIndex: number, value: TargetItem) => void onRemove: (groupIndex: number, memberIndex: number) => void } +const formatTargetItem = (item: TargetItem): string => { + if (!item.platform && !item.item_id) return '' + return `${item.platform}:${item.item_id}:${item.rule_type}` +} + +const parseTargetString = (s: string): TargetItem => { + const parts = s.split(':') + return { platform: parts[0] || 'qq', item_id: parts[1] || '', rule_type: (parts[2] === 'private' ? 'private' : 'group') } +} + const ExpressionGroupMemberInput = React.memo(function ExpressionGroupMemberInput({ member, groupIndex, @@ -46,8 +56,8 @@ const ExpressionGroupMemberInput = React.memo(function ExpressionGroupMemberInpu onUpdate, onRemove, }: ExpressionGroupMemberInputProps) { - // 判断当前成员是否在可选列表中 - const isFromList = availableChatIds.includes(member) || member === '*' + const memberStr = formatTargetItem(member) + const isFromList = availableChatIds.includes(memberStr) || memberStr === '*' const [inputMode, setInputMode] = useState(!isFromList) return ( @@ -58,9 +68,9 @@ const ExpressionGroupMemberInput = React.memo(function ExpressionGroupMemberInpu // 手动输入模式 <> onUpdate(groupIndex, memberIndex, e.target.value)} - placeholder='输入 "*" 或 "qq:123456:group"' + value={memberStr} + onChange={(e) => onUpdate(groupIndex, memberIndex, parseTargetString(e.target.value))} + placeholder='输入 "qq:123456:group"' className="flex-1" /> {availableChatIds.length > 0 && ( @@ -78,14 +88,13 @@ const ExpressionGroupMemberInput = React.memo(function ExpressionGroupMemberInpu // 下拉选择模式 <> { - updateLearningRule(index, 0, `${value}:${chatId}:${chatType}`) + updateLearningRule(index, 'platform', value) }} > @@ -426,9 +448,9 @@ export const ExpressionSection = React.memo(function ExpressionSection({
{ - updateLearningRule(index, 0, `${platform}:${e.target.value}:${chatType}`) + updateLearningRule(index, 'item_id', e.target.value) }} placeholder="输入群 ID" className="font-mono text-sm" @@ -439,9 +461,9 @@ export const ExpressionSection = React.memo(function ExpressionSection({
{ - onChange({ ...config, manual_reflect_operator_id: `${value}:${chatId}:${chatType}` }) + onChange({ ...config, manual_reflect_operator_id: { platform: value, item_id: chatId, rule_type: chatType } }) }} > @@ -754,7 +775,7 @@ export const ExpressionSection = React.memo(function ExpressionSection({ { - onChange({ ...config, manual_reflect_operator_id: `${platform}:${e.target.value}:${chatType}` }) + onChange({ ...config, manual_reflect_operator_id: { platform, item_id: e.target.value, rule_type: chatType } }) }} placeholder="输入 ID" className="font-mono text-sm" @@ -766,8 +787,8 @@ export const ExpressionSection = React.memo(function ExpressionSection({ { - const newList = [...config.allow_reflect] - newList[index] = `${value}:${id}:${chatType}` + const newList = config.allow_reflect.map((r, i) => + i === index ? { ...r, platform: value } : r + ) onChange({ ...config, allow_reflect: newList }) }} > @@ -843,10 +861,11 @@ export const ExpressionSection = React.memo(function ExpressionSection({ { - const newList = [...config.allow_reflect] - newList[index] = `${platform}:${e.target.value}:${chatType}` + const newList = config.allow_reflect.map((r, i) => + i === index ? { ...r, item_id: e.target.value } : r + ) onChange({ ...config, allow_reflect: newList }) }} placeholder="ID" @@ -854,10 +873,11 @@ export const ExpressionSection = React.memo(function ExpressionSection({ /> { - if (value === 'global') { - onUpdate(index, 'target', '') - } else { - onUpdate(index, 'target', 'qq::group') - } - }} - > - - - - - 全局配置 - 详细配置 - - -
- - {/* 详细配置选项 - 只在非全局时显示 */} - {rule.target !== '' && (() => { - const parts = rule.target.split(':') - const platform = parts[0] || 'qq' - const chatId = parts[1] || '' - const chatType = parts[2] || 'group' - - return ( -
-
-
- - -
- -
- - { - onUpdate(index, 'target', `${platform}:${e.target.value}:${chatType}`) - }} - placeholder="输入群 ID" - className="font-mono text-sm" - /> -
- -
- - -
-
-

- 当前聊天流 ID:{rule.target || '(未设置)'} -

-
- ) - })()} - - {/* 时间段选择器 */} -
- - onUpdate(index, 'time', v)} - /> -

- 支持跨夜区间,例如 23:00-02:00 -

-
- - {/* 发言频率滑块 */} -
-
- - { - const val = parseFloat(e.target.value) - if (!isNaN(val)) { - onUpdate(index, 'value', Math.max(0.01, Math.min(1, val))) - } - }} - className="w-20 h-8 text-xs" - /> -
- - onUpdate(index, 'value', values[0]) - } - min={0.01} - max={1} - step={0.01} - className="w-full" - /> -
- 0.01 (极少发言) - 0.5 - 1.0 (正常) -
-
-
- - ) -} diff --git a/dashboard/src/routes/config/bot/sections/RuleList.tsx b/dashboard/src/routes/config/bot/sections/RuleList.tsx deleted file mode 100644 index b7373f6e..00000000 --- a/dashboard/src/routes/config/bot/sections/RuleList.tsx +++ /dev/null @@ -1,70 +0,0 @@ - -import { Button } from '@/components/ui/button' - -import { Plus } from 'lucide-react' - -import { RuleEditor } from './RuleEditor' - -interface TalkValueRule { - target: string - time: string - value: number -} - -interface RuleListProps { - rules: TalkValueRule[] - onAdd: () => void - onUpdate: (index: number, field: 'target' | 'time' | 'value', value: string | number) => void - onRemove: (index: number) => void -} - -// 规则列表组件 -export function RuleList({ rules, onAdd, onUpdate, onRemove }: RuleListProps) { - return ( -
-
-
-

动态发言频率规则

-

- 按时段或聊天流ID调整发言频率,优先匹配具体聊天,再匹配全局规则 -

-
- -
- - {rules && rules.length > 0 ? ( -
- {rules.map((rule, index) => ( - - ))} -
- ) : ( -
-

暂无规则,点击"添加规则"按钮创建

-
- )} - -
-
- 📝 规则说明 -
-
    -
  • Target 为空:全局规则,对所有聊天生效
  • -
  • Target 指定:仅对特定聊天流生效(格式:platform:id:type)
  • -
  • 优先级:先匹配具体聊天流规则,再匹配全局规则
  • -
  • 时间支持跨夜:例如 23:00-02:00 表示晚上11点到次日凌晨2点
  • -
  • 数值范围:建议 0-1,0 表示完全沉默,1 表示正常发言
  • -
-
-
- ) -} diff --git a/dashboard/src/routes/config/bot/sections/RulePreview.tsx b/dashboard/src/routes/config/bot/sections/RulePreview.tsx deleted file mode 100644 index 2b26c1f6..00000000 --- a/dashboard/src/routes/config/bot/sections/RulePreview.tsx +++ /dev/null @@ -1,40 +0,0 @@ - -import { Button } from '@/components/ui/button' -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' - -import { Eye } from 'lucide-react' - -interface RulePreviewProps { - rule: { - target: string - time: string - value: number - } -} - -// 预览窗口组件 -export function RulePreview({ rule }: RulePreviewProps) { - const previewText = `{ target = "${rule.target}", time = "${rule.time}", value = ${rule.value.toFixed(1)} }` - - return ( - - - - - -
-

配置预览

-
- {previewText} -
-

- 这是保存到 bot_config.toml 文件中的格式 -

-
-
-
- ) -} diff --git a/dashboard/src/routes/config/bot/sections/TimeRangePicker.tsx b/dashboard/src/routes/config/bot/sections/TimeRangePicker.tsx deleted file mode 100644 index c6c7df12..00000000 --- a/dashboard/src/routes/config/bot/sections/TimeRangePicker.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { useEffect, useMemo, useState } from 'react' - -import { Button } from '@/components/ui/button' -import { Label } from '@/components/ui/label' -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' - -import { Clock } from 'lucide-react' - -interface TimeRangePickerProps { - value: string - onChange: (value: string) => void -} - -// 时间选择组件 -export function TimeRangePicker({ value, onChange }: TimeRangePickerProps) { - // 解析初始值 - const parsedValue = useMemo(() => { - const parts = value.split('-') - if (parts.length === 2) { - const [start, end] = parts - const [sh, sm] = start.split(':') - const [eh, em] = end.split(':') - return { - startHour: sh ? sh.padStart(2, '0') : '00', - startMinute: sm ? sm.padStart(2, '0') : '00', - endHour: eh ? eh.padStart(2, '0') : '23', - endMinute: em ? em.padStart(2, '0') : '59', - } - } - return { - startHour: '00', - startMinute: '00', - endHour: '23', - endMinute: '59', - } - }, [value]) - - const [startHour, setStartHour] = useState(parsedValue.startHour) - const [startMinute, setStartMinute] = useState(parsedValue.startMinute) - const [endHour, setEndHour] = useState(parsedValue.endHour) - const [endMinute, setEndMinute] = useState(parsedValue.endMinute) - - // 当value变化时同步状态 - useEffect(() => { - setStartHour(parsedValue.startHour) - setStartMinute(parsedValue.startMinute) - setEndHour(parsedValue.endHour) - setEndMinute(parsedValue.endMinute) - }, [parsedValue]) - - const updateTime = ( - newStartHour: string, - newStartMinute: string, - newEndHour: string, - newEndMinute: string - ) => { - const newValue = `${newStartHour}:${newStartMinute}-${newEndHour}:${newEndMinute}` - onChange(newValue) - } - - return ( - - - - - -
-
-

开始时间

-
-
- - -
-
- - -
-
-
-
-

结束时间

-
-
- - -
-
- - -
-
-
-
-
-
- ) -} diff --git a/dashboard/src/routes/config/bot/sections/VoiceSection.tsx b/dashboard/src/routes/config/bot/sections/VoiceSection.tsx deleted file mode 100644 index 27e5e20d..00000000 --- a/dashboard/src/routes/config/bot/sections/VoiceSection.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react' -import { Label } from '@/components/ui/label' -import { Switch } from '@/components/ui/switch' -import type { VoiceConfig } from '../types' - -interface VoiceSectionProps { - config: VoiceConfig - onChange: (config: VoiceConfig) => void -} - -export const VoiceSection = React.memo(function VoiceSection({ config, onChange }: VoiceSectionProps) { - return ( -
-

语音设置

-
- onChange({ ...config, enable_asr: checked })} - /> - -
-

- 启用后麦麦可以识别语音消息,需要配置语音识别模型 -

-
- ) -}) diff --git a/dashboard/src/routes/config/bot/sections/index.ts b/dashboard/src/routes/config/bot/sections/index.ts index 783523c0..60cf10b4 100644 --- a/dashboard/src/routes/config/bot/sections/index.ts +++ b/dashboard/src/routes/config/bot/sections/index.ts @@ -4,7 +4,6 @@ export { BotInfoSection } from './BotInfoSection' export { PersonalitySection } from './PersonalitySection' -export { ChatSection } from './ChatSection' export { DreamSection } from './DreamSection' export { LPMMSection } from './LPMMSection' export { LogSection } from './LogSection' diff --git a/dashboard/src/routes/config/bot/types.ts b/dashboard/src/routes/config/bot/types.ts index 6decde0b..7dc60537 100644 --- a/dashboard/src/routes/config/bot/types.ts +++ b/dashboard/src/routes/config/bot/types.ts @@ -36,12 +36,31 @@ export interface ChatConfig { }> } +export interface TargetItem { + platform: string + item_id: string + rule_type: 'group' | 'private' +} + +export interface LearningItem { + platform: string + item_id: string + rule_type: 'group' | 'private' + use_expression: boolean + enable_learning: boolean + enable_jargon_learning: boolean +} + +export interface ExpressionGroup { + expression_groups: TargetItem[] +} + export interface ExpressionConfig { - learning_list: Array<[string, string, string, string]> - expression_groups: Array + learning_list: LearningItem[] + expression_groups: ExpressionGroup[] expression_manual_reflect: boolean - manual_reflect_operator_id: string - allow_reflect: string[] + manual_reflect_operator_id: TargetItem | null + allow_reflect: TargetItem[] expression_self_reflect: boolean expression_auto_check_interval: number expression_auto_check_count: number diff --git a/dashboard/src/routes/config/model.tsx b/dashboard/src/routes/config/model.tsx index 8d39dc1c..535d97de 100644 --- a/dashboard/src/routes/config/model.tsx +++ b/dashboard/src/routes/config/model.tsx @@ -47,7 +47,8 @@ import { Switch } from '@/components/ui/switch' import { Slider } from '@/components/ui/slider' import { Badge } from '@/components/ui/badge' import { Plus, Pencil, Trash2, Save, Search, Info, Power, Check, ChevronsUpDown, RefreshCw, Loader2, GraduationCap, Share2, AlertTriangle, Settings, Lock, Unlock } from 'lucide-react' -import { getModelConfig, updateModelConfig } from '@/lib/config-api' +import { getModelConfig, getModelConfigSchema, updateModelConfig } from '@/lib/config-api' +import type { ConfigSchema } from '@/types/config-schema' import { useToast } from '@/hooks/use-toast' import { Alert, AlertDescription } from '@/components/ui/alert' import { HelpTooltip } from '@/components/ui/help-tooltip' @@ -58,12 +59,16 @@ import { SharePackDialog } from '@/components/share-pack-dialog' // 导入模块化的类型定义和组件 import type { ModelInfo, ProviderConfig, ModelTaskConfig, TaskConfig } from './model/types' -import { Pagination, ModelTable, ModelCardList } from './model/components' -import { useModelTour, useModelFetcher, useModelAutoSave } from './model/hooks' -// 导入动态表单和 Hook 系统 -import { DynamicConfigForm } from '@/components/dynamic-form' -import { fieldHooks } from '@/lib/field-hooks' +/** Unwrap backend `{ success, config }` envelope to get the actual config */ +function unwrapModelConfig(data: unknown): Record { + if (data && typeof data === 'object' && 'config' in data) { + return (data as { config: Record }).config + } + return data as Record +} +import { TaskConfigCard, Pagination, ModelTable, ModelCardList } from './model/components' +import { useModelTour, useModelFetcher, useModelAutoSave } from './model/hooks' // 主导出组件:包装 RestartProvider export function ModelConfigPage() { @@ -79,6 +84,7 @@ function ModelConfigPageContent() { const [models, setModels] = useState([]) const [providers, setProviders] = useState([]) const [providerConfigs, setProviderConfigs] = useState([]) + const [modelNames, setModelNames] = useState([]) const [taskConfig, setTaskConfig] = useState(null) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) @@ -93,6 +99,7 @@ function ModelConfigPageContent() { const [searchQuery, setSearchQuery] = useState('') const [selectedModels, setSelectedModels] = useState>(new Set()) const [batchDeleteDialogOpen, setBatchDeleteDialogOpen] = useState(false) + const [taskConfigSchema, setTaskConfigSchema] = useState(null) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) const [jumpToPage, setJumpToPage] = useState('') @@ -142,32 +149,19 @@ function ModelConfigPageContent() { const invalidRefs: { taskName: string; invalidModels: string[] }[] = [] const emptyTaskList: string[] = [] - const taskNames: Array<{ key: keyof ModelTaskConfig; label: string }> = [ - { key: 'utils', label: '工具模型' }, - { key: 'tool_use', label: '工具调用模型' }, - { key: 'replyer', label: '回复模型' }, - { key: 'planner', label: '规划器模型' }, - { key: 'vlm', label: '视觉模型' }, - { key: 'voice', label: '语音模型' }, - { key: 'embedding', label: '嵌入模型' }, - { key: 'lpmm_entity_extract', label: 'LPMM实体抽取' }, - { key: 'lpmm_rdf_build', label: 'LPMM关系构建' }, - ] - - for (const { key, label } of taskNames) { - const task = taskConf[key] + for (const [key, task] of Object.entries(taskConf)) { if (!task) continue // 检查是否有模型 if (!task.model_list || task.model_list.length === 0) { - emptyTaskList.push(label) + emptyTaskList.push(key) continue } // 检查是否引用了不存在的模型 const invalid = task.model_list.filter(modelName => !modelNameSet.has(modelName)) if (invalid.length > 0) { - invalidRefs.push({ taskName: label, invalidModels: invalid }) + invalidRefs.push({ taskName: key, invalidModels: invalid }) } } @@ -179,7 +173,7 @@ function ModelConfigPageContent() { const loadConfig = useCallback(async () => { try { setLoading(true) - const result = await getModelConfig() + const [result, schemaResult] = await Promise.all([getModelConfig(), getModelConfigSchema()]) if (!result.success) { toast({ title: '加载失败', @@ -189,9 +183,10 @@ function ModelConfigPageContent() { setLoading(false) return } - const config = result.data + const config = unwrapModelConfig(result.data) const modelList = (config.models as ModelInfo[]) || [] setModels(modelList) + setModelNames(modelList.map((m) => m.name)) const providerList = (config.api_providers as ProviderConfig[]) || [] setProviders(providerList.map((p) => p.name)) @@ -200,6 +195,12 @@ function ModelConfigPageContent() { const taskConf = (config.model_task_config as ModelTaskConfig) || null setTaskConfig(taskConf) + // 解析 model_task_config 的 schema + if (schemaResult.success && schemaResult.data) { + const schema = (schemaResult.data as unknown as Record).schema as ConfigSchema + setTaskConfigSchema(schema.nested?.model_task_config ?? null) + } + // 检查任务配置问题 checkTaskConfigIssues(taskConf, modelList) @@ -252,14 +253,14 @@ function ModelConfigPageContent() { if (!taskConfig) return const modelNameSet = new Set(models.map(m => m.name)) - const newTaskConfig = { ...taskConfig } + const newTaskConfig: ModelTaskConfig = {} // 遍历所有任务,过滤掉无效的模型引用 - const taskKeys = Object.keys(newTaskConfig) as Array - for (const key of taskKeys) { - const task = newTaskConfig[key] + for (const [key, task] of Object.entries(taskConfig)) { if (task && task.model_list) { - task.model_list = task.model_list.filter(modelName => modelNameSet.has(modelName)) + newTaskConfig[key] = { ...task, model_list: task.model_list.filter(modelName => modelNameSet.has(modelName)) } + } else { + newTaskConfig[key] = task } } @@ -308,7 +309,7 @@ function ModelConfigPageContent() { setSaving(false) return } - const config = resultGet.data + const config = unwrapModelConfig(resultGet.data) // 清理每个模型中的 null 值 config.models = models.map(cleanModelForSave) config.model_task_config = taskConfig @@ -357,7 +358,7 @@ function ModelConfigPageContent() { setSaving(false) return } - const config = resultGet.data + const config = unwrapModelConfig(resultGet.data) // 清理每个模型中的 null 值 config.models = models.map(cleanModelForSave) config.model_task_config = taskConfig @@ -479,6 +480,7 @@ function ModelConfigPageContent() { } setModels(newModels) + setModelNames(newModels.map((m) => m.name)) // 如果模型名称发生变化,更新任务配置中对该模型的引用 if (oldModelName && oldModelName !== modelToSave.name && taskConfig) { @@ -486,18 +488,11 @@ function ModelConfigPageContent() { return list.map(name => name === oldModelName ? modelToSave.name : name) } - setTaskConfig({ - ...taskConfig, - utils: { ...taskConfig.utils, model_list: updateModelList(taskConfig.utils?.model_list || []) }, - tool_use: { ...taskConfig.tool_use, model_list: updateModelList(taskConfig.tool_use?.model_list || []) }, - replyer: { ...taskConfig.replyer, model_list: updateModelList(taskConfig.replyer?.model_list || []) }, - planner: { ...taskConfig.planner, model_list: updateModelList(taskConfig.planner?.model_list || []) }, - vlm: { ...taskConfig.vlm, model_list: updateModelList(taskConfig.vlm?.model_list || []) }, - voice: { ...taskConfig.voice, model_list: updateModelList(taskConfig.voice?.model_list || []) }, - embedding: { ...taskConfig.embedding, model_list: updateModelList(taskConfig.embedding?.model_list || []) }, - lpmm_entity_extract: { ...taskConfig.lpmm_entity_extract, model_list: updateModelList(taskConfig.lpmm_entity_extract?.model_list || []) }, - lpmm_rdf_build: { ...taskConfig.lpmm_rdf_build, model_list: updateModelList(taskConfig.lpmm_rdf_build?.model_list || []) }, - }) + const newTaskConfig: ModelTaskConfig = {} + for (const [key, task] of Object.entries(taskConfig)) { + newTaskConfig[key] = { ...task, model_list: updateModelList(task?.model_list || []) } + } + setTaskConfig(newTaskConfig) } setEditDialogOpen(false) @@ -536,6 +531,7 @@ function ModelConfigPageContent() { if (deletingIndex !== null) { const newModels = models.filter((_, i) => i !== deletingIndex) setModels(newModels) + setModelNames(newModels.map((m) => m.name)) // 重新检查任务配置问题 checkTaskConfigIssues(taskConfig, newModels) toast({ @@ -588,6 +584,7 @@ function ModelConfigPageContent() { const deletedCount = selectedModels.size const newModels = models.filter((_, index) => !selectedModels.has(index)) setModels(newModels) + setModelNames(newModels.map((m) => m.name)) // 重新检查任务配置问题 checkTaskConfigIssues(taskConfig, newModels) setSelectedModels(new Set()) @@ -598,6 +595,50 @@ function ModelConfigPageContent() { }) } + // 更新任务配置 + const updateTaskConfig = ( + taskName: string, + field: keyof TaskConfig, + value: string[] | number | string + ) => { + if (!taskConfig) return + + // 检测 embedding 模型列表变化 + if (taskName === 'embedding' && field === 'model_list' && Array.isArray(value)) { + const previousModels = previousEmbeddingModelsRef.current + const newModels = value as string[] + + const hasChanges = + previousModels.length !== newModels.length || + previousModels.some(model => !newModels.includes(model)) || + newModels.some(model => !previousModels.includes(model)) + + if (hasChanges && previousModels.length > 0) { + pendingEmbeddingUpdateRef.current = { field, value } + setEmbeddingWarningOpen(true) + return + } + } + + // 正常更新配置 + const newTaskConfig = { + ...taskConfig, + [taskName]: { + ...taskConfig[taskName], + [field]: value, + }, + } + setTaskConfig(newTaskConfig) + + // 重新检查任务配置问题 + checkTaskConfigIssues(newTaskConfig, models) + + // 如果是 embedding 模型列表,更新 ref + if (taskName === 'embedding' && field === 'model_list' && Array.isArray(value)) { + previousEmbeddingModelsRef.current = [...(value as string[])] + } + } + // 确认更新嵌入模型 const handleConfirmEmbeddingChange = () => { if (!taskConfig || !pendingEmbeddingUpdateRef.current) return @@ -667,20 +708,7 @@ function ModelConfigPageContent() { // 检查模型是否被任务使用 const isModelUsed = (modelName: string): boolean => { if (!taskConfig) return false - - const allTaskLists = [ - taskConfig.utils?.model_list || [], - taskConfig.tool_use?.model_list || [], - taskConfig.replyer?.model_list || [], - taskConfig.planner?.model_list || [], - taskConfig.vlm?.model_list || [], - taskConfig.voice?.model_list || [], - taskConfig.embedding?.model_list || [], - taskConfig.lpmm_entity_extract?.model_list || [], - taskConfig.lpmm_rdf_build?.model_list || [], - ] - - return allTaskLists.some(list => list.includes(modelName)) + return Object.values(taskConfig).some(task => task?.model_list?.includes(modelName)) } if (loading) { @@ -914,23 +942,28 @@ function ModelConfigPageContent() { 为不同的任务配置使用的模型和参数

- {taskConfig && ( - { - if (field === 'taskConfig') { - setTaskConfig(value as ModelTaskConfig) - setHasUnsavedChanges(true) - } - }} - hooks={fieldHooks} - /> + {taskConfig && taskConfigSchema && ( +
+ {taskConfigSchema.fields + .filter(f => f.type === 'object') + .map((field, index) => { + const desc = field.description || field.name + const commaIdx = desc.search(/[,,]/) + const title = commaIdx > 0 ? desc.slice(0, commaIdx).trim() : desc + const subtitle = commaIdx > 0 ? desc.slice(commaIdx + 1).trim() : '' + return ( + updateTaskConfig(field.name, f, value)} + {...(index === 0 ? { dataTour: 'task-model-select' } : {})} + /> + ) + })} +
)} diff --git a/dashboard/src/routes/config/model/types.ts b/dashboard/src/routes/config/model/types.ts index a3808943..24c04ff3 100644 --- a/dashboard/src/routes/config/model/types.ts +++ b/dashboard/src/routes/config/model/types.ts @@ -42,19 +42,9 @@ export interface TaskConfig { } /** - * 所有模型任务配置 + * 所有模型任务配置(动态,由后端 schema 决定字段) */ -export interface ModelTaskConfig { - utils: TaskConfig - tool_use: TaskConfig - replyer: TaskConfig - planner: TaskConfig - vlm: TaskConfig - voice: TaskConfig - embedding: TaskConfig - lpmm_entity_extract: TaskConfig - lpmm_rdf_build: TaskConfig -} +export type ModelTaskConfig = Record /** * 表单验证错误 @@ -64,8 +54,3 @@ export interface FormErrors { api_provider?: string model_identifier?: string } - -/** - * 任务名称类型 - */ -export type TaskName = keyof ModelTaskConfig diff --git a/dashboard/src/routes/config/modelProvider/index.tsx b/dashboard/src/routes/config/modelProvider/index.tsx index 87042466..1d0dd05b 100644 --- a/dashboard/src/routes/config/modelProvider/index.tsx +++ b/dashboard/src/routes/config/modelProvider/index.tsx @@ -28,6 +28,14 @@ interface ModelConfig extends Record { model_task_config?: Record } +/** Unwrap backend `{ success, config }` envelope to get the actual config */ +function unwrapModelConfig(data: unknown): ModelConfig { + if (data && typeof data === 'object' && 'config' in data) { + return (data as { config: ModelConfig }).config + } + return data as ModelConfig +} + export function ModelProviderConfigPage() { return ( @@ -149,7 +157,7 @@ function ModelProviderConfigPageContent() { setLoading(false) return } - const config = result.data as ModelConfig + const config = unwrapModelConfig(result.data) setProviders(Array.isArray(config.api_providers) ? config.api_providers as APIProvider[] : []) setHasUnsavedChanges(false) initialLoadRef.current = false @@ -194,7 +202,7 @@ function ModelProviderConfigPageContent() { setSaving(false) return } - const config = resultGet.data as ModelConfig + const config = unwrapModelConfig(resultGet.data) const validProviderNames = new Set(cleanedProviders.map(p => p.name)) const originalModels = Array.isArray(config.models) ? config.models : [] @@ -242,7 +250,7 @@ function ModelProviderConfigPageContent() { console.error('加载配置失败:', result.error) return { shouldProceed: true, providers: newProviders } } - const config = result.data + const config = unwrapModelConfig(result.data) const oldProviderNames = new Set(providers.map(p => p.name)) const newProviderNames = new Set(newProviders.map(p => p.name)) @@ -296,7 +304,7 @@ function ModelProviderConfigPageContent() { savingFlag(false) return } - const config = resultGet.data as ModelConfig + const config = unwrapModelConfig(resultGet.data) const cleanedProviders = deleteConfirmState.pendingProviders.map(cleanProviderData) const validProviderNames = new Set(cleanedProviders.map(p => p.name)) @@ -475,7 +483,7 @@ function ModelProviderConfigPageContent() { setSaving(false) return } - const config = resultGet.data as ModelConfig + const config = unwrapModelConfig(resultGet.data) const validProviderNames = new Set(cleanedProviders.map(p => p.name)) const originalModels = Array.isArray(config.models) ? config.models : []