上传完整的WebUI前端仓库
This commit is contained in:
6
dashboard/src/routes/config/bot/hooks/index.ts
Normal file
6
dashboard/src/routes/config/bot/hooks/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Bot 配置页面相关 hooks
|
||||
*/
|
||||
|
||||
export { useAutoSave, useConfigAutoSave } from './useAutoSave'
|
||||
export type { UseAutoSaveOptions, UseAutoSaveReturn, AutoSaveState } from './useAutoSave'
|
||||
166
dashboard/src/routes/config/bot/hooks/useAutoSave.ts
Normal file
166
dashboard/src/routes/config/bot/hooks/useAutoSave.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { useEffect, useRef, useCallback } from 'react'
|
||||
import { updateBotConfigSection } from '@/lib/config-api'
|
||||
import type { ConfigSectionName } from '../types'
|
||||
|
||||
export interface UseAutoSaveOptions {
|
||||
/** 防抖延迟时间(毫秒),默认 2000ms */
|
||||
debounceMs?: number
|
||||
/** 保存成功回调 */
|
||||
onSaveSuccess?: () => void
|
||||
/** 保存失败回调 */
|
||||
onSaveError?: (error: Error) => void
|
||||
}
|
||||
|
||||
export interface UseAutoSaveReturn {
|
||||
/** 触发自动保存 */
|
||||
triggerAutoSave: (sectionName: ConfigSectionName, sectionData: unknown) => void
|
||||
/** 立即保存(不防抖) */
|
||||
saveNow: (sectionName: ConfigSectionName, sectionData: unknown) => Promise<void>
|
||||
/** 取消待处理的自动保存 */
|
||||
cancelPendingAutoSave: () => void
|
||||
}
|
||||
|
||||
export interface AutoSaveState {
|
||||
/** 是否正在保存中 */
|
||||
isAutoSaving: boolean
|
||||
/** 是否有未保存的更改 */
|
||||
hasUnsavedChanges: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动保存 hook
|
||||
*
|
||||
* 用于监听配置变化并自动防抖保存到后端
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { triggerAutoSave } = useAutoSave({
|
||||
* isInitialLoad,
|
||||
* setAutoSaving,
|
||||
* setHasUnsavedChanges,
|
||||
* })
|
||||
*
|
||||
* // 配置变化时触发
|
||||
* useEffect(() => {
|
||||
* if (config) triggerAutoSave('bot', config)
|
||||
* }, [config])
|
||||
* ```
|
||||
*/
|
||||
export function useAutoSave(
|
||||
isInitialLoad: boolean,
|
||||
setAutoSaving: (saving: boolean) => void,
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => void,
|
||||
options: UseAutoSaveOptions = {}
|
||||
): UseAutoSaveReturn {
|
||||
const { debounceMs = 2000, onSaveSuccess, onSaveError } = options
|
||||
const autoSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
// 执行保存操作
|
||||
const saveSection = useCallback(
|
||||
async (sectionName: ConfigSectionName, sectionData: unknown) => {
|
||||
try {
|
||||
setAutoSaving(true)
|
||||
await updateBotConfigSection(sectionName, sectionData)
|
||||
setHasUnsavedChanges(false)
|
||||
onSaveSuccess?.()
|
||||
} catch (error) {
|
||||
console.error(`自动保存 ${sectionName} 失败:`, error)
|
||||
setHasUnsavedChanges(true)
|
||||
onSaveError?.(error instanceof Error ? error : new Error(String(error)))
|
||||
} finally {
|
||||
setAutoSaving(false)
|
||||
}
|
||||
},
|
||||
[setAutoSaving, setHasUnsavedChanges, onSaveSuccess, onSaveError]
|
||||
)
|
||||
|
||||
// 触发自动保存(带防抖)
|
||||
const triggerAutoSave = useCallback(
|
||||
(sectionName: ConfigSectionName, sectionData: unknown) => {
|
||||
if (isInitialLoad) return
|
||||
|
||||
setHasUnsavedChanges(true)
|
||||
|
||||
if (autoSaveTimerRef.current) {
|
||||
clearTimeout(autoSaveTimerRef.current)
|
||||
}
|
||||
|
||||
autoSaveTimerRef.current = setTimeout(() => {
|
||||
saveSection(sectionName, sectionData)
|
||||
}, debounceMs)
|
||||
},
|
||||
[isInitialLoad, setHasUnsavedChanges, saveSection, debounceMs]
|
||||
)
|
||||
|
||||
// 立即保存(不防抖)
|
||||
const saveNow = useCallback(
|
||||
async (sectionName: ConfigSectionName, sectionData: unknown) => {
|
||||
if (autoSaveTimerRef.current) {
|
||||
clearTimeout(autoSaveTimerRef.current)
|
||||
autoSaveTimerRef.current = null
|
||||
}
|
||||
await saveSection(sectionName, sectionData)
|
||||
},
|
||||
[saveSection]
|
||||
)
|
||||
|
||||
// 取消待处理的自动保存
|
||||
const cancelPendingAutoSave = useCallback(() => {
|
||||
if (autoSaveTimerRef.current) {
|
||||
clearTimeout(autoSaveTimerRef.current)
|
||||
autoSaveTimerRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (autoSaveTimerRef.current) {
|
||||
clearTimeout(autoSaveTimerRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
triggerAutoSave,
|
||||
saveNow,
|
||||
cancelPendingAutoSave,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建配置自动保存 effect
|
||||
*
|
||||
* 这是一个工厂函数,用于创建监听特定配置变化并触发自动保存的 effect
|
||||
* 简化重复的 useEffect 代码
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // 使用方式 1: 直接在组件中调用
|
||||
* useConfigAutoSave(botConfig, 'bot', isInitialLoad, triggerAutoSave)
|
||||
* useConfigAutoSave(chatConfig, 'chat', isInitialLoad, triggerAutoSave)
|
||||
*
|
||||
* // 使用方式 2: 批量配置
|
||||
* const configs = [
|
||||
* { config: botConfig, section: 'bot' },
|
||||
* { config: chatConfig, section: 'chat' },
|
||||
* ] as const
|
||||
*
|
||||
* configs.forEach(({ config, section }) => {
|
||||
* useConfigAutoSave(config, section, isInitialLoad, triggerAutoSave)
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function useConfigAutoSave<T>(
|
||||
config: T | null,
|
||||
sectionName: ConfigSectionName,
|
||||
isInitialLoad: boolean,
|
||||
triggerAutoSave: (sectionName: ConfigSectionName, data: unknown) => void
|
||||
): void {
|
||||
useEffect(() => {
|
||||
if (config && !isInitialLoad) {
|
||||
triggerAutoSave(sectionName, config)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config])
|
||||
}
|
||||
24
dashboard/src/routes/config/bot/index.ts
Normal file
24
dashboard/src/routes/config/bot/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Bot 配置模块
|
||||
*
|
||||
* 这个模块包含麦麦主程序配置页面的所有组件和类型
|
||||
*
|
||||
* 目录结构:
|
||||
* - types.ts: 类型定义
|
||||
* - hooks/: 自定义 hooks
|
||||
* - useAutoSave.ts: 自动保存 hook
|
||||
* - sections/: 各个配置区块组件
|
||||
* - BotInfoSection.tsx
|
||||
* - PersonalitySection.tsx
|
||||
* - ChatSection.tsx
|
||||
* - ...等
|
||||
*/
|
||||
|
||||
// 类型导出
|
||||
export * from './types'
|
||||
|
||||
// Hooks 导出
|
||||
export * from './hooks'
|
||||
|
||||
// Section 组件导出
|
||||
export * from './sections'
|
||||
192
dashboard/src/routes/config/bot/sections/BotInfoSection.tsx
Normal file
192
dashboard/src/routes/config/bot/sections/BotInfoSection.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import React from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
import type { BotConfig } from '../types'
|
||||
|
||||
interface BotInfoSectionProps {
|
||||
config: BotConfig
|
||||
onChange: (config: BotConfig) => void
|
||||
}
|
||||
|
||||
export const BotInfoSection = React.memo(function BotInfoSection({ config, onChange }: BotInfoSectionProps) {
|
||||
// 确保 platforms 和 alias_names 始终是数组
|
||||
const platforms = config.platforms || []
|
||||
const aliasNames = config.alias_names || []
|
||||
|
||||
const addPlatform = () => {
|
||||
onChange({ ...config, platforms: [...platforms, ''] })
|
||||
}
|
||||
|
||||
const removePlatform = (index: number) => {
|
||||
onChange({
|
||||
...config,
|
||||
platforms: platforms.filter((_, i) => i !== index),
|
||||
})
|
||||
}
|
||||
|
||||
const updatePlatform = (index: number, value: string) => {
|
||||
const newPlatforms = [...platforms]
|
||||
newPlatforms[index] = value
|
||||
onChange({ ...config, platforms: newPlatforms })
|
||||
}
|
||||
|
||||
const addAlias = () => {
|
||||
onChange({ ...config, alias_names: [...aliasNames, ''] })
|
||||
}
|
||||
|
||||
const removeAlias = (index: number) => {
|
||||
onChange({
|
||||
...config,
|
||||
alias_names: aliasNames.filter((_, i) => i !== index),
|
||||
})
|
||||
}
|
||||
|
||||
const updateAlias = (index: number, value: string) => {
|
||||
const newAliases = [...aliasNames]
|
||||
newAliases[index] = value
|
||||
onChange({ ...config, alias_names: newAliases })
|
||||
}
|
||||
|
||||
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="platform">平台</Label>
|
||||
<Input
|
||||
id="platform"
|
||||
value={config.platform}
|
||||
onChange={(e) => onChange({ ...config, platform: e.target.value })}
|
||||
placeholder="qq"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="qq_account">QQ账号</Label>
|
||||
<Input
|
||||
id="qq_account"
|
||||
value={config.qq_account}
|
||||
onChange={(e) => onChange({ ...config, qq_account: e.target.value })}
|
||||
placeholder="123456789"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="nickname">昵称</Label>
|
||||
<Input
|
||||
id="nickname"
|
||||
value={config.nickname}
|
||||
onChange={(e) => onChange({ ...config, nickname: e.target.value })}
|
||||
placeholder="麦麦"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>别名</Label>
|
||||
<Button onClick={addAlias} size="sm" variant="outline">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{aliasNames.map((alias, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
value={alias}
|
||||
onChange={(e) => updateAlias(index, e.target.value)}
|
||||
placeholder="小麦"
|
||||
/>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="icon" variant="outline">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除别名 "{alias || '(空)'}" 吗?此操作无法撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => removeAlias(index)}>
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
))}
|
||||
{aliasNames.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">暂无别名</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>其他平台账号</Label>
|
||||
<Button onClick={addPlatform} size="sm" variant="outline">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{platforms.map((platform, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
value={platform}
|
||||
onChange={(e) => updatePlatform(index, e.target.value)}
|
||||
placeholder="wx:114514"
|
||||
/>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="icon" variant="outline">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除平台账号 "{platform || '(空)'}" 吗?此操作无法撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => removePlatform(index)}>
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
))}
|
||||
{platforms.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">暂无其他平台账号</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
610
dashboard/src/routes/config/bot/sections/ChatSection.tsx
Normal file
610
dashboard/src/routes/config/bot/sections/ChatSection.tsx
Normal file
@@ -0,0 +1,610 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { Plus, Trash2, Eye, Clock } from 'lucide-react'
|
||||
import type { ChatConfig } from '../types'
|
||||
|
||||
interface ChatSectionProps {
|
||||
config: ChatConfig
|
||||
onChange: (config: ChatConfig) => void
|
||||
}
|
||||
|
||||
// 时间选择组件
|
||||
const TimeRangePicker = React.memo(function TimeRangePicker({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}) {
|
||||
// 解析初始值
|
||||
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>
|
||||
)
|
||||
})
|
||||
|
||||
// 预览窗口组件
|
||||
const RulePreview = React.memo(function RulePreview({ rule }: { rule: { target: string; time: string; value: number } }) {
|
||||
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>
|
||||
)
|
||||
})
|
||||
|
||||
export const ChatSection = React.memo(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 && (
|
||||
<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={addTalkValueRule} size="sm">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加规则
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{config.talk_value_rules && config.talk_value_rules.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{config.talk_value_rules.map((rule, index) => (
|
||||
<div key={index} 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={() => removeTalkValueRule(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') {
|
||||
updateTalkValueRule(index, 'target', '')
|
||||
} else {
|
||||
updateTalkValueRule(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) => {
|
||||
updateTalkValueRule(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) => {
|
||||
updateTalkValueRule(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) => {
|
||||
updateTalkValueRule(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) => updateTalkValueRule(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)) {
|
||||
updateTalkValueRule(index, 'value', Math.max(0.01, Math.min(1, val)))
|
||||
}
|
||||
}}
|
||||
className="w-20 h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
value={[rule.value]}
|
||||
onValueChange={(values) =>
|
||||
updateTalkValueRule(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>
|
||||
))}
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
97
dashboard/src/routes/config/bot/sections/DebugSection.tsx
Normal file
97
dashboard/src/routes/config/bot/sections/DebugSection.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React from 'react'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import type { DebugConfig } from '../types'
|
||||
|
||||
interface DebugSectionProps {
|
||||
config: DebugConfig
|
||||
onChange: (config: DebugConfig) => void
|
||||
}
|
||||
|
||||
export const DebugSection = React.memo(function DebugSection({ config, onChange }: DebugSectionProps) {
|
||||
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="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>显示 Prompt</Label>
|
||||
<p className="text-sm text-muted-foreground">是否在日志中显示提示词</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.show_prompt}
|
||||
onCheckedChange={(checked) => onChange({ ...config, show_prompt: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>显示回复器 Prompt</Label>
|
||||
<p className="text-sm text-muted-foreground">是否显示回复器的提示词</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.show_replyer_prompt}
|
||||
onCheckedChange={(checked) => onChange({ ...config, show_replyer_prompt: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>显示回复器推理</Label>
|
||||
<p className="text-sm text-muted-foreground">是否显示回复器的推理过程</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.show_replyer_reasoning}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, show_replyer_reasoning: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>显示 Jargon Prompt</Label>
|
||||
<p className="text-sm text-muted-foreground">是否显示术语相关的提示词</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.show_jargon_prompt}
|
||||
onCheckedChange={(checked) => onChange({ ...config, show_jargon_prompt: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>显示记忆检索 Prompt</Label>
|
||||
<p className="text-sm text-muted-foreground">是否显示记忆检索相关的提示词</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.show_memory_prompt}
|
||||
onCheckedChange={(checked) => onChange({ ...config, show_memory_prompt: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>显示 Planner Prompt</Label>
|
||||
<p className="text-sm text-muted-foreground">是否显示 Planner 的提示词和原始返回结果</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.show_planner_prompt}
|
||||
onCheckedChange={(checked) => onChange({ ...config, show_planner_prompt: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>显示 LPMM 相关文段</Label>
|
||||
<p className="text-sm text-muted-foreground">是否显示 LPMM 知识库找到的相关文段日志</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.show_lpmm_paragraph}
|
||||
onCheckedChange={(checked) => onChange({ ...config, show_lpmm_paragraph: checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
215
dashboard/src/routes/config/bot/sections/DreamSection.tsx
Normal file
215
dashboard/src/routes/config/bot/sections/DreamSection.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { X } from 'lucide-react'
|
||||
import type { DreamConfig } from '../types'
|
||||
|
||||
interface DreamSectionProps {
|
||||
config: DreamConfig
|
||||
onChange: (config: DreamConfig) => void
|
||||
}
|
||||
|
||||
interface TimeRange {
|
||||
startTime: string
|
||||
endTime: string
|
||||
}
|
||||
|
||||
export const DreamSection = React.memo(function DreamSection({ config, onChange }: DreamSectionProps) {
|
||||
// 解析 dream_send 为 platform 和 userId
|
||||
const parseDreamSend = (dreamSend: string): { platform: string; userId: string } => {
|
||||
if (!dreamSend || !dreamSend.includes(':')) {
|
||||
return { platform: 'qq', userId: '' }
|
||||
}
|
||||
const [platform, userId] = dreamSend.split(':')
|
||||
return { platform, userId }
|
||||
}
|
||||
|
||||
const { platform: initialPlatform, userId: initialUserId } = parseDreamSend(config.dream_send)
|
||||
const [platform, setPlatform] = useState(initialPlatform)
|
||||
const [userId, setUserId] = useState(initialUserId)
|
||||
|
||||
// 解析时间段字符串为开始和结束时间
|
||||
const parseTimeRange = (range: string): TimeRange => {
|
||||
const [start, end] = range.split('-')
|
||||
return { startTime: start || '09:00', endTime: end || '22:00' }
|
||||
}
|
||||
|
||||
// 更新 dream_send
|
||||
const updateDreamSend = (newPlatform: string, newUserId: string) => {
|
||||
const dreamSend = newUserId ? `${newPlatform}:${newUserId}` : ''
|
||||
onChange({ ...config, dream_send: dreamSend })
|
||||
}
|
||||
|
||||
const handlePlatformChange = (value: string) => {
|
||||
setPlatform(value)
|
||||
updateDreamSend(value, userId)
|
||||
}
|
||||
|
||||
const handleUserIdChange = (value: string) => {
|
||||
setUserId(value)
|
||||
updateDreamSend(platform, value)
|
||||
}
|
||||
|
||||
const handleAddTimeRange = () => {
|
||||
onChange({
|
||||
...config,
|
||||
dream_time_ranges: [...config.dream_time_ranges, '09:00-22:00']
|
||||
})
|
||||
}
|
||||
|
||||
const handleRemoveTimeRange = (index: number) => {
|
||||
onChange({
|
||||
...config,
|
||||
dream_time_ranges: config.dream_time_ranges.filter((_, i) => i !== index)
|
||||
})
|
||||
}
|
||||
|
||||
const handleTimeRangeChange = (index: number, field: 'startTime' | 'endTime', value: string) => {
|
||||
const newRanges = [...config.dream_time_ranges]
|
||||
const currentRange = parseTimeRange(newRanges[index])
|
||||
|
||||
if (field === 'startTime') {
|
||||
currentRange.startTime = value
|
||||
} else {
|
||||
currentRange.endTime = value
|
||||
}
|
||||
|
||||
newRanges[index] = `${currentRange.startTime}-${currentRange.endTime}`
|
||||
onChange({
|
||||
...config,
|
||||
dream_time_ranges: newRanges
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-6">
|
||||
<h3 className="text-lg font-semibold">做梦配置</h3>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="interval_minutes">做梦时间间隔(分钟)</Label>
|
||||
<Input
|
||||
id="interval_minutes"
|
||||
type="number"
|
||||
min="1"
|
||||
value={config.interval_minutes}
|
||||
onChange={(e) => onChange({ ...config, interval_minutes: Number(e.target.value) })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">默认30分钟</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="max_iterations">做梦最大轮次</Label>
|
||||
<Input
|
||||
id="max_iterations"
|
||||
type="number"
|
||||
min="1"
|
||||
value={config.max_iterations}
|
||||
onChange={(e) => onChange({ ...config, max_iterations: Number(e.target.value) })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">默认20轮</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="first_delay_seconds">首次做梦延迟(秒)</Label>
|
||||
<Input
|
||||
id="first_delay_seconds"
|
||||
type="number"
|
||||
min="0"
|
||||
value={config.first_delay_seconds}
|
||||
onChange={(e) => onChange({ ...config, first_delay_seconds: Number(e.target.value) })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">程序启动后首次做梦前的延迟时间,默认60秒</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>做梦结果推送目标</Label>
|
||||
<div className="flex gap-2">
|
||||
<Select value={platform} onValueChange={handlePlatformChange}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="选择平台" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="qq">QQ</SelectItem>
|
||||
<SelectItem value="wx">微信</SelectItem>
|
||||
<SelectItem value="webui">WebUI</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="输入用户ID (例如: 123456)"
|
||||
value={userId}
|
||||
onChange={(e) => handleUserIdChange(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
选择平台并输入用户ID,做梦结束后将梦境发送给该用户。用户ID为空则不推送
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>做梦时间段配置</Label>
|
||||
<Button type="button" size="sm" onClick={handleAddTimeRange}>
|
||||
添加时间段
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
设置允许做梦的时间段,支持跨夜区间(如 23:00 到次日 02:00)。列表为空则全天允许做梦
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{config.dream_time_ranges.map((range, index) => {
|
||||
const { startTime, endTime } = parseTimeRange(range)
|
||||
return (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<Input
|
||||
type="time"
|
||||
value={startTime}
|
||||
onChange={(e) => handleTimeRangeChange(index, 'startTime', e.target.value)}
|
||||
className="w-[140px]"
|
||||
/>
|
||||
<span className="text-muted-foreground">至</span>
|
||||
<Input
|
||||
type="time"
|
||||
value={endTime}
|
||||
onChange={(e) => handleTimeRangeChange(index, 'endTime', e.target.value)}
|
||||
className="w-[140px]"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveTimeRange(index)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{config.dream_time_ranges.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">当前配置为全天允许做梦</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="dream_visible"
|
||||
checked={config.dream_visible}
|
||||
onCheckedChange={(checked) => onChange({ ...config, dream_visible: checked })}
|
||||
/>
|
||||
<Label htmlFor="dream_visible" className="cursor-pointer">
|
||||
梦境结果存储到上下文
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
开启后,梦境发送给配置的用户后,也会存储到聊天上下文中,在后续对话中可见
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
311
dashboard/src/routes/config/bot/sections/ExperimentalSection.tsx
Normal file
311
dashboard/src/routes/config/bot/sections/ExperimentalSection.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
import React from 'react'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { Plus, Trash2, AlertTriangle, Eye, Code2 } from 'lucide-react'
|
||||
import type { ExperimentalConfig } from '../types'
|
||||
|
||||
interface ChatPromptData {
|
||||
platform: string
|
||||
id: string
|
||||
type: 'group' | 'private'
|
||||
prompt: string
|
||||
}
|
||||
|
||||
interface ExperimentalSectionProps {
|
||||
config: ExperimentalConfig
|
||||
onChange: (config: ExperimentalConfig) => void
|
||||
}
|
||||
|
||||
export const ExperimentalSection = React.memo(function ExperimentalSection({ config, onChange }: ExperimentalSectionProps) {
|
||||
// 解析 chat_prompt 字符串为结构化数据
|
||||
const parseChatPrompt = (promptStr: string): ChatPromptData => {
|
||||
const parts = promptStr.split(':')
|
||||
if (parts.length >= 4) {
|
||||
const platform = parts[0]
|
||||
const id = parts[1]
|
||||
const type = parts[2] as 'group' | 'private'
|
||||
const prompt = parts.slice(3).join(':') // 处理 prompt 中可能包含的冒号
|
||||
return { platform, id, type, prompt }
|
||||
}
|
||||
return { platform: 'qq', id: '', type: 'group', prompt: '' }
|
||||
}
|
||||
|
||||
// 将结构化数据转换为字符串
|
||||
const stringifyChatPrompt = (data: ChatPromptData): string => {
|
||||
return `${data.platform}:${data.id}:${data.type}:${data.prompt}`
|
||||
}
|
||||
|
||||
const addChatPrompt = () => {
|
||||
onChange({ ...config, chat_prompts: [...config.chat_prompts, 'qq::group:'] })
|
||||
}
|
||||
|
||||
const removeChatPrompt = (index: number) => {
|
||||
onChange({
|
||||
...config,
|
||||
chat_prompts: config.chat_prompts.filter((_, i) => i !== index),
|
||||
})
|
||||
}
|
||||
|
||||
const updateChatPrompt = (index: number, data: Partial<ChatPromptData>) => {
|
||||
const currentData = parseChatPrompt(config.chat_prompts[index])
|
||||
const newData = { ...currentData, ...data }
|
||||
const newPrompts = [...config.chat_prompts]
|
||||
newPrompts[index] = stringifyChatPrompt(newData)
|
||||
onChange({ ...config, chat_prompts: newPrompts })
|
||||
}
|
||||
|
||||
// 预览组件
|
||||
const ChatPromptPreview = ({ promptStr }: { promptStr: string }) => {
|
||||
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">
|
||||
"{promptStr}"
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
这是保存到 bot_config.toml 文件中的格式
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-6">
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg bg-orange-500/10 border border-orange-500/20">
|
||||
<AlertTriangle className="h-5 w-5 text-orange-500 shrink-0 mt-0.5" />
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-medium text-orange-500">实验性功能</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
此部分包含实验性功能,可能不稳定或在未来版本中发生变化。请谨慎使用,并注意不推荐在生产环境中修改私聊规则。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">实验性设置</h3>
|
||||
|
||||
<div className="grid gap-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="lpmm_memory"
|
||||
checked={config.lpmm_memory ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, lpmm_memory: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="lpmm_memory" className="cursor-pointer">
|
||||
将聊天历史总结导入到 LPMM 知识库
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground -mt-4">
|
||||
开启后,chat_history_summarizer 总结出的历史记录会同时导入到知识库
|
||||
</p>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="private_plan_style">私聊规则(实验性)</Label>
|
||||
<Textarea
|
||||
id="private_plan_style"
|
||||
value={config.private_plan_style}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange({ ...config, private_plan_style: e.target.value })}
|
||||
placeholder="私聊的说话规则和行为风格(不推荐修改)"
|
||||
rows={4}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
⚠️ 不推荐修改此项,可能会影响私聊对话的稳定性
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label>特定聊天 Prompt 配置</Label>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
为指定聊天添加额外的 prompt,用于定制特定场景的对话行为
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={addChatPrompt} size="sm" variant="outline">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加配置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{config.chat_prompts.map((promptStr, index) => {
|
||||
const data = parseChatPrompt(promptStr)
|
||||
|
||||
return (
|
||||
<div key={index} className="rounded-lg border p-4 space-y-4 bg-card">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">
|
||||
Prompt 配置 {index + 1}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<ChatPromptPreview promptStr={promptStr} />
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="sm" variant="ghost">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除这个 prompt 配置吗?此操作无法撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => removeChatPrompt(index)}>
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{/* 平台选择 */}
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-xs font-medium">平台</Label>
|
||||
<Select
|
||||
value={data.platform}
|
||||
onValueChange={(value) => updateChatPrompt(index, { platform: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择平台" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="qq">QQ</SelectItem>
|
||||
<SelectItem value="wx">微信</SelectItem>
|
||||
<SelectItem value="webui">WebUI</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* ID 输入 */}
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-xs font-medium">
|
||||
{data.type === 'group' ? '群号' : '用户ID'}
|
||||
</Label>
|
||||
<Input
|
||||
value={data.id}
|
||||
onChange={(e) => updateChatPrompt(index, { id: e.target.value })}
|
||||
placeholder={data.type === 'group' ? '输入群号' : '输入用户ID'}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 类型选择 */}
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-xs font-medium">类型</Label>
|
||||
<Select
|
||||
value={data.type}
|
||||
onValueChange={(value: 'group' | 'private') => updateChatPrompt(index, { type: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="group">群聊 (group)</SelectItem>
|
||||
<SelectItem value="private">私聊 (private)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Prompt 内容 */}
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-xs font-medium">Prompt 内容</Label>
|
||||
<Textarea
|
||||
value={data.prompt}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => updateChatPrompt(index, { prompt: e.target.value })}
|
||||
placeholder="输入额外的 prompt 内容,例如:这是一个摄影群,你精通摄影知识"
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
这段文本会作为系统提示添加到该聊天的上下文中
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 原始格式显示 */}
|
||||
<div className="rounded-md bg-muted/50 p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Code2 className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-xs font-medium text-muted-foreground">原始格式</span>
|
||||
</div>
|
||||
<code className="text-xs font-mono text-muted-foreground break-all">
|
||||
{promptStr || '(未配置)'}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{config.chat_prompts.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<p className="text-sm">暂无特定聊天 prompt 配置</p>
|
||||
<p className="text-xs mt-1">点击上方"添加配置"按钮创建新配置</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 使用说明 */}
|
||||
<div className="text-xs text-muted-foreground space-y-2 p-4 rounded-lg bg-muted/30 border">
|
||||
<p className="font-medium text-foreground">💡 使用说明</p>
|
||||
<ul className="list-disc list-inside space-y-1 pl-2">
|
||||
<li>为不同的聊天环境配置专属的行为提示</li>
|
||||
<li>支持多个平台:QQ、微信、WebUI</li>
|
||||
<li>可为群聊或私聊分别配置</li>
|
||||
<li>Prompt 会自动注入到该聊天的上下文中</li>
|
||||
</ul>
|
||||
<p className="font-medium text-foreground mt-3">📝 配置示例</p>
|
||||
<ul className="list-disc list-inside space-y-1 pl-2">
|
||||
<li>摄影群:<code className="text-xs bg-muted px-1 py-0.5 rounded">这是一个摄影群,你精通摄影知识</code></li>
|
||||
<li>二次元群:<code className="text-xs bg-muted px-1 py-0.5 rounded">这是一个二次元交流群</code></li>
|
||||
<li>好友私聊:<code className="text-xs bg-muted px-1 py-0.5 rounded">这是你与好朋友的私聊</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
996
dashboard/src/routes/config/bot/sections/ExpressionSection.tsx
Normal file
996
dashboard/src/routes/config/bot/sections/ExpressionSection.tsx
Normal file
@@ -0,0 +1,996 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { Plus, Trash2, Eye } from 'lucide-react'
|
||||
import type { ExpressionConfig } from '../types'
|
||||
|
||||
interface ExpressionGroupMemberInputProps {
|
||||
member: string
|
||||
groupIndex: number
|
||||
memberIndex: number
|
||||
availableChatIds: string[]
|
||||
onUpdate: (groupIndex: number, memberIndex: number, value: string) => void
|
||||
onRemove: (groupIndex: number, memberIndex: number) => void
|
||||
}
|
||||
|
||||
const ExpressionGroupMemberInput = React.memo(function ExpressionGroupMemberInput({
|
||||
member,
|
||||
groupIndex,
|
||||
memberIndex,
|
||||
availableChatIds,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
}: ExpressionGroupMemberInputProps) {
|
||||
// 判断当前成员是否在可选列表中
|
||||
const isFromList = availableChatIds.includes(member) || member === '*'
|
||||
const [inputMode, setInputMode] = useState(!isFromList)
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
{/* 输入模式切换 */}
|
||||
<div className="flex-1 flex gap-2">
|
||||
{inputMode ? (
|
||||
// 手动输入模式
|
||||
<>
|
||||
<Input
|
||||
value={member}
|
||||
onChange={(e) => onUpdate(groupIndex, memberIndex, e.target.value)}
|
||||
placeholder='输入 "*" 或 "qq:123456:group"'
|
||||
className="flex-1"
|
||||
/>
|
||||
{availableChatIds.length > 0 && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setInputMode(false)}
|
||||
title="切换到下拉选择"
|
||||
>
|
||||
下拉
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// 下拉选择模式
|
||||
<>
|
||||
<Select
|
||||
value={member}
|
||||
onValueChange={(value) => onUpdate(groupIndex, memberIndex, value)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="选择聊天流" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="*">* (全局共享)</SelectItem>
|
||||
{availableChatIds.map((chatId, idx) => (
|
||||
<SelectItem key={idx} value={chatId}>
|
||||
{chatId}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setInputMode(true)}
|
||||
title="切换到手动输入"
|
||||
>
|
||||
输入
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 删除按钮 */}
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="icon" variant="outline">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除组成员 "{member || '(空)'}" 吗?此操作无法撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => onRemove(groupIndex, memberIndex)}>
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
interface ExpressionSectionProps {
|
||||
config: ExpressionConfig
|
||||
onChange: (config: ExpressionConfig) => void
|
||||
}
|
||||
|
||||
export const ExpressionSection = React.memo(function ExpressionSection({
|
||||
config,
|
||||
onChange,
|
||||
}: ExpressionSectionProps) {
|
||||
// 添加学习规则
|
||||
const addLearningRule = () => {
|
||||
onChange({
|
||||
...config,
|
||||
learning_list: [...config.learning_list, ['', 'enable', 'enable', '1.0']],
|
||||
})
|
||||
}
|
||||
|
||||
// 删除学习规则
|
||||
const removeLearningRule = (index: number) => {
|
||||
onChange({
|
||||
...config,
|
||||
learning_list: config.learning_list.filter((_, i) => i !== index),
|
||||
})
|
||||
}
|
||||
|
||||
// 更新学习规则
|
||||
const updateLearningRule = (
|
||||
index: number,
|
||||
field: 0 | 1 | 2 | 3,
|
||||
value: string
|
||||
) => {
|
||||
const newList = [...config.learning_list]
|
||||
newList[index][field] = value
|
||||
onChange({
|
||||
...config,
|
||||
learning_list: newList,
|
||||
})
|
||||
}
|
||||
|
||||
// 预览组件
|
||||
const LearningRulePreview = ({ rule }: { rule: [string, string, string, string] }) => {
|
||||
const previewText = `["${rule[0]}", "${rule[1]}", "${rule[2]}", "${rule[3]}"]`
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
// 添加表达组
|
||||
const addExpressionGroup = () => {
|
||||
onChange({
|
||||
...config,
|
||||
expression_groups: [...config.expression_groups, []],
|
||||
})
|
||||
}
|
||||
|
||||
// 删除表达组
|
||||
const removeExpressionGroup = (index: number) => {
|
||||
onChange({
|
||||
...config,
|
||||
expression_groups: config.expression_groups.filter((_, i) => i !== index),
|
||||
})
|
||||
}
|
||||
|
||||
// 添加组成员
|
||||
const addGroupMember = (groupIndex: number) => {
|
||||
const newGroups = [...config.expression_groups]
|
||||
newGroups[groupIndex] = [...newGroups[groupIndex], '']
|
||||
onChange({
|
||||
...config,
|
||||
expression_groups: newGroups,
|
||||
})
|
||||
}
|
||||
|
||||
// 删除组成员
|
||||
const removeGroupMember = (groupIndex: number, memberIndex: number) => {
|
||||
const newGroups = [...config.expression_groups]
|
||||
newGroups[groupIndex] = newGroups[groupIndex].filter((_, i) => i !== memberIndex)
|
||||
onChange({
|
||||
...config,
|
||||
expression_groups: newGroups,
|
||||
})
|
||||
}
|
||||
|
||||
// 更新组成员
|
||||
const updateGroupMember = (groupIndex: number, memberIndex: number, value: string) => {
|
||||
const newGroups = [...config.expression_groups]
|
||||
newGroups[groupIndex][memberIndex] = value
|
||||
onChange({
|
||||
...config,
|
||||
expression_groups: newGroups,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 黑话设置 - 移到顶部 */}
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
|
||||
<h3 className="text-lg font-semibold mb-4">黑话设置</h3>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="all_global_jargon"
|
||||
checked={config.all_global_jargon ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, all_global_jargon: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="all_global_jargon" className="cursor-pointer">
|
||||
全局黑话模式
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
开启后,新增的黑话将默认设为全局(所有聊天流共享)。关闭后,已记录的全局黑话不会改变,需要手动删除。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="enable_jargon_explanation"
|
||||
checked={config.enable_jargon_explanation ?? true}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, enable_jargon_explanation: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="enable_jargon_explanation" className="cursor-pointer">
|
||||
启用黑话解释
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
在回复前尝试对上下文中的黑话进行解释。关闭可减少一次LLM调用,仅影响回复前的黑话匹配与解释,不影响黑话学习。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="jargon_mode">黑话解释来源模式</Label>
|
||||
<Select
|
||||
value={config.jargon_mode ?? 'context'}
|
||||
onValueChange={(value) => onChange({ ...config, jargon_mode: value })}
|
||||
>
|
||||
<SelectTrigger id="jargon_mode" className="mt-2">
|
||||
<SelectValue placeholder="选择黑话解释来源" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="context">上下文模式(自动匹配黑话)</SelectItem>
|
||||
<SelectItem value="planner">Planner模式(使用unknown_words列表)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
上下文模式:使用上下文自动匹配黑话并解释<br />
|
||||
Planner模式:仅使用Planner在reply动作中给出的unknown_words列表进行黑话检索
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 表达学习配置 */}
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-6">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">表达学习配置</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
配置麦麦如何学习和使用表达方式
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={addLearningRule} size="sm" variant="outline">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加规则
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{config.learning_list.map((rule, index) => {
|
||||
// 检查是否已有全局配置(rule[0] === '')
|
||||
const hasGlobalConfig = config.learning_list.some((r, i) => i !== index && r[0] === '')
|
||||
const isGlobal = rule[0] === ''
|
||||
|
||||
// 解析聊天流 ID(格式:platform:id:type)
|
||||
const parts = rule[0].split(':')
|
||||
const platform = parts[0] || 'qq'
|
||||
const chatId = parts[1] || ''
|
||||
const chatType = parts[2] || 'group'
|
||||
|
||||
return (
|
||||
<div key={index} className="rounded-lg border p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">
|
||||
规则 {index + 1} {isGlobal && '(全局配置)'}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<LearningRulePreview rule={rule} />
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="sm" variant="ghost">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除学习规则 {index + 1} 吗?此操作无法撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => removeLearningRule(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={isGlobal ? 'global' : 'specific'}
|
||||
onValueChange={(value) => {
|
||||
if (value === 'global') {
|
||||
updateLearningRule(index, 0, '')
|
||||
} else {
|
||||
// 切换到详细配置时,设置默认值
|
||||
updateLearningRule(index, 0, 'qq::group')
|
||||
}
|
||||
}}
|
||||
disabled={hasGlobalConfig && !isGlobal}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="global">全局配置</SelectItem>
|
||||
<SelectItem value="specific" disabled={hasGlobalConfig && !isGlobal}>
|
||||
详细配置
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{hasGlobalConfig && !isGlobal && (
|
||||
<p className="text-xs text-amber-600">
|
||||
已存在全局配置,无法创建新的全局配置
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 详细配置选项 - 只在非全局时显示 */}
|
||||
{!isGlobal && (
|
||||
<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) => {
|
||||
updateLearningRule(index, 0, `${value}:${chatId}:${chatType}`)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="qq">QQ</SelectItem>
|
||||
<SelectItem value="wx">微信</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 群 ID 输入 */}
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-xs font-medium">群 ID</Label>
|
||||
<Input
|
||||
value={chatId}
|
||||
onChange={(e) => {
|
||||
updateLearningRule(index, 0, `${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) => {
|
||||
updateLearningRule(index, 0, `${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[0] || '(未设置)'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 使用学到的表达 - 改为开关 */}
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-xs font-medium">使用学到的表达</Label>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
允许麦麦使用从聊天中学到的表达方式
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={rule[1] === 'enable'}
|
||||
onCheckedChange={(checked) =>
|
||||
updateLearningRule(index, 1, checked ? 'enable' : 'disable')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 学习表达 - 改为开关 */}
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-xs font-medium">学习表达</Label>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
允许麦麦从聊天中学习新的表达方式
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={rule[2] === 'enable'}
|
||||
onCheckedChange={(checked) =>
|
||||
updateLearningRule(index, 2, checked ? 'enable' : 'disable')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 启用黑话学习 - 改为开关 */}
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-xs font-medium">启用黑话学习</Label>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
允许麦麦在此聊天流中学习和记录黑话
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={rule[3] === 'true' || rule[3] === 'enable'}
|
||||
onCheckedChange={(checked) =>
|
||||
updateLearningRule(index, 3, checked ? 'true' : 'false')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{config.learning_list.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
暂无学习规则,点击"添加规则"开始配置
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 表达反思配置 */}
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-6">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">表达优化配置</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
配置麦麦如何优化和改进表达方式
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 自动表达优化 */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="expression_self_reflect" className="cursor-pointer font-medium">
|
||||
自动表达优化
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
启用后,麦麦会自动检查并优化表达方式,无需管理员手动干预
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="expression_self_reflect"
|
||||
checked={config.expression_self_reflect ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, expression_self_reflect: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.expression_self_reflect && (
|
||||
<div className="space-y-4 pl-4 border-l-2 border-primary/20">
|
||||
{/* 自动检查间隔 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="expression_auto_check_interval">
|
||||
自动检查间隔(秒)
|
||||
</Label>
|
||||
<Input
|
||||
id="expression_auto_check_interval"
|
||||
type="number"
|
||||
min="60"
|
||||
value={config.expression_auto_check_interval ?? 3600}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...config,
|
||||
expression_auto_check_interval: parseInt(e.target.value) || 3600,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
表达方式自动检查的间隔时间(单位:秒),默认值:3600秒(1小时)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 每次检查数量 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="expression_auto_check_count">
|
||||
每次检查数量
|
||||
</Label>
|
||||
<Input
|
||||
id="expression_auto_check_count"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
value={config.expression_auto_check_count ?? 10}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
...config,
|
||||
expression_auto_check_count: parseInt(e.target.value) || 10,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
每次自动检查时随机选取的表达方式数量,默认值:10条
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 自定义评估标准 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>自定义评估标准</Label>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange({
|
||||
...config,
|
||||
expression_auto_check_custom_criteria: [
|
||||
...(config.expression_auto_check_custom_criteria || []),
|
||||
'',
|
||||
],
|
||||
})
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加标准
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{(config.expression_auto_check_custom_criteria || []).map((criterion, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
value={criterion}
|
||||
onChange={(e) => {
|
||||
const newCriteria = [...(config.expression_auto_check_custom_criteria || [])]
|
||||
newCriteria[index] = e.target.value
|
||||
onChange({ ...config, expression_auto_check_custom_criteria: newCriteria })
|
||||
}}
|
||||
placeholder="输入评估标准,例如:是否符合角色人设"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange({
|
||||
...config,
|
||||
expression_auto_check_custom_criteria: (config.expression_auto_check_custom_criteria || []).filter((_, i) => i !== index),
|
||||
})
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{(!config.expression_auto_check_custom_criteria || config.expression_auto_check_custom_criteria.length === 0) && (
|
||||
<div className="text-center py-4 text-muted-foreground text-sm">
|
||||
暂无自定义标准,点击"添加标准"开始配置
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
这些标准会被添加到评估提示词中,作为额外的评估要求
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 仅使用已检查的表达方式 */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="expression_checked_only" className="cursor-pointer font-medium">
|
||||
仅使用已审核通过的表达方式
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
开启后,只有通过审核(已检查)的项目会被使用;关闭时,未审核的项目也会被使用。无论开关状态,被拒绝的项目永远不会被使用。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="expression_checked_only"
|
||||
checked={config.expression_checked_only ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, expression_checked_only: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 手动表达优化 */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="expression_manual_reflect" className="cursor-pointer font-medium">
|
||||
手动表达优化
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
启用后,麦麦会主动向管理员询问表达方式是否合适
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="expression_manual_reflect"
|
||||
checked={config.expression_manual_reflect ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, expression_manual_reflect: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.expression_manual_reflect && (
|
||||
<div className="space-y-4 pl-4 border-l-2 border-primary/20">
|
||||
{/* 表达反思操作员 ID */}
|
||||
<div className="rounded-lg border p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">反思操作员</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{(() => {
|
||||
const operatorId = config.manual_reflect_operator_id || ''
|
||||
const parts = operatorId.split(':')
|
||||
const platform = parts[0] || 'qq'
|
||||
const chatId = parts[1] || ''
|
||||
const chatType = parts[2] || 'private'
|
||||
|
||||
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) => {
|
||||
onChange({ ...config, manual_reflect_operator_id: `${value}:${chatId}:${chatType}` })
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="qq">QQ</SelectItem>
|
||||
<SelectItem value="wx">微信</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* ID 输入 */}
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-xs font-medium">用户/群 ID</Label>
|
||||
<Input
|
||||
value={chatId}
|
||||
onChange={(e) => {
|
||||
onChange({ ...config, manual_reflect_operator_id: `${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) => {
|
||||
onChange({ ...config, manual_reflect_operator_id: `${platform}:${chatId}:${value}` })
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="private">私聊(private)</SelectItem>
|
||||
<SelectItem value="group">群组(group)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
当前操作员 ID:{config.manual_reflect_operator_id || '(未设置)'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
手动表达优化操作员ID,格式:platform:id:type (例如 "qq:123456:private" 或 "qq:654321:group")
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 允许反思的聊天流列表 */}
|
||||
<div className="rounded-lg border p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium">允许进行表达反思的聊天流</span>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
只有在此列表中的聊天流才会提出问题并跟踪。如果列表为空,则所有聊天流都可以进行表达反思(前提是启用了手动表达优化)
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange({
|
||||
...config,
|
||||
allow_reflect: [...(config.allow_reflect || []), 'qq::group'],
|
||||
})
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加聊天流
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{(config.allow_reflect || []).map((chatId, index) => {
|
||||
const parts = chatId.split(':')
|
||||
const platform = parts[0] || 'qq'
|
||||
const id = parts[1] || ''
|
||||
const chatType = parts[2] || 'group'
|
||||
|
||||
return (
|
||||
<div key={index} className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
|
||||
<Select
|
||||
value={platform}
|
||||
onValueChange={(value) => {
|
||||
const newList = [...config.allow_reflect]
|
||||
newList[index] = `${value}:${id}:${chatType}`
|
||||
onChange({ ...config, allow_reflect: newList })
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="qq">QQ</SelectItem>
|
||||
<SelectItem value="wx">微信</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
value={id}
|
||||
onChange={(e) => {
|
||||
const newList = [...config.allow_reflect]
|
||||
newList[index] = `${platform}:${e.target.value}:${chatType}`
|
||||
onChange({ ...config, allow_reflect: newList })
|
||||
}}
|
||||
placeholder="ID"
|
||||
className="flex-1 font-mono text-sm"
|
||||
/>
|
||||
|
||||
<Select
|
||||
value={chatType}
|
||||
onValueChange={(value) => {
|
||||
const newList = [...config.allow_reflect]
|
||||
newList[index] = `${platform}:${id}:${value}`
|
||||
onChange({ ...config, allow_reflect: newList })
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="group">群组</SelectItem>
|
||||
<SelectItem value="private">私聊</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange({
|
||||
...config,
|
||||
allow_reflect: config.allow_reflect.filter((_, i) => i !== index),
|
||||
})
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{(!config.allow_reflect || config.allow_reflect.length === 0) && (
|
||||
<div className="text-center py-4 text-muted-foreground text-sm">
|
||||
列表为空,所有聊天流都可以进行表达反思
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 表达共享组配置 */}
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-6">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">表达共享组配置</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
配置不同聊天流之间如何共享学到的表达方式
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={addExpressionGroup} size="sm" variant="outline">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加共享组
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{config.expression_groups.map((group, groupIndex) => {
|
||||
// 获取所有已配置的聊天流 ID(用于下拉框选项)
|
||||
const availableChatIds = config.learning_list
|
||||
.map(rule => rule[0])
|
||||
.filter(id => id !== '') // 过滤掉全局配置
|
||||
|
||||
return (
|
||||
<div key={groupIndex} className="rounded-lg border p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">
|
||||
共享组 {groupIndex + 1}
|
||||
{group.length === 1 && group[0] === '*' && '(全局共享)'}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => addGroupMember(groupIndex)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="sm" variant="ghost">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除共享组 {groupIndex + 1} 吗?此操作无法撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => removeExpressionGroup(groupIndex)}>
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{group.map((member, memberIndex) => (
|
||||
<ExpressionGroupMemberInput
|
||||
key={`${groupIndex}-${memberIndex}`}
|
||||
member={member}
|
||||
groupIndex={groupIndex}
|
||||
memberIndex={memberIndex}
|
||||
availableChatIds={availableChatIds}
|
||||
onUpdate={updateGroupMember}
|
||||
onRemove={removeGroupMember}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
提示:可以从下拉框选择已配置的聊天流,或手动输入。输入 "*" 启用全局共享
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{config.expression_groups.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
暂无共享组,点击"添加共享组"开始配置
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
})
|
||||
336
dashboard/src/routes/config/bot/sections/FeaturesSection.tsx
Normal file
336
dashboard/src/routes/config/bot/sections/FeaturesSection.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
import React from 'react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import type { EmojiConfig, MemoryConfig, ToolConfig, VoiceConfig } from '../types'
|
||||
|
||||
interface FeaturesSectionProps {
|
||||
emojiConfig: EmojiConfig
|
||||
memoryConfig: MemoryConfig
|
||||
toolConfig: ToolConfig
|
||||
voiceConfig: VoiceConfig
|
||||
onEmojiChange: (config: EmojiConfig) => void
|
||||
onMemoryChange: (config: MemoryConfig) => void
|
||||
onToolChange: (config: ToolConfig) => void
|
||||
onVoiceChange: (config: VoiceConfig) => void
|
||||
}
|
||||
|
||||
export const FeaturesSection = React.memo(function FeaturesSection({
|
||||
emojiConfig,
|
||||
memoryConfig,
|
||||
toolConfig,
|
||||
voiceConfig,
|
||||
onEmojiChange,
|
||||
onMemoryChange,
|
||||
onToolChange,
|
||||
onVoiceChange,
|
||||
}: FeaturesSectionProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 工具设置 */}
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">工具设置</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="enable_tool"
|
||||
checked={toolConfig.enable_tool}
|
||||
onCheckedChange={(checked) => onToolChange({ ...toolConfig, enable_tool: checked })}
|
||||
/>
|
||||
<Label htmlFor="enable_tool" className="cursor-pointer">
|
||||
启用工具系统
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground -mt-2">
|
||||
允许麦麦使用各种工具来增强功能
|
||||
</p>
|
||||
|
||||
<div className="flex items-center space-x-2 pt-2">
|
||||
<Switch
|
||||
id="enable_asr"
|
||||
checked={voiceConfig.enable_asr}
|
||||
onCheckedChange={(checked) => onVoiceChange({ ...voiceConfig, enable_asr: checked })}
|
||||
/>
|
||||
<Label htmlFor="enable_asr" className="cursor-pointer">
|
||||
启用语音识别
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground -mt-2">
|
||||
启用后麦麦可以识别语音消息,需要配置语音识别模型
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 记忆设置 */}
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">记忆设置</h3>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="max_agent_iterations">记忆思考深度</Label>
|
||||
<Input
|
||||
id="max_agent_iterations"
|
||||
type="number"
|
||||
min="1"
|
||||
value={memoryConfig.max_agent_iterations}
|
||||
onChange={(e) =>
|
||||
onMemoryChange({ ...memoryConfig, max_agent_iterations: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">最低为 1(不深入思考)</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="agent_timeout_seconds">最长回忆时间(秒)</Label>
|
||||
<Input
|
||||
id="agent_timeout_seconds"
|
||||
type="number"
|
||||
min="1"
|
||||
step="0.1"
|
||||
value={memoryConfig.agent_timeout_seconds ?? 120}
|
||||
onChange={(e) =>
|
||||
onMemoryChange({ ...memoryConfig, agent_timeout_seconds: parseFloat(e.target.value) })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">记忆检索的超时时间,避免过长的等待</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="enable_jargon_detection"
|
||||
checked={memoryConfig.enable_jargon_detection ?? true}
|
||||
onCheckedChange={(checked) =>
|
||||
onMemoryChange({ ...memoryConfig, enable_jargon_detection: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="enable_jargon_detection" className="cursor-pointer">
|
||||
启用黑话识别
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground -mt-2">
|
||||
记忆检索过程中是否启用黑话识别
|
||||
</p>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="global_memory"
|
||||
checked={memoryConfig.global_memory ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
onMemoryChange({ ...memoryConfig, global_memory: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="global_memory" className="cursor-pointer">
|
||||
全局记忆查询
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground -mt-2">
|
||||
允许记忆检索在所有聊天记录中进行全局查询(忽略当前聊天流)
|
||||
</p>
|
||||
|
||||
{/* 聊天历史总结配置 */}
|
||||
<div className="border-t pt-4 mt-4">
|
||||
<h4 className="text-sm font-semibold mb-3">聊天历史总结配置</h4>
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="chat_history_topic_check_message_threshold">话题检查消息数阈值</Label>
|
||||
<Input
|
||||
id="chat_history_topic_check_message_threshold"
|
||||
type="number"
|
||||
min="1"
|
||||
value={memoryConfig.chat_history_topic_check_message_threshold ?? 80}
|
||||
onChange={(e) =>
|
||||
onMemoryChange({ ...memoryConfig, chat_history_topic_check_message_threshold: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
当累积消息数达到此值时触发话题检查
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="chat_history_topic_check_time_hours">话题检查时间阈值(小时)</Label>
|
||||
<Input
|
||||
id="chat_history_topic_check_time_hours"
|
||||
type="number"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
value={memoryConfig.chat_history_topic_check_time_hours ?? 8.0}
|
||||
onChange={(e) =>
|
||||
onMemoryChange({ ...memoryConfig, chat_history_topic_check_time_hours: parseFloat(e.target.value) })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
当距离上次检查超过此时间且消息数达到最小阈值时触发话题检查
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="chat_history_topic_check_min_messages">时间触发最小消息数</Label>
|
||||
<Input
|
||||
id="chat_history_topic_check_min_messages"
|
||||
type="number"
|
||||
min="1"
|
||||
value={memoryConfig.chat_history_topic_check_min_messages ?? 20}
|
||||
onChange={(e) =>
|
||||
onMemoryChange({ ...memoryConfig, chat_history_topic_check_min_messages: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
时间触发模式下的最小消息数阈值
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="chat_history_finalize_no_update_checks">打包存储连续无更新次数</Label>
|
||||
<Input
|
||||
id="chat_history_finalize_no_update_checks"
|
||||
type="number"
|
||||
min="1"
|
||||
value={memoryConfig.chat_history_finalize_no_update_checks ?? 3}
|
||||
onChange={(e) =>
|
||||
onMemoryChange({ ...memoryConfig, chat_history_finalize_no_update_checks: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
当话题连续N次检查无新增内容时触发打包存储
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="chat_history_finalize_message_count">打包存储消息条数阈值</Label>
|
||||
<Input
|
||||
id="chat_history_finalize_message_count"
|
||||
type="number"
|
||||
min="1"
|
||||
value={memoryConfig.chat_history_finalize_message_count ?? 5}
|
||||
onChange={(e) =>
|
||||
onMemoryChange({ ...memoryConfig, chat_history_finalize_message_count: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
当话题的消息条数超过此值时触发打包存储
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 表情包设置 */}
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">表情包设置</h3>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="emoji_chance">表情包激活概率</Label>
|
||||
<Input
|
||||
id="emoji_chance"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="1"
|
||||
value={emojiConfig.emoji_chance}
|
||||
onChange={(e) =>
|
||||
onEmojiChange({ ...emojiConfig, emoji_chance: parseFloat(e.target.value) })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">范围 0-1,越大越容易发送表情包</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="max_reg_num">最大注册数量</Label>
|
||||
<Input
|
||||
id="max_reg_num"
|
||||
type="number"
|
||||
min="1"
|
||||
value={emojiConfig.max_reg_num}
|
||||
onChange={(e) =>
|
||||
onEmojiChange({ ...emojiConfig, max_reg_num: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">麦麦最多可以注册的表情包数量</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="check_interval">检查间隔(分钟)</Label>
|
||||
<Input
|
||||
id="check_interval"
|
||||
type="number"
|
||||
min="1"
|
||||
value={emojiConfig.check_interval}
|
||||
onChange={(e) =>
|
||||
onEmojiChange({ ...emojiConfig, check_interval: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
检查表情包(注册、破损、删除)的时间间隔
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="do_replace"
|
||||
checked={emojiConfig.do_replace}
|
||||
onCheckedChange={(checked) =>
|
||||
onEmojiChange({ ...emojiConfig, do_replace: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="do_replace" className="cursor-pointer">
|
||||
达到最大数量时替换表情包
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="steal_emoji"
|
||||
checked={emojiConfig.steal_emoji}
|
||||
onCheckedChange={(checked) =>
|
||||
onEmojiChange({ ...emojiConfig, steal_emoji: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="steal_emoji" className="cursor-pointer">
|
||||
偷取表情包
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground -mt-2">
|
||||
允许麦麦将看到的表情包据为己有
|
||||
</p>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="content_filtration"
|
||||
checked={emojiConfig.content_filtration}
|
||||
onCheckedChange={(checked) =>
|
||||
onEmojiChange({ ...emojiConfig, content_filtration: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="content_filtration" className="cursor-pointer">
|
||||
启用表情包过滤
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{emojiConfig.content_filtration && (
|
||||
<div className="grid gap-2 pl-6 border-l-2 border-primary/20">
|
||||
<Label htmlFor="filtration_prompt">过滤要求</Label>
|
||||
<Input
|
||||
id="filtration_prompt"
|
||||
value={emojiConfig.filtration_prompt}
|
||||
onChange={(e) =>
|
||||
onEmojiChange({ ...emojiConfig, filtration_prompt: e.target.value })
|
||||
}
|
||||
placeholder="符合公序良俗"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
只有符合此要求的表情包才会被保存
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
150
dashboard/src/routes/config/bot/sections/LPMMSection.tsx
Normal file
150
dashboard/src/routes/config/bot/sections/LPMMSection.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React from 'react'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import type { LPMMKnowledgeConfig } from '../types'
|
||||
|
||||
interface LPMMSectionProps {
|
||||
config: LPMMKnowledgeConfig
|
||||
onChange: (config: LPMMKnowledgeConfig) => void
|
||||
}
|
||||
|
||||
export const LPMMSection = React.memo(function LPMMSection({ config, onChange }: LPMMSectionProps) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
|
||||
<h3 className="text-lg font-semibold">LPMM 知识库设置</h3>
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={config.enable}
|
||||
onCheckedChange={(checked) => onChange({ ...config, enable: checked })}
|
||||
/>
|
||||
<Label className="cursor-pointer">启用 LPMM 知识库</Label>
|
||||
</div>
|
||||
|
||||
{config.enable && (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label>LPMM 模式</Label>
|
||||
<Select
|
||||
value={config.lpmm_mode}
|
||||
onValueChange={(value) => onChange({ ...config, lpmm_mode: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择 LPMM 模式" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="classic">经典模式</SelectItem>
|
||||
<SelectItem value="agent">Agent 模式</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label>同义词搜索 TopK</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={config.rag_synonym_search_top_k}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, rag_synonym_search_top_k: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>同义词阈值</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="1"
|
||||
value={config.rag_synonym_threshold}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, rag_synonym_threshold: parseFloat(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>实体提取线程数</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={config.info_extraction_workers}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, info_extraction_workers: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>嵌入向量维度</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={config.embedding_dimension}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, embedding_dimension: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>嵌入并发线程数</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={config.max_embedding_workers}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, max_embedding_workers: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>每批嵌入条数</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={config.embedding_chunk_size}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, embedding_chunk_size: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>同义实体数上限</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={config.max_synonym_entities}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, max_synonym_entities: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={config.enable_ppr}
|
||||
onCheckedChange={(checked) => onChange({ ...config, enable_ppr: checked })}
|
||||
/>
|
||||
<Label className="cursor-pointer">启用 PPR (低配机器可关闭)</Label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
264
dashboard/src/routes/config/bot/sections/LogSection.tsx
Normal file
264
dashboard/src/routes/config/bot/sections/LogSection.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import React, { useState } from 'react'
|
||||
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 { Plus, Trash2 } from 'lucide-react'
|
||||
import type { LogConfig } from '../types'
|
||||
|
||||
interface LogSectionProps {
|
||||
config: LogConfig
|
||||
onChange: (config: LogConfig) => void
|
||||
}
|
||||
|
||||
export const LogSection = React.memo(function LogSection({ config, onChange }: LogSectionProps) {
|
||||
const [newLibrary, setNewLibrary] = useState('')
|
||||
const [newLogLevel, setNewLogLevel] = useState('WARNING')
|
||||
|
||||
const addSuppressedLibrary = () => {
|
||||
if (newLibrary && !config.suppress_libraries.includes(newLibrary)) {
|
||||
onChange({
|
||||
...config,
|
||||
suppress_libraries: [...config.suppress_libraries, newLibrary],
|
||||
})
|
||||
setNewLibrary('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeSuppressedLibrary = (library: string) => {
|
||||
onChange({
|
||||
...config,
|
||||
suppress_libraries: config.suppress_libraries.filter((l) => l !== library),
|
||||
})
|
||||
}
|
||||
|
||||
const addLibraryLogLevel = () => {
|
||||
if (newLibrary && !config.library_log_levels[newLibrary]) {
|
||||
onChange({
|
||||
...config,
|
||||
library_log_levels: { ...config.library_log_levels, [newLibrary]: newLogLevel },
|
||||
})
|
||||
setNewLibrary('')
|
||||
setNewLogLevel('WARNING')
|
||||
}
|
||||
}
|
||||
|
||||
const removeLibraryLogLevel = (library: string) => {
|
||||
const newLevels = { ...config.library_log_levels }
|
||||
delete newLevels[library]
|
||||
onChange({ ...config, library_log_levels: newLevels })
|
||||
}
|
||||
|
||||
const logLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
|
||||
const logLevelStyles = ['FULL', 'compact', 'lite']
|
||||
const colorTextOptions = ['none', 'title', 'full']
|
||||
|
||||
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 grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label>日期格式</Label>
|
||||
<Input
|
||||
value={config.date_style}
|
||||
onChange={(e) => onChange({ ...config, date_style: e.target.value })}
|
||||
placeholder="例如: m-d H:i:s"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">m=月, d=日, H=时, i=分, s=秒</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>日志级别样式</Label>
|
||||
<Select
|
||||
value={config.log_level_style}
|
||||
onValueChange={(value) => onChange({ ...config, log_level_style: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{logLevelStyles.map((style) => (
|
||||
<SelectItem key={style} value={style}>
|
||||
{style}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>日志文本颜色</Label>
|
||||
<Select
|
||||
value={config.color_text}
|
||||
onValueChange={(value) => onChange({ ...config, color_text: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{colorTextOptions.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>全局日志级别</Label>
|
||||
<Select
|
||||
value={config.log_level}
|
||||
onValueChange={(value) => onChange({ ...config, log_level: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{logLevels.map((level) => (
|
||||
<SelectItem key={level} value={level}>
|
||||
{level}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>控制台日志级别</Label>
|
||||
<Select
|
||||
value={config.console_log_level}
|
||||
onValueChange={(value) => onChange({ ...config, console_log_level: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{logLevels.map((level) => (
|
||||
<SelectItem key={level} value={level}>
|
||||
{level}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>文件日志级别</Label>
|
||||
<Select
|
||||
value={config.file_log_level}
|
||||
onValueChange={(value) => onChange({ ...config, file_log_level: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{logLevels.map((level) => (
|
||||
<SelectItem key={level} value={level}>
|
||||
{level}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 屏蔽的库 */}
|
||||
<div>
|
||||
<Label className="mb-2 block">完全屏蔽的库</Label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<Input
|
||||
value={newLibrary}
|
||||
onChange={(e) => setNewLibrary(e.target.value)}
|
||||
placeholder="输入库名"
|
||||
className="flex-1"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
addSuppressedLibrary()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button onClick={addSuppressedLibrary} size="sm" className="flex-shrink-0">
|
||||
<Plus className="h-4 w-4" strokeWidth={2} fill="none" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{config.suppress_libraries.map((library) => (
|
||||
<div
|
||||
key={library}
|
||||
className="flex items-center gap-1 bg-secondary px-3 py-1 rounded-md"
|
||||
>
|
||||
<span className="text-sm">{library}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0"
|
||||
onClick={() => removeSuppressedLibrary(library)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" strokeWidth={2} fill="none" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 特定库日志级别 */}
|
||||
<div>
|
||||
<Label className="mb-2 block">特定库的日志级别</Label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<Input
|
||||
value={newLibrary}
|
||||
onChange={(e) => setNewLibrary(e.target.value)}
|
||||
placeholder="输入库名"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Select value={newLogLevel} onValueChange={setNewLogLevel}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{logLevels.map((level) => (
|
||||
<SelectItem key={level} value={level}>
|
||||
{level}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={addLibraryLogLevel} size="sm">
|
||||
<Plus className="h-4 w-4" strokeWidth={2} fill="none" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(config.library_log_levels).map(([library, level]) => (
|
||||
<div
|
||||
key={library}
|
||||
className="flex items-center justify-between bg-secondary px-3 py-2 rounded-md"
|
||||
>
|
||||
<span className="text-sm font-medium">{library}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">{level}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={() => removeLibraryLogLevel(library)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" strokeWidth={2} fill="none" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
203
dashboard/src/routes/config/bot/sections/MaimMessageSection.tsx
Normal file
203
dashboard/src/routes/config/bot/sections/MaimMessageSection.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
import type { MaimMessageConfig } from '../types'
|
||||
|
||||
interface MaimMessageSectionProps {
|
||||
config: MaimMessageConfig
|
||||
onChange: (config: MaimMessageConfig) => void
|
||||
}
|
||||
|
||||
export const MaimMessageSection = React.memo(function MaimMessageSection({ config, onChange }: MaimMessageSectionProps) {
|
||||
const [newToken, setNewToken] = useState('')
|
||||
const [newApiKey, setNewApiKey] = useState('')
|
||||
|
||||
const addToken = () => {
|
||||
if (newToken && !config.auth_token.includes(newToken)) {
|
||||
onChange({ ...config, auth_token: [...config.auth_token, newToken] })
|
||||
setNewToken('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeToken = (index: number) => {
|
||||
onChange({
|
||||
...config,
|
||||
auth_token: config.auth_token.filter((_, i) => i !== index),
|
||||
})
|
||||
}
|
||||
|
||||
const addApiKey = () => {
|
||||
if (newApiKey && !config.api_server_allowed_api_keys.includes(newApiKey)) {
|
||||
onChange({ ...config, api_server_allowed_api_keys: [...config.api_server_allowed_api_keys, newApiKey] })
|
||||
setNewApiKey('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeApiKey = (index: number) => {
|
||||
onChange({
|
||||
...config,
|
||||
api_server_allowed_api_keys: config.api_server_allowed_api_keys.filter((_, i) => i !== index),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-6">
|
||||
{/* 认证令牌 */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">旧版 API 认证令牌</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">用于旧版 API 验证,为空则不启用验证</p>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<Input
|
||||
value={newToken}
|
||||
onChange={(e) => setNewToken(e.target.value)}
|
||||
placeholder="输入认证令牌"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
addToken()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button onClick={addToken} size="sm">
|
||||
<Plus className="h-4 w-4" strokeWidth={2} fill="none" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{config.auth_token.map((token, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between bg-secondary px-3 py-2 rounded-md"
|
||||
>
|
||||
<span className="text-sm font-mono">{token}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={() => removeToken(index)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" strokeWidth={2} fill="none" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 新版 API Server */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">新版 API Server 配置</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>启用新版 API Server</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
是否启用额外的新版 API Server(额外监听端口)
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.enable_api_server}
|
||||
onCheckedChange={(checked) => onChange({ ...config, enable_api_server: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.enable_api_server && (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label>主机地址</Label>
|
||||
<Input
|
||||
value={config.api_server_host}
|
||||
onChange={(e) => onChange({ ...config, api_server_host: e.target.value })}
|
||||
placeholder="0.0.0.0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>端口号</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.api_server_port}
|
||||
onChange={(e) => onChange({ ...config, api_server_port: parseInt(e.target.value) })}
|
||||
placeholder="8090"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={config.api_server_use_wss}
|
||||
onCheckedChange={(checked) => onChange({ ...config, api_server_use_wss: checked })}
|
||||
/>
|
||||
<Label>启用 WSS 安全连接</Label>
|
||||
</div>
|
||||
|
||||
{config.api_server_use_wss && (
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label>SSL 证书文件路径</Label>
|
||||
<Input
|
||||
value={config.api_server_cert_file}
|
||||
onChange={(e) => onChange({ ...config, api_server_cert_file: e.target.value })}
|
||||
placeholder="cert.pem"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>SSL 密钥文件路径</Label>
|
||||
<Input
|
||||
value={config.api_server_key_file}
|
||||
onChange={(e) => onChange({ ...config, api_server_key_file: e.target.value })}
|
||||
placeholder="key.pem"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* API Keys */}
|
||||
<div>
|
||||
<Label className="mb-2 block">允许的 API Key 列表</Label>
|
||||
<p className="text-sm text-muted-foreground mb-2">为空则允许所有连接</p>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<Input
|
||||
value={newApiKey}
|
||||
onChange={(e) => setNewApiKey(e.target.value)}
|
||||
placeholder="输入 API Key"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
addApiKey()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button onClick={addApiKey} size="sm">
|
||||
<Plus className="h-4 w-4" strokeWidth={2} fill="none" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{config.api_server_allowed_api_keys.map((apiKey, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between bg-secondary px-3 py-2 rounded-md"
|
||||
>
|
||||
<span className="text-sm font-mono">{apiKey}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
onClick={() => removeApiKey(index)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" strokeWidth={2} fill="none" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,259 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Plus, Trash2, AlertTriangle } from 'lucide-react'
|
||||
import type { MessageReceiveConfig } from '../types'
|
||||
|
||||
interface MessageReceiveSectionProps {
|
||||
config: MessageReceiveConfig
|
||||
onChange: (config: MessageReceiveConfig) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息过滤配置模块
|
||||
* 管理 ban_words、ban_msgs_regex 和 mute_group_list
|
||||
*/
|
||||
export default function MessageReceiveSection({
|
||||
config,
|
||||
onChange,
|
||||
}: MessageReceiveSectionProps) {
|
||||
const [newBanWord, setNewBanWord] = useState('')
|
||||
const [newBanRegex, setNewBanRegex] = useState('')
|
||||
|
||||
// === 禁用词管理 ===
|
||||
const handleAddBanWord = () => {
|
||||
const trimmed = newBanWord.trim()
|
||||
if (trimmed && !config.ban_words.includes(trimmed)) {
|
||||
onChange({
|
||||
...config,
|
||||
ban_words: [...config.ban_words, trimmed],
|
||||
})
|
||||
setNewBanWord('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveBanWord = (index: number) => {
|
||||
onChange({
|
||||
...config,
|
||||
ban_words: config.ban_words.filter((_, i) => i !== index),
|
||||
})
|
||||
}
|
||||
|
||||
const handleBanWordKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleAddBanWord()
|
||||
}
|
||||
}
|
||||
|
||||
// === 禁用正则表达式管理 ===
|
||||
const handleAddBanRegex = () => {
|
||||
const trimmed = newBanRegex.trim()
|
||||
if (trimmed && !config.ban_msgs_regex.includes(trimmed)) {
|
||||
// 验证正则表达式语法
|
||||
try {
|
||||
new RegExp(trimmed)
|
||||
onChange({
|
||||
...config,
|
||||
ban_msgs_regex: [...config.ban_msgs_regex, trimmed],
|
||||
})
|
||||
setNewBanRegex('')
|
||||
} catch (err) {
|
||||
alert(`正则表达式语法错误:${(err as Error).message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveBanRegex = (index: number) => {
|
||||
onChange({
|
||||
...config,
|
||||
ban_msgs_regex: config.ban_msgs_regex.filter((_, i) => i !== index),
|
||||
})
|
||||
}
|
||||
|
||||
const handleBanRegexKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleAddBanRegex()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>消息过滤配置</CardTitle>
|
||||
<CardDescription>
|
||||
配置消息过滤规则,过滤特定消息或在特定群组启用静默模式
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="ban_words" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="ban_words">禁用关键词</TabsTrigger>
|
||||
<TabsTrigger value="ban_regex">禁用正则</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 禁用关键词 Tab */}
|
||||
<TabsContent value="ban_words" className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-yellow-500 mt-1 flex-shrink-0" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
包含以下关键词的消息将被过滤,Bot 不会读取这些消息
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="输入要禁用的关键词(按回车添加)"
|
||||
value={newBanWord}
|
||||
onChange={(e) => setNewBanWord(e.target.value)}
|
||||
onKeyDown={handleBanWordKeyDown}
|
||||
/>
|
||||
<Button onClick={handleAddBanWord} size="icon">
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{config.ban_words.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed p-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
暂无禁用关键词,点击上方添加
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{config.ban_words.map((word, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between rounded-md border p-3"
|
||||
>
|
||||
<code className="text-sm">{word}</code>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除关键词 <code>"{word}"</code> 吗?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => handleRemoveBanWord(index)}
|
||||
>
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 禁用正则表达式 Tab */}
|
||||
<TabsContent value="ban_regex" className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-yellow-500 mt-1 flex-shrink-0" />
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<p>匹配以下正则表达式的消息将被过滤</p>
|
||||
<p className="text-xs">
|
||||
⚠️ 若不了解正则表达式,请勿随意修改
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Textarea
|
||||
placeholder="输入正则表达式(按回车添加) 示例:https?://[^\s]+ 匹配链接"
|
||||
value={newBanRegex}
|
||||
onChange={(e) => setNewBanRegex(e.target.value)}
|
||||
onKeyDown={handleBanRegexKeyDown}
|
||||
className="min-h-[60px] font-mono text-sm"
|
||||
/>
|
||||
<Button onClick={handleAddBanRegex} size="icon">
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{config.ban_msgs_regex.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed p-8 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
暂无禁用正则表达式,点击上方添加
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{config.ban_msgs_regex.map((regex, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between rounded-md border p-3"
|
||||
>
|
||||
<code className="text-sm font-mono flex-1 break-all">
|
||||
{regex}
|
||||
</code>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="ml-2 flex-shrink-0">
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除正则表达式 <code>"{regex}"</code> 吗?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => handleRemoveBanRegex(index)}
|
||||
>
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
164
dashboard/src/routes/config/bot/sections/PersonalitySection.tsx
Normal file
164
dashboard/src/routes/config/bot/sections/PersonalitySection.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import React from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
import type { PersonalityConfig } from '../types'
|
||||
|
||||
interface PersonalitySectionProps {
|
||||
config: PersonalityConfig
|
||||
onChange: (config: PersonalityConfig) => void
|
||||
}
|
||||
|
||||
export const PersonalitySection = React.memo(function PersonalitySection({ config, onChange }: PersonalitySectionProps) {
|
||||
const addState = () => {
|
||||
onChange({ ...config, states: [...config.states, ''] })
|
||||
}
|
||||
|
||||
const removeState = (index: number) => {
|
||||
onChange({
|
||||
...config,
|
||||
states: config.states.filter((_, i) => i !== index),
|
||||
})
|
||||
}
|
||||
|
||||
const updateState = (index: number, value: string) => {
|
||||
const newStates = [...config.states]
|
||||
newStates[index] = value
|
||||
onChange({ ...config, states: newStates })
|
||||
}
|
||||
|
||||
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="personality">人格特质</Label>
|
||||
<Textarea
|
||||
id="personality"
|
||||
value={config.personality}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange({ ...config, personality: e.target.value })}
|
||||
placeholder="描述人格特质和身份特征(建议120字以内)"
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
建议120字以内,描述人格特质和身份特征
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 多重人格配置 - 移到人格特质下方 */}
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>多重人格</Label>
|
||||
<Button onClick={addState} size="sm" variant="outline">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
添加人格
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
可以定义多个不同的人格状态,麦麦会随机切换
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{config.states.map((state, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Textarea
|
||||
value={state}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => updateState(index, e.target.value)}
|
||||
placeholder="描述一个人格状态"
|
||||
rows={2}
|
||||
/>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="icon" variant="outline">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除这个人格状态吗?此操作无法撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => removeState(index)}>
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="state_probability">替换为多重人格概率</Label>
|
||||
<Input
|
||||
id="state_probability"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="1"
|
||||
value={config.state_probability}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, state_probability: parseFloat(e.target.value) })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
每次构建人格时,用多重人格替换主人格的概率(0.0-1.0)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="reply_style">表达风格</Label>
|
||||
<Textarea
|
||||
id="reply_style"
|
||||
value={config.reply_style}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange({ ...config, reply_style: e.target.value })}
|
||||
placeholder="描述说话的表达风格和习惯"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="plan_style">说话规则与行为风格</Label>
|
||||
<Textarea
|
||||
id="plan_style"
|
||||
value={config.plan_style}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange({ ...config, plan_style: e.target.value })}
|
||||
placeholder="麦麦的说话规则和行为风格"
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="visual_style">识图规则</Label>
|
||||
<Textarea
|
||||
id="visual_style"
|
||||
value={config.visual_style}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange({ ...config, visual_style: e.target.value })}
|
||||
placeholder="识图时的处理规则"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
1121
dashboard/src/routes/config/bot/sections/ProcessingSection.tsx
Normal file
1121
dashboard/src/routes/config/bot/sections/ProcessingSection.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import type { TelemetryConfig } from '../types'
|
||||
|
||||
interface TelemetrySectionProps {
|
||||
config: TelemetryConfig
|
||||
onChange: (config: TelemetryConfig) => void
|
||||
}
|
||||
|
||||
export const TelemetrySection = React.memo(function TelemetrySection({ config, onChange }: TelemetrySectionProps) {
|
||||
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 justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>启用统计信息发送</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
发送匿名统计信息,帮助我们了解全球有多少只麦麦在运行
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={config.enable}
|
||||
onCheckedChange={(checked) => onChange({ ...config, enable: checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
27
dashboard/src/routes/config/bot/sections/VoiceSection.tsx
Normal file
27
dashboard/src/routes/config/bot/sections/VoiceSection.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
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>
|
||||
)
|
||||
})
|
||||
287
dashboard/src/routes/config/bot/sections/WebUISection.tsx
Normal file
287
dashboard/src/routes/config/bot/sections/WebUISection.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { X, Plus } from 'lucide-react'
|
||||
import type { WebUIConfig } from '../types'
|
||||
|
||||
interface WebUISectionProps {
|
||||
config: WebUIConfig
|
||||
onChange: (config: WebUIConfig) => void
|
||||
}
|
||||
|
||||
export const WebUISection = React.memo(function WebUISection({ config, onChange }: WebUISectionProps) {
|
||||
const [newAllowedIp, setNewAllowedIp] = useState('')
|
||||
const [newTrustedProxy, setNewTrustedProxy] = useState('')
|
||||
const [showDisableWarning, setShowDisableWarning] = useState(false)
|
||||
|
||||
// 将逗号分隔的字符串转换为数组
|
||||
const allowedIpsList = config.allowed_ips
|
||||
? config.allowed_ips.split(',').map(ip => ip.trim()).filter(ip => ip)
|
||||
: []
|
||||
|
||||
const trustedProxiesList = config.trusted_proxies
|
||||
? config.trusted_proxies.split(',').map(ip => ip.trim()).filter(ip => ip)
|
||||
: []
|
||||
|
||||
// 处理添加IP白名单
|
||||
const handleAddAllowedIp = () => {
|
||||
if (!newAllowedIp.trim()) return
|
||||
const updatedList = [...allowedIpsList, newAllowedIp.trim()]
|
||||
onChange({ ...config, allowed_ips: updatedList.join(',') })
|
||||
setNewAllowedIp('')
|
||||
}
|
||||
|
||||
// 处理删除IP白名单
|
||||
const handleRemoveAllowedIp = (index: number) => {
|
||||
const updatedList = allowedIpsList.filter((_, i) => i !== index)
|
||||
onChange({ ...config, allowed_ips: updatedList.join(',') })
|
||||
}
|
||||
|
||||
// 处理添加信任代理
|
||||
const handleAddTrustedProxy = () => {
|
||||
if (!newTrustedProxy.trim()) return
|
||||
const updatedList = [...trustedProxiesList, newTrustedProxy.trim()]
|
||||
onChange({ ...config, trusted_proxies: updatedList.join(',') })
|
||||
setNewTrustedProxy('')
|
||||
}
|
||||
|
||||
// 处理删除信任代理
|
||||
const handleRemoveTrustedProxy = (index: number) => {
|
||||
const updatedList = trustedProxiesList.filter((_, i) => i !== index)
|
||||
onChange({ ...config, trusted_proxies: updatedList.join(',') })
|
||||
}
|
||||
|
||||
// 处理WebUI开关变更
|
||||
const handleEnabledChange = (checked: boolean) => {
|
||||
if (!checked && config.enabled) {
|
||||
// 用户尝试关闭WebUI,显示警告
|
||||
setShowDisableWarning(true)
|
||||
} else {
|
||||
// 用户开启WebUI,直接更新
|
||||
onChange({ ...config, enabled: checked })
|
||||
}
|
||||
}
|
||||
|
||||
// 确认关闭WebUI
|
||||
const confirmDisableWebUI = () => {
|
||||
onChange({ ...config, enabled: false })
|
||||
setShowDisableWarning(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
|
||||
<h3 className="text-lg font-semibold">WebUI 服务配置</h3>
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={config.enabled}
|
||||
onCheckedChange={handleEnabledChange}
|
||||
/>
|
||||
<Label className="cursor-pointer">启用 WebUI</Label>
|
||||
</div>
|
||||
|
||||
{config.enabled && (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label>运行模式</Label>
|
||||
<Select
|
||||
value={config.mode}
|
||||
onValueChange={(value) => onChange({ ...config, mode: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择运行模式" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="development">开发模式</SelectItem>
|
||||
<SelectItem value="production">生产模式</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
注意: WebUI 的监听地址和端口请在 .env 文件中配置 WEBUI_HOST 和 WEBUI_PORT
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>防爬虫模式</Label>
|
||||
<Select
|
||||
value={config.anti_crawler_mode}
|
||||
onValueChange={(value) => onChange({ ...config, anti_crawler_mode: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择防爬虫模式" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="false">禁用</SelectItem>
|
||||
<SelectItem value="basic">基础(只记录不阻止)</SelectItem>
|
||||
<SelectItem value="loose">宽松</SelectItem>
|
||||
<SelectItem value="strict">严格</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 sm:col-span-2">
|
||||
<Label>IP 白名单</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={newAllowedIp}
|
||||
onChange={(e) => setNewAllowedIp(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleAddAllowedIp()
|
||||
}
|
||||
}}
|
||||
placeholder="输入IP地址后按回车或点击添加"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={handleAddAllowedIp}
|
||||
disabled={!newAllowedIp.trim()}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{allowedIpsList.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{allowedIpsList.map((ip, index) => (
|
||||
<Badge key={index} variant="secondary" className="flex items-center gap-1">
|
||||
{ip}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveAllowedIp(index)}
|
||||
className="ml-1 hover:bg-destructive/20 rounded-full p-0.5"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
支持精确IP、CIDR格式和通配符(如:127.0.0.1、192.168.1.0/24)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 sm:col-span-2">
|
||||
<Label>信任的代理 IP</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={newTrustedProxy}
|
||||
onChange={(e) => setNewTrustedProxy(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleAddTrustedProxy()
|
||||
}
|
||||
}}
|
||||
placeholder="输入代理IP后按回车或点击添加"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
onClick={handleAddTrustedProxy}
|
||||
disabled={!newTrustedProxy.trim()}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{trustedProxiesList.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{trustedProxiesList.map((ip, index) => (
|
||||
<Badge key={index} variant="secondary" className="flex items-center gap-1">
|
||||
{ip}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveTrustedProxy(index)}
|
||||
className="ml-1 hover:bg-destructive/20 rounded-full p-0.5"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
只有来自这些IP的X-Forwarded-For头才被信任
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={config.trust_xff}
|
||||
onCheckedChange={(checked) => onChange({ ...config, trust_xff: checked })}
|
||||
/>
|
||||
<Label className="cursor-pointer">启用 X-Forwarded-For 代理解析</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={config.secure_cookie}
|
||||
onCheckedChange={(checked) => onChange({ ...config, secure_cookie: checked })}
|
||||
/>
|
||||
<Label className="cursor-pointer">启用安全 Cookie(仅 HTTPS)</Label>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={config.enable_paragraph_content}
|
||||
onCheckedChange={(checked) => onChange({ ...config, enable_paragraph_content: checked })}
|
||||
/>
|
||||
<Label className="cursor-pointer">在知识图谱中加载段落完整内容</Label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
启用后,知识图谱可视化界面会显示段落节点的完整内容。需要加载 embedding store,会占用额外内存(约数百MB)。
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 关闭WebUI警告对话框 */}
|
||||
<AlertDialog open={showDisableWarning} onOpenChange={setShowDisableWarning}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>警告:即将关闭 WebUI</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
关闭 WebUI 后,在您下次重启麦麦之前,WebUI 界面将无法访问。
|
||||
<br />
|
||||
<br />
|
||||
您需要通过修改配置文件或命令行重新启用 WebUI 才能再次访问此界面。
|
||||
<br />
|
||||
<br />
|
||||
确定要关闭 WebUI 吗?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive" onClick={confirmDisableWebUI}>
|
||||
确认关闭
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
19
dashboard/src/routes/config/bot/sections/index.ts
Normal file
19
dashboard/src/routes/config/bot/sections/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Bot 配置页面各个 Section 组件
|
||||
*/
|
||||
|
||||
export { BotInfoSection } from './BotInfoSection'
|
||||
export { PersonalitySection } from './PersonalitySection'
|
||||
export { ChatSection } from './ChatSection'
|
||||
export { DreamSection } from './DreamSection'
|
||||
export { LPMMSection } from './LPMMSection'
|
||||
export { LogSection } from './LogSection'
|
||||
export { DebugSection } from './DebugSection'
|
||||
export { ExperimentalSection } from './ExperimentalSection'
|
||||
export { MaimMessageSection } from './MaimMessageSection'
|
||||
export { TelemetrySection } from './TelemetrySection'
|
||||
export { FeaturesSection } from './FeaturesSection'
|
||||
export { ExpressionSection } from './ExpressionSection'
|
||||
export { ProcessingSection } from './ProcessingSection'
|
||||
export { default as MessageReceiveSection } from './MessageReceiveSection'
|
||||
export { WebUISection } from './WebUISection'
|
||||
259
dashboard/src/routes/config/bot/types.ts
Normal file
259
dashboard/src/routes/config/bot/types.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* Bot 配置页面相关类型定义
|
||||
*/
|
||||
|
||||
export interface BotConfig {
|
||||
platform: string
|
||||
qq_account: string | number
|
||||
nickname: string
|
||||
platforms: string[]
|
||||
alias_names: string[]
|
||||
}
|
||||
|
||||
export interface PersonalityConfig {
|
||||
personality: string
|
||||
reply_style: string
|
||||
interest: string
|
||||
plan_style: string
|
||||
visual_style: string
|
||||
states: string[]
|
||||
state_probability: number
|
||||
}
|
||||
|
||||
export interface ChatConfig {
|
||||
talk_value: number
|
||||
mentioned_bot_reply: boolean
|
||||
max_context_size: number
|
||||
planner_smooth: number
|
||||
think_mode: 'classic' | 'deep' | 'dynamic'
|
||||
plan_reply_log_max_per_chat: number
|
||||
llm_quote: boolean
|
||||
enable_talk_value_rules: boolean
|
||||
talk_value_rules: Array<{
|
||||
target: string
|
||||
time: string
|
||||
value: number
|
||||
}>
|
||||
}
|
||||
|
||||
export interface ExpressionConfig {
|
||||
learning_list: Array<[string, string, string, string]>
|
||||
expression_groups: Array<string[]>
|
||||
expression_manual_reflect: boolean
|
||||
manual_reflect_operator_id: string
|
||||
allow_reflect: string[]
|
||||
expression_self_reflect: boolean
|
||||
expression_auto_check_interval: number
|
||||
expression_auto_check_count: number
|
||||
expression_auto_check_custom_criteria: string[]
|
||||
expression_checked_only: boolean
|
||||
all_global_jargon: boolean
|
||||
enable_jargon_explanation: boolean
|
||||
jargon_mode: string
|
||||
}
|
||||
|
||||
export interface EmojiConfig {
|
||||
emoji_chance: number
|
||||
max_reg_num: number
|
||||
do_replace: boolean
|
||||
check_interval: number
|
||||
steal_emoji: boolean
|
||||
content_filtration: boolean
|
||||
filtration_prompt: string
|
||||
}
|
||||
|
||||
export interface MemoryConfig {
|
||||
max_agent_iterations: number
|
||||
agent_timeout_seconds: number
|
||||
enable_jargon_detection: boolean
|
||||
global_memory: boolean
|
||||
chat_history_topic_check_message_threshold: number
|
||||
chat_history_topic_check_time_hours: number
|
||||
chat_history_topic_check_min_messages: number
|
||||
chat_history_finalize_no_update_checks: number
|
||||
chat_history_finalize_message_count: number
|
||||
}
|
||||
|
||||
export interface ToolConfig {
|
||||
enable_tool: boolean
|
||||
}
|
||||
|
||||
// MoodConfig 已在后端移除
|
||||
|
||||
export interface VoiceConfig {
|
||||
enable_asr: boolean
|
||||
}
|
||||
|
||||
export interface MessageReceiveConfig {
|
||||
ban_words: string[]
|
||||
ban_msgs_regex: string[]
|
||||
}
|
||||
|
||||
export interface DreamConfig {
|
||||
interval_minutes: number
|
||||
max_iterations: number
|
||||
first_delay_seconds: number
|
||||
dream_send: string
|
||||
dream_time_ranges: string[]
|
||||
dream_visible: boolean
|
||||
}
|
||||
|
||||
export interface LPMMKnowledgeConfig {
|
||||
enable: boolean
|
||||
lpmm_mode: string
|
||||
rag_synonym_search_top_k: number
|
||||
rag_synonym_threshold: number
|
||||
info_extraction_workers: number
|
||||
qa_relation_search_top_k: number
|
||||
qa_relation_threshold: number
|
||||
qa_paragraph_search_top_k: number
|
||||
qa_paragraph_node_weight: number
|
||||
qa_ent_filter_top_k: number
|
||||
qa_ppr_damping: number
|
||||
qa_res_top_k: number
|
||||
embedding_dimension: number
|
||||
max_embedding_workers: number
|
||||
embedding_chunk_size: number
|
||||
max_synonym_entities: number
|
||||
enable_ppr: boolean
|
||||
}
|
||||
|
||||
export interface KeywordRule {
|
||||
keywords?: string[]
|
||||
regex?: string[]
|
||||
reaction: string
|
||||
}
|
||||
|
||||
export interface KeywordReactionConfig {
|
||||
keyword_rules: KeywordRule[]
|
||||
regex_rules: KeywordRule[]
|
||||
}
|
||||
|
||||
export interface ResponsePostProcessConfig {
|
||||
enable_response_post_process: boolean
|
||||
}
|
||||
|
||||
export interface ChineseTypoConfig {
|
||||
enable: boolean
|
||||
error_rate: number
|
||||
min_freq: number
|
||||
tone_error_rate: number
|
||||
word_replace_rate: number
|
||||
}
|
||||
|
||||
export interface ResponseSplitterConfig {
|
||||
enable: boolean
|
||||
max_length: number
|
||||
max_sentence_num: number
|
||||
enable_kaomoji_protection: boolean
|
||||
enable_overflow_return_all: boolean
|
||||
}
|
||||
|
||||
export interface LogConfig {
|
||||
date_style: string
|
||||
log_level_style: string
|
||||
color_text: string
|
||||
log_level: string
|
||||
console_log_level: string
|
||||
file_log_level: string
|
||||
suppress_libraries: string[]
|
||||
library_log_levels: Record<string, string>
|
||||
}
|
||||
|
||||
export interface DebugConfig {
|
||||
show_prompt: boolean
|
||||
show_replyer_prompt: boolean
|
||||
show_replyer_reasoning: boolean
|
||||
show_jargon_prompt: boolean
|
||||
show_memory_prompt: boolean
|
||||
show_planner_prompt: boolean
|
||||
show_lpmm_paragraph: boolean
|
||||
}
|
||||
|
||||
export interface ExperimentalConfig {
|
||||
private_plan_style: string
|
||||
chat_prompts: string[]
|
||||
lpmm_memory: boolean
|
||||
}
|
||||
|
||||
export interface MaimMessageConfig {
|
||||
auth_token: string[]
|
||||
enable_api_server: boolean
|
||||
api_server_host: string
|
||||
api_server_port: number
|
||||
api_server_use_wss: boolean
|
||||
api_server_cert_file: string
|
||||
api_server_key_file: string
|
||||
api_server_allowed_api_keys: string[]
|
||||
}
|
||||
|
||||
export interface TelemetryConfig {
|
||||
enable: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* WebUI 配置
|
||||
* 注意: host 和 port 配置已移至环境变量 WEBUI_HOST 和 WEBUI_PORT
|
||||
*/
|
||||
export interface WebUIConfig {
|
||||
enabled: boolean
|
||||
mode: string
|
||||
anti_crawler_mode: string
|
||||
allowed_ips: string
|
||||
trusted_proxies: string
|
||||
trust_xff: boolean
|
||||
secure_cookie: boolean
|
||||
enable_paragraph_content: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有配置的聚合类型
|
||||
*/
|
||||
export interface AllBotConfigs {
|
||||
botConfig: BotConfig | null
|
||||
personalityConfig: PersonalityConfig | null
|
||||
chatConfig: ChatConfig | null
|
||||
expressionConfig: ExpressionConfig | null
|
||||
emojiConfig: EmojiConfig | null
|
||||
memoryConfig: MemoryConfig | null
|
||||
toolConfig: ToolConfig | null
|
||||
voiceConfig: VoiceConfig | null
|
||||
messageReceiveConfig: MessageReceiveConfig | null
|
||||
dreamConfig: DreamConfig | null
|
||||
lpmmConfig: LPMMKnowledgeConfig | null
|
||||
keywordReactionConfig: KeywordReactionConfig | null
|
||||
responsePostProcessConfig: ResponsePostProcessConfig | null
|
||||
chineseTypoConfig: ChineseTypoConfig | null
|
||||
responseSplitterConfig: ResponseSplitterConfig | null
|
||||
logConfig: LogConfig | null
|
||||
debugConfig: DebugConfig | null
|
||||
experimentalConfig: ExperimentalConfig | null
|
||||
maimMessageConfig: MaimMessageConfig | null
|
||||
telemetryConfig: TelemetryConfig | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置节名称到类型的映射
|
||||
*/
|
||||
export type ConfigSectionName =
|
||||
| 'bot'
|
||||
| 'personality'
|
||||
| 'chat'
|
||||
| 'expression'
|
||||
| 'emoji'
|
||||
| 'memory'
|
||||
| 'tool'
|
||||
| 'voice'
|
||||
| 'message_receive'
|
||||
| 'dream'
|
||||
| 'lpmm_knowledge'
|
||||
| 'keyword_reaction'
|
||||
| 'response_post_process'
|
||||
| 'chinese_typo'
|
||||
| 'response_splitter'
|
||||
| 'log'
|
||||
| 'debug'
|
||||
| 'experimental'
|
||||
| 'maim_message'
|
||||
| 'telemetry'
|
||||
| 'webui'
|
||||
Reference in New Issue
Block a user