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.
This commit is contained in:
@@ -843,7 +843,7 @@ function DynamicConfigTabs(props: DynamicConfigTabsProps) {
|
|||||||
),
|
),
|
||||||
chat: props.chatConfig && (
|
chat: props.chatConfig && (
|
||||||
<DynamicConfigForm
|
<DynamicConfigForm
|
||||||
schema={{ className: 'ChatConfig', classDoc: '聊天配置', fields: [], nested: {} }}
|
schema={{ className: 'ChatConfig', classDoc: '聊天配置', fields: [{ name: 'chat', type: 'object', label: '聊天', description: '聊天配置', required: false }], nested: {} }}
|
||||||
values={{ chat: props.chatConfig }}
|
values={{ chat: props.chatConfig }}
|
||||||
onChange={(field, value) => {
|
onChange={(field, value) => {
|
||||||
if (field === 'chat') {
|
if (field === 'chat') {
|
||||||
|
|||||||
@@ -454,7 +454,7 @@ export const ChatSectionHook: FieldHookComponent = ({ value, onChange }) => {
|
|||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label className="text-xs font-medium">配置类型</Label>
|
<Label className="text-xs font-medium">配置类型</Label>
|
||||||
<Select
|
<Select
|
||||||
value={rule.target === '' ? 'global' : 'specific'}
|
value={rule.target ? 'specific' : 'global'}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
if (value === 'global') {
|
if (value === 'global') {
|
||||||
updateTalkValueRule(index, 'target', '')
|
updateTalkValueRule(index, 'target', '')
|
||||||
@@ -474,7 +474,7 @@ export const ChatSectionHook: FieldHookComponent = ({ value, onChange }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 详细配置选项 - 只在非全局时显示 */}
|
{/* 详细配置选项 - 只在非全局时显示 */}
|
||||||
{rule.target !== '' && (() => {
|
{rule.target && (() => {
|
||||||
const parts = rule.target.split(':')
|
const parts = rule.target.split(':')
|
||||||
const platform = parts[0] || 'qq'
|
const platform = parts[0] || 'qq'
|
||||||
const chatId = parts[1] || ''
|
const chatId = parts[1] || ''
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
||||||
import { Switch } from '@/components/ui/switch'
|
|
||||||
|
|
||||||
import type { ChatConfig } from '../types'
|
|
||||||
import { RuleList } from './RuleList'
|
|
||||||
interface ChatSectionProps {
|
|
||||||
config: ChatConfig
|
|
||||||
onChange: (config: ChatConfig) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ChatSection({ config, onChange }: ChatSectionProps) {
|
|
||||||
// 添加发言频率规则
|
|
||||||
const addTalkValueRule = () => {
|
|
||||||
onChange({
|
|
||||||
...config,
|
|
||||||
talk_value_rules: [
|
|
||||||
...config.talk_value_rules,
|
|
||||||
{ target: '', time: '00:00-23:59', value: 1.0 },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除发言频率规则
|
|
||||||
const removeTalkValueRule = (index: number) => {
|
|
||||||
onChange({
|
|
||||||
...config,
|
|
||||||
talk_value_rules: config.talk_value_rules.filter((_, i) => i !== index),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新发言频率规则
|
|
||||||
const updateTalkValueRule = (
|
|
||||||
index: number,
|
|
||||||
field: 'target' | 'time' | 'value',
|
|
||||||
value: string | number
|
|
||||||
) => {
|
|
||||||
const newRules = [...config.talk_value_rules]
|
|
||||||
newRules[index] = {
|
|
||||||
...newRules[index],
|
|
||||||
[field]: value,
|
|
||||||
}
|
|
||||||
onChange({
|
|
||||||
...config,
|
|
||||||
talk_value_rules: newRules,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-4">聊天设置</h3>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="talk_value">聊天频率(基础值)</Label>
|
|
||||||
<Input
|
|
||||||
id="talk_value"
|
|
||||||
type="number"
|
|
||||||
step="0.1"
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
value={config.talk_value}
|
|
||||||
onChange={(e) => onChange({ ...config, talk_value: parseFloat(e.target.value) })}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">越小越沉默,范围 0-1</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="think_mode">思考模式</Label>
|
|
||||||
<Select
|
|
||||||
value={config.think_mode || 'classic'}
|
|
||||||
onValueChange={(value) => onChange({ ...config, think_mode: value as 'classic' | 'deep' | 'dynamic' })}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="think_mode">
|
|
||||||
<SelectValue placeholder="选择思考模式" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="classic">经典模式 - 浅度思考和回复</SelectItem>
|
|
||||||
<SelectItem value="deep">深度模式 - 进行深度思考和回复</SelectItem>
|
|
||||||
<SelectItem value="dynamic">动态模式 - 自动选择思考深度</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
控制麦麦的思考深度。经典模式回复快但简单;深度模式更深入但较慢;动态模式根据情况自动选择
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Switch
|
|
||||||
id="mentioned_bot_reply"
|
|
||||||
checked={config.mentioned_bot_reply}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
onChange({ ...config, mentioned_bot_reply: checked })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="mentioned_bot_reply" className="cursor-pointer">
|
|
||||||
启用提及必回复
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="max_context_size">上下文长度</Label>
|
|
||||||
<Input
|
|
||||||
id="max_context_size"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
value={config.max_context_size}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange({ ...config, max_context_size: parseInt(e.target.value) })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="planner_smooth">规划器平滑</Label>
|
|
||||||
<Input
|
|
||||||
id="planner_smooth"
|
|
||||||
type="number"
|
|
||||||
step="1"
|
|
||||||
min="0"
|
|
||||||
value={config.planner_smooth}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange({ ...config, planner_smooth: parseFloat(e.target.value) })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
增大数值会减小 planner 负荷,推荐 1-5,0 为关闭
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="plan_reply_log_max_per_chat">每个聊天流最大日志数量</Label>
|
|
||||||
<Input
|
|
||||||
id="plan_reply_log_max_per_chat"
|
|
||||||
type="number"
|
|
||||||
step="1"
|
|
||||||
min="100"
|
|
||||||
value={config.plan_reply_log_max_per_chat ?? 1024}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange({ ...config, plan_reply_log_max_per_chat: parseInt(e.target.value) })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
每个聊天流保存的 Plan/Reply 日志最大数量,超过此数量时会自动删除最老的日志
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Switch
|
|
||||||
id="llm_quote"
|
|
||||||
checked={config.llm_quote ?? false}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
onChange({ ...config, llm_quote: checked })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="llm_quote" className="cursor-pointer">
|
|
||||||
启用 LLM 控制引用
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground -mt-2 ml-10">
|
|
||||||
启用后,LLM 可以决定是否在回复时引用消息
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Switch
|
|
||||||
id="enable_talk_value_rules"
|
|
||||||
checked={config.enable_talk_value_rules}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
onChange({ ...config, enable_talk_value_rules: checked })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="enable_talk_value_rules" className="cursor-pointer">
|
|
||||||
启用动态发言频率规则
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 动态发言频率规则配置 */}
|
|
||||||
{config.enable_talk_value_rules && (
|
|
||||||
<RuleList
|
|
||||||
rules={config.talk_value_rules}
|
|
||||||
onAdd={addTalkValueRule}
|
|
||||||
onUpdate={updateTalkValueRule}
|
|
||||||
onRemove={removeTalkValueRule}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -27,17 +27,27 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@/components/ui/popover'
|
} from '@/components/ui/popover'
|
||||||
import { Plus, Trash2, Eye } from 'lucide-react'
|
import { Plus, Trash2, Eye } from 'lucide-react'
|
||||||
import type { ExpressionConfig } from '../types'
|
import type { ExpressionConfig, LearningItem, TargetItem, ExpressionGroup } from '../types'
|
||||||
|
|
||||||
interface ExpressionGroupMemberInputProps {
|
interface ExpressionGroupMemberInputProps {
|
||||||
member: string
|
member: TargetItem
|
||||||
groupIndex: number
|
groupIndex: number
|
||||||
memberIndex: number
|
memberIndex: number
|
||||||
availableChatIds: string[]
|
availableChatIds: string[]
|
||||||
onUpdate: (groupIndex: number, memberIndex: number, value: string) => void
|
onUpdate: (groupIndex: number, memberIndex: number, value: TargetItem) => void
|
||||||
onRemove: (groupIndex: number, memberIndex: number) => 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({
|
const ExpressionGroupMemberInput = React.memo(function ExpressionGroupMemberInput({
|
||||||
member,
|
member,
|
||||||
groupIndex,
|
groupIndex,
|
||||||
@@ -46,8 +56,8 @@ const ExpressionGroupMemberInput = React.memo(function ExpressionGroupMemberInpu
|
|||||||
onUpdate,
|
onUpdate,
|
||||||
onRemove,
|
onRemove,
|
||||||
}: ExpressionGroupMemberInputProps) {
|
}: ExpressionGroupMemberInputProps) {
|
||||||
// 判断当前成员是否在可选列表中
|
const memberStr = formatTargetItem(member)
|
||||||
const isFromList = availableChatIds.includes(member) || member === '*'
|
const isFromList = availableChatIds.includes(memberStr) || memberStr === '*'
|
||||||
const [inputMode, setInputMode] = useState(!isFromList)
|
const [inputMode, setInputMode] = useState(!isFromList)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,9 +68,9 @@ const ExpressionGroupMemberInput = React.memo(function ExpressionGroupMemberInpu
|
|||||||
// 手动输入模式
|
// 手动输入模式
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
value={member}
|
value={memberStr}
|
||||||
onChange={(e) => onUpdate(groupIndex, memberIndex, e.target.value)}
|
onChange={(e) => onUpdate(groupIndex, memberIndex, parseTargetString(e.target.value))}
|
||||||
placeholder='输入 "*" 或 "qq:123456:group"'
|
placeholder='输入 "qq:123456:group"'
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
{availableChatIds.length > 0 && (
|
{availableChatIds.length > 0 && (
|
||||||
@@ -78,14 +88,13 @@ const ExpressionGroupMemberInput = React.memo(function ExpressionGroupMemberInpu
|
|||||||
// 下拉选择模式
|
// 下拉选择模式
|
||||||
<>
|
<>
|
||||||
<Select
|
<Select
|
||||||
value={member}
|
value={memberStr}
|
||||||
onValueChange={(value) => onUpdate(groupIndex, memberIndex, value)}
|
onValueChange={(value) => onUpdate(groupIndex, memberIndex, parseTargetString(value))}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="flex-1">
|
<SelectTrigger className="flex-1">
|
||||||
<SelectValue placeholder="选择聊天流" />
|
<SelectValue placeholder="选择聊天流" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="*">* (全局共享)</SelectItem>
|
|
||||||
{availableChatIds.map((chatId, idx) => (
|
{availableChatIds.map((chatId, idx) => (
|
||||||
<SelectItem key={idx} value={chatId}>
|
<SelectItem key={idx} value={chatId}>
|
||||||
{chatId}
|
{chatId}
|
||||||
@@ -116,7 +125,7 @@ const ExpressionGroupMemberInput = React.memo(function ExpressionGroupMemberInpu
|
|||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
确定要删除组成员 "{member || '(空)'}" 吗?此操作无法撤销。
|
确定要删除组成员 "{memberStr || '(空)'}" 吗?此操作无法撤销。
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
@@ -142,9 +151,17 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
}: ExpressionSectionProps) {
|
}: ExpressionSectionProps) {
|
||||||
// 添加学习规则
|
// 添加学习规则
|
||||||
const addLearningRule = () => {
|
const addLearningRule = () => {
|
||||||
|
const newItem: LearningItem = {
|
||||||
|
platform: '',
|
||||||
|
item_id: '',
|
||||||
|
rule_type: 'group',
|
||||||
|
use_expression: true,
|
||||||
|
enable_learning: true,
|
||||||
|
enable_jargon_learning: true,
|
||||||
|
}
|
||||||
onChange({
|
onChange({
|
||||||
...config,
|
...config,
|
||||||
learning_list: [...config.learning_list, ['', 'enable', 'enable', '1.0']],
|
learning_list: [...config.learning_list, newItem],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,11 +176,12 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
// 更新学习规则
|
// 更新学习规则
|
||||||
const updateLearningRule = (
|
const updateLearningRule = (
|
||||||
index: number,
|
index: number,
|
||||||
field: 0 | 1 | 2 | 3,
|
field: keyof LearningItem,
|
||||||
value: string
|
value: string | boolean
|
||||||
) => {
|
) => {
|
||||||
const newList = [...config.learning_list]
|
const newList = config.learning_list.map((item, i) =>
|
||||||
newList[index][field] = value
|
i === index ? { ...item, [field]: value } : item
|
||||||
|
)
|
||||||
onChange({
|
onChange({
|
||||||
...config,
|
...config,
|
||||||
learning_list: newList,
|
learning_list: newList,
|
||||||
@@ -171,8 +189,8 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 预览组件
|
// 预览组件
|
||||||
const LearningRulePreview = ({ rule }: { rule: [string, string, string, string] }) => {
|
const LearningRulePreview = ({ rule }: { rule: LearningItem }) => {
|
||||||
const previewText = `["${rule[0]}", "${rule[1]}", "${rule[2]}", "${rule[3]}"]`
|
const previewText = JSON.stringify(rule, null, 2)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
@@ -199,9 +217,10 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
|
|
||||||
// 添加表达组
|
// 添加表达组
|
||||||
const addExpressionGroup = () => {
|
const addExpressionGroup = () => {
|
||||||
|
const newGroup: ExpressionGroup = { expression_groups: [] }
|
||||||
onChange({
|
onChange({
|
||||||
...config,
|
...config,
|
||||||
expression_groups: [...config.expression_groups, []],
|
expression_groups: [...config.expression_groups, newGroup],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,8 +234,11 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
|
|
||||||
// 添加组成员
|
// 添加组成员
|
||||||
const addGroupMember = (groupIndex: number) => {
|
const addGroupMember = (groupIndex: number) => {
|
||||||
const newGroups = [...config.expression_groups]
|
const newGroups = config.expression_groups.map((g, i) =>
|
||||||
newGroups[groupIndex] = [...newGroups[groupIndex], '']
|
i === groupIndex
|
||||||
|
? { ...g, expression_groups: [...g.expression_groups, { platform: 'qq', item_id: '', rule_type: 'group' as const }] }
|
||||||
|
: g
|
||||||
|
)
|
||||||
onChange({
|
onChange({
|
||||||
...config,
|
...config,
|
||||||
expression_groups: newGroups,
|
expression_groups: newGroups,
|
||||||
@@ -225,8 +247,11 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
|
|
||||||
// 删除组成员
|
// 删除组成员
|
||||||
const removeGroupMember = (groupIndex: number, memberIndex: number) => {
|
const removeGroupMember = (groupIndex: number, memberIndex: number) => {
|
||||||
const newGroups = [...config.expression_groups]
|
const newGroups = config.expression_groups.map((g, i) =>
|
||||||
newGroups[groupIndex] = newGroups[groupIndex].filter((_, i) => i !== memberIndex)
|
i === groupIndex
|
||||||
|
? { ...g, expression_groups: g.expression_groups.filter((_, j) => j !== memberIndex) }
|
||||||
|
: g
|
||||||
|
)
|
||||||
onChange({
|
onChange({
|
||||||
...config,
|
...config,
|
||||||
expression_groups: newGroups,
|
expression_groups: newGroups,
|
||||||
@@ -234,9 +259,12 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新组成员
|
// 更新组成员
|
||||||
const updateGroupMember = (groupIndex: number, memberIndex: number, value: string) => {
|
const updateGroupMember = (groupIndex: number, memberIndex: number, value: TargetItem) => {
|
||||||
const newGroups = [...config.expression_groups]
|
const newGroups = config.expression_groups.map((g, i) =>
|
||||||
newGroups[groupIndex][memberIndex] = value
|
i === groupIndex
|
||||||
|
? { ...g, expression_groups: g.expression_groups.map((m, j) => j === memberIndex ? value : m) }
|
||||||
|
: g
|
||||||
|
)
|
||||||
onChange({
|
onChange({
|
||||||
...config,
|
...config,
|
||||||
expression_groups: newGroups,
|
expression_groups: newGroups,
|
||||||
@@ -324,15 +352,9 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{config.learning_list.map((rule, index) => {
|
{config.learning_list.map((rule, index) => {
|
||||||
// 检查是否已有全局配置(rule[0] === '')
|
// 检查是否已有全局配置(platform 和 item_id 都为空)
|
||||||
const hasGlobalConfig = config.learning_list.some((r, i) => i !== index && r[0] === '')
|
const isGlobal = rule.platform === '' && rule.item_id === ''
|
||||||
const isGlobal = rule[0] === ''
|
const hasGlobalConfig = config.learning_list.some((r, i) => i !== index && r.platform === '' && r.item_id === '')
|
||||||
|
|
||||||
// 解析聊天流 ID(格式:platform:id:type)
|
|
||||||
const parts = rule[0].split(':')
|
|
||||||
const platform = parts[0] || 'qq'
|
|
||||||
const chatId = parts[1] || ''
|
|
||||||
const chatType = parts[2] || 'group'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className="rounded-lg border p-4 space-y-4">
|
<div key={index} className="rounded-lg border p-4 space-y-4">
|
||||||
@@ -374,10 +396,10 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
value={isGlobal ? 'global' : 'specific'}
|
value={isGlobal ? 'global' : 'specific'}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
if (value === 'global') {
|
if (value === 'global') {
|
||||||
updateLearningRule(index, 0, '')
|
updateLearningRule(index, 'platform', '')
|
||||||
|
updateLearningRule(index, 'item_id', '')
|
||||||
} else {
|
} else {
|
||||||
// 切换到详细配置时,设置默认值
|
updateLearningRule(index, 'platform', 'qq')
|
||||||
updateLearningRule(index, 0, 'qq::group')
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={hasGlobalConfig && !isGlobal}
|
disabled={hasGlobalConfig && !isGlobal}
|
||||||
@@ -407,9 +429,9 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label className="text-xs font-medium">平台</Label>
|
<Label className="text-xs font-medium">平台</Label>
|
||||||
<Select
|
<Select
|
||||||
value={platform}
|
value={rule.platform || 'qq'}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
updateLearningRule(index, 0, `${value}:${chatId}:${chatType}`)
|
updateLearningRule(index, 'platform', value)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
@@ -426,9 +448,9 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label className="text-xs font-medium">群 ID</Label>
|
<Label className="text-xs font-medium">群 ID</Label>
|
||||||
<Input
|
<Input
|
||||||
value={chatId}
|
value={rule.item_id}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
updateLearningRule(index, 0, `${platform}:${e.target.value}:${chatType}`)
|
updateLearningRule(index, 'item_id', e.target.value)
|
||||||
}}
|
}}
|
||||||
placeholder="输入群 ID"
|
placeholder="输入群 ID"
|
||||||
className="font-mono text-sm"
|
className="font-mono text-sm"
|
||||||
@@ -439,9 +461,9 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label className="text-xs font-medium">类型</Label>
|
<Label className="text-xs font-medium">类型</Label>
|
||||||
<Select
|
<Select
|
||||||
value={chatType}
|
value={rule.rule_type}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
updateLearningRule(index, 0, `${platform}:${chatId}:${value}`)
|
updateLearningRule(index, 'rule_type', value)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
@@ -455,7 +477,7 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
当前聊天流 ID:{rule[0] || '(未设置)'}
|
当前聊天流 ID:{rule.platform && rule.item_id ? `${rule.platform}:${rule.item_id}:${rule.rule_type}` : '(未设置)'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -470,9 +492,9 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={rule[1] === 'enable'}
|
checked={rule.use_expression}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
updateLearningRule(index, 1, checked ? 'enable' : 'disable')
|
updateLearningRule(index, 'use_expression', checked)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -488,9 +510,9 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={rule[2] === 'enable'}
|
checked={rule.enable_learning}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
updateLearningRule(index, 2, checked ? 'enable' : 'disable')
|
updateLearningRule(index, 'enable_learning', checked)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -506,9 +528,9 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={rule[3] === 'true' || rule[3] === 'enable'}
|
checked={rule.enable_jargon_learning}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
updateLearningRule(index, 3, checked ? 'true' : 'false')
|
updateLearningRule(index, 'enable_jargon_learning', checked)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -720,11 +742,10 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{(() => {
|
{(() => {
|
||||||
const operatorId = config.manual_reflect_operator_id || ''
|
const operator = config.manual_reflect_operator_id
|
||||||
const parts = operatorId.split(':')
|
const platform = operator?.platform || 'qq'
|
||||||
const platform = parts[0] || 'qq'
|
const chatId = operator?.item_id || ''
|
||||||
const chatId = parts[1] || ''
|
const chatType = operator?.rule_type || 'private'
|
||||||
const chatType = parts[2] || 'private'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 p-3 sm:p-4 rounded-lg bg-muted/50">
|
<div className="grid gap-4 p-3 sm:p-4 rounded-lg bg-muted/50">
|
||||||
@@ -735,7 +756,7 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
<Select
|
<Select
|
||||||
value={platform}
|
value={platform}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
onChange({ ...config, manual_reflect_operator_id: `${value}:${chatId}:${chatType}` })
|
onChange({ ...config, manual_reflect_operator_id: { platform: value, item_id: chatId, rule_type: chatType } })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
@@ -754,7 +775,7 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
<Input
|
<Input
|
||||||
value={chatId}
|
value={chatId}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
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"
|
placeholder="输入 ID"
|
||||||
className="font-mono text-sm"
|
className="font-mono text-sm"
|
||||||
@@ -766,8 +787,8 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
<Label className="text-xs font-medium">类型</Label>
|
<Label className="text-xs font-medium">类型</Label>
|
||||||
<Select
|
<Select
|
||||||
value={chatType}
|
value={chatType}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value: 'group' | 'private') => {
|
||||||
onChange({ ...config, manual_reflect_operator_id: `${platform}:${chatId}:${value}` })
|
onChange({ ...config, manual_reflect_operator_id: { platform, item_id: chatId, rule_type: value } })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
@@ -781,7 +802,7 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
当前操作员 ID:{config.manual_reflect_operator_id || '(未设置)'}
|
当前操作员:{operator ? `${operator.platform}:${operator.item_id}:${operator.rule_type}` : '(未设置)'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
手动表达优化操作员ID,格式:platform:id:type (例如 "qq:123456:private" 或 "qq:654321:group")
|
手动表达优化操作员ID,格式:platform:id:type (例如 "qq:123456:private" 或 "qq:654321:group")
|
||||||
@@ -803,9 +824,10 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
const newItem: TargetItem = { platform: 'qq', item_id: '', rule_type: 'group' }
|
||||||
onChange({
|
onChange({
|
||||||
...config,
|
...config,
|
||||||
allow_reflect: [...(config.allow_reflect || []), 'qq::group'],
|
allow_reflect: [...(config.allow_reflect || []), newItem],
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -817,19 +839,15 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{(config.allow_reflect || []).map((chatId, index) => {
|
{(config.allow_reflect || []).map((item, index) => {
|
||||||
const parts = chatId.split(':')
|
|
||||||
const platform = parts[0] || 'qq'
|
|
||||||
const id = parts[1] || ''
|
|
||||||
const chatType = parts[2] || 'group'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
|
<div key={index} className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
|
||||||
<Select
|
<Select
|
||||||
value={platform}
|
value={item.platform || 'qq'}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
const newList = [...config.allow_reflect]
|
const newList = config.allow_reflect.map((r, i) =>
|
||||||
newList[index] = `${value}:${id}:${chatType}`
|
i === index ? { ...r, platform: value } : r
|
||||||
|
)
|
||||||
onChange({ ...config, allow_reflect: newList })
|
onChange({ ...config, allow_reflect: newList })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -843,10 +861,11 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
value={id}
|
value={item.item_id}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newList = [...config.allow_reflect]
|
const newList = config.allow_reflect.map((r, i) =>
|
||||||
newList[index] = `${platform}:${e.target.value}:${chatType}`
|
i === index ? { ...r, item_id: e.target.value } : r
|
||||||
|
)
|
||||||
onChange({ ...config, allow_reflect: newList })
|
onChange({ ...config, allow_reflect: newList })
|
||||||
}}
|
}}
|
||||||
placeholder="ID"
|
placeholder="ID"
|
||||||
@@ -854,10 +873,11 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={chatType}
|
value={item.rule_type}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value: 'group' | 'private') => {
|
||||||
const newList = [...config.allow_reflect]
|
const newList = config.allow_reflect.map((r, i) =>
|
||||||
newList[index] = `${platform}:${id}:${value}`
|
i === index ? { ...r, rule_type: value } : r
|
||||||
|
)
|
||||||
onChange({ ...config, allow_reflect: newList })
|
onChange({ ...config, allow_reflect: newList })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -919,15 +939,14 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
{config.expression_groups.map((group, groupIndex) => {
|
{config.expression_groups.map((group, groupIndex) => {
|
||||||
// 获取所有已配置的聊天流 ID(用于下拉框选项)
|
// 获取所有已配置的聊天流 ID(用于下拉框选项)
|
||||||
const availableChatIds = config.learning_list
|
const availableChatIds = config.learning_list
|
||||||
.map(rule => rule[0])
|
.filter(rule => rule.platform !== '' || rule.item_id !== '')
|
||||||
.filter(id => id !== '') // 过滤掉全局配置
|
.map(rule => `${rule.platform}:${rule.item_id}:${rule.rule_type}`)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={groupIndex} className="rounded-lg border p-4 space-y-3">
|
<div key={groupIndex} className="rounded-lg border p-4 space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
共享组 {groupIndex + 1}
|
共享组 {groupIndex + 1}
|
||||||
{group.length === 1 && group[0] === '*' && '(全局共享)'}
|
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -962,7 +981,7 @@ export const ExpressionSection = React.memo(function ExpressionSection({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{group.map((member, memberIndex) => (
|
{group.expression_groups.map((member, memberIndex) => (
|
||||||
<ExpressionGroupMemberInput
|
<ExpressionGroupMemberInput
|
||||||
key={`${groupIndex}-${memberIndex}`}
|
key={`${groupIndex}-${memberIndex}`}
|
||||||
member={member}
|
member={member}
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
||||||
import { Slider } from '@/components/ui/slider'
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from '@/components/ui/alert-dialog'
|
|
||||||
|
|
||||||
import { Trash2 } from 'lucide-react'
|
|
||||||
|
|
||||||
import { RulePreview } from './RulePreview'
|
|
||||||
import { TimeRangePicker } from './TimeRangePicker'
|
|
||||||
|
|
||||||
interface TalkValueRule {
|
|
||||||
target: string
|
|
||||||
time: string
|
|
||||||
value: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RuleEditorProps {
|
|
||||||
rule: TalkValueRule
|
|
||||||
index: number
|
|
||||||
onUpdate: (index: number, field: 'target' | 'time' | 'value', value: string | number) => void
|
|
||||||
onRemove: (index: number) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
// 规则编辑器组件
|
|
||||||
export function RuleEditor({ rule, index, onUpdate, onRemove }: RuleEditorProps) {
|
|
||||||
return (
|
|
||||||
<div className="rounded-lg border p-4 bg-muted/50 space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
|
||||||
规则 #{index + 1}
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<RulePreview rule={rule} />
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button variant="ghost" size="sm">
|
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
确定要删除规则 #{index + 1} 吗?此操作无法撤销。
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={() => onRemove(index)}>
|
|
||||||
删除
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{/* 配置类型选择 */}
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label className="text-xs font-medium">配置类型</Label>
|
|
||||||
<Select
|
|
||||||
value={rule.target === '' ? 'global' : 'specific'}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
if (value === 'global') {
|
|
||||||
onUpdate(index, 'target', '')
|
|
||||||
} else {
|
|
||||||
onUpdate(index, 'target', 'qq::group')
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="global">全局配置</SelectItem>
|
|
||||||
<SelectItem value="specific">详细配置</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 详细配置选项 - 只在非全局时显示 */}
|
|
||||||
{rule.target !== '' && (() => {
|
|
||||||
const parts = rule.target.split(':')
|
|
||||||
const platform = parts[0] || 'qq'
|
|
||||||
const chatId = parts[1] || ''
|
|
||||||
const chatType = parts[2] || 'group'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid gap-4 p-3 sm:p-4 rounded-lg bg-muted/50">
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label className="text-xs font-medium">平台</Label>
|
|
||||||
<Select
|
|
||||||
value={platform}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
onUpdate(index, 'target', `${value}:${chatId}:${chatType}`)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="qq">QQ</SelectItem>
|
|
||||||
<SelectItem value="wx">微信</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label className="text-xs font-medium">群 ID</Label>
|
|
||||||
<Input
|
|
||||||
value={chatId}
|
|
||||||
onChange={(e) => {
|
|
||||||
onUpdate(index, 'target', `${platform}:${e.target.value}:${chatType}`)
|
|
||||||
}}
|
|
||||||
placeholder="输入群 ID"
|
|
||||||
className="font-mono text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label className="text-xs font-medium">类型</Label>
|
|
||||||
<Select
|
|
||||||
value={chatType}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
onUpdate(index, 'target', `${platform}:${chatId}:${value}`)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="group">群组(group)</SelectItem>
|
|
||||||
<SelectItem value="private">私聊(private)</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
当前聊天流 ID:{rule.target || '(未设置)'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
|
|
||||||
{/* 时间段选择器 */}
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label className="text-xs font-medium">时间段 (Time)</Label>
|
|
||||||
<TimeRangePicker
|
|
||||||
value={rule.time}
|
|
||||||
onChange={(v) => onUpdate(index, 'time', v)}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
支持跨夜区间,例如 23:00-02:00
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 发言频率滑块 */}
|
|
||||||
<div className="grid gap-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label htmlFor={`rule-value-${index}`} className="text-xs font-medium">
|
|
||||||
发言频率值 (Value)
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id={`rule-value-${index}`}
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
min="0.01"
|
|
||||||
max="1"
|
|
||||||
value={rule.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Slider
|
|
||||||
value={[rule.value]}
|
|
||||||
onValueChange={(values) =>
|
|
||||||
onUpdate(index, 'value', values[0])
|
|
||||||
}
|
|
||||||
min={0.01}
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<div className="flex justify-between text-xs text-muted-foreground">
|
|
||||||
<span>0.01 (极少发言)</span>
|
|
||||||
<span>0.5</span>
|
|
||||||
<span>1.0 (正常)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<div className="border-t pt-6">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-base font-semibold">动态发言频率规则</h4>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
按时段或聊天流ID调整发言频率,优先匹配具体聊天,再匹配全局规则
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button onClick={onAdd} size="sm">
|
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
|
||||||
添加规则
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{rules && rules.length > 0 ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{rules.map((rule, index) => (
|
|
||||||
<RuleEditor
|
|
||||||
key={index}
|
|
||||||
rule={rule}
|
|
||||||
index={index}
|
|
||||||
onUpdate={onUpdate}
|
|
||||||
onRemove={onRemove}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
<p className="text-sm">暂无规则,点击"添加规则"按钮创建</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
|
||||||
<h5 className="text-sm font-semibold text-blue-900 dark:text-blue-100 mb-2">
|
|
||||||
📝 规则说明
|
|
||||||
</h5>
|
|
||||||
<ul className="text-xs text-blue-800 dark:text-blue-200 space-y-1">
|
|
||||||
<li>• <strong>Target 为空</strong>:全局规则,对所有聊天生效</li>
|
|
||||||
<li>• <strong>Target 指定</strong>:仅对特定聊天流生效(格式:platform:id:type)</li>
|
|
||||||
<li>• <strong>优先级</strong>:先匹配具体聊天流规则,再匹配全局规则</li>
|
|
||||||
<li>• <strong>时间支持跨夜</strong>:例如 23:00-02:00 表示晚上11点到次日凌晨2点</li>
|
|
||||||
<li>• <strong>数值范围</strong>:建议 0-1,0 表示完全沉默,1 表示正常发言</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<Eye className="h-4 w-4 mr-1" />
|
|
||||||
预览
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-80 sm:w-96">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h4 className="font-medium text-sm">配置预览</h4>
|
|
||||||
<div className="rounded-md bg-muted p-3 font-mono text-xs break-all">
|
|
||||||
{previewText}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
这是保存到 bot_config.toml 文件中的格式
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button variant="outline" className="w-full justify-start font-mono text-sm">
|
|
||||||
<Clock className="h-4 w-4 mr-2" />
|
|
||||||
{value || '选择时间段'}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-72 sm:w-80">
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-sm mb-3">开始时间</h4>
|
|
||||||
<div className="grid grid-cols-2 gap-2 sm:gap-3">
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">小时</Label>
|
|
||||||
<Select
|
|
||||||
value={startHour}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
setStartHour(v)
|
|
||||||
updateTime(v, startMinute, endHour, endMinute)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{Array.from({ length: 24 }, (_, i) => i).map((h) => (
|
|
||||||
<SelectItem key={h} value={h.toString().padStart(2, '0')}>
|
|
||||||
{h.toString().padStart(2, '0')}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">分钟</Label>
|
|
||||||
<Select
|
|
||||||
value={startMinute}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
setStartMinute(v)
|
|
||||||
updateTime(startHour, v, endHour, endMinute)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{Array.from({ length: 60 }, (_, i) => i).map((m) => (
|
|
||||||
<SelectItem key={m} value={m.toString().padStart(2, '0')}>
|
|
||||||
{m.toString().padStart(2, '0')}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-sm mb-3">结束时间</h4>
|
|
||||||
<div className="grid grid-cols-2 gap-2 sm:gap-3">
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">小时</Label>
|
|
||||||
<Select
|
|
||||||
value={endHour}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
setEndHour(v)
|
|
||||||
updateTime(startHour, startMinute, v, endMinute)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{Array.from({ length: 24 }, (_, i) => i).map((h) => (
|
|
||||||
<SelectItem key={h} value={h.toString().padStart(2, '0')}>
|
|
||||||
{h.toString().padStart(2, '0')}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">分钟</Label>
|
|
||||||
<Select
|
|
||||||
value={endMinute}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
setEndMinute(v)
|
|
||||||
updateTime(startHour, startMinute, endHour, v)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{Array.from({ length: 60 }, (_, i) => i).map((m) => (
|
|
||||||
<SelectItem key={m} value={m.toString().padStart(2, '0')}>
|
|
||||||
{m.toString().padStart(2, '0')}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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 (
|
|
||||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold">语音设置</h3>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Switch
|
|
||||||
checked={config.enable_asr}
|
|
||||||
onCheckedChange={(checked) => onChange({ ...config, enable_asr: checked })}
|
|
||||||
/>
|
|
||||||
<Label className="cursor-pointer">启用语音识别</Label>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
启用后麦麦可以识别语音消息,需要配置语音识别模型
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
export { BotInfoSection } from './BotInfoSection'
|
export { BotInfoSection } from './BotInfoSection'
|
||||||
export { PersonalitySection } from './PersonalitySection'
|
export { PersonalitySection } from './PersonalitySection'
|
||||||
export { ChatSection } from './ChatSection'
|
|
||||||
export { DreamSection } from './DreamSection'
|
export { DreamSection } from './DreamSection'
|
||||||
export { LPMMSection } from './LPMMSection'
|
export { LPMMSection } from './LPMMSection'
|
||||||
export { LogSection } from './LogSection'
|
export { LogSection } from './LogSection'
|
||||||
|
|||||||
@@ -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 {
|
export interface ExpressionConfig {
|
||||||
learning_list: Array<[string, string, string, string]>
|
learning_list: LearningItem[]
|
||||||
expression_groups: Array<string[]>
|
expression_groups: ExpressionGroup[]
|
||||||
expression_manual_reflect: boolean
|
expression_manual_reflect: boolean
|
||||||
manual_reflect_operator_id: string
|
manual_reflect_operator_id: TargetItem | null
|
||||||
allow_reflect: string[]
|
allow_reflect: TargetItem[]
|
||||||
expression_self_reflect: boolean
|
expression_self_reflect: boolean
|
||||||
expression_auto_check_interval: number
|
expression_auto_check_interval: number
|
||||||
expression_auto_check_count: number
|
expression_auto_check_count: number
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ import { Switch } from '@/components/ui/switch'
|
|||||||
import { Slider } from '@/components/ui/slider'
|
import { Slider } from '@/components/ui/slider'
|
||||||
import { Badge } from '@/components/ui/badge'
|
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 { 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 { useToast } from '@/hooks/use-toast'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import { HelpTooltip } from '@/components/ui/help-tooltip'
|
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 type { ModelInfo, ProviderConfig, ModelTaskConfig, TaskConfig } from './model/types'
|
||||||
import { Pagination, ModelTable, ModelCardList } from './model/components'
|
|
||||||
import { useModelTour, useModelFetcher, useModelAutoSave } from './model/hooks'
|
|
||||||
|
|
||||||
// 导入动态表单和 Hook 系统
|
/** Unwrap backend `{ success, config }` envelope to get the actual config */
|
||||||
import { DynamicConfigForm } from '@/components/dynamic-form'
|
function unwrapModelConfig(data: unknown): Record<string, unknown> {
|
||||||
import { fieldHooks } from '@/lib/field-hooks'
|
if (data && typeof data === 'object' && 'config' in data) {
|
||||||
|
return (data as { config: Record<string, unknown> }).config
|
||||||
|
}
|
||||||
|
return data as Record<string, unknown>
|
||||||
|
}
|
||||||
|
import { TaskConfigCard, Pagination, ModelTable, ModelCardList } from './model/components'
|
||||||
|
import { useModelTour, useModelFetcher, useModelAutoSave } from './model/hooks'
|
||||||
|
|
||||||
// 主导出组件:包装 RestartProvider
|
// 主导出组件:包装 RestartProvider
|
||||||
export function ModelConfigPage() {
|
export function ModelConfigPage() {
|
||||||
@@ -79,6 +84,7 @@ function ModelConfigPageContent() {
|
|||||||
const [models, setModels] = useState<ModelInfo[]>([])
|
const [models, setModels] = useState<ModelInfo[]>([])
|
||||||
const [providers, setProviders] = useState<string[]>([])
|
const [providers, setProviders] = useState<string[]>([])
|
||||||
const [providerConfigs, setProviderConfigs] = useState<ProviderConfig[]>([])
|
const [providerConfigs, setProviderConfigs] = useState<ProviderConfig[]>([])
|
||||||
|
const [modelNames, setModelNames] = useState<string[]>([])
|
||||||
const [taskConfig, setTaskConfig] = useState<ModelTaskConfig | null>(null)
|
const [taskConfig, setTaskConfig] = useState<ModelTaskConfig | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
@@ -93,6 +99,7 @@ function ModelConfigPageContent() {
|
|||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [selectedModels, setSelectedModels] = useState<Set<number>>(new Set())
|
const [selectedModels, setSelectedModels] = useState<Set<number>>(new Set())
|
||||||
const [batchDeleteDialogOpen, setBatchDeleteDialogOpen] = useState(false)
|
const [batchDeleteDialogOpen, setBatchDeleteDialogOpen] = useState(false)
|
||||||
|
const [taskConfigSchema, setTaskConfigSchema] = useState<ConfigSchema | null>(null)
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(20)
|
const [pageSize, setPageSize] = useState(20)
|
||||||
const [jumpToPage, setJumpToPage] = useState('')
|
const [jumpToPage, setJumpToPage] = useState('')
|
||||||
@@ -142,32 +149,19 @@ function ModelConfigPageContent() {
|
|||||||
const invalidRefs: { taskName: string; invalidModels: string[] }[] = []
|
const invalidRefs: { taskName: string; invalidModels: string[] }[] = []
|
||||||
const emptyTaskList: string[] = []
|
const emptyTaskList: string[] = []
|
||||||
|
|
||||||
const taskNames: Array<{ key: keyof ModelTaskConfig; label: string }> = [
|
for (const [key, task] of Object.entries(taskConf)) {
|
||||||
{ 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]
|
|
||||||
if (!task) continue
|
if (!task) continue
|
||||||
|
|
||||||
// 检查是否有模型
|
// 检查是否有模型
|
||||||
if (!task.model_list || task.model_list.length === 0) {
|
if (!task.model_list || task.model_list.length === 0) {
|
||||||
emptyTaskList.push(label)
|
emptyTaskList.push(key)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否引用了不存在的模型
|
// 检查是否引用了不存在的模型
|
||||||
const invalid = task.model_list.filter(modelName => !modelNameSet.has(modelName))
|
const invalid = task.model_list.filter(modelName => !modelNameSet.has(modelName))
|
||||||
if (invalid.length > 0) {
|
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 () => {
|
const loadConfig = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const result = await getModelConfig()
|
const [result, schemaResult] = await Promise.all([getModelConfig(), getModelConfigSchema()])
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
toast({
|
toast({
|
||||||
title: '加载失败',
|
title: '加载失败',
|
||||||
@@ -189,9 +183,10 @@ function ModelConfigPageContent() {
|
|||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const config = result.data
|
const config = unwrapModelConfig(result.data)
|
||||||
const modelList = (config.models as ModelInfo[]) || []
|
const modelList = (config.models as ModelInfo[]) || []
|
||||||
setModels(modelList)
|
setModels(modelList)
|
||||||
|
setModelNames(modelList.map((m) => m.name))
|
||||||
|
|
||||||
const providerList = (config.api_providers as ProviderConfig[]) || []
|
const providerList = (config.api_providers as ProviderConfig[]) || []
|
||||||
setProviders(providerList.map((p) => p.name))
|
setProviders(providerList.map((p) => p.name))
|
||||||
@@ -200,6 +195,12 @@ function ModelConfigPageContent() {
|
|||||||
const taskConf = (config.model_task_config as ModelTaskConfig) || null
|
const taskConf = (config.model_task_config as ModelTaskConfig) || null
|
||||||
setTaskConfig(taskConf)
|
setTaskConfig(taskConf)
|
||||||
|
|
||||||
|
// 解析 model_task_config 的 schema
|
||||||
|
if (schemaResult.success && schemaResult.data) {
|
||||||
|
const schema = (schemaResult.data as unknown as Record<string, unknown>).schema as ConfigSchema
|
||||||
|
setTaskConfigSchema(schema.nested?.model_task_config ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
// 检查任务配置问题
|
// 检查任务配置问题
|
||||||
checkTaskConfigIssues(taskConf, modelList)
|
checkTaskConfigIssues(taskConf, modelList)
|
||||||
|
|
||||||
@@ -252,14 +253,14 @@ function ModelConfigPageContent() {
|
|||||||
if (!taskConfig) return
|
if (!taskConfig) return
|
||||||
|
|
||||||
const modelNameSet = new Set(models.map(m => m.name))
|
const modelNameSet = new Set(models.map(m => m.name))
|
||||||
const newTaskConfig = { ...taskConfig }
|
const newTaskConfig: ModelTaskConfig = {}
|
||||||
|
|
||||||
// 遍历所有任务,过滤掉无效的模型引用
|
// 遍历所有任务,过滤掉无效的模型引用
|
||||||
const taskKeys = Object.keys(newTaskConfig) as Array<keyof ModelTaskConfig>
|
for (const [key, task] of Object.entries(taskConfig)) {
|
||||||
for (const key of taskKeys) {
|
|
||||||
const task = newTaskConfig[key]
|
|
||||||
if (task && task.model_list) {
|
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)
|
setSaving(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const config = resultGet.data
|
const config = unwrapModelConfig(resultGet.data)
|
||||||
// 清理每个模型中的 null 值
|
// 清理每个模型中的 null 值
|
||||||
config.models = models.map(cleanModelForSave)
|
config.models = models.map(cleanModelForSave)
|
||||||
config.model_task_config = taskConfig
|
config.model_task_config = taskConfig
|
||||||
@@ -357,7 +358,7 @@ function ModelConfigPageContent() {
|
|||||||
setSaving(false)
|
setSaving(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const config = resultGet.data
|
const config = unwrapModelConfig(resultGet.data)
|
||||||
// 清理每个模型中的 null 值
|
// 清理每个模型中的 null 值
|
||||||
config.models = models.map(cleanModelForSave)
|
config.models = models.map(cleanModelForSave)
|
||||||
config.model_task_config = taskConfig
|
config.model_task_config = taskConfig
|
||||||
@@ -479,6 +480,7 @@ function ModelConfigPageContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setModels(newModels)
|
setModels(newModels)
|
||||||
|
setModelNames(newModels.map((m) => m.name))
|
||||||
|
|
||||||
// 如果模型名称发生变化,更新任务配置中对该模型的引用
|
// 如果模型名称发生变化,更新任务配置中对该模型的引用
|
||||||
if (oldModelName && oldModelName !== modelToSave.name && taskConfig) {
|
if (oldModelName && oldModelName !== modelToSave.name && taskConfig) {
|
||||||
@@ -486,18 +488,11 @@ function ModelConfigPageContent() {
|
|||||||
return list.map(name => name === oldModelName ? modelToSave.name : name)
|
return list.map(name => name === oldModelName ? modelToSave.name : name)
|
||||||
}
|
}
|
||||||
|
|
||||||
setTaskConfig({
|
const newTaskConfig: ModelTaskConfig = {}
|
||||||
...taskConfig,
|
for (const [key, task] of Object.entries(taskConfig)) {
|
||||||
utils: { ...taskConfig.utils, model_list: updateModelList(taskConfig.utils?.model_list || []) },
|
newTaskConfig[key] = { ...task, model_list: updateModelList(task?.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 || []) },
|
setTaskConfig(newTaskConfig)
|
||||||
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 || []) },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setEditDialogOpen(false)
|
setEditDialogOpen(false)
|
||||||
@@ -536,6 +531,7 @@ function ModelConfigPageContent() {
|
|||||||
if (deletingIndex !== null) {
|
if (deletingIndex !== null) {
|
||||||
const newModels = models.filter((_, i) => i !== deletingIndex)
|
const newModels = models.filter((_, i) => i !== deletingIndex)
|
||||||
setModels(newModels)
|
setModels(newModels)
|
||||||
|
setModelNames(newModels.map((m) => m.name))
|
||||||
// 重新检查任务配置问题
|
// 重新检查任务配置问题
|
||||||
checkTaskConfigIssues(taskConfig, newModels)
|
checkTaskConfigIssues(taskConfig, newModels)
|
||||||
toast({
|
toast({
|
||||||
@@ -588,6 +584,7 @@ function ModelConfigPageContent() {
|
|||||||
const deletedCount = selectedModels.size
|
const deletedCount = selectedModels.size
|
||||||
const newModels = models.filter((_, index) => !selectedModels.has(index))
|
const newModels = models.filter((_, index) => !selectedModels.has(index))
|
||||||
setModels(newModels)
|
setModels(newModels)
|
||||||
|
setModelNames(newModels.map((m) => m.name))
|
||||||
// 重新检查任务配置问题
|
// 重新检查任务配置问题
|
||||||
checkTaskConfigIssues(taskConfig, newModels)
|
checkTaskConfigIssues(taskConfig, newModels)
|
||||||
setSelectedModels(new Set())
|
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 = () => {
|
const handleConfirmEmbeddingChange = () => {
|
||||||
if (!taskConfig || !pendingEmbeddingUpdateRef.current) return
|
if (!taskConfig || !pendingEmbeddingUpdateRef.current) return
|
||||||
@@ -667,20 +708,7 @@ function ModelConfigPageContent() {
|
|||||||
// 检查模型是否被任务使用
|
// 检查模型是否被任务使用
|
||||||
const isModelUsed = (modelName: string): boolean => {
|
const isModelUsed = (modelName: string): boolean => {
|
||||||
if (!taskConfig) return false
|
if (!taskConfig) return false
|
||||||
|
return Object.values(taskConfig).some(task => task?.model_list?.includes(modelName))
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -914,23 +942,28 @@ function ModelConfigPageContent() {
|
|||||||
为不同的任务配置使用的模型和参数
|
为不同的任务配置使用的模型和参数
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{taskConfig && (
|
{taskConfig && taskConfigSchema && (
|
||||||
<DynamicConfigForm
|
<div className="grid gap-4 sm:gap-6">
|
||||||
schema={{
|
{taskConfigSchema.fields
|
||||||
className: 'TaskConfig',
|
.filter(f => f.type === 'object')
|
||||||
classDoc: '任务配置',
|
.map((field, index) => {
|
||||||
fields: [],
|
const desc = field.description || field.name
|
||||||
nested: {},
|
const commaIdx = desc.search(/[,,]/)
|
||||||
}}
|
const title = commaIdx > 0 ? desc.slice(0, commaIdx).trim() : desc
|
||||||
values={{ taskConfig }}
|
const subtitle = commaIdx > 0 ? desc.slice(commaIdx + 1).trim() : ''
|
||||||
onChange={(field, value) => {
|
return (
|
||||||
if (field === 'taskConfig') {
|
<TaskConfigCard
|
||||||
setTaskConfig(value as ModelTaskConfig)
|
key={field.name}
|
||||||
setHasUnsavedChanges(true)
|
title={`${title} (${field.name})`}
|
||||||
}
|
description={subtitle}
|
||||||
}}
|
taskConfig={taskConfig[field.name] ?? { model_list: [] }}
|
||||||
hooks={fieldHooks}
|
modelNames={modelNames}
|
||||||
/>
|
onChange={(f, value) => updateTaskConfig(field.name, f, value)}
|
||||||
|
{...(index === 0 ? { dataTour: 'task-model-select' } : {})}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -42,19 +42,9 @@ export interface TaskConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 所有模型任务配置
|
* 所有模型任务配置(动态,由后端 schema 决定字段)
|
||||||
*/
|
*/
|
||||||
export interface ModelTaskConfig {
|
export type ModelTaskConfig = Record<string, TaskConfig>
|
||||||
utils: TaskConfig
|
|
||||||
tool_use: TaskConfig
|
|
||||||
replyer: TaskConfig
|
|
||||||
planner: TaskConfig
|
|
||||||
vlm: TaskConfig
|
|
||||||
voice: TaskConfig
|
|
||||||
embedding: TaskConfig
|
|
||||||
lpmm_entity_extract: TaskConfig
|
|
||||||
lpmm_rdf_build: TaskConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表单验证错误
|
* 表单验证错误
|
||||||
@@ -64,8 +54,3 @@ export interface FormErrors {
|
|||||||
api_provider?: string
|
api_provider?: string
|
||||||
model_identifier?: string
|
model_identifier?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 任务名称类型
|
|
||||||
*/
|
|
||||||
export type TaskName = keyof ModelTaskConfig
|
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ interface ModelConfig extends Record<string, unknown> {
|
|||||||
model_task_config?: Record<string, unknown>
|
model_task_config?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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() {
|
export function ModelProviderConfigPage() {
|
||||||
return (
|
return (
|
||||||
<RestartProvider>
|
<RestartProvider>
|
||||||
@@ -149,7 +157,7 @@ function ModelProviderConfigPageContent() {
|
|||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const config = result.data as ModelConfig
|
const config = unwrapModelConfig(result.data)
|
||||||
setProviders(Array.isArray(config.api_providers) ? config.api_providers as APIProvider[] : [])
|
setProviders(Array.isArray(config.api_providers) ? config.api_providers as APIProvider[] : [])
|
||||||
setHasUnsavedChanges(false)
|
setHasUnsavedChanges(false)
|
||||||
initialLoadRef.current = false
|
initialLoadRef.current = false
|
||||||
@@ -194,7 +202,7 @@ function ModelProviderConfigPageContent() {
|
|||||||
setSaving(false)
|
setSaving(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const config = resultGet.data as ModelConfig
|
const config = unwrapModelConfig(resultGet.data)
|
||||||
|
|
||||||
const validProviderNames = new Set(cleanedProviders.map(p => p.name))
|
const validProviderNames = new Set(cleanedProviders.map(p => p.name))
|
||||||
const originalModels = Array.isArray(config.models) ? config.models : []
|
const originalModels = Array.isArray(config.models) ? config.models : []
|
||||||
@@ -242,7 +250,7 @@ function ModelProviderConfigPageContent() {
|
|||||||
console.error('加载配置失败:', result.error)
|
console.error('加载配置失败:', result.error)
|
||||||
return { shouldProceed: true, providers: newProviders }
|
return { shouldProceed: true, providers: newProviders }
|
||||||
}
|
}
|
||||||
const config = result.data
|
const config = unwrapModelConfig(result.data)
|
||||||
const oldProviderNames = new Set(providers.map(p => p.name))
|
const oldProviderNames = new Set(providers.map(p => p.name))
|
||||||
const newProviderNames = new Set(newProviders.map(p => p.name))
|
const newProviderNames = new Set(newProviders.map(p => p.name))
|
||||||
|
|
||||||
@@ -296,7 +304,7 @@ function ModelProviderConfigPageContent() {
|
|||||||
savingFlag(false)
|
savingFlag(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const config = resultGet.data as ModelConfig
|
const config = unwrapModelConfig(resultGet.data)
|
||||||
|
|
||||||
const cleanedProviders = deleteConfirmState.pendingProviders.map(cleanProviderData)
|
const cleanedProviders = deleteConfirmState.pendingProviders.map(cleanProviderData)
|
||||||
const validProviderNames = new Set(cleanedProviders.map(p => p.name))
|
const validProviderNames = new Set(cleanedProviders.map(p => p.name))
|
||||||
@@ -475,7 +483,7 @@ function ModelProviderConfigPageContent() {
|
|||||||
setSaving(false)
|
setSaving(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const config = resultGet.data as ModelConfig
|
const config = unwrapModelConfig(resultGet.data)
|
||||||
|
|
||||||
const validProviderNames = new Set(cleanedProviders.map(p => p.name))
|
const validProviderNames = new Set(cleanedProviders.map(p => p.name))
|
||||||
const originalModels = Array.isArray(config.models) ? config.models : []
|
const originalModels = Array.isArray(config.models) ? config.models : []
|
||||||
|
|||||||
Reference in New Issue
Block a user