perf:优化webui配置展示,优化log显示,修复表达审核

This commit is contained in:
SengokuCola
2026-05-05 18:34:20 +08:00
parent 424287387a
commit 16de259955
13 changed files with 326 additions and 164 deletions

View File

@@ -80,6 +80,7 @@ export function ExpressionReviewer({ open, onOpenChange }: ExpressionReviewerPro
// 快速审核模式状态
const [quickFilterType, setQuickFilterType] = useState<'unchecked' | 'passed' | 'rejected' | 'all'>('unchecked')
const [quickExpressions, setQuickExpressions] = useState<Expression[]>([])
const quickExpressionsRef = useRef<Expression[]>([])
const [quickCurrentIndex, setQuickCurrentIndex] = useState(0)
const [quickLoading, setQuickLoading] = useState(false)
const [quickTotal, setQuickTotal] = useState(0)
@@ -92,6 +93,10 @@ export function ExpressionReviewer({ open, onOpenChange }: ExpressionReviewerPro
const cardRef = useRef<HTMLDivElement>(null)
const dragStartRef = useRef<{ x: number; y: number } | null>(null)
const isDraggingRef = useRef(false)
useEffect(() => {
quickExpressionsRef.current = quickExpressions
}, [quickExpressions])
const [loading, setLoading] = useState(false)
const [statsLoading, setStatsLoading] = useState(false)
const [total, setTotal] = useState(0)
@@ -180,9 +185,13 @@ export function ExpressionReviewer({ open, onOpenChange }: ExpressionReviewerPro
setQuickLoading(true)
const pageToLoad = append ? quickPage + 1 : quickPage
const result = await getReviewList({
page: pageToLoad,
page: quickFilterType === 'unchecked' ? 1 : pageToLoad,
page_size: 20,
filter_type: quickFilterType,
order: quickFilterType === 'unchecked' ? 'random' : 'latest',
exclude_ids: quickFilterType === 'unchecked' && append
? quickExpressionsRef.current.map((expr) => expr.id)
: undefined,
})
if (result.success) {

View File

@@ -442,16 +442,20 @@ export async function getReviewList(params: {
page?: number
page_size?: number
filter_type?: 'unchecked' | 'passed' | 'rejected' | 'all'
order?: 'latest' | 'random'
search?: string
chat_id?: string
exclude_ids?: number[]
}): Promise<ApiResponse<ReviewListResponse>> {
const queryParams = new URLSearchParams()
if (params.page) queryParams.append('page', params.page.toString())
if (params.page_size) queryParams.append('page_size', params.page_size.toString())
if (params.filter_type) queryParams.append('filter_type', params.filter_type)
if (params.order) queryParams.append('order', params.order)
if (params.search) queryParams.append('search', params.search)
if (params.chat_id) queryParams.append('chat_id', params.chat_id)
params.exclude_ids?.forEach((id) => queryParams.append('exclude_ids', id.toString()))
const response = await fetchWithAuth(`${API_BASE}/review/list?${queryParams}`)

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { parse as parseToml } from 'smol-toml'
import { AlertDescription, Alert } from '@/components/ui/alert'
@@ -23,11 +23,13 @@ import { useToast } from '@/hooks/use-toast'
import { getBotConfig, getBotConfigRaw, getBotConfigSchema, updateBotConfig, updateBotConfigRaw } from '@/lib/config-api'
import { fieldHooks } from '@/lib/field-hooks'
import { RestartProvider, useRestart } from '@/lib/restart-context'
import { cn } from '@/lib/utils'
import { ChevronDown, ChevronUp, Code2, Info, Layout, Power, RefreshCw, Save } from 'lucide-react'
import type { ConfigSchema } from '@/types/config-schema'
import {
BotPlatformsHook,
ChatPromptsHook,
ChatTalkValueRulesHook,
ExpressionGroupsHook,
@@ -413,6 +415,7 @@ function BotConfigPageContent() {
useEffect(() => {
const hookEntries = [
['bot.platforms', BotPlatformsHook],
['chat.chat_prompts', ChatPromptsHook],
['chat.talk_value_rules', ChatTalkValueRulesHook],
['expression.expression_groups', ExpressionGroupsHook],
@@ -773,7 +776,23 @@ function BotConfigPageContent() {
<p className="text-muted-foreground mt-1 text-xs sm:text-sm"></p>
</div>
{/* 按钮组 - 桌面端靠右 */}
<div className="flex gap-2 flex-shrink-0">
<div className="flex flex-wrap gap-2 flex-shrink-0 sm:justify-end">
<Tabs
value={editMode}
onValueChange={(v) => handleModeChange(v as 'visual' | 'source')}
className="w-full min-w-[13rem] sm:w-[14rem]"
>
<TabsList className="grid h-8 w-full grid-cols-2 sm:h-9">
<TabsTrigger value="visual" className="px-2 text-xs">
<Layout className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
</TabsTrigger>
<TabsTrigger value="source" className="px-2 text-xs">
<Code2 className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
</TabsTrigger>
</TabsList>
</Tabs>
<Button
onClick={handleReloadFromFile}
disabled={saving || autoSaving || isRestarting}
@@ -833,22 +852,6 @@ function BotConfigPageContent() {
</AlertDialog>
</div>
</div>
{/* 模式切换 - 单独一行 */}
<div className="flex">
<Tabs value={editMode} onValueChange={(v) => handleModeChange(v as 'visual' | 'source')} className="w-full">
<TabsList className="h-8 sm:h-9 w-full grid grid-cols-2">
<TabsTrigger value="visual" className="text-xs sm:text-sm">
<Layout className="h-3 w-3 sm:h-4 sm:w-4 mr-1" />
</TabsTrigger>
<TabsTrigger value="source" className="text-xs sm:text-sm">
<Code2 className="h-3 w-3 sm:h-4 sm:w-4 mr-1" />
</TabsTrigger>
</TabsList>
</Tabs>
</div>
</div>
{/* 重启提示 */}
@@ -975,6 +978,9 @@ function DynamicConfigTabs(props: DynamicConfigTabsProps) {
? tabGroups
: tabGroups.filter((tab) => DEFAULT_VISIBLE_TAB_IDS.has(tab.id))
const hasCollapsibleTabs = tabGroups.some((tab) => !DEFAULT_VISIBLE_TAB_IDS.has(tab.id))
const firstExpandedTabId = visibleTabGroups.find(
(tab) => !DEFAULT_VISIBLE_TAB_IDS.has(tab.id)
)?.id
const toggleExpanded = () => {
setExpanded((current) => {
@@ -1033,15 +1039,26 @@ function DynamicConfigTabs(props: DynamicConfigTabsProps) {
return (
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="flex flex-wrap h-auto gap-1 p-1">
{visibleTabGroups.map((tab) => (
<TabsTrigger
key={tab.id}
value={tab.id}
className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"
>
{tab.label}
</TabsTrigger>
))}
{visibleTabGroups.map((tab) => {
const isExpandedOnlyTab = !DEFAULT_VISIBLE_TAB_IDS.has(tab.id)
return (
<Fragment key={tab.id}>
{tab.id === firstExpandedTabId && (
<span className="mx-1 hidden h-6 w-px bg-border/80 sm:block" />
)}
<TabsTrigger
value={tab.id}
className={cn(
"text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm",
isExpandedOnlyTab &&
"border border-dashed border-border/70 bg-background/45 text-muted-foreground/80 hover:bg-background/70 data-[state=active]:border-primary/45 data-[state=active]:bg-primary/10 data-[state=active]:text-primary data-[state=active]:shadow-none"
)}
>
{tab.label}
</TabsTrigger>
</Fragment>
)
})}
{hasCollapsibleTabs && (
<Button
type="button"

View File

@@ -28,6 +28,11 @@ interface ExpressionGroupValue {
expression_groups: ExpressionGroupTarget[]
}
interface PlatformAccountRow {
platform: string
account: string
}
const ruleTypeLabel = (rule: unknown) => {
if (rule === 'private') return '私聊'
if (rule === 'group') return '群聊'
@@ -102,6 +107,30 @@ const formatExpressionTarget = (target: ExpressionGroupTarget): string => {
return `${platform}:${itemId} · ${rule}`
}
const normalizePlatformAccounts = (value: unknown): string[] => {
if (!Array.isArray(value)) return []
return value.map((item) => String(item ?? ''))
}
const parsePlatformAccount = (value: string): PlatformAccountRow => {
const separatorIndex = value.indexOf(':')
if (separatorIndex < 0) {
return { platform: '', account: value }
}
return {
platform: value.slice(0, separatorIndex),
account: value.slice(separatorIndex + 1),
}
}
const formatPlatformAccount = (row: PlatformAccountRow): string => {
const platform = row.platform.trim()
const account = row.account.trim()
if (!platform) return account
if (!account) return `${platform}:`
return `${platform}:${account}`
}
export const ChatTalkValueRulesHook = createListItemEditorHook({
addLabel: '添加发言频率规则',
addButtonPlacement: 'top',
@@ -175,6 +204,96 @@ export const ExpressionLearningListHook = createListItemEditorHook({
},
})
export const BotPlatformsHook: FieldHookComponent = ({ onChange, value }) => {
const platforms = normalizePlatformAccounts(value)
const rows = platforms.map(parsePlatformAccount)
const updateRows = (nextRows: PlatformAccountRow[]) => {
onChange?.(nextRows.map(formatPlatformAccount))
}
const addRow = () => {
updateRows([...rows, { platform: '', account: '' }])
}
const removeRow = (rowIndex: number) => {
updateRows(rows.filter((_, index) => index !== rowIndex))
}
const updateRow = (rowIndex: number, patch: Partial<PlatformAccountRow>) => {
updateRows(
rows.map((row, index) =>
index === rowIndex ? { ...row, ...patch } : row
)
)
}
return (
<div className="space-y-3">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1">
<Label className="text-sm font-medium"></Label>
<p className="text-xs text-muted-foreground">
platform:account wx:114514
</p>
</div>
<Button type="button" size="sm" variant="outline" onClick={addRow}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
{rows.length === 0 ? (
<div className="rounded-md border border-dashed bg-muted/30 px-4 py-5 text-center text-sm text-muted-foreground">
</div>
) : (
<div className="space-y-2">
{rows.map((row, rowIndex) => (
<div
key={rowIndex}
className="grid gap-2 rounded-md border bg-muted/20 p-3 sm:grid-cols-[minmax(7rem,0.6fr)_minmax(10rem,1fr)_auto]"
>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={row.platform}
placeholder="wx"
onChange={(event) =>
updateRow(rowIndex, { platform: event.target.value })
}
/>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
className="font-mono"
value={row.account}
placeholder="114514"
onChange={(event) =>
updateRow(rowIndex, { account: event.target.value })
}
/>
</div>
<div className="flex items-end justify-end">
<Button
type="button"
size="icon"
variant="ghost"
aria-label={`删除其他平台 ${rowIndex + 1}`}
onClick={() => removeRow(rowIndex)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
)}
</div>
)
}
export const KeywordRulesHook = createListItemEditorHook({
addLabel: '添加关键词规则',
helperText: '匹配命中后会用 reaction 内容作为额外上下文。keywords 至少填一条,或使用正则模式。',
@@ -279,8 +398,8 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
}
return (
<div className="space-y-4 rounded-lg border bg-card p-4 sm:p-6">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-3 rounded-lg border bg-card p-4 sm:p-5">
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1">
<h3 className="text-base font-semibold"></h3>
<p className="text-sm text-muted-foreground">
@@ -299,13 +418,13 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
</div>
) : (
<div className="space-y-3">
<div className="space-y-2">
{groups.map((group, groupIndex) => (
<div
key={groupIndex}
className="space-y-3 rounded-md border bg-muted/20 p-3 sm:p-4"
className="space-y-2 rounded-md border bg-muted/20 p-2.5 sm:p-3"
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-medium">
{groupIndex + 1}
@@ -341,15 +460,16 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
</div>
) : (
<div className="space-y-2">
<div className="space-y-1.5">
{group.expression_groups.map((member, memberIndex) => (
<div
key={`${groupIndex}-${memberIndex}`}
className="grid gap-3 rounded-md bg-background/80 p-3 md:grid-cols-[minmax(7rem,0.75fr)_minmax(10rem,1fr)_minmax(8rem,0.8fr)_auto]"
className="grid items-end gap-2 rounded-md bg-background/80 px-2.5 py-2 md:grid-cols-[minmax(6rem,0.65fr)_minmax(9rem,1fr)_minmax(7rem,0.75fr)_2.25rem]"
>
<div className="space-y-1">
<Label className="text-xs"></Label>
<div className="space-y-0.5">
<Label className="text-[11px] leading-none text-muted-foreground"></Label>
<Input
className="h-8"
value={member.platform}
placeholder="qq"
onChange={(event) =>
@@ -359,10 +479,10 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
}
/>
</div>
<div className="space-y-1">
<Label className="text-xs"> / </Label>
<div className="space-y-0.5">
<Label className="text-[11px] leading-none text-muted-foreground"> / </Label>
<Input
className="font-mono"
className="h-8 font-mono"
value={member.item_id}
placeholder="123456"
onChange={(event) =>
@@ -372,8 +492,8 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
}
/>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<div className="space-y-0.5">
<Label className="text-[11px] leading-none text-muted-foreground"></Label>
<Select
value={member.rule_type}
onValueChange={(nextRuleType) =>
@@ -382,7 +502,7 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
})
}
>
<SelectTrigger>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -399,6 +519,7 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
type="button"
size="icon"
variant="ghost"
className="h-8 w-8"
aria-label={`删除互通组 ${groupIndex + 1} 的成员 ${memberIndex + 1}`}
onClick={() => removeMember(groupIndex, memberIndex)}
>
@@ -409,16 +530,6 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) =>
))}
</div>
)}
{group.expression_groups.length > 0 && (
<div className="flex flex-wrap gap-2">
{group.expression_groups.map((member, memberIndex) => (
<Badge key={memberIndex} variant="outline">
{formatExpressionTarget(member)}
</Badge>
))}
</div>
)}
</div>
))}
</div>

View File

@@ -11,6 +11,7 @@ export type {
UseAutoSaveReturnGeneric,
} from './useAutoSave'
export {
BotPlatformsHook,
ChatPromptsHook,
ChatTalkValueRulesHook,
ExpressionGroupsHook,

View File

@@ -449,8 +449,8 @@ export function EmojiManagementPage() {
</div>
</div>
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 pt-4 border-t">
<div className="flex items-center gap-4">
<div className="flex flex-col gap-3 border-t pt-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-wrap items-center gap-3">
{selectedIds.size > 0 && (
<span className="text-sm text-muted-foreground">
{selectedIds.size}
@@ -477,8 +477,41 @@ export function EmojiManagementPage() {
</SelectContent>
</Select>
</div>
<Button
variant="outline"
size="sm"
onClick={loadEmojiList}
disabled={loading}
>
<RefreshCw
className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`}
/>
</Button>
{selectedIds.size > 0 && (
<>
<Button
variant="outline"
size="sm"
onClick={() => setSelectedIds(new Set())}
>
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => setBatchDeleteDialogOpen(true)}
>
<Trash2 className="h-4 w-4 mr-1" />
</Button>
</>
)}
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 sm:ml-auto">
<Label
htmlFor="emoji-page-size"
className="text-sm whitespace-nowrap"
@@ -503,41 +536,8 @@ export function EmojiManagementPage() {
<SelectItem value="100">100</SelectItem>
</SelectContent>
</Select>
{selectedIds.size > 0 && (
<>
<Button
variant="outline"
size="sm"
onClick={() => setSelectedIds(new Set())}
>
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => setBatchDeleteDialogOpen(true)}
>
<Trash2 className="h-4 w-4 mr-1" />
</Button>
</>
)}
</div>
</div>
<div className="flex justify-end pt-4 border-t">
<Button
variant="outline"
size="sm"
onClick={loadEmojiList}
disabled={loading}
>
<RefreshCw
className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`}
/>
</Button>
</div>
</CardContent>
</Card>