From 16de25995528588edaa30048384662d084b19392 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 5 May 2026 18:34:20 +0800 Subject: [PATCH] =?UTF-8?q?perf=EF=BC=9A=E4=BC=98=E5=8C=96webui=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=B1=95=E7=A4=BA=EF=BC=8C=E4=BC=98=E5=8C=96log?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=EF=BC=8C=E4=BF=AE=E5=A4=8D=E8=A1=A8=E8=BE=BE?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/expression-reviewer.tsx | 11 +- dashboard/src/lib/expression-api.ts | 4 + dashboard/src/routes/config/bot.tsx | 71 +++++--- .../config/bot/hooks/complexFieldHooks.tsx | 161 +++++++++++++++--- .../src/routes/config/bot/hooks/index.ts | 1 + dashboard/src/routes/resource/emoji/index.tsx | 72 ++++---- locales/zh-CN/startup.json | 6 +- pytests/webui/test_expression_routes.py | 97 +++++++---- src/chat/message_receive/chat_manager.py | 2 +- src/common/logger.py | 25 +-- src/config/official_configs.py | 2 + src/webui/app.py | 7 +- src/webui/routers/expression.py | 31 ++-- 13 files changed, 326 insertions(+), 164 deletions(-) diff --git a/dashboard/src/components/expression-reviewer.tsx b/dashboard/src/components/expression-reviewer.tsx index 35b82819..bced38f1 100644 --- a/dashboard/src/components/expression-reviewer.tsx +++ b/dashboard/src/components/expression-reviewer.tsx @@ -80,6 +80,7 @@ export function ExpressionReviewer({ open, onOpenChange }: ExpressionReviewerPro // 快速审核模式状态 const [quickFilterType, setQuickFilterType] = useState<'unchecked' | 'passed' | 'rejected' | 'all'>('unchecked') const [quickExpressions, setQuickExpressions] = useState([]) + const quickExpressionsRef = useRef([]) 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(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) { diff --git a/dashboard/src/lib/expression-api.ts b/dashboard/src/lib/expression-api.ts index 8937f598..030d1614 100644 --- a/dashboard/src/lib/expression-api.ts +++ b/dashboard/src/lib/expression-api.ts @@ -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> { 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}`) diff --git a/dashboard/src/routes/config/bot.tsx b/dashboard/src/routes/config/bot.tsx index 03fdac29..53a26153 100644 --- a/dashboard/src/routes/config/bot.tsx +++ b/dashboard/src/routes/config/bot.tsx @@ -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() {

管理麦麦的核心功能和行为设置

{/* 按钮组 - 桌面端靠右 */} -
+
+ handleModeChange(v as 'visual' | 'source')} + className="w-full min-w-[13rem] sm:w-[14rem]" + > + + + + 可视化 + + + + 源代码 + + +
- - {/* 模式切换 - 单独一行 */} -
- handleModeChange(v as 'visual' | 'source')} className="w-full"> - - - - 可视化编辑 - - - - 源代码编辑 - - - -
{/* 重启提示 */} @@ -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 ( - {visibleTabGroups.map((tab) => ( - - {tab.label} - - ))} + {visibleTabGroups.map((tab) => { + const isExpandedOnlyTab = !DEFAULT_VISIBLE_TAB_IDS.has(tab.id) + return ( + + {tab.id === firstExpandedTabId && ( + + )} + + {tab.label} + + + ) + })} {hasCollapsibleTabs && ( + + + {rows.length === 0 ? ( +
+ 暂无其他平台账号。 +
+ ) : ( +
+ {rows.map((row, rowIndex) => ( +
+
+ + + updateRow(rowIndex, { platform: event.target.value }) + } + /> +
+
+ + + updateRow(rowIndex, { account: event.target.value }) + } + /> +
+
+ +
+
+ ))} +
+ )} + + ) +} + export const KeywordRulesHook = createListItemEditorHook({ addLabel: '添加关键词规则', helperText: '匹配命中后会用 reaction 内容作为额外上下文。keywords 至少填一条,或使用正则模式。', @@ -279,8 +398,8 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) => } return ( -
-
+
+

表达互通组

@@ -299,13 +418,13 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) => 暂无互通组,点击“添加互通组”开始配置。

) : ( -
+
{groups.map((group, groupIndex) => (
-
+
互通组 {groupIndex + 1} @@ -341,15 +460,16 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) => 这个互通组还没有成员。
) : ( -
+
{group.expression_groups.map((member, memberIndex) => (
-
- +
+ @@ -359,10 +479,10 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) => } />
-
- +
+ @@ -372,8 +492,8 @@ export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) => } />
-
- +
+
+ + + + {selectedIds.size > 0 && ( + <> + + + + )}
-
+ +
- -
- -
diff --git a/locales/zh-CN/startup.json b/locales/zh-CN/startup.json index cd7e3c99..38808c46 100644 --- a/locales/zh-CN/startup.json +++ b/locales/zh-CN/startup.json @@ -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}", diff --git a/pytests/webui/test_expression_routes.py b/pytests/webui/test_expression_routes.py index 3dcd9fba..45e476e1 100644 --- a/pytests/webui/test_expression_routes.py +++ b/pytests/webui/test_expression_routes.py @@ -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): diff --git a/src/chat/message_receive/chat_manager.py b/src/chat/message_receive/chat_manager.py index 48d89956..f5ab47a9 100644 --- a/src/chat/message_receive/chat_manager.py +++ b/src/chat/message_receive/chat_manager.py @@ -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}") diff --git a/src/common/logger.py b/src/common/logger.py index 507fa59b..e89fc0e7 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -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(): """优雅关闭日志系统,释放所有文件句柄""" diff --git a/src/config/official_configs.py b/src/config/official_configs.py index e29743e5..fb96da9a 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -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账号""" diff --git a/src/webui/app.py b/src/webui/app.py index 6a0d5cf2..6fcb8f40 100644 --- a/src/webui/app.py +++ b/src/webui/app.py @@ -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)) diff --git a/src/webui/routers/expression.py b/src/webui/routers/expression.py index 696a1953..7283c350 100644 --- a/src/webui/routers/expression.py +++ b/src/webui/routers/expression.py @@ -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(