import sys import os import logging import httpx import urllib.parse import asyncio from datetime import datetime, timezone from typing import Optional, List, Any, Union from fastapi import APIRouter, Depends, HTTPException, Request, Response, BackgroundTasks from fastapi.responses import HTMLResponse from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from app.sample.schemas.sample_schemas import BatchEnterRequest, BatchDeleteRequest, ConvertRequest, DocumentAdd, UploadUrlRequest from app.services.sample_service import SampleService from app.services.jwt_token import verify_token from app.schemas.base import ApiResponse from app.base import get_mineru_manager from app.services.task_service import task_service # 获取logger logger = logging.getLogger(__name__) router = APIRouter(prefix="/sample", tags=["样本中心"]) security = HTTPBearer() security_optional = HTTPBearer(auto_error=False) @router.get("/tasks") async def get_tasks(type: str, credentials: HTTPAuthorizationCredentials = Depends(security)): """获取任务列表""" try: payload = verify_token(credentials.credentials) if not payload: return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() tasks = await task_service.get_task_list(type) return ApiResponse(code=0, message="成功", data=tasks, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() except Exception as e: logger.exception("获取任务列表失败") return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump() # --- 文档管理中心 API --- @router.post("/documents/upload-url") async def get_upload_url(req: UploadUrlRequest, credentials: HTTPAuthorizationCredentials = Depends(security)): """获取 MinIO 预签名上传 URL""" try: payload = verify_token(credentials.credentials) if not payload: return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() sample_service = SampleService() success, message, data = await sample_service.get_upload_url(req.filename, req.content_type, prefix=req.prefix) if success: return ApiResponse(code=0, message=message, data=data, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() else: return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() except Exception as e: logger.exception("获取上传链接失败") return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.get("/documents/proxy-view") async def proxy_view(url: str, token: Optional[str] = None, credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_optional)): """抓取外部文档内容并返回,支持 HTML 和 PDF 等二进制文件。支持从 Header 或 Query 参数获取 Token。""" try: # 确保 URL 已解码 url = urllib.parse.unquote(url) # 优先从 Header 获取,如果没有则从参数获取 actual_token = None if credentials: actual_token = credentials.credentials elif token: actual_token = token if not actual_token: return ApiResponse(code=401, message="未提供认证令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() payload = verify_token(actual_token) if not payload or not payload.get("is_superuser"): return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() # 增加超时时间,支持大文件下载 async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } response = await client.get(url, headers=headers) response.raise_for_status() content_type = response.headers.get("content-type", "").lower() # 如果是 PDF 或其他二进制文件 binary_extensions = { ".pdf": "application/pdf", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".doc": "application/msword", ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".xls": "application/vnd.ms-excel", ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".ppt": "application/vnd.ms-powerpoint", ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", ".zip": "application/zip", ".rar": "application/x-rar-compressed", ".7z": "application/x-7z-compressed" } is_binary = "application/pdf" in content_type or \ "application/vnd." in content_type or \ "application/msword" in content_type or \ "application/octet-stream" in content_type or \ any(ext in url.lower() for ext in binary_extensions.keys()) if is_binary: # 尝试根据扩展名修正 media_type for ext, m_type in binary_extensions.items(): if ext in url.lower(): content_type = m_type break return Response( content=response.content, media_type=content_type, headers={"Content-Disposition": "inline"} ) # 默认处理为 HTML try: # 尝试多种编码解码 content data = response.content content = None encodings = ['utf-8', 'gbk', 'utf-8-sig', 'gb18030'] for enc in encodings: try: content = data.decode(enc) break except UnicodeDecodeError: continue if content is None: content = data.decode('utf-8', errors='ignore') # 简单的注入一些基础样式,确保内容在 iframe 中显示良好 base_style = """ """ if "" in content: content = content.replace("", f"{base_style}") else: content = f"{base_style}{content}" return HTMLResponse(content=content) except Exception: # 如果文本解析失败,返回原始字节 return Response(content=response.content, media_type=content_type) except Exception as e: error_msg = f"
错误原因: {str(e)}
URL: {url}
" return HTMLResponse(content=error_msg, status_code=500) @router.get("/documents/download") async def download_document(url: str, filename: Optional[str] = None, token: Optional[str] = None, credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_optional)): """代理下载云端文件,支持从 MinIO 等外部地址下载""" try: if not url: return ApiResponse(code=400, message="缺少URL参数", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() # 确保 URL 已解码 url = urllib.parse.unquote(url) # 优先从 Header 获取,如果没有则从参数获取 actual_token = None if credentials: actual_token = credentials.credentials elif token: actual_token = token if not actual_token: return ApiResponse(code=401, message="未提供认证令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() payload = verify_token(actual_token) if not payload or not payload.get("is_superuser"): return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() # 增加超时时间,支持大文件下载 async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } response = await client.get(url, headers=headers) response.raise_for_status() content_type = response.headers.get("content-type", "application/octet-stream") # 设置下载文件名 headers = { "Content-Disposition": f"attachment; filename*=UTF-8''{urllib.parse.quote(filename or 'downloaded_file')}" if filename else "attachment", "Content-Type": content_type } return Response( content=response.content, media_type=content_type, headers=headers ) except Exception as e: logger.exception(f"文件下载失败, url={url}") return ApiResponse(code=500, message=f"下载失败: {str(e)} (URL: {url})", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.post("/documents/batch-enter") async def batch_enter_knowledge_base(req: BatchEnterRequest, credentials: HTTPAuthorizationCredentials = Depends(security)): """批量将文档加入知识库""" try: payload = verify_token(credentials.credentials) if not payload or not payload.get("is_superuser"): return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() username = payload.get("sub") if not username: return ApiResponse(code=401, message="令牌中缺少用户信息", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() sample_service = SampleService() success_count, message = await sample_service.batch_enter_knowledge_base( req.ids, username, kb_method=req.kb_method, chunk_size=req.chunk_size, separator=req.separator ) # 如果全部失败,返回非零状态码,触发前端错误提示 code = 0 if success_count > 0 else 1 return ApiResponse(code=code, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() except Exception as e: logger.exception("批量操作失败") return ApiResponse(code=500, message=f"批量操作失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.post("/documents/batch-delete") async def batch_delete_documents(req: BatchDeleteRequest, credentials: HTTPAuthorizationCredentials = Depends(security)): """批量删除文档""" try: payload = verify_token(credentials.credentials) if not payload or not payload.get("is_superuser"): return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() sample_service = SampleService() affected_rows, message = await sample_service.batch_delete_documents(req.ids) return ApiResponse( code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat() ).model_dump() except Exception as e: logger.exception("批量删除失败") return ApiResponse(code=500, message=f"批量删除失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.post("/documents/batch-add-to-task") async def batch_add_to_task(req: BatchDeleteRequest, credentials: HTTPAuthorizationCredentials = Depends(security)): """批量加入任务中心 (设置 whether_to_task = 1)""" try: payload = verify_token(credentials.credentials) if not payload or not payload.get("is_superuser"): return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() user_id = payload.get("sub") if not user_id: return ApiResponse(code=401, message="令牌中缺少用户信息", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() username = payload.get("username", user_id) sample_service = SampleService() success, message = await sample_service.batch_add_to_task(req.ids, username) return ApiResponse( code=0 if success else 500, message=message, timestamp=datetime.now(timezone.utc).isoformat() ).model_dump() except Exception as e: logger.exception("批量加入任务失败") return ApiResponse(code=500, message=f"批量加入任务失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.post("/documents/convert") async def convert_document(req: ConvertRequest, background_tasks: BackgroundTasks, credentials: HTTPAuthorizationCredentials = Depends(security)): """启动文档转换 (使用 MinerUManager 在后台执行)""" try: payload = verify_token(credentials.credentials) if not payload or not payload.get("is_superuser"): return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() doc_id = str(req.id) sample_service = SampleService() # 1. 获取文档详情以取得 title 和 file_url doc = await sample_service.get_document_detail(doc_id) if not doc: return ApiResponse(code=404, message="文档不存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() title = doc.get("title") file_url = doc.get("file_url") table_type = doc.get("source_type") # 获取业务模块前缀 status = doc.get("conversion_status") # 2. 检查当前状态,避免重复请求 if status == 1: return ApiResponse(code=0, message="文档正在转换中,请勿重复操作", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() if status == 2: return ApiResponse(code=0, message="文档已转换完成", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() if not file_url: return ApiResponse(code=400, message="文档缺少文件链接,无法转换", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() # 3. 立即将状态更新为“转换中”,避免前端轮询延迟 await sample_service.update_conversion_status(doc_id, status=1) # 4. 启动后台任务 manager = get_mineru_manager() background_tasks.add_task(manager.process_document, doc_id, title, file_url, table_type) return ApiResponse( code=0, message="转换任务已在后台启动", timestamp=datetime.now(timezone.utc).isoformat() ).model_dump() except Exception as e: logger.exception("启动转换失败") return ApiResponse(code=500, message=f"启动转换失败: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.post("/documents/add") async def add_document(doc: DocumentAdd, credentials: HTTPAuthorizationCredentials = Depends(security)): """添加新文档 (同步主表和子表)""" try: payload = verify_token(credentials.credentials) if not payload: return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() user_id = payload.get("sub") if not user_id: return ApiResponse(code=401, message="令牌中缺少用户信息", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() sample_service = SampleService() # 将 DocumentAdd 对象转换为字典,包含所有字段 doc_data = doc.model_dump() success, message, doc_id = await sample_service.add_document(doc_data, user_id) if success: return ApiResponse(code=0, message=message, data={"id": doc_id}, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() else: return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() except Exception as e: logger.exception("添加文档失败") return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.get("/documents/detail/{doc_id}") async def get_document_detail(doc_id: str, credentials: HTTPAuthorizationCredentials = Depends(security)): """获取文档详情 (关联查询子表)""" try: payload = verify_token(credentials.credentials) if not payload: return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() sample_service = SampleService() doc = await sample_service.get_document_detail(doc_id) if not doc: return ApiResponse(code=404, message="文档不存在", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() return ApiResponse(code=0, message="获取详情成功", data=doc, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() except Exception as e: logger.exception("获取文档详情失败") return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.get("/documents/list") async def get_document_list( whether_to_enter: Optional[int] = None, conversion_status: Optional[int] = None, keyword: Optional[str] = None, table_type: Optional[str] = None, plan_category: Optional[str] = None, level_2_classification: Optional[str] = None, level_3_classification: Optional[str] = None, level_4_classification: Optional[str] = None, page: int = 1, size: int = 50, credentials: HTTPAuthorizationCredentials = Depends(security) ): """获取文档列表 (从主表查询)""" try: payload = verify_token(credentials.credentials) if not payload: return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() sample_service = SampleService() items, total, all_total, total_entered = await sample_service.get_document_list( whether_to_enter=whether_to_enter, conversion_status=conversion_status, keyword=keyword, table_type=table_type, plan_category=plan_category, level_2_classification=level_2_classification, level_3_classification=level_3_classification, level_4_classification=level_4_classification, page=page, size=size ) return ApiResponse( code=0, message="查询成功", data={ "items": items, "total": total, "page": page, "size": size, "all_total": all_total, "total_entered": total_entered }, timestamp=datetime.now(timezone.utc).isoformat() ).model_dump() except Exception as e: logger.exception("获取文档列表失败") return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.post("/documents/edit") async def edit_document(doc: DocumentAdd, credentials: HTTPAuthorizationCredentials = Depends(security)): """编辑文档 (同步主表和子表)""" try: payload = verify_token(credentials.credentials) if not payload: return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() if not doc.id: return ApiResponse(code=400, message="缺少ID参数", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() # 调用 service 层 sample_service = SampleService() # 获取更新人ID updater_id = payload.get("sub", "admin") # 将 DocumentAdd 对象转换为字典,包含所有字段 doc_data = doc.model_dump() success, message = await sample_service.edit_document(doc_data, updater_id) if success: return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() else: return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() except Exception as e: logger.exception("编辑文档失败") return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.post("/documents/enter") async def enter_document(data: dict, credentials: HTTPAuthorizationCredentials = Depends(security)): """文档入库""" try: doc_id = data.get("id") if not doc_id: return ApiResponse(code=400, message="缺少ID", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() payload = verify_token(credentials.credentials) username = payload.get("sub", "admin") if payload else "admin" # 调用 service 层 sample_service = SampleService() success, message = await sample_service.enter_document(doc_id, username) if success: return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() else: return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() except Exception as e: logger.exception("入库失败") return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.get("/basic-info/list") async def get_basic_info_list( type: str, page: int = 1, size: int = 50, keyword: Optional[str] = None, title: Optional[str] = None, standard_no: Optional[str] = None, document_type: Optional[str] = None, professional_field: Optional[str] = None, validity: Optional[str] = None, issuing_authority: Optional[str] = None, release_date_start: Optional[str] = None, release_date_end: Optional[str] = None, plan_category: Optional[str] = None, level_1_classification: Optional[str] = None, level_2_classification: Optional[str] = None, level_3_classification: Optional[str] = None, level_4_classification: Optional[str] = None, credentials: HTTPAuthorizationCredentials = Depends(security) ): """获取基本信息列表 (支持多条件检索)""" try: payload = verify_token(credentials.credentials) if not payload: return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() sample_service = SampleService() # 构建过滤条件 filters = {} if title: filters['title'] = title if standard_no: filters['standard_no'] = standard_no if document_type: filters['document_type'] = document_type if professional_field: filters['professional_field'] = professional_field if validity: filters['validity'] = validity if issuing_authority: filters['issuing_authority'] = issuing_authority if release_date_start: filters['release_date_start'] = release_date_start if release_date_end: filters['release_date_end'] = release_date_end if plan_category: filters['plan_category'] = plan_category if level_1_classification: filters['level_1_classification'] = level_1_classification if level_2_classification: filters['level_2_classification'] = level_2_classification if level_3_classification: filters['level_3_classification'] = level_3_classification if level_4_classification: filters['level_4_classification'] = level_4_classification items, total = await sample_service.get_basic_info_list( type=type, page=page, size=size, keyword=keyword, **filters ) return ApiResponse( code=0, message="查询成功", data={"items": items, "total": total, "page": page, "size": size}, timestamp=datetime.now(timezone.utc).isoformat() ).model_dump() except Exception as e: logger.exception("查询基本信息失败") return ApiResponse(code=500, message=f"服务器内部错误: {str(e)}", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.post("/basic-info/add") async def add_basic_info(type: str, data: dict, credentials: HTTPAuthorizationCredentials = Depends(security)): """新增基本信息""" try: payload = verify_token(credentials.credentials) if not payload: return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() user_id = payload.get("sub") if not user_id: return ApiResponse(code=401, message="令牌中缺少用户信息", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() sample_service = SampleService() success, message, doc_id = await sample_service.add_basic_info(type, data, user_id) if success: return ApiResponse(code=0, message=message, data={"id": doc_id}, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() else: return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() except Exception as e: logger.exception("新增基本信息失败") return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.post("/basic-info/edit") async def edit_basic_info(type: str, id: str, data: dict, credentials: HTTPAuthorizationCredentials = Depends(security)): """编辑基本信息""" try: payload = verify_token(credentials.credentials) if not payload: return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() user_id = payload.get("sub") if not user_id: return ApiResponse(code=401, message="令牌中缺少用户信息", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() sample_service = SampleService() success, message = await sample_service.edit_basic_info(type, id, data, user_id) if success: return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() else: return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() except Exception as e: logger.exception("编辑基本信息失败") return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.post("/basic-info/delete") async def delete_basic_info(type: str, id: str, credentials: HTTPAuthorizationCredentials = Depends(security)): """删除基本信息""" try: payload = verify_token(credentials.credentials) if not payload: return ApiResponse(code=401, message="无效的访问令牌", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() sample_service = SampleService() success, message = await sample_service.delete_basic_info(type, id) if success: return ApiResponse(code=0, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() else: return ApiResponse(code=500, message=message, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() except Exception as e: logger.exception("删除基本信息失败") return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.get("/documents/categories/primary") async def get_primary_categories(credentials: HTTPAuthorizationCredentials = Depends(security)): """获取所有一级分类(仅保留指定的分类)""" try: payload = verify_token(credentials.credentials) if not payload or not payload.get("is_superuser"): return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() # 仅保留用户要求的分类 default_categories = ["办公制度", "行业标准", "法律法规", "施工方案", "施工图片"] categories = [{"id": name, "name": name} for name in default_categories] return ApiResponse(code=0, message="获取成功", data=categories, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() except Exception as e: return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.get("/documents/categories/secondary") async def get_secondary_categories(primaryId: str, credentials: HTTPAuthorizationCredentials = Depends(security)): """根据一级分类获取二级分类(仅保留指定的分类)""" try: payload = verify_token(credentials.credentials) if not payload or not payload.get("is_superuser"): return ApiResponse(code=403, message="权限不足", timestamp=datetime.now(timezone.utc).isoformat()).model_dump() # 针对“办公制度”的预设二级分类,其他分类暂时没有二级分类 categories = [] if primaryId == "办公制度": secondary_names = ["采购", "报销", "审批"] categories = [{"id": name, "name": name} for name in secondary_names] return ApiResponse(code=0, message="获取成功", data=categories, timestamp=datetime.now(timezone.utc).isoformat()).model_dump() except Exception as e: return ApiResponse(code=500, message=str(e), timestamp=datetime.now(timezone.utc).isoformat()).model_dump() @router.get("/documents/search") async def search_documents( keyword: str, whether_to_enter: Optional[int] = None, table_type: Optional[str] = "standard", page: int = 1, size: int = 50, credentials: HTTPAuthorizationCredentials = Depends(security) ): """关键词搜索文档,统一调用 get_document_list 以支持组合过滤""" return await get_document_list( whether_to_enter=whether_to_enter, keyword=keyword, table_type=table_type, page=page, size=size, credentials=credentials )