From ce674f3422b9fab4dbe411d49e60c853f1c91dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Mon, 29 Dec 2025 21:10:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=A1=A8=E8=BE=BE=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=E7=9A=84=E6=9C=80=E5=90=8E=E4=BF=AE=E6=94=B9=E6=9D=A5?= =?UTF-8?q?=E6=BA=90=E5=AD=97=E6=AE=B5=EF=BC=8C=E5=B9=B6=E5=9C=A8AI?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E5=92=8CLLM=E5=88=A4=E6=96=AD=E6=97=B6?= =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E6=A0=87=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bw_learner/expression_auto_check_task.py | 1 + src/bw_learner/reflect_tracker.py | 2 + src/common/database/database_model.py | 1 + src/plugin_system/__init__.py | 1 - src/webui/expression_routes.py | 267 +++++++++++++++++++ 5 files changed, 271 insertions(+), 1 deletion(-) diff --git a/src/bw_learner/expression_auto_check_task.py b/src/bw_learner/expression_auto_check_task.py index 4827faa8..5fa7cbdb 100644 --- a/src/bw_learner/expression_auto_check_task.py +++ b/src/bw_learner/expression_auto_check_task.py @@ -184,6 +184,7 @@ class ExpressionAutoCheckTask(AsyncTask): try: expression.checked = True expression.rejected = not suitable # 通过则rejected=0,不通过则rejected=1 + expression.modified_by = 'ai' # 标记为AI检查 expression.save() status = "通过" if suitable else "不通过" diff --git a/src/bw_learner/reflect_tracker.py b/src/bw_learner/reflect_tracker.py index aa012534..a905bf06 100644 --- a/src/bw_learner/reflect_tracker.py +++ b/src/bw_learner/reflect_tracker.py @@ -134,12 +134,14 @@ class ReflectTracker: if judgment == "Approve": self.expression.checked = True self.expression.rejected = False + self.expression.modified_by = 'ai' # 通过LLM判断也标记为ai self.expression.save() logger.info(f"Expression {self.expression.id} approved by operator.") return True elif judgment == "Reject": self.expression.checked = True + self.expression.modified_by = 'ai' # 通过LLM判断也标记为ai corrected_situation = json_obj.get("corrected_situation") corrected_style = json_obj.get("corrected_style") diff --git a/src/common/database/database_model.py b/src/common/database/database_model.py index 4c7dbc47..0615f012 100644 --- a/src/common/database/database_model.py +++ b/src/common/database/database_model.py @@ -328,6 +328,7 @@ class Expression(BaseModel): create_date = FloatField(null=True) # 创建日期,允许为空以兼容老数据 checked = BooleanField(default=False) # 是否已检查 rejected = BooleanField(default=False) # 是否被拒绝但未更新 + modified_by = TextField(null=True) # 最后修改来源:'ai' 或 'user',为空表示未检查 class Meta: table_name = "expression" diff --git a/src/plugin_system/__init__.py b/src/plugin_system/__init__.py index 7e92a60d..36f4ff5b 100644 --- a/src/plugin_system/__init__.py +++ b/src/plugin_system/__init__.py @@ -56,7 +56,6 @@ from .apis import ( person_api, plugin_manage_api, send_api, - auto_talk_api, register_plugin, get_logger, ) diff --git a/src/webui/expression_routes.py b/src/webui/expression_routes.py index 3a3ae2e6..f1befe2b 100644 --- a/src/webui/expression_routes.py +++ b/src/webui/expression_routes.py @@ -25,6 +25,7 @@ class ExpressionResponse(BaseModel): create_date: Optional[float] checked: bool rejected: bool + modified_by: Optional[str] = None # 'ai' 或 'user' 或 None class ExpressionListResponse(BaseModel): @@ -60,6 +61,7 @@ class ExpressionUpdateRequest(BaseModel): chat_id: Optional[str] = None checked: Optional[bool] = None rejected: Optional[bool] = None + require_unchecked: Optional[bool] = False # 用于人工审核时的冲突检测 class ExpressionUpdateResponse(BaseModel): @@ -104,6 +106,7 @@ def expression_to_response(expression: Expression) -> ExpressionResponse: create_date=expression.create_date, checked=expression.checked, rejected=expression.rejected, + modified_by=expression.modified_by, ) @@ -356,12 +359,26 @@ async def update_expression( if not expression: raise HTTPException(status_code=404, detail=f"未找到 ID 为 {expression_id} 的表达方式") + # 冲突检测:如果要求未检查状态,但已经被检查了 + if request.require_unchecked and expression.checked: + raise HTTPException( + status_code=409, + detail=f"此表达方式已被{'AI自动' if expression.modified_by == 'ai' else '人工'}检查,请刷新列表" + ) + # 只更新提供的字段 update_data = request.model_dump(exclude_unset=True) + + # 移除 require_unchecked,它不是数据库字段 + update_data.pop('require_unchecked', None) if not update_data: raise HTTPException(status_code=400, detail="未提供任何需要更新的字段") + # 如果更新了 checked 或 rejected,标记为用户修改 + if 'checked' in update_data or 'rejected' in update_data: + update_data['modified_by'] = 'user' + # 更新最后活跃时间 update_data["last_active_time"] = time.time() @@ -521,3 +538,253 @@ async def get_expression_stats( except Exception as e: logger.exception(f"获取统计数据失败: {e}") raise HTTPException(status_code=500, detail=f"获取统计数据失败: {str(e)}") from e + + +# ============ 审核相关接口 ============ + +class ReviewStatsResponse(BaseModel): + """审核统计响应""" + total: int + unchecked: int + passed: int + rejected: int + ai_checked: int + user_checked: int + + +@router.get("/review/stats", response_model=ReviewStatsResponse) +async def get_review_stats( + maibot_session: Optional[str] = Cookie(None), + authorization: Optional[str] = Header(None) +): + """ + 获取审核统计数据 + + Returns: + 审核统计数据 + """ + try: + verify_auth_token(maibot_session, authorization) + + total = Expression.select().count() + unchecked = Expression.select().where(Expression.checked == False).count() + passed = Expression.select().where( + (Expression.checked == True) & (Expression.rejected == False) + ).count() + rejected = Expression.select().where( + (Expression.checked == True) & (Expression.rejected == True) + ).count() + ai_checked = Expression.select().where(Expression.modified_by == 'ai').count() + user_checked = Expression.select().where(Expression.modified_by == 'user').count() + + return ReviewStatsResponse( + total=total, + unchecked=unchecked, + passed=passed, + rejected=rejected, + ai_checked=ai_checked, + user_checked=user_checked + ) + + except HTTPException: + raise + except Exception as e: + logger.exception(f"获取审核统计失败: {e}") + raise HTTPException(status_code=500, detail=f"获取审核统计失败: {str(e)}") from e + + +class ReviewListResponse(BaseModel): + """审核列表响应""" + success: bool + total: int + page: int + page_size: int + data: List[ExpressionResponse] + + +@router.get("/review/list", response_model=ReviewListResponse) +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"), + search: Optional[str] = Query(None, description="搜索关键词"), + chat_id: Optional[str] = Query(None, description="聊天ID筛选"), + maibot_session: Optional[str] = Cookie(None), + authorization: Optional[str] = Header(None), +): + """ + 获取待审核/已审核的表达方式列表 + + Args: + page: 页码 + page_size: 每页数量 + filter_type: 筛选类型 (unchecked/passed/rejected/all) + search: 搜索关键词 + chat_id: 聊天ID筛选 + + Returns: + 表达方式列表 + """ + try: + verify_auth_token(maibot_session, authorization) + + query = Expression.select() + + # 根据筛选类型过滤 + if filter_type == "unchecked": + query = query.where(Expression.checked == False) + elif filter_type == "passed": + query = query.where((Expression.checked == True) & (Expression.rejected == False)) + elif filter_type == "rejected": + query = query.where((Expression.checked == True) & (Expression.rejected == True)) + # all 不需要额外过滤 + + # 搜索过滤 + if search: + query = query.where( + (Expression.situation.contains(search)) | (Expression.style.contains(search)) + ) + + # 聊天ID过滤 + if chat_id: + query = query.where(Expression.chat_id == chat_id) + + # 排序:创建时间倒序 + from peewee import Case + query = query.order_by( + Case(None, [(Expression.create_date.is_null(), 1)], 0), + Expression.create_date.desc() + ) + + total = query.count() + offset = (page - 1) * page_size + expressions = query.offset(offset).limit(page_size) + + return ReviewListResponse( + success=True, + total=total, + page=page, + page_size=page_size, + data=[expression_to_response(expr) for expr in expressions] + ) + + except HTTPException: + raise + except Exception as e: + logger.exception(f"获取审核列表失败: {e}") + raise HTTPException(status_code=500, detail=f"获取审核列表失败: {str(e)}") from e + + +class BatchReviewItem(BaseModel): + """批量审核项""" + id: int + rejected: bool + require_unchecked: bool = True # 默认要求未检查状态 + + +class BatchReviewRequest(BaseModel): + """批量审核请求""" + items: List[BatchReviewItem] + + +class BatchReviewResultItem(BaseModel): + """批量审核结果项""" + id: int + success: bool + message: str + + +class BatchReviewResponse(BaseModel): + """批量审核响应""" + success: bool + total: int + succeeded: int + failed: int + results: List[BatchReviewResultItem] + + +@router.post("/review/batch", response_model=BatchReviewResponse) +async def batch_review_expressions( + request: BatchReviewRequest, + maibot_session: Optional[str] = Cookie(None), + authorization: Optional[str] = Header(None), +): + """ + 批量审核表达方式 + + Args: + request: 批量审核请求 + + Returns: + 批量审核结果 + """ + try: + verify_auth_token(maibot_session, authorization) + + if not request.items: + raise HTTPException(status_code=400, detail="未提供要审核的表达方式") + + results = [] + succeeded = 0 + failed = 0 + + for item in request.items: + try: + expression = Expression.get_or_none(Expression.id == item.id) + + if not expression: + results.append(BatchReviewResultItem( + id=item.id, + success=False, + message=f"未找到 ID 为 {item.id} 的表达方式" + )) + failed += 1 + continue + + # 冲突检测 + if item.require_unchecked and expression.checked: + results.append(BatchReviewResultItem( + id=item.id, + success=False, + message=f"已被{'AI自动' if expression.modified_by == 'ai' else '人工'}检查" + )) + failed += 1 + continue + + # 更新状态 + expression.checked = True + expression.rejected = item.rejected + expression.modified_by = 'user' + expression.last_active_time = time.time() + expression.save() + + results.append(BatchReviewResultItem( + id=item.id, + success=True, + message="通过" if not item.rejected else "拒绝" + )) + succeeded += 1 + + except Exception as e: + results.append(BatchReviewResultItem( + id=item.id, + success=False, + message=str(e) + )) + failed += 1 + + logger.info(f"批量审核完成: 成功 {succeeded}, 失败 {failed}") + + return BatchReviewResponse( + success=True, + total=len(request.items), + succeeded=succeeded, + failed=failed, + results=results + ) + + except HTTPException: + raise + except Exception as e: + logger.exception(f"批量审核失败: {e}") + raise HTTPException(status_code=500, detail=f"批量审核失败: {str(e)}") from e