fix: complete admin knowledge audit improvements
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
@@ -18,6 +19,8 @@ from app.models.knowledge import (
|
||||
)
|
||||
from app.schemas.knowledge import AttentionUpdateRequest
|
||||
from app.services.admin_service import OperationLogService
|
||||
from app.models.logs import LogRetentionPolicy
|
||||
from app.services.maintenance_service import MaintenanceService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -29,10 +32,12 @@ def retrieval_logs(
|
||||
knowledgeCalled: bool | None = Query(default=None),
|
||||
statusValue: str = Query(default="", alias="status"),
|
||||
attentionCreated: bool | None = Query(default=None),
|
||||
page: int = Query(default=1, ge=1),
|
||||
pageSize: int = Query(default=50, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
query = select(KnowledgeRetrievalLog).order_by(KnowledgeRetrievalLog.id.desc()).limit(300)
|
||||
query = select(KnowledgeRetrievalLog).order_by(KnowledgeRetrievalLog.id.desc())
|
||||
if sessionId is not None:
|
||||
query = query.where(KnowledgeRetrievalLog.session_id == sessionId)
|
||||
if knowledgeId is not None:
|
||||
@@ -43,9 +48,55 @@ def retrieval_logs(
|
||||
query = query.where(KnowledgeRetrievalLog.status == statusValue)
|
||||
if attentionCreated is not None:
|
||||
query = query.where(KnowledgeRetrievalLog.attention_created == (1 if attentionCreated else 0))
|
||||
query = query.offset((page - 1) * pageSize).limit(pageSize)
|
||||
return api_success([_retrieval_dict(item, detail=False) for item in db.scalars(query).all()])
|
||||
|
||||
|
||||
@router.post("/retrieval-log/cleanup/estimate")
|
||||
def estimate_cleanup(payload: dict, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
before = _cleanup_before(payload)
|
||||
count = db.scalar(select(func.count()).select_from(KnowledgeRetrievalLog).where(KnowledgeRetrievalLog.created_at < before)) or 0
|
||||
return api_success({"before": before, "estimatedCount": count})
|
||||
|
||||
|
||||
@router.post("/retrieval-log/cleanup")
|
||||
def cleanup_logs(payload: dict, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
before = _cleanup_before(payload)
|
||||
total = MaintenanceService.delete_retrieval_logs(db, before)
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="retrieval_log", action=f"cleanup:{total}", target_id=None)
|
||||
db.commit()
|
||||
return api_success({"deleted": total, "before": before})
|
||||
|
||||
|
||||
@router.get("/retrieval-log/retention")
|
||||
def get_retention(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
policy = db.scalar(select(LogRetentionPolicy).order_by(LogRetentionPolicy.id).limit(1))
|
||||
return api_success({"enabled": bool(policy and policy.enabled), "retentionDays": policy.retention_days if policy else None, "lastRunAt": policy.last_run_at if policy else None, "lastResult": policy.last_result if policy else None})
|
||||
|
||||
|
||||
@router.put("/retrieval-log/retention")
|
||||
def set_retention(payload: dict, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict:
|
||||
days = payload.get("retentionDays")
|
||||
if days is not None and (not isinstance(days, int) or days < 1):
|
||||
raise HTTPException(status_code=400, detail="保留天数必须为正整数,永久保留请传 null")
|
||||
policy = db.scalar(select(LogRetentionPolicy).order_by(LogRetentionPolicy.id).limit(1)) or LogRetentionPolicy()
|
||||
policy.retention_days, policy.enabled, policy.updated_by = days, 0 if days is None else 1, current_admin.id
|
||||
db.add(policy)
|
||||
OperationLogService.write(db, admin_id=current_admin.id, module="retrieval_log", action="retention_update", target_id=None)
|
||||
db.commit()
|
||||
return api_success({"enabled": bool(policy.enabled), "retentionDays": policy.retention_days})
|
||||
|
||||
|
||||
def _cleanup_before(payload: dict) -> datetime:
|
||||
raw = str(payload.get("before") or "").strip()
|
||||
if not raw:
|
||||
raise HTTPException(status_code=400, detail="必须指定清理日期")
|
||||
try:
|
||||
return datetime.fromisoformat(raw.replace("Z", "+00:00")).replace(tzinfo=None)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail="清理日期格式错误") from exc
|
||||
|
||||
|
||||
@router.get("/retrieval-log/{log_id}")
|
||||
def retrieval_detail(
|
||||
log_id: int,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
import json
|
||||
from pathlib import Path
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
@@ -11,6 +14,8 @@ from app.core.responses import api_success
|
||||
from app.models.admin import Admin
|
||||
from app.schemas.admin import DashboardStats
|
||||
from app.services.admin_service import AdminDashboardService
|
||||
from app.models.logs import StorageSnapshot
|
||||
from app.services.redis_client import get_sync_redis_client
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -30,3 +35,53 @@ def dashboard(
|
||||
end_dt = None
|
||||
stats = AdminDashboardService.stats(db, start_dt, end_dt)
|
||||
return api_success(DashboardStats.model_validate(stats).model_dump())
|
||||
|
||||
|
||||
@router.get("/dashboard/storage")
|
||||
def storage_stats(
|
||||
refresh: bool = Query(default=False),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
latest = db.scalar(select(StorageSnapshot).where(StorageSnapshot.scope == "project").order_by(StorageSnapshot.id.desc()).limit(1))
|
||||
if latest is not None and not refresh:
|
||||
return api_success(_snapshot_dict(latest, db))
|
||||
detail: dict = {"database": {"status": "unavailable", "bytes": None, "tables": []}, "redis": {"status": "unavailable", "bytes": None}, "files": {"status": "unavailable", "bytes": None}}
|
||||
errors: list[str] = []
|
||||
try:
|
||||
if db.bind and db.bind.dialect.name == "mysql":
|
||||
rows = db.execute(text("SELECT table_name, data_length + index_length AS bytes FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY bytes DESC")).all()
|
||||
tables = [{"name": row[0], "bytes": int(row[1] or 0)} for row in rows]
|
||||
detail["database"] = {"status": "ok", "bytes": sum(x["bytes"] for x in tables), "tables": tables}
|
||||
else:
|
||||
detail["database"] = {"status": "unavailable", "bytes": None, "tables": [], "message": "当前数据库不支持精确表占用统计"}
|
||||
except Exception as exc:
|
||||
errors.append(f"database: {exc}")
|
||||
try:
|
||||
redis = get_sync_redis_client()
|
||||
if redis is not None:
|
||||
info = redis.info("memory")
|
||||
detail["redis"] = {"status": "ok", "bytes": int(info.get("used_memory", 0))}
|
||||
except Exception as exc:
|
||||
errors.append(f"redis: {exc}")
|
||||
try:
|
||||
root = Path(".uploads")
|
||||
if root.exists():
|
||||
detail["files"] = {"status": "ok", "bytes": sum(p.stat().st_size for p in root.rglob("*") if p.is_file())}
|
||||
else:
|
||||
detail["files"] = {"status": "unavailable", "bytes": None, "message": "项目未配置本地附件目录"}
|
||||
except Exception as exc:
|
||||
errors.append(f"files: {exc}")
|
||||
known = [x.get("bytes") for x in detail.values() if x.get("status") == "ok"]
|
||||
snapshot = StorageSnapshot(scope="project", total_bytes=sum(known) if known else None, detail_json=json.dumps(detail, ensure_ascii=False), status="partial" if errors or any(x.get("status") != "ok" for x in detail.values()) else "ok", error_message="; ".join(errors) or None)
|
||||
db.add(snapshot); db.commit(); db.refresh(snapshot)
|
||||
return api_success(_snapshot_dict(snapshot, db))
|
||||
|
||||
|
||||
def _snapshot_dict(item: StorageSnapshot, db: Session) -> dict:
|
||||
with_growth = {"totalBytes": item.total_bytes, "detail": json.loads(item.detail_json), "status": item.status, "errorMessage": item.error_message, "createdAt": item.created_at}
|
||||
# Growth is intentionally unknown until enough snapshots exist; never report a misleading zero.
|
||||
for days, key in ((7, "growth7DaysBytes"), (30, "growth30DaysBytes")):
|
||||
previous = db.scalar(select(StorageSnapshot).where(StorageSnapshot.scope == item.scope, StorageSnapshot.created_at <= item.created_at - timedelta(days=days)).order_by(StorageSnapshot.created_at.desc()).limit(1))
|
||||
with_growth[key] = item.total_bytes - previous.total_bytes if previous and item.total_bytes is not None and previous.total_bytes is not None else None
|
||||
return with_growth
|
||||
|
||||
@@ -36,6 +36,31 @@ def list_knowledge(db: Session = Depends(get_db), current_admin: Admin = Depends
|
||||
return api_success([_knowledge_dict(item) for item in items])
|
||||
|
||||
|
||||
@router.post("/knowledge/resolve-node")
|
||||
def resolve_knowledge_node(
|
||||
payload: dict,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
_require_super_admin(current_admin)
|
||||
node_id = str(payload.get("nodeId") or "").strip()
|
||||
if not node_id or len(node_id) > 100 or not node_id.replace("_", "").replace("-", "").isalnum():
|
||||
raise HTTPException(status_code=400, detail="Node ID 格式错误")
|
||||
existing = db.scalar(select(Knowledge).where(Knowledge.feishu_node_id == node_id))
|
||||
if existing is not None:
|
||||
raise HTTPException(status_code=409, detail=f"该 Node ID 已添加(知识库:{existing.name})")
|
||||
try:
|
||||
info = FeishuKnowledgeService.get_node_info(node_id, db)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=f"飞书节点信息获取失败:{exc}") from exc
|
||||
space_id, title = str(info.get("space_id") or "").strip(), str(info.get("title") or "").strip()
|
||||
if not space_id:
|
||||
raise HTTPException(status_code=422, detail="飞书接口未返回 Space ID,请检查节点是否移动或当前应用权限")
|
||||
if not title:
|
||||
raise HTTPException(status_code=422, detail="飞书接口未返回文章标题,请检查文章是否已删除")
|
||||
return api_success({"nodeId": node_id, "spaceId": space_id, "sourceTitle": title, "name": title, "remark": str(info.get("description") or ""), "objType": info.get("obj_type")})
|
||||
|
||||
|
||||
@router.post("/knowledge/import")
|
||||
def import_knowledge_from_space(
|
||||
payload: dict,
|
||||
@@ -91,12 +116,7 @@ def create_knowledge(
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
_require_super_admin(current_admin)
|
||||
existing = db.scalar(
|
||||
select(Knowledge).where(
|
||||
Knowledge.feishu_space_id == payload.feishuSpaceId,
|
||||
Knowledge.feishu_node_id == payload.feishuNodeId,
|
||||
)
|
||||
)
|
||||
existing = db.scalar(select(Knowledge).where(Knowledge.feishu_node_id == payload.feishuNodeId))
|
||||
if existing is not None:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="该飞书文章已创建知识库")
|
||||
knowledge = Knowledge(
|
||||
|
||||
Reference in New Issue
Block a user