上传完整的WebUI前端仓库

This commit is contained in:
墨梓柒
2026-01-13 06:24:35 +08:00
parent a9187dc312
commit 812296590e
184 changed files with 47854 additions and 1 deletions

View File

@@ -0,0 +1,483 @@
// 设置向导各步骤表单组件
import { useState } from 'react'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Switch } from '@/components/ui/switch'
import { Separator } from '@/components/ui/separator'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { X, ExternalLink, Eye, EyeOff } from 'lucide-react'
import type {
BotBasicConfig,
PersonalityConfig,
EmojiConfig,
OtherBasicConfig,
SiliconFlowConfig,
} from './types'
// ====== 步骤1Bot基础配置 ======
interface BotBasicFormProps {
config: BotBasicConfig
onChange: (config: BotBasicConfig) => void
}
export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
const handleAddAlias = (alias: string) => {
if (alias.trim() && !config.alias_names.includes(alias.trim())) {
onChange({
...config,
alias_names: [...config.alias_names, alias.trim()],
})
}
}
const handleRemoveAlias = (index: number) => {
onChange({
...config,
alias_names: config.alias_names.filter((_, i) => i !== index),
})
}
return (
<div className="space-y-6">
<div className="space-y-3">
<Label htmlFor="qq_account">QQ账号 *</Label>
<Input
id="qq_account"
type="number"
placeholder="请输入机器人的QQ账号"
value={config.qq_account || ''}
onChange={(e) =>
onChange({ ...config, qq_account: Number(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground">
使QQ账号
</p>
</div>
<div className="space-y-3">
<Label htmlFor="nickname"> *</Label>
<Input
id="nickname"
placeholder="请输入机器人的昵称"
value={config.nickname}
onChange={(e) => onChange({ ...config, nickname: e.target.value })}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="space-y-3">
<Label></Label>
<div className="flex flex-wrap gap-2 mb-2">
{config.alias_names.map((alias, index) => (
<Badge key={index} variant="secondary" className="gap-1">
{alias}
<button
type="button"
onClick={() => handleRemoveAlias(index)}
className="ml-1 hover:text-destructive"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
<div className="flex gap-2">
<Input
id="alias_input"
placeholder="输入别名后按回车添加"
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleAddAlias((e.target as HTMLInputElement).value)
;(e.target as HTMLInputElement).value = ''
}
}}
/>
<Button
type="button"
variant="outline"
onClick={() => {
const input = document.getElementById(
'alias_input'
) as HTMLInputElement
if (input) {
handleAddAlias(input.value)
input.value = ''
}
}}
>
</Button>
</div>
<p className="text-xs text-muted-foreground">
</p>
</div>
</div>
)
}
// ====== 步骤2人格配置 ======
interface PersonalityFormProps {
config: PersonalityConfig
onChange: (config: PersonalityConfig) => void
}
export function PersonalityForm({ config, onChange }: PersonalityFormProps) {
return (
<div className="space-y-6">
<div className="space-y-3">
<Label htmlFor="personality"> *</Label>
<Textarea
id="personality"
placeholder="描述机器人的人格特质和身份特征建议120字以内"
value={config.personality}
onChange={(e) => onChange({ ...config, personality: e.target.value })}
rows={3}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="space-y-3">
<Label htmlFor="reply_style"> *</Label>
<Textarea
id="reply_style"
placeholder="描述机器人说话的表达风格、表达习惯"
value={config.reply_style}
onChange={(e) => onChange({ ...config, reply_style: e.target.value })}
rows={3}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="space-y-3">
<Label htmlFor="interest"> *</Label>
<Textarea
id="interest"
placeholder="描述机器人感兴趣的话题"
value={config.interest}
onChange={(e) => onChange({ ...config, interest: e.target.value })}
rows={2}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<Separator />
<div className="space-y-3">
<Label htmlFor="plan_style"> *</Label>
<Textarea
id="plan_style"
placeholder="机器人在群聊中的行为风格和规则"
value={config.plan_style}
onChange={(e) => onChange({ ...config, plan_style: e.target.value })}
rows={4}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="space-y-3">
<Label htmlFor="private_plan_style"> *</Label>
<Textarea
id="private_plan_style"
placeholder="机器人在私聊中的行为风格和规则"
value={config.private_plan_style}
onChange={(e) =>
onChange({ ...config, private_plan_style: e.target.value })
}
rows={3}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
</div>
)
}
// ====== 步骤3表情包配置 ======
interface EmojiFormProps {
config: EmojiConfig
onChange: (config: EmojiConfig) => void
}
export function EmojiForm({ config, onChange }: EmojiFormProps) {
return (
<div className="space-y-6">
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label htmlFor="emoji_chance"></Label>
<span className="text-sm text-muted-foreground">
{(config.emoji_chance * 100).toFixed(0)}%
</span>
</div>
<Input
id="emoji_chance"
type="range"
min="0"
max="1"
step="0.1"
value={config.emoji_chance}
onChange={(e) =>
onChange({ ...config, emoji_chance: Number(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="space-y-3">
<Label htmlFor="max_reg_num"></Label>
<Input
id="max_reg_num"
type="number"
min="1"
max="200"
value={config.max_reg_num}
onChange={(e) =>
onChange({ ...config, max_reg_num: Number(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label htmlFor="do_replace"></Label>
<p className="text-xs text-muted-foreground">
</p>
</div>
<Switch
id="do_replace"
checked={config.do_replace}
onCheckedChange={(checked) =>
onChange({ ...config, do_replace: checked })
}
/>
</div>
<div className="space-y-3">
<Label htmlFor="check_interval"></Label>
<Input
id="check_interval"
type="number"
min="1"
max="120"
value={config.check_interval}
onChange={(e) =>
onChange({ ...config, check_interval: Number(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label htmlFor="steal_emoji"></Label>
<p className="text-xs text-muted-foreground">
</p>
</div>
<Switch
id="steal_emoji"
checked={config.steal_emoji}
onCheckedChange={(checked) =>
onChange({ ...config, steal_emoji: checked })
}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label htmlFor="content_filtration"></Label>
<p className="text-xs text-muted-foreground">
</p>
</div>
<Switch
id="content_filtration"
checked={config.content_filtration}
onCheckedChange={(checked) =>
onChange({ ...config, content_filtration: checked })
}
/>
</div>
{config.content_filtration && (
<div className="space-y-3">
<Label htmlFor="filtration_prompt"></Label>
<Input
id="filtration_prompt"
placeholder="例如:符合公序良俗"
value={config.filtration_prompt}
onChange={(e) =>
onChange({ ...config, filtration_prompt: e.target.value })
}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
)}
</div>
)
}
// ====== 步骤4其他基础配置 ======
interface OtherBasicFormProps {
config: OtherBasicConfig
onChange: (config: OtherBasicConfig) => void
}
export function OtherBasicForm({ config, onChange }: OtherBasicFormProps) {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label htmlFor="enable_tool"></Label>
<p className="text-xs text-muted-foreground">
使
</p>
</div>
<Switch
id="enable_tool"
checked={config.enable_tool}
onCheckedChange={(checked) =>
onChange({ ...config, enable_tool: checked })
}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label htmlFor="all_global"></Label>
<p className="text-xs text-muted-foreground">
使
</p>
</div>
<Switch
id="all_global"
checked={config.all_global}
onCheckedChange={(checked) =>
onChange({ ...config, all_global: checked })
}
/>
</div>
</div>
)
}
// ====== 步骤5硅基流动API配置 ======
interface SiliconFlowFormProps {
config: SiliconFlowConfig
onChange: (config: SiliconFlowConfig) => void
}
export function SiliconFlowForm({ config, onChange }: SiliconFlowFormProps) {
const [showApiKey, setShowApiKey] = useState(false)
return (
<div className="space-y-6">
<div className="rounded-lg bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800 p-4">
<div className="flex items-start gap-3">
<div className="mt-0.5">
<svg className="h-5 w-5 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="flex-1 text-sm">
<p className="font-medium text-blue-900 dark:text-blue-100 mb-1">
(SiliconFlow)
</p>
<p className="text-blue-700 dark:text-blue-300 mb-2">
DeepSeek V3Qwen
API Key 使
</p>
<a
href="https://cloud.siliconflow.cn"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline font-medium"
>
API Key
<ExternalLink className="h-3 w-3" />
</a>
</div>
</div>
</div>
<div className="space-y-3">
<Label htmlFor="siliconflow_api_key">SiliconFlow API Key *</Label>
<div className="relative">
<Input
id="siliconflow_api_key"
type={showApiKey ? 'text' : 'password'}
placeholder="sk-..."
value={config.api_key}
onChange={(e) => onChange({ api_key: e.target.value })}
className="font-mono pr-10"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
onClick={() => setShowApiKey(!showApiKey)}
>
{showApiKey ? (
<EyeOff className="h-4 w-4 text-muted-foreground" />
) : (
<Eye className="h-4 w-4 text-muted-foreground" />
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
API
</p>
</div>
<div className="rounded-lg bg-muted/50 p-4 text-sm space-y-2">
<p className="font-medium"></p>
<ul className="list-disc list-inside space-y-1 text-muted-foreground ml-2">
<li>DeepSeek V3 - </li>
<li>Qwen3 30B - </li>
<li>Qwen3 VL 30B - </li>
<li>SenseVoice - </li>
<li>BGE-M3 - </li>
<li> (LPMM)</li>
</ul>
</div>
<div className="rounded-lg border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-950/30 p-4">
<p className="text-sm text-amber-900 dark:text-amber-100">
<span className="font-medium">💡 </span>
"系统设置 → 模型配置" API
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,289 @@
// 设置向导API调用函数
import { fetchWithAuth, getAuthHeaders } from '@/lib/fetch-with-auth'
import type {
BotBasicConfig,
PersonalityConfig,
EmojiConfig,
OtherBasicConfig,
SiliconFlowConfig,
} from './types'
// ===== 读取配置 =====
// 读取Bot基础配置
export async function loadBotBasicConfig(): Promise<BotBasicConfig> {
const response = await fetchWithAuth('/api/webui/config/bot', {
method: 'GET',
headers: getAuthHeaders(),
})
if (!response.ok) {
throw new Error('读取Bot配置失败')
}
const data = await response.json()
const botConfig = data.config.bot || {}
return {
qq_account: botConfig.qq_account || 0,
nickname: botConfig.nickname || '',
alias_names: botConfig.alias_names || [],
}
}
// 读取人格配置
export async function loadPersonalityConfig(): Promise<PersonalityConfig> {
const response = await fetchWithAuth('/api/webui/config/bot', {
method: 'GET',
headers: getAuthHeaders(),
})
if (!response.ok) {
throw new Error('读取人格配置失败')
}
const data = await response.json()
const personalityConfig = data.config.personality || {}
return {
personality: personalityConfig.personality || '',
reply_style: personalityConfig.reply_style || '',
interest: personalityConfig.interest || '',
plan_style: personalityConfig.plan_style || '',
private_plan_style: personalityConfig.private_plan_style || '',
}
}
// 读取表情包配置
export async function loadEmojiConfig(): Promise<EmojiConfig> {
const response = await fetchWithAuth('/api/webui/config/bot', {
method: 'GET',
headers: getAuthHeaders(),
})
if (!response.ok) {
throw new Error('读取表情包配置失败')
}
const data = await response.json()
const emojiConfig = data.config.emoji || {}
return {
emoji_chance: emojiConfig.emoji_chance ?? 0.4,
max_reg_num: emojiConfig.max_reg_num ?? 40,
do_replace: emojiConfig.do_replace ?? true,
check_interval: emojiConfig.check_interval ?? 10,
steal_emoji: emojiConfig.steal_emoji ?? true,
content_filtration: emojiConfig.content_filtration ?? false,
filtration_prompt: emojiConfig.filtration_prompt || '',
}
}
// 读取其他基础配置
export async function loadOtherBasicConfig(): Promise<OtherBasicConfig> {
const response = await fetchWithAuth('/api/webui/config/bot', {
method: 'GET',
headers: getAuthHeaders(),
})
if (!response.ok) {
throw new Error('读取其他配置失败')
}
const data = await response.json()
const config = data.config
const toolConfig = config.tool || {}
const expressionConfig = config.expression || {}
return {
enable_tool: toolConfig.enable_tool ?? true,
all_global: expressionConfig.all_global_jargon ?? true,
}
}
// 读取硅基流动API配置
export async function loadSiliconFlowConfig(): Promise<SiliconFlowConfig> {
const response = await fetchWithAuth('/api/webui/config/model', {
method: 'GET',
headers: getAuthHeaders(),
})
if (!response.ok) {
throw new Error('读取模型配置失败')
}
const data = await response.json()
const modelConfig = data.config
// 获取SiliconFlow提供商的API Key
const apiProviders = modelConfig.api_providers || []
const siliconFlowProvider = apiProviders.find(
(p: Record<string, unknown>) => p.name === 'SiliconFlow'
)
return {
api_key: siliconFlowProvider?.api_key || '',
}
}
// ===== 保存配置 =====
// 保存Bot基础配置
export async function saveBotBasicConfig(config: BotBasicConfig) {
const response = await fetchWithAuth('/api/webui/config/bot/section/bot', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(config),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.detail || '保存Bot基础配置失败')
}
return await response.json()
}
// 保存人格配置
export async function savePersonalityConfig(config: PersonalityConfig) {
const response = await fetchWithAuth('/api/webui/config/bot/section/personality', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(config),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.detail || '保存人格配置失败')
}
return await response.json()
}
// 保存表情包配置
export async function saveEmojiConfig(config: EmojiConfig) {
const response = await fetchWithAuth('/api/webui/config/bot/section/emoji', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(config),
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.detail || '保存表情包配置失败')
}
return await response.json()
}
// 保存其他基础配置(工具、情绪、黑话)
export async function saveOtherBasicConfig(config: OtherBasicConfig) {
// 需要分别保存到不同的section
const promises = []
// 保存tool配置
promises.push(
fetchWithAuth('/api/webui/config/bot/section/tool', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ enable_tool: config.enable_tool }),
})
)
// 保存expression配置中的all_global_jargon
promises.push(
fetchWithAuth('/api/webui/config/bot/section/expression', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ all_global_jargon: config.all_global }),
})
)
const results = await Promise.all(promises)
// 检查所有请求是否成功
for (const response of results) {
if (!response.ok) {
const error = await response.json()
throw new Error(error.detail || '保存其他配置失败')
}
}
return { success: true }
}
// 保存硅基流动API配置
export async function saveSiliconFlowConfig(config: SiliconFlowConfig) {
// 1. 读取现有配置
const response = await fetchWithAuth('/api/webui/config/model', {
method: 'GET',
headers: getAuthHeaders(),
})
if (!response.ok) {
throw new Error('读取模型配置失败')
}
const currentModelConfig = await response.json()
const modelConfig = currentModelConfig.config
// 2. 更新SiliconFlow提供商的API Key
const apiProviders = modelConfig.api_providers || []
const siliconFlowIndex = apiProviders.findIndex(
(p: Record<string, unknown>) => p.name === 'SiliconFlow'
)
if (siliconFlowIndex >= 0) {
// 更新现有提供商的API Key
apiProviders[siliconFlowIndex] = {
...apiProviders[siliconFlowIndex],
api_key: config.api_key,
}
} else {
// 如果不存在,创建新的SiliconFlow提供商
apiProviders.push({
name: 'SiliconFlow',
base_url: 'https://api.siliconflow.cn/v1',
api_key: config.api_key,
client_type: 'openai',
max_retry: 3,
timeout: 120,
retry_interval: 5,
})
}
// 3. 保存更新后的配置
const updatedConfig = {
...modelConfig,
api_providers: apiProviders,
}
const saveResponse = await fetchWithAuth('/api/webui/config/model', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(updatedConfig),
})
if (!saveResponse.ok) {
const error = await saveResponse.json()
throw new Error(error.detail || '保存模型配置失败')
}
return await saveResponse.json()
}
// 标记设置完成
export async function completeSetup() {
const response = await fetchWithAuth('/api/webui/setup/complete', {
method: 'POST',
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.message || '标记配置完成失败')
}
return await response.json()
}

View File

@@ -0,0 +1,46 @@
// 设置向导相关类型定义
export interface SetupStep {
id: string
title: string
description: string
icon: React.ComponentType<{ className?: string }>
}
// 步骤1Bot基础信息
export interface BotBasicConfig {
qq_account: number
nickname: string
alias_names: string[]
}
// 步骤2人格配置
export interface PersonalityConfig {
personality: string
reply_style: string
interest: string
plan_style: string
private_plan_style: string
}
// 步骤3表情包配置
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
}
// 步骤4其他基础配置
export interface OtherBasicConfig {
enable_tool: boolean
all_global: boolean // 全局黑话模式expression.all_global_jargon
}
// 步骤5硅基流动API配置
export interface SiliconFlowConfig {
api_key: string
}