feat:优化webui多个页面的人机交互,修复插件地址问题,放宽插件id限制,增加高级页面缩进,统计页面快捷按钮,优化新手引导

This commit is contained in:
SengokuCola
2026-05-04 12:46:55 +08:00
parent 75665a4d38
commit 75e9453495
29 changed files with 1101 additions and 831 deletions

View File

@@ -1,10 +1,9 @@
// 设置向导各步骤表单组件
import { ExternalLink, Eye, EyeOff, X } from 'lucide-react'
import { Eye, EyeOff } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
@@ -15,16 +14,14 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import type {
ApiProviderSetupConfig,
BotBasicConfig,
EmojiConfig,
OtherBasicConfig,
ModelSetupConfig,
PersonalityConfig,
SiliconFlowConfig,
} from './types'
// ====== 步骤1Bot基础配置 ======
@@ -156,22 +153,6 @@ 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((_, aliasIndex) => aliasIndex !== index),
})
}
return (
<div className="space-y-6">
<div className="space-y-3">
@@ -254,53 +235,6 @@ export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
{t('setupPage.forms.botBasic.nickname.description')}
</p>
</div>
<div className="space-y-3">
<Label>{t('setupPage.forms.botBasic.alias.label')}</Label>
<div className="mb-2 flex flex-wrap gap-2">
{config.alias_names.map((alias, index) => (
<Badge key={index} variant="secondary" className="gap-1">
{alias}
<button
type="button"
onClick={() => handleRemoveAlias(index)}
className="hover:text-destructive ml-1"
aria-label={t('setupPage.forms.botBasic.alias.remove', { alias })}
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
<div className="flex gap-2">
<Input
id="alias_input"
placeholder={t('setupPage.forms.botBasic.alias.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 = ''
}
}}
>
{t('setupPage.forms.botBasic.alias.add')}
</Button>
</div>
<p className="text-muted-foreground text-xs">
{t('setupPage.forms.botBasic.alias.description')}
</p>
</div>
</div>
)
}
@@ -313,7 +247,6 @@ interface PersonalityFormProps {
export function PersonalityForm({ config, onChange }: PersonalityFormProps) {
const { t } = useTranslation()
const multipleReplyStyleText = config.multiple_reply_style.join('\n')
return (
<div className="space-y-6">
@@ -344,276 +277,61 @@ export function PersonalityForm({ config, onChange }: PersonalityFormProps) {
{t('setupPage.forms.personality.replyStyle.description')}
</p>
</div>
<div className="space-y-3">
<Label htmlFor="multiple_reply_style">
{t('setupPage.forms.personality.multipleReplyStyle.label')}
</Label>
<Textarea
id="multiple_reply_style"
placeholder={t('setupPage.forms.personality.multipleReplyStyle.placeholder')}
value={multipleReplyStyleText}
onChange={(e) =>
onChange({
...config,
multiple_reply_style: e.target.value
.split('\n')
.map((style) => style.trim())
.filter(Boolean),
})
}
rows={5}
/>
<p className="text-muted-foreground text-xs">
{t('setupPage.forms.personality.multipleReplyStyle.description')}
</p>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label htmlFor="multiple_probability">
{t('setupPage.forms.personality.multipleProbability.label')}
</Label>
<span className="text-muted-foreground text-sm">
{(config.multiple_probability * 100).toFixed(0)}%
</span>
</div>
<Input
id="multiple_probability"
type="range"
min="0"
max="1"
step="0.1"
value={config.multiple_probability}
onChange={(e) => onChange({ ...config, multiple_probability: Number(e.target.value) })}
/>
<p className="text-muted-foreground text-xs">
{t('setupPage.forms.personality.multipleProbability.description')}
</p>
</div>
</div>
)
}
// ====== 步骤3表情包配置 ======
interface EmojiFormProps {
config: EmojiConfig
onChange: (config: EmojiConfig) => void
// ====== 步骤3API 提供商配置 ======
interface ApiProviderSetupFormProps {
config: ApiProviderSetupConfig
onChange: (config: ApiProviderSetupConfig) => void
}
export function EmojiForm({ config, onChange }: EmojiFormProps) {
const { t } = useTranslation()
return (
<div className="space-y-6">
<div className="space-y-3">
<Label htmlFor="emoji_send_num">{t('setupPage.forms.emoji.emojiSendNum.label')}</Label>
<Input
id="emoji_send_num"
type="number"
min="1"
max="64"
value={config.emoji_send_num}
onChange={(e) => onChange({ ...config, emoji_send_num: Number(e.target.value) })}
/>
<p className="text-muted-foreground text-xs">
{t('setupPage.forms.emoji.emojiSendNum.description')}
</p>
</div>
<div className="space-y-3">
<Label htmlFor="max_reg_num">{t('setupPage.forms.emoji.maxRegNum.label')}</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-muted-foreground text-xs">
{t('setupPage.forms.emoji.maxRegNum.description')}
</p>
</div>
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label htmlFor="do_replace">{t('setupPage.forms.emoji.doReplace.label')}</Label>
<p className="text-muted-foreground text-xs">
{t('setupPage.forms.emoji.doReplace.description')}
</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">{t('setupPage.forms.emoji.checkInterval.label')}</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-muted-foreground text-xs">
{t('setupPage.forms.emoji.checkInterval.description')}
</p>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label htmlFor="steal_emoji">{t('setupPage.forms.emoji.stealEmoji.label')}</Label>
<p className="text-muted-foreground text-xs">
{t('setupPage.forms.emoji.stealEmoji.description')}
</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">
{t('setupPage.forms.emoji.contentFiltration.label')}
</Label>
<p className="text-muted-foreground text-xs">
{t('setupPage.forms.emoji.contentFiltration.description')}
</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">
{t('setupPage.forms.emoji.filtrationPrompt.label')}
</Label>
<Input
id="filtration_prompt"
placeholder={t('setupPage.forms.emoji.filtrationPrompt.placeholder')}
value={config.filtration_prompt}
onChange={(e) => onChange({ ...config, filtration_prompt: e.target.value })}
/>
<p className="text-muted-foreground text-xs">
{t('setupPage.forms.emoji.filtrationPrompt.description')}
</p>
</div>
)}
</div>
)
}
// ====== 步骤4其他基础配置 ======
interface OtherBasicFormProps {
config: OtherBasicConfig
onChange: (config: OtherBasicConfig) => void
}
export function OtherBasicForm({ config, onChange }: OtherBasicFormProps) {
const { t } = useTranslation()
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label htmlFor="all_global">{t('setupPage.forms.other.allGlobal.label')}</Label>
<p className="text-muted-foreground text-xs">
{t('setupPage.forms.other.allGlobal.description')}
</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) {
export function ApiProviderSetupForm({ config, onChange }: ApiProviderSetupFormProps) {
const { t } = useTranslation()
const [showApiKey, setShowApiKey] = useState(false)
const apiKeyToggleLabel = showApiKey
? t('setupPage.forms.siliconFlow.apiKey.hide')
: t('setupPage.forms.siliconFlow.apiKey.show')
const autoConfigItems = [
t('setupPage.forms.siliconFlow.autoConfig.items.deepseek'),
t('setupPage.forms.siliconFlow.autoConfig.items.qwen3'),
t('setupPage.forms.siliconFlow.autoConfig.items.qwen3Vl'),
t('setupPage.forms.siliconFlow.autoConfig.items.senseVoice'),
t('setupPage.forms.siliconFlow.autoConfig.items.bgeM3'),
t('setupPage.forms.siliconFlow.autoConfig.items.lpmm'),
]
? t('setupPage.forms.apiProvider.apiKey.hide')
: t('setupPage.forms.apiProvider.apiKey.show')
return (
<div className="space-y-6">
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-800 dark:bg-blue-950/30">
<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="mb-1 font-medium text-blue-900 dark:text-blue-100">
{t('setupPage.forms.siliconFlow.about.title')}
</p>
<p className="mb-2 text-blue-700 dark:text-blue-300">
{t('setupPage.forms.siliconFlow.about.description')}
</p>
<a
href="https://cloud.siliconflow.cn"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 font-medium text-blue-600 hover:underline dark:text-blue-400"
>
{t('setupPage.forms.siliconFlow.about.link')}
<ExternalLink className="h-3 w-3" />
</a>
</div>
</div>
<div className="space-y-3">
<Label htmlFor="provider_name">{t('setupPage.forms.apiProvider.providerName.label')}</Label>
<Input
id="provider_name"
placeholder={t('setupPage.forms.apiProvider.providerName.placeholder')}
value={config.provider_name}
onChange={(e) => onChange({ ...config, provider_name: e.target.value })}
/>
<p className="text-muted-foreground text-xs">
{t('setupPage.forms.apiProvider.providerName.description')}
</p>
</div>
<div className="space-y-3">
<Label htmlFor="siliconflow_api_key">{t('setupPage.forms.siliconFlow.apiKey.label')}</Label>
<Label htmlFor="base_url">{t('setupPage.forms.apiProvider.baseUrl.label')}</Label>
<Input
id="base_url"
placeholder="https://api.example.com/v1"
value={config.base_url}
onChange={(e) => onChange({ ...config, base_url: e.target.value })}
className="font-mono"
/>
<p className="text-muted-foreground text-xs">
{t('setupPage.forms.apiProvider.baseUrl.description')}
</p>
</div>
<div className="space-y-3">
<Label htmlFor="api_key">{t('setupPage.forms.apiProvider.apiKey.label')}</Label>
<div className="relative">
<Input
id="siliconflow_api_key"
id="api_key"
type={showApiKey ? 'text' : 'password'}
placeholder="sk-..."
value={config.api_key}
onChange={(e) => onChange({ api_key: e.target.value })}
onChange={(e) => onChange({ ...config, api_key: e.target.value })}
className="pr-10 font-mono"
/>
<Button
@@ -633,25 +351,103 @@ export function SiliconFlowForm({ config, onChange }: SiliconFlowFormProps) {
</Button>
</div>
<p className="text-muted-foreground text-xs">
{t('setupPage.forms.siliconFlow.apiKey.description')}
</p>
</div>
<div className="bg-muted/50 space-y-2 rounded-lg p-4 text-sm">
<p className="font-medium">{t('setupPage.forms.siliconFlow.autoConfig.title')}</p>
<ul className="text-muted-foreground ml-2 list-inside list-disc space-y-1">
{autoConfigItems.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950/30">
<p className="text-sm text-amber-900 dark:text-amber-100">
<span className="font-medium">{t('setupPage.forms.siliconFlow.hint.title')}</span>
{t('setupPage.forms.siliconFlow.hint.description')}
{t('setupPage.forms.apiProvider.apiKey.description')}
</p>
</div>
</div>
)
}
// ====== 步骤4基础模型配置 ======
interface ModelSetupFormProps {
config: ModelSetupConfig
onChange: (config: ModelSetupConfig) => void
}
export function ModelSetupForm({ config, onChange }: ModelSetupFormProps) {
const { t } = useTranslation()
return (
<div className="space-y-6">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-4 rounded-lg border p-4">
<div className="space-y-3">
<Label htmlFor="planner_model_identifier">
{t('setupPage.forms.modelSetup.planner.identifier.label')}
</Label>
<Input
id="planner_model_identifier"
placeholder="gpt-4.1-mini"
value={config.planner_model_identifier}
onChange={(e) =>
onChange({
...config,
planner_model_identifier: e.target.value,
planner_model_name: e.target.value,
})
}
className="font-mono"
/>
<p className="text-muted-foreground text-xs">
{t('setupPage.forms.modelSetup.planner.identifier.description')}
</p>
</div>
<div className="flex items-center justify-between gap-4 rounded-md bg-muted/40 p-3">
<Label htmlFor="planner_visual" className="text-sm font-medium">
{t('setupPage.forms.modelSetup.planner.visual.label')}
</Label>
<Switch
id="planner_visual"
checked={config.planner_visual}
onCheckedChange={(checked) =>
onChange({ ...config, planner_visual: checked })
}
/>
</div>
</div>
<div className="space-y-4 rounded-lg border p-4">
<div className="space-y-3">
<Label htmlFor="replyer_model_identifier">
{t('setupPage.forms.modelSetup.replyer.identifier.label')}
</Label>
<Input
id="replyer_model_identifier"
placeholder="gpt-4.1"
value={config.replyer_model_identifier}
onChange={(e) =>
onChange({
...config,
replyer_model_identifier: e.target.value,
replyer_model_name: e.target.value,
})
}
className="font-mono"
/>
<p className="text-muted-foreground text-xs">
{t('setupPage.forms.modelSetup.replyer.identifier.description')}
</p>
</div>
<div className="flex items-center justify-between gap-4 rounded-md bg-muted/40 p-3">
<Label htmlFor="replyer_visual" className="text-sm font-medium">
{t('setupPage.forms.modelSetup.replyer.visual.label')}
</Label>
<Switch
id="replyer_visual"
checked={config.replyer_visual}
onCheckedChange={(checked) =>
onChange({ ...config, replyer_visual: checked })
}
/>
</div>
</div>
</div>
<div className="bg-muted/50 rounded-lg p-4 text-sm text-muted-foreground">
{t('setupPage.forms.modelSetup.saveHint')}
</div>
</div>
)
}

View File

@@ -4,13 +4,49 @@ import { parseResponse, throwIfError } from '@/lib/api-helpers'
import { fetchWithAuth, getAuthHeaders } from '@/lib/fetch-with-auth'
import type {
ApiProviderSetupConfig,
BotBasicConfig,
EmojiConfig,
OtherBasicConfig,
ModelSetupConfig,
PersonalityConfig,
SiliconFlowConfig,
} from './types'
interface ModelInfo {
model_identifier: string
name: string
api_provider: string
price_in?: number
cache?: boolean
cache_price_in?: number
price_out?: number
force_stream_mode?: boolean
visual?: boolean
extra_params?: Record<string, unknown>
}
interface ApiProviderConfig {
name: string
base_url: string
api_key: string
client_type?: string
max_retry?: number
timeout?: number
retry_interval?: number
}
interface TaskConfig {
model_list?: string[]
max_tokens?: number
temperature?: number
slow_threshold?: number
selection_strategy?: string
}
interface ModelConfig {
models?: ModelInfo[]
api_providers?: ApiProviderConfig[]
model_task_config?: Record<string, TaskConfig>
}
// ===== 读取配置 =====
// 读取Bot基础配置
@@ -56,73 +92,57 @@ export async function loadPersonalityConfig(): Promise<PersonalityConfig> {
}
}
// 读取表情包配置
export async function loadEmojiConfig(): Promise<EmojiConfig> {
const response = await fetchWithAuth('/api/webui/config/bot', {
method: 'GET',
headers: getAuthHeaders(),
})
const result = await parseResponse<{ config: { emoji?: EmojiConfig } }>(
response
)
const data = throwIfError(result)
const emojiConfig = (data.config.emoji || {}) as Partial<EmojiConfig>
return {
emoji_send_num: emojiConfig.emoji_send_num ?? 25,
max_reg_num: emojiConfig.max_reg_num ?? 64,
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(),
})
const result = await parseResponse<{
config: {
expression?: { all_global_jargon?: boolean }
}
}>(response)
const data = throwIfError(result)
const config = data.config
const expressionConfig = config.expression || {}
return {
all_global: expressionConfig.all_global_jargon ?? true,
}
}
// 读取硅基流动API配置
export async function loadSiliconFlowConfig(): Promise<SiliconFlowConfig> {
async function loadModelConfig(): Promise<ModelConfig> {
const response = await fetchWithAuth('/api/webui/config/model', {
method: 'GET',
headers: getAuthHeaders(),
})
const result = await parseResponse<{
config: {
api_providers?: Array<{ name: string; api_key?: string }>
}
}>(response)
const result = await parseResponse<{ config: ModelConfig }>(response)
const data = throwIfError(result)
const modelConfig = data.config
return data.config || {}
}
// 获取SiliconFlow提供商的API Key
const apiProviders = modelConfig.api_providers || []
const siliconFlowProvider = apiProviders.find((p) => p.name === 'SiliconFlow')
// 读取 API 提供商配置
export async function loadApiProviderSetupConfig(): Promise<ApiProviderSetupConfig> {
const modelConfig = await loadModelConfig()
const models = modelConfig.models || []
const taskConfig = modelConfig.model_task_config || {}
const plannerName = taskConfig.planner?.model_list?.[0] || ''
const replyerName = taskConfig.replyer?.model_list?.[0] || ''
const plannerModel = models.find((model) => model.name === plannerName)
const replyerModel = models.find((model) => model.name === replyerName)
const providerName =
plannerModel?.api_provider ||
replyerModel?.api_provider ||
modelConfig.api_providers?.[0]?.name ||
''
const provider = modelConfig.api_providers?.find((item) => item.name === providerName)
return {
api_key: siliconFlowProvider?.api_key || '',
provider_name: providerName,
base_url: provider?.base_url || '',
api_key: '',
}
}
// 读取基础模型配置
export async function loadModelSetupConfig(): Promise<ModelSetupConfig> {
const modelConfig = await loadModelConfig()
const models = modelConfig.models || []
const taskConfig = modelConfig.model_task_config || {}
const plannerName = taskConfig.planner?.model_list?.[0] || ''
const replyerName = taskConfig.replyer?.model_list?.[0] || ''
const plannerModel = models.find((model) => model.name === plannerName)
const replyerModel = models.find((model) => model.name === replyerName)
return {
planner_model_name: plannerName,
planner_model_identifier: plannerModel?.model_identifier || plannerName,
planner_visual: Boolean(plannerModel?.visual),
replyer_model_name: replyerName,
replyer_model_identifier: replyerModel?.model_identifier || replyerName,
replyer_visual: Boolean(replyerModel?.visual),
}
}
@@ -143,19 +163,6 @@ export async function saveBotBasicConfig(config: BotBasicConfig) {
// 保存人格配置
export async function savePersonalityConfig(config: PersonalityConfig) {
const response = await fetchWithAuth('/api/webui/config/bot/section/personality', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(config),
}
)
const result = await parseResponse(response)
return throwIfError(result)
}
// 保存表情包配置
export async function saveEmojiConfig(config: EmojiConfig) {
const response = await fetchWithAuth('/api/webui/config/bot/section/emoji', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(config),
@@ -165,58 +172,62 @@ export async function saveEmojiConfig(config: EmojiConfig) {
return throwIfError(result)
}
// 保存其他基础配置(黑话)
export async function saveOtherBasicConfig(config: OtherBasicConfig) {
const response = await fetchWithAuth('/api/webui/config/bot/section/expression', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ all_global_jargon: config.all_global }),
})
const result = await parseResponse(response)
return throwIfError(result)
function createBasicModel(
modelName: string,
modelIdentifier: string,
providerName: string,
visual: boolean,
existing?: ModelInfo
): ModelInfo {
return {
price_in: 0,
cache: false,
cache_price_in: 0,
price_out: 0,
force_stream_mode: false,
extra_params: {},
...existing,
visual,
model_identifier: modelIdentifier,
name: modelName,
api_provider: providerName,
}
}
// 保存硅基流动API配置
export async function saveSiliconFlowConfig(config: SiliconFlowConfig) {
// 1. 读取现有配置
const response = await fetchWithAuth('/api/webui/config/model', {
method: 'GET',
headers: getAuthHeaders(),
})
function upsertModel(models: ModelInfo[], model: ModelInfo): ModelInfo[] {
const index = models.findIndex((item) => item.name === model.name)
if (index >= 0) {
return models.map((item, itemIndex) => (itemIndex === index ? model : item))
}
return [...models, model]
}
const result = await parseResponse<{
config: {
api_providers?: Array<Record<string, unknown>>
}
}>(response)
const currentModelConfig = throwIfError(result)
const modelConfig = currentModelConfig.config
// 保存 API 提供商配置
export async function saveApiProviderSetupConfig(config: ApiProviderSetupConfig) {
const modelConfig = await loadModelConfig()
const providerName = config.provider_name.trim()
// 2. 更新SiliconFlow提供商的API Key
const apiProviders = modelConfig.api_providers || []
const siliconFlowIndex = apiProviders.findIndex((p) => 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,
})
const providerIndex = apiProviders.findIndex((provider) => provider.name === providerName)
const providerConfig: ApiProviderConfig = {
name: providerName,
base_url: config.base_url.trim(),
api_key: config.api_key.trim(),
client_type: 'openai',
max_retry: 3,
timeout: 120,
retry_interval: 5,
}
if (providerIndex >= 0) {
apiProviders[providerIndex] = {
...apiProviders[providerIndex],
...providerConfig,
}
} else {
apiProviders.push(providerConfig)
}
// 3. 保存更新后的配置
const updatedConfig = {
...modelConfig,
api_providers: apiProviders,
@@ -232,6 +243,77 @@ export async function saveSiliconFlowConfig(config: SiliconFlowConfig) {
return throwIfError(saveResult)
}
// 保存基础模型配置
export async function saveModelSetupConfig(
config: ModelSetupConfig,
providerName: string
) {
const modelConfig = await loadModelConfig()
const trimmedProviderName = providerName.trim()
const plannerModelIdentifier = config.planner_model_identifier.trim()
const plannerModelName = plannerModelIdentifier
const replyerModelIdentifier = config.replyer_model_identifier.trim()
const replyerModelName = replyerModelIdentifier
// 新增或更新 planner/replyer 模型,并仅同步 utils 到 planner。
let models = modelConfig.models || []
const existingPlannerModel = models.find((model) => model.name === plannerModelName)
const existingReplyerModel = models.find((model) => model.name === replyerModelName)
models = upsertModel(
models,
createBasicModel(
plannerModelName,
plannerModelIdentifier,
trimmedProviderName,
config.planner_visual,
existingPlannerModel
)
)
models = upsertModel(
models,
createBasicModel(
replyerModelName,
replyerModelIdentifier,
trimmedProviderName,
config.replyer_visual,
existingReplyerModel
)
)
const modelTaskConfig = modelConfig.model_task_config || {}
const updatedTaskConfig = {
...modelTaskConfig,
planner: {
...(modelTaskConfig.planner || {}),
model_list: [plannerModelName],
},
replyer: {
...(modelTaskConfig.replyer || {}),
model_list: [replyerModelName],
},
utils: {
...(modelTaskConfig.utils || {}),
model_list: [plannerModelName],
},
}
// vlm/voice/embedding 等其他任务配置保持原样。
const updatedConfig = {
...modelConfig,
models,
model_task_config: updatedTaskConfig,
}
const saveResponse = await fetchWithAuth('/api/webui/config/model', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(updatedConfig),
})
const saveResult = await parseResponse(saveResponse)
return throwIfError(saveResult)
}
// 标记设置完成
export async function completeSetup() {
const response = await fetchWithAuth('/api/webui/setup/complete', {

View File

@@ -1,13 +1,12 @@
import { useNavigate } from '@tanstack/react-router'
import {
ArrowRight,
Brain,
Bot,
CheckCircle2,
Globe,
Key,
Settings,
SkipForward,
Smile,
Sparkles,
User,
} from 'lucide-react'
@@ -38,31 +37,27 @@ import { cn } from '@/lib/utils'
import { APP_NAME } from '@/lib/version'
import { useToast } from '@/hooks/use-toast'
import type {
ApiProviderSetupConfig,
SetupStep,
BotBasicConfig,
ModelSetupConfig,
PersonalityConfig,
EmojiConfig,
OtherBasicConfig,
SiliconFlowConfig,
} from './types'
import {
ApiProviderSetupForm,
BotBasicForm,
ModelSetupForm,
PersonalityForm,
EmojiForm,
OtherBasicForm,
SiliconFlowForm,
} from './StepForms'
import {
loadBotBasicConfig,
loadPersonalityConfig,
loadEmojiConfig,
loadOtherBasicConfig,
loadSiliconFlowConfig,
loadApiProviderSetupConfig,
loadModelSetupConfig,
saveBotBasicConfig,
savePersonalityConfig,
saveEmojiConfig,
saveOtherBasicConfig,
saveSiliconFlowConfig,
saveApiProviderSetupConfig,
saveModelSetupConfig,
completeSetup,
} from './api'
import { RestartProvider, useRestart } from '@/lib/restart-context'
@@ -103,15 +98,6 @@ function SetupPageContent() {
],
multiple_probability: 0.2,
})
const createDefaultEmojiConfig = (): EmojiConfig => ({
emoji_send_num: 25,
max_reg_num: 64,
do_replace: true,
check_interval: 10,
steal_emoji: true,
content_filtration: false,
filtration_prompt: t('setupPage.defaults.emoji.filtrationPrompt'),
})
const [currentStep, setCurrentStep] = useState(0)
const [isCompleting, setIsCompleting] = useState(false)
const [isSaving, setIsSaving] = useState(false)
@@ -131,17 +117,21 @@ function SetupPageContent() {
createDefaultPersonalityConfig()
)
// 步骤3表情包配置
const [emoji, setEmoji] = useState<EmojiConfig>(() => createDefaultEmojiConfig())
// 步骤4其他基础配置
const [otherBasic, setOtherBasic] = useState<OtherBasicConfig>({
all_global: true,
// 步骤3API 提供商配置
const [apiProviderSetup, setApiProviderSetup] = useState<ApiProviderSetupConfig>({
provider_name: '',
base_url: '',
api_key: '',
})
// 步骤5硅基流动API配置
const [siliconFlow, setSiliconFlow] = useState<SiliconFlowConfig>({
api_key: '',
// 步骤4基础模型配置
const [modelSetup, setModelSetup] = useState<ModelSetupConfig>({
planner_model_name: '',
planner_model_identifier: '',
planner_visual: false,
replyer_model_name: '',
replyer_model_identifier: '',
replyer_visual: false,
})
const steps: SetupStep[] = [
@@ -158,23 +148,17 @@ function SetupPageContent() {
icon: User,
},
{
id: 'emoji',
title: t('setupPage.steps.emoji.title'),
description: t('setupPage.steps.emoji.description'),
icon: Smile,
},
{
id: 'other',
title: t('setupPage.steps.other.title'),
description: t('setupPage.steps.other.description'),
icon: Settings,
},
{
id: 'siliconflow',
title: t('setupPage.steps.siliconFlow.title'),
description: t('setupPage.steps.siliconFlow.description'),
id: 'api-provider',
title: t('setupPage.steps.apiProvider.title'),
description: t('setupPage.steps.apiProvider.description'),
icon: Key,
},
{
id: 'model-setup',
title: t('setupPage.steps.modelSetup.title'),
description: t('setupPage.steps.modelSetup.description'),
icon: Brain,
},
]
const progress = ((currentStep + 1) / steps.length) * 100
@@ -186,19 +170,17 @@ function SetupPageContent() {
setIsLoading(true)
// 并行加载所有配置
const [bot, personality, emoji, other, silicon] = await Promise.all([
const [bot, personality, apiProvider, model] = await Promise.all([
loadBotBasicConfig(),
loadPersonalityConfig(),
loadEmojiConfig(),
loadOtherBasicConfig(),
loadSiliconFlowConfig(),
loadApiProviderSetupConfig(),
loadModelSetupConfig(),
])
setBotBasic(bot)
setPersonality(personality)
setEmoji(emoji)
setOtherBasic(other)
setSiliconFlow(silicon)
setApiProviderSetup(apiProvider)
setModelSetup(model)
} catch (error) {
toast({
title: t('setupPage.toast.loadFailedTitle'),
@@ -225,14 +207,11 @@ function SetupPageContent() {
case 1: // 人格配置
await savePersonalityConfig(personality)
break
case 2: // 表情包
await saveEmojiConfig(emoji)
case 2: // API 提供商
await saveApiProviderSetupConfig(apiProviderSetup)
break
case 3: // 其他设置
await saveOtherBasicConfig(otherBasic)
break
case 4: // 硅基流动API
await saveSiliconFlowConfig(siliconFlow)
case 3: // 基础模型
await saveModelSetupConfig(modelSetup, apiProviderSetup.provider_name)
break
}
@@ -272,6 +251,24 @@ function SetupPageContent() {
return null
}
function validateApiProviderSetup(config: ApiProviderSetupConfig): string | null {
if (!config.provider_name.trim()) return t('setupPage.validation.enterProviderName')
if (!config.base_url.trim()) return t('setupPage.validation.enterBaseUrl')
if (!config.api_key.trim()) return t('setupPage.validation.enterApiKey')
return null
}
function validateModelSetup(config: ModelSetupConfig): string | null {
if (!config.planner_model_identifier.trim()) {
return t('setupPage.validation.enterPlannerModelIdentifier')
}
if (!config.replyer_model_identifier.trim()) {
return t('setupPage.validation.enterReplyerModelIdentifier')
}
if (!apiProviderSetup.provider_name.trim()) return t('setupPage.validation.enterProviderName')
return null
}
const handleNext = async () => {
// Step 1 验证
if (currentStep === 0) {
@@ -285,6 +282,28 @@ function SetupPageContent() {
return
}
}
if (currentStep === 2) {
const error = validateApiProviderSetup(apiProviderSetup)
if (error) {
toast({
title: t('setupPage.toast.validationFailedTitle'),
description: error,
variant: 'destructive',
})
return
}
}
if (currentStep === 3) {
const error = validateModelSetup(modelSetup)
if (error) {
toast({
title: t('setupPage.toast.validationFailedTitle'),
description: error,
variant: 'destructive',
})
return
}
}
// 保存当前步骤
const saved = await saveCurrentStep()
@@ -306,7 +325,18 @@ function SetupPageContent() {
setIsCompleting(true)
try {
// 1. 保存最后一步的配置(硅基流动API Key)
const error = validateModelSetup(modelSetup)
if (error) {
toast({
title: t('setupPage.toast.validationFailedTitle'),
description: error,
variant: 'destructive',
})
setIsCompleting(false)
return
}
// 1. 保存最后一步的基础模型配置
const saved = await saveCurrentStep()
if (!saved) {
setIsCompleting(false)
@@ -357,11 +387,9 @@ function SetupPageContent() {
case 1:
return <PersonalityForm config={personality} onChange={setPersonality} />
case 2:
return <EmojiForm config={emoji} onChange={setEmoji} />
return <ApiProviderSetupForm config={apiProviderSetup} onChange={setApiProviderSetup} />
case 3:
return <OtherBasicForm config={otherBasic} onChange={setOtherBasic} />
case 4:
return <SiliconFlowForm config={siliconFlow} onChange={setSiliconFlow} />
return <ModelSetupForm config={modelSetup} onChange={setModelSetup} />
default:
return null
}

View File

@@ -24,23 +24,19 @@ export interface PersonalityConfig {
multiple_probability: number
}
// 步骤3表情包配置
export interface EmojiConfig {
emoji_send_num: number
max_reg_num: number
do_replace: boolean
check_interval: number
steal_emoji: boolean
content_filtration: boolean
filtration_prompt: string
}
// 步骤4其他基础配置
export interface OtherBasicConfig {
all_global: boolean // 全局黑话模式expression.all_global_jargon
}
// 步骤5硅基流动API配置
export interface SiliconFlowConfig {
// 步骤3API 提供商配置
export interface ApiProviderSetupConfig {
provider_name: string
base_url: string
api_key: string
}
// 步骤4基础模型配置
export interface ModelSetupConfig {
planner_model_name: string
planner_model_identifier: string
planner_visual: boolean
replyer_model_name: string
replyer_model_identifier: string
replyer_visual: boolean
}