perf:优化webui配置展示,优化log显示,修复表达审核
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,6 +11,7 @@ export type {
|
||||
UseAutoSaveReturnGeneric,
|
||||
} from './useAutoSave'
|
||||
export {
|
||||
BotPlatformsHook,
|
||||
ChatPromptsHook,
|
||||
ChatTalkValueRulesHook,
|
||||
ExpressionGroupsHook,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
"startup.event_loop_closed": "[主程序] 事件循环已关闭",
|
||||
"startup.file_not_found": "{file_type} 文件不存在",
|
||||
"startup.graceful_shutdown_error": "优雅关闭时发生错误: {error}",
|
||||
"startup.initialization_completed_banner": "\n--------------------------------\n全部系统初始化完成,{nickname} 已成功唤醒\n--------------------------------\n如果想要自定义 {nickname} 的功能,请查阅:https://docs.mai-mai.org/manual/usage/\n或者遇到了问题,请访问我们的文档:https://docs.mai-mai.org/\n--------------------------------\n如果你想要编写或了解插件相关内容,请访问开发文档 https://docs.mai-mai.org/develop/\n--------------------------------\n如果你需要查阅模型的消耗以及麦麦的统计数据,请访问根目录的 maibot_statistics.html 文件\n",
|
||||
"startup.initialization_completed_banner": "全部系统初始化完成,{nickname} 已成功唤醒",
|
||||
"startup.initialization_completed_cycles": "初始化完成,神经元放电 {init_time} 次",
|
||||
"startup.interrupt_received": "收到中断信号,正在优雅关闭...",
|
||||
"startup.launching_script": "正在启动 {script_file}...",
|
||||
"startup.launching_script": "正在启动MaiBot",
|
||||
"startup.logging_shutdown_error": "关闭日志系统时出错: {error}",
|
||||
"startup.main_error": "主程序发生异常: {error}",
|
||||
"startup.opensource_free_notice": " 本项目是完全免费的开源软件,基于 GPL-3.0 协议发布",
|
||||
@@ -52,7 +52,7 @@
|
||||
"startup.shutdown_failed": "麦麦关闭失败: {error}",
|
||||
"startup.shutdown_started": "正在优雅关闭麦麦...",
|
||||
"startup.waking_up": "正在唤醒 {nickname}......",
|
||||
"startup.webui_access_token": "🔑 WebUI Access Token: {token}",
|
||||
"startup.webui_access_token": "🔑 WebUI 登录 Token: {token}",
|
||||
"startup.webui_access_token_failed": "❌ 获取 Access Token 失败: {error}",
|
||||
"startup.webui_access_token_login_hint": "💡 请使用此 Token 登录 WebUI",
|
||||
"startup.webui_anti_crawler_config_failed": "❌ 配置防爬虫中间件失败: {error}",
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"""Expression routes pytest tests"""
|
||||
|
||||
from typing import Generator
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI, APIRouter
|
||||
from fastapi import APIRouter, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
|
||||
from src.common.database.database_model import Expression
|
||||
from src.common.database.database_model import Expression, ModifiedBy
|
||||
from src.webui.dependencies import require_auth
|
||||
|
||||
|
||||
def create_test_app() -> FastAPI:
|
||||
@@ -63,6 +63,7 @@ def client_fixture(test_session: Session, monkeypatch) -> Generator[TestClient,
|
||||
@contextmanager
|
||||
def get_test_db_session():
|
||||
yield test_session
|
||||
test_session.commit()
|
||||
|
||||
monkeypatch.setattr("src.webui.routers.expression.get_db_session", get_test_db_session)
|
||||
|
||||
@@ -71,10 +72,11 @@ def client_fixture(test_session: Session, monkeypatch) -> Generator[TestClient,
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_auth")
|
||||
def mock_auth_fixture(monkeypatch):
|
||||
def mock_auth_fixture():
|
||||
"""Mock authentication to always return True"""
|
||||
mock_verify = MagicMock(return_value=True)
|
||||
monkeypatch.setattr("src.webui.routers.expression.verify_auth_token_from_cookie_or_header", mock_verify)
|
||||
app.dependency_overrides[require_auth] = lambda: "test-token"
|
||||
yield
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest.fixture(name="sample_expression")
|
||||
@@ -82,8 +84,8 @@ def sample_expression_fixture(test_session: Session) -> Expression:
|
||||
"""Insert a sample expression into test database"""
|
||||
test_session.execute(
|
||||
text(
|
||||
"INSERT INTO expressions (id, situation, style, context, up_content, content_list, count, last_active_time, create_time, session_id) "
|
||||
"VALUES (1, '测试情景', '测试风格', '测试上下文', '测试上文', '[\"测试内容1\", \"测试内容2\"]', 10, '2026-02-17 12:00:00', '2026-02-15 10:00:00', 'test_chat_001')"
|
||||
"INSERT INTO expressions (id, situation, style, content_list, count, last_active_time, create_time, session_id, checked, rejected) "
|
||||
"VALUES (1, '测试情景', '测试风格', '[\"测试内容1\", \"测试内容2\"]', 10, '2026-02-17 12:00:00', '2026-02-15 10:00:00', 'test_chat_001', 0, 0)"
|
||||
)
|
||||
)
|
||||
test_session.commit()
|
||||
@@ -131,8 +133,8 @@ def test_list_expressions_pagination(client: TestClient, mock_auth, test_session
|
||||
for i in range(5):
|
||||
test_session.execute(
|
||||
text(
|
||||
f"INSERT INTO expressions (id, situation, style, context, up_content, content_list, count, last_active_time, create_time, session_id) "
|
||||
f"VALUES ({i + 1}, '情景{i}', '风格{i}', '', '', '[]', 0, '2026-02-17 12:0{i}:00', '2026-02-15 10:00:00', 'chat_{i}')"
|
||||
f"INSERT INTO expressions (id, situation, style, content_list, count, last_active_time, create_time, session_id, checked, rejected) "
|
||||
f"VALUES ({i + 1}, '情景{i}', '风格{i}', '[]', 0, '2026-02-17 12:0{i}:00', '2026-02-15 10:00:00', 'chat_{i}', 0, 0)"
|
||||
)
|
||||
)
|
||||
test_session.commit()
|
||||
@@ -158,14 +160,14 @@ def test_list_expressions_search(client: TestClient, mock_auth, test_session: Se
|
||||
"""Test GET /expression/list with search filter"""
|
||||
test_session.execute(
|
||||
text(
|
||||
"INSERT INTO expressions (id, situation, style, context, up_content, content_list, count, last_active_time, create_time, session_id) "
|
||||
"VALUES (1, '找人吃饭', '热情', '', '', '[]', 0, datetime('now'), datetime('now'), 'chat_001')"
|
||||
"INSERT INTO expressions (id, situation, style, content_list, count, last_active_time, create_time, session_id, checked, rejected) "
|
||||
"VALUES (1, '找人吃饭', '热情', '[]', 0, datetime('now'), datetime('now'), 'chat_001', 0, 0)"
|
||||
)
|
||||
)
|
||||
test_session.execute(
|
||||
text(
|
||||
"INSERT INTO expressions (id, situation, style, context, up_content, content_list, count, last_active_time, create_time, session_id) "
|
||||
"VALUES (2, '拒绝邀请', '礼貌', '', '', '[]', 0, datetime('now'), datetime('now'), 'chat_002')"
|
||||
"INSERT INTO expressions (id, situation, style, content_list, count, last_active_time, create_time, session_id, checked, rejected) "
|
||||
"VALUES (2, '拒绝邀请', '礼貌', '[]', 0, datetime('now'), datetime('now'), 'chat_002', 0, 0)"
|
||||
)
|
||||
)
|
||||
test_session.commit()
|
||||
@@ -183,14 +185,14 @@ def test_list_expressions_chat_filter(client: TestClient, mock_auth, test_sessio
|
||||
"""Test GET /expression/list with chat_id filter"""
|
||||
test_session.execute(
|
||||
text(
|
||||
"INSERT INTO expressions (id, situation, style, context, up_content, content_list, count, last_active_time, create_time, session_id) "
|
||||
"VALUES (1, '情景A', '风格A', '', '', '[]', 0, datetime('now'), datetime('now'), 'chat_A')"
|
||||
"INSERT INTO expressions (id, situation, style, content_list, count, last_active_time, create_time, session_id, checked, rejected) "
|
||||
"VALUES (1, '情景A', '风格A', '[]', 0, datetime('now'), datetime('now'), 'chat_A', 0, 0)"
|
||||
)
|
||||
)
|
||||
test_session.execute(
|
||||
text(
|
||||
"INSERT INTO expressions (id, situation, style, context, up_content, content_list, count, last_active_time, create_time, session_id) "
|
||||
"VALUES (2, '情景B', '风格B', '', '', '[]', 0, datetime('now'), datetime('now'), 'chat_B')"
|
||||
"INSERT INTO expressions (id, situation, style, content_list, count, last_active_time, create_time, session_id, checked, rejected) "
|
||||
"VALUES (2, '情景B', '风格B', '[]', 0, datetime('now'), datetime('now'), 'chat_B', 0, 0)"
|
||||
)
|
||||
)
|
||||
test_session.commit()
|
||||
@@ -378,8 +380,8 @@ def test_batch_delete_expressions_success(client: TestClient, mock_auth, test_se
|
||||
for i in range(3):
|
||||
test_session.execute(
|
||||
text(
|
||||
f"INSERT INTO expressions (id, situation, style, context, up_content, content_list, count, last_active_time, create_time, session_id) "
|
||||
f"VALUES ({i + 1}, '批量删除{i}', '风格{i}', '', '', '[]', 0, datetime('now'), datetime('now'), 'chat_{i}')"
|
||||
f"INSERT INTO expressions (id, situation, style, content_list, count, last_active_time, create_time, session_id, checked, rejected) "
|
||||
f"VALUES ({i + 1}, '批量删除{i}', '风格{i}', '[]', 0, datetime('now'), datetime('now'), 'chat_{i}', 0, 0)"
|
||||
)
|
||||
)
|
||||
expression_ids.append(i + 1)
|
||||
@@ -416,8 +418,8 @@ def test_get_expression_stats(client: TestClient, mock_auth, test_session: Sessi
|
||||
for i in range(3):
|
||||
test_session.execute(
|
||||
text(
|
||||
f"INSERT INTO expressions (id, situation, style, context, up_content, content_list, count, last_active_time, create_time, session_id) "
|
||||
f"VALUES ({i + 1}, '情景{i}', '风格{i}', '', '', '[]', 0, datetime('now'), datetime('now'), 'chat_{i % 2}')"
|
||||
f"INSERT INTO expressions (id, situation, style, content_list, count, last_active_time, create_time, session_id, checked, rejected) "
|
||||
f"VALUES ({i + 1}, '情景{i}', '风格{i}', '[]', 0, datetime('now'), datetime('now'), 'chat_{i % 2}', 0, 0)"
|
||||
)
|
||||
)
|
||||
test_session.commit()
|
||||
@@ -432,11 +434,11 @@ def test_get_expression_stats(client: TestClient, mock_auth, test_session: Sessi
|
||||
|
||||
|
||||
def test_get_review_stats(client: TestClient, mock_auth, test_session: Session):
|
||||
"""Test GET /expression/review/stats returns hardcoded 0 counts"""
|
||||
"""Test GET /expression/review/stats returns review status counts"""
|
||||
test_session.execute(
|
||||
text(
|
||||
"INSERT INTO expressions (id, situation, style, context, up_content, content_list, count, last_active_time, create_time, session_id) "
|
||||
"VALUES (1, '待审核', '风格', '', '', '[]', 0, datetime('now'), datetime('now'), 'chat_001')"
|
||||
"INSERT INTO expressions (id, situation, style, content_list, count, last_active_time, create_time, session_id, checked, rejected) "
|
||||
"VALUES (1, '待审核', '风格', '[]', 0, datetime('now'), datetime('now'), 'chat_001', 0, 0)"
|
||||
)
|
||||
)
|
||||
test_session.commit()
|
||||
@@ -445,9 +447,8 @@ def test_get_review_stats(client: TestClient, mock_auth, test_session: Session):
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
# Verify all review counts are 0 (hardcoded in refactored code)
|
||||
assert data["total"] == 1 # Total expressions exists
|
||||
assert data["unchecked"] == 0
|
||||
assert data["unchecked"] == 1
|
||||
assert data["passed"] == 0
|
||||
assert data["rejected"] == 0
|
||||
assert data["ai_checked"] == 0
|
||||
@@ -455,14 +456,14 @@ def test_get_review_stats(client: TestClient, mock_auth, test_session: Session):
|
||||
|
||||
|
||||
def test_get_review_list_filter_unchecked(client: TestClient, mock_auth, sample_expression: Expression):
|
||||
"""Test GET /expression/review/list with filter_type=unchecked returns empty (legacy behavior)"""
|
||||
# filter_type=unchecked should return no results (legacy removed)
|
||||
"""Test GET /expression/review/list with filter_type=unchecked returns unchecked expressions"""
|
||||
response = client.get("/api/webui/expression/review/list?filter_type=unchecked")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["total"] == 0 # No results (legacy fields removed)
|
||||
assert data["total"] == 1
|
||||
assert len(data["data"]) == 1
|
||||
|
||||
|
||||
def test_get_review_list_filter_all(client: TestClient, mock_auth, sample_expression: Expression):
|
||||
@@ -476,8 +477,8 @@ def test_get_review_list_filter_all(client: TestClient, mock_auth, sample_expres
|
||||
assert len(data["data"]) == 1
|
||||
|
||||
|
||||
def test_batch_review_expressions_unsupported(client: TestClient, mock_auth, sample_expression: Expression):
|
||||
"""Test POST /expression/review/batch returns failure for require_unchecked=True"""
|
||||
def test_batch_review_expressions_with_unchecked_marker(client: TestClient, mock_auth, sample_expression: Expression):
|
||||
"""Test POST /expression/review/batch succeeds with require_unchecked=True"""
|
||||
review_payload = {"items": [{"id": sample_expression.id, "rejected": False, "require_unchecked": True}]}
|
||||
|
||||
response = client.post("/api/webui/expression/review/batch", json=review_payload)
|
||||
@@ -485,8 +486,34 @@ def test_batch_review_expressions_unsupported(client: TestClient, mock_auth, sam
|
||||
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["failed"] == 1 # Should fail because require_unchecked=True
|
||||
assert "不支持审核状态过滤" in data["results"][0]["message"]
|
||||
assert data["succeeded"] == 1
|
||||
assert data["results"][0]["success"] is True
|
||||
|
||||
|
||||
def test_batch_review_expressions_overwrites_ai_checked(
|
||||
client: TestClient, mock_auth, test_session: Session, sample_expression: Expression
|
||||
):
|
||||
"""Test POST /expression/review/batch lets manual review override AI checked state"""
|
||||
sample_expression.checked = True
|
||||
sample_expression.rejected = True
|
||||
sample_expression.modified_by = ModifiedBy.AI
|
||||
test_session.add(sample_expression)
|
||||
test_session.commit()
|
||||
|
||||
review_payload = {"items": [{"id": sample_expression.id, "rejected": False, "require_unchecked": True}]}
|
||||
|
||||
response = client.post("/api/webui/expression/review/batch", json=review_payload)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["succeeded"] == 1
|
||||
test_session.expire_all()
|
||||
reviewed_expression = test_session.exec(select(Expression).where(Expression.id == sample_expression.id)).first()
|
||||
assert reviewed_expression is not None
|
||||
assert reviewed_expression.checked is True
|
||||
assert reviewed_expression.rejected is False
|
||||
assert reviewed_expression.modified_by == ModifiedBy.USER
|
||||
|
||||
|
||||
def test_batch_review_expressions_no_unchecked_check(client: TestClient, mock_auth, sample_expression: Expression):
|
||||
|
||||
@@ -78,7 +78,7 @@ class ChatManager:
|
||||
"""初始化聊天管理器"""
|
||||
try:
|
||||
await self.load_all_sessions_from_db()
|
||||
logger.info(f"已加载 {len(self.sessions)} 个会话记录到内存中")
|
||||
logger.debug(f"已加载 {len(self.sessions)} 个会话记录到内存中")
|
||||
except Exception as e:
|
||||
logger.error(f"初始化聊天管理器出现错误: {e}")
|
||||
|
||||
|
||||
@@ -829,20 +829,19 @@ def initialize_logging(verbose: bool = True):
|
||||
reconfigure_existing_loggers()
|
||||
|
||||
# 启动日志清理任务
|
||||
start_log_cleanup_task(verbose=verbose)
|
||||
start_log_cleanup_task()
|
||||
|
||||
# 只在 verbose=True 时输出详细的初始化信息
|
||||
if verbose:
|
||||
logger = get_logger("logger")
|
||||
console_level = LOG_CONFIG.get("console_log_level", LOG_CONFIG.get("log_level", "INFO"))
|
||||
file_level = LOG_CONFIG.get("file_log_level", LOG_CONFIG.get("log_level", "INFO"))
|
||||
|
||||
logger.info("日志系统已初始化:")
|
||||
logger.info(f" - 控制台级别: {console_level}")
|
||||
logger.info(f" - 文件级别: {file_level}")
|
||||
max_log_files = max(1, int(LOG_CONFIG.get("max_log_files", 30) or 30))
|
||||
log_cleanup_days = max(1, int(LOG_CONFIG.get("log_cleanup_days", 30) or 30))
|
||||
logger.info(f" - 轮转份数: {max_log_files}个文件|自动清理: {log_cleanup_days}天前的日志")
|
||||
logger.info(
|
||||
f"日志系统已初始化:控制台={console_level},文件={file_level},"
|
||||
f"轮转={max_log_files}个文件,清理={log_cleanup_days}天前"
|
||||
)
|
||||
|
||||
|
||||
def cleanup_old_logs():
|
||||
@@ -875,12 +874,8 @@ def cleanup_old_logs():
|
||||
logger.error(f"清理旧日志文件时出错: {e}")
|
||||
|
||||
|
||||
def start_log_cleanup_task(verbose: bool = True):
|
||||
"""启动日志清理任务
|
||||
|
||||
Args:
|
||||
verbose: 是否输出启动信息。默认为 True。
|
||||
"""
|
||||
def start_log_cleanup_task():
|
||||
"""启动日志清理任务"""
|
||||
global _cleanup_task_started
|
||||
|
||||
# 防止重复启动清理任务
|
||||
@@ -897,12 +892,6 @@ def start_log_cleanup_task(verbose: bool = True):
|
||||
cleanup_thread = threading.Thread(target=cleanup_task, daemon=True)
|
||||
cleanup_thread.start()
|
||||
|
||||
if verbose:
|
||||
logger = get_logger("logger")
|
||||
max_log_files = max(1, int(LOG_CONFIG.get("max_log_files", 30) or 30))
|
||||
log_cleanup_days = max(1, int(LOG_CONFIG.get("log_cleanup_days", 30) or 30))
|
||||
logger.info(f"已启动日志清理任务,将自动清理{log_cleanup_days}天前的日志文件(轮转份数限制: {max_log_files}个文件)")
|
||||
|
||||
|
||||
def shutdown_logging():
|
||||
"""优雅关闭日志系统,释放所有文件句柄"""
|
||||
|
||||
@@ -40,6 +40,7 @@ class BotConfig(ConfigBase):
|
||||
"x-icon": "wifi",
|
||||
"x-layout": "inline-right",
|
||||
"x-input-width": "12rem",
|
||||
"x-row": "bot-platform-account",
|
||||
},
|
||||
)
|
||||
"""平台"""
|
||||
@@ -51,6 +52,7 @@ class BotConfig(ConfigBase):
|
||||
"x-icon": "user",
|
||||
"x-layout": "inline-right",
|
||||
"x-input-width": "12rem",
|
||||
"x-row": "bot-platform-account",
|
||||
},
|
||||
)
|
||||
"""QQ账号"""
|
||||
|
||||
@@ -134,7 +134,7 @@ def _setup_anti_crawler(app: FastAPI):
|
||||
"basic": t("startup.webui_anti_crawler_mode_basic"),
|
||||
}
|
||||
mode_desc = mode_descriptions.get(anti_crawler_mode, t("startup.webui_anti_crawler_mode_basic"))
|
||||
logger.info(t("startup.webui_anti_crawler_configured", mode_desc=mode_desc))
|
||||
logger.debug(t("startup.webui_anti_crawler_configured", mode_desc=mode_desc))
|
||||
except Exception as e:
|
||||
logger.error(t("startup.webui_anti_crawler_config_failed", error=e), exc_info=True)
|
||||
|
||||
@@ -159,7 +159,7 @@ def _register_api_routes(app: FastAPI):
|
||||
for router in get_all_routers():
|
||||
app.include_router(router)
|
||||
|
||||
logger.info(t("startup.webui_api_routes_registered"))
|
||||
logger.debug(t("startup.webui_api_routes_registered"))
|
||||
except Exception as e:
|
||||
logger.error(t("startup.webui_api_routes_register_failed", error=e), exc_info=True)
|
||||
|
||||
@@ -217,7 +217,7 @@ def _setup_static_files(app: FastAPI):
|
||||
response.headers["X-Robots-Tag"] = "noindex, nofollow, noarchive"
|
||||
return response
|
||||
|
||||
logger.info(t("startup.webui_static_files_configured", static_path=static_path))
|
||||
logger.debug(t("startup.webui_static_files_configured", static_path=static_path))
|
||||
|
||||
|
||||
def _resolve_static_path() -> Path | None:
|
||||
@@ -247,6 +247,5 @@ def show_access_token():
|
||||
token_manager = get_token_manager()
|
||||
current_token = token_manager.get_token()
|
||||
logger.info(t("startup.webui_access_token", token=current_token))
|
||||
logger.info(t("startup.webui_access_token_login_hint"))
|
||||
except Exception as e:
|
||||
logger.error(t("startup.webui_access_token_failed", error=e))
|
||||
|
||||
@@ -15,6 +15,7 @@ from src.common.logger import get_logger
|
||||
from src.webui.dependencies import require_auth
|
||||
|
||||
logger = get_logger("webui.expression")
|
||||
EXCLUDE_IDS_QUERY = Query(None, description="需要排除的表达方式 ID")
|
||||
|
||||
# 创建路由器
|
||||
router = APIRouter(prefix="/expression", tags=["Expression"], dependencies=[Depends(require_auth)])
|
||||
@@ -660,8 +661,10 @@ async def get_review_list(
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
|
||||
filter_type: str = Query("unchecked", description="筛选类型: unchecked/passed/rejected/all"),
|
||||
order: str = Query("latest", description="排序方式: latest/random"),
|
||||
search: Optional[str] = Query(None, description="搜索关键词"),
|
||||
chat_id: Optional[str] = Query(None, description="聊天ID筛选"),
|
||||
exclude_ids: Optional[List[int]] = EXCLUDE_IDS_QUERY,
|
||||
) -> ReviewListResponse:
|
||||
"""获取待审核或已审核的表达方式列表。
|
||||
|
||||
@@ -669,8 +672,10 @@ async def get_review_list(
|
||||
page: 页码。
|
||||
page_size: 每页数量。
|
||||
filter_type: 筛选类型,可选 unchecked、passed、rejected 或 all。
|
||||
order: 排序方式,可选 latest 或 random。
|
||||
search: 搜索关键词。
|
||||
chat_id: 聊天 ID 筛选条件。
|
||||
exclude_ids: 需要排除的表达方式 ID。
|
||||
|
||||
Returns:
|
||||
ReviewListResponse: 审核列表响应。
|
||||
@@ -689,11 +694,17 @@ async def get_review_list(
|
||||
if chat_id:
|
||||
statement = statement.where(col(Expression.session_id) == chat_id)
|
||||
|
||||
# 排序:创建时间倒序
|
||||
statement = statement.order_by(
|
||||
case((col(Expression.create_time).is_(None), 1), else_=0),
|
||||
col(Expression.create_time).desc(),
|
||||
)
|
||||
if exclude_ids:
|
||||
statement = statement.where(~col(Expression.id).in_(exclude_ids))
|
||||
|
||||
if order == "random":
|
||||
statement = statement.order_by(func.random())
|
||||
else:
|
||||
# 排序:创建时间倒序
|
||||
statement = statement.order_by(
|
||||
case((col(Expression.create_time).is_(None), 1), else_=0),
|
||||
col(Expression.create_time).desc(),
|
||||
)
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
statement = statement.offset(offset).limit(page_size)
|
||||
@@ -731,7 +742,7 @@ class BatchReviewItem(BaseModel):
|
||||
|
||||
id: int
|
||||
rejected: bool
|
||||
require_unchecked: bool = True # 默认要求未检查状态
|
||||
require_unchecked: bool = True # 前端保留的来源标记,人工审核提交时不再阻断覆盖
|
||||
|
||||
|
||||
class BatchReviewRequest(BaseModel):
|
||||
@@ -790,14 +801,6 @@ async def batch_review_expressions(
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
# 冲突检测:未审核列表发起的操作只允许处理仍处于未审核状态的条目。
|
||||
if item.require_unchecked and expression.checked:
|
||||
results.append(
|
||||
BatchReviewResultItem(id=item.id, success=False, message="该表达方式已被审核,请刷新列表后重试")
|
||||
)
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
# 更新状态
|
||||
with get_db_session() as session:
|
||||
db_expression = session.exec(
|
||||
|
||||
Reference in New Issue
Block a user