- #549 平台MCP列举: 注入全量已启用MCP清单, AI不再凭空编造 - #550 微信Bot技能自动发现上限 15->300 - #551 知识库Word文档同步资产后Word栏可见 + 双向删除同步(assets/knowledge级联软删) - #552 对话反馈实际使用的技能/MCP(used_mcp字段), 含历史消息序列化 - 同步容器内其他未提交live改动(im_bot_platform/TokenDashboard/WechatBot/ChatPage)
393 lines
11 KiB
Python
393 lines
11 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import and_, desc, asc
|
|
from pydantic import BaseModel, Field
|
|
from typing import Optional
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from ..database import get_db
|
|
from ..models import (
|
|
User, Asset, AssetCategory, NovelChapter, KnowledgeDocument,
|
|
KnowledgeBase,
|
|
gen_uuid
|
|
)
|
|
from ..auth import get_current_user
|
|
from ..middleware.log_service import LogService
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class CreateAssetRequest(BaseModel):
|
|
name: str = Field(min_length=1, max_length=200)
|
|
category: Optional[str] = None
|
|
description: Optional[str] = None
|
|
file_type: Optional[str] = None
|
|
content: Optional[str] = None
|
|
|
|
|
|
class UpdateAssetRequest(BaseModel):
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
file_type: Optional[str] = None
|
|
|
|
|
|
class CreateChapterRequest(BaseModel):
|
|
title: str = Field(min_length=1, max_length=500)
|
|
content: Optional[str] = None
|
|
sort_order: int = Field(default=0)
|
|
|
|
|
|
VALID_ASSET_CATEGORIES = ("file", "novel", "image", "video", "audio", "custom")
|
|
|
|
|
|
# ============================================================
|
|
# 静态路由 - 必须在动态路由之前定义
|
|
# ============================================================
|
|
|
|
@router.get("/storage-overview")
|
|
def get_storage_overview(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""获取用户资产存储概览,包含资产和知识库文档的总存储量"""
|
|
# Asset表中的文件大小
|
|
from sqlalchemy import func as sa_func
|
|
asset_size = db.query(sa_func.sum(Asset.size)).filter(
|
|
and_(
|
|
Asset.user_id == current_user.id,
|
|
Asset.status != "deleted",
|
|
)
|
|
).scalar() or 0
|
|
|
|
# 知识库文档的文件大小
|
|
kb_doc_size = db.query(sa_func.sum(KnowledgeDocument.file_size)).join(
|
|
KnowledgeBase, KnowledgeDocument.knowledge_base_id == KnowledgeBase.id
|
|
).filter(
|
|
KnowledgeBase.user_id == current_user.id
|
|
).scalar() or 0
|
|
|
|
total_storage = asset_size + kb_doc_size
|
|
|
|
return {
|
|
"code": 0,
|
|
"data": {
|
|
"asset_storage": asset_size,
|
|
"knowledge_doc_storage": kb_doc_size,
|
|
"total_storage": total_storage,
|
|
},
|
|
"message": "ok",
|
|
}
|
|
|
|
|
|
@router.get("/categories")
|
|
def list_categories(
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
categories = (
|
|
db.query(AssetCategory)
|
|
.filter(AssetCategory.user_id == current_user.id)
|
|
.order_by(asc(AssetCategory.name))
|
|
.all()
|
|
)
|
|
|
|
result = []
|
|
for c in categories:
|
|
count = db.query(Asset).filter(
|
|
and_(
|
|
Asset.category == c.name,
|
|
Asset.user_id == current_user.id,
|
|
Asset.status != "deleted",
|
|
)
|
|
).count()
|
|
|
|
result.append({
|
|
"id": c.id,
|
|
"name": c.name,
|
|
"type": c.type,
|
|
"asset_count": count,
|
|
"created_at": str(c.created_at) if c.created_at else None,
|
|
})
|
|
|
|
return {"code": 0, "data": result, "message": "ok"}
|
|
|
|
|
|
@router.get("/novels/{asset_id}/chapters")
|
|
def get_novel_chapters(
|
|
asset_id: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
asset = db.query(Asset).filter(
|
|
and_(Asset.id == asset_id, Asset.user_id == current_user.id)
|
|
).first()
|
|
if not asset:
|
|
raise HTTPException(status_code=404, detail="资产不存在")
|
|
|
|
chapters = (
|
|
db.query(NovelChapter)
|
|
.filter(NovelChapter.asset_id == asset_id)
|
|
.order_by(asc(NovelChapter.sort_order), asc(NovelChapter.created_at))
|
|
.all()
|
|
)
|
|
|
|
result = []
|
|
for ch in chapters:
|
|
result.append({
|
|
"id": ch.id,
|
|
"asset_id": ch.asset_id,
|
|
"title": ch.title,
|
|
"sort_order": ch.sort_order,
|
|
"word_count": ch.word_count or 0,
|
|
"created_at": str(ch.created_at) if ch.created_at else None,
|
|
"updated_at": str(ch.updated_at) if ch.updated_at else None,
|
|
})
|
|
|
|
return {"code": 0, "data": result, "message": "ok"}
|
|
|
|
|
|
@router.post("/novels/{asset_id}/chapters")
|
|
def add_novel_chapter(
|
|
asset_id: str,
|
|
req: CreateChapterRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
asset = db.query(Asset).filter(
|
|
and_(Asset.id == asset_id, Asset.user_id == current_user.id)
|
|
).first()
|
|
if not asset:
|
|
raise HTTPException(status_code=404, detail="资产不存在")
|
|
|
|
chapter = NovelChapter(
|
|
id=gen_uuid(),
|
|
asset_id=asset_id,
|
|
title=req.title,
|
|
content=req.content,
|
|
sort_order=req.sort_order,
|
|
word_count=len(req.content or ""),
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now(),
|
|
)
|
|
db.add(chapter)
|
|
|
|
LogService.log_operation(
|
|
user_id=current_user.id, username=current_user.username, action="add_chapter",
|
|
target="novel_chapter", target_id=chapter.id,
|
|
detail=f"为资产 {asset.name} 添加章节 {chapter.title}",
|
|
)
|
|
|
|
db.commit()
|
|
db.refresh(chapter)
|
|
|
|
return {
|
|
"code": 0,
|
|
"data": {
|
|
"id": chapter.id, "asset_id": chapter.asset_id, "title": chapter.title,
|
|
"sort_order": chapter.sort_order, "word_count": chapter.word_count,
|
|
"created_at": str(chapter.created_at),
|
|
},
|
|
"message": "章节添加成功",
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# 动态路由 - {asset_id} 必须放在静态路由之后
|
|
# ============================================================
|
|
|
|
@router.get("/")
|
|
def list_assets(
|
|
category: Optional[str] = Query(None),
|
|
search: Optional[str] = Query(None),
|
|
page: int = Query(default=1, ge=1),
|
|
page_size: int = Query(default=20, ge=1, le=100),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
query = db.query(Asset).filter(
|
|
and_(
|
|
Asset.user_id == current_user.id,
|
|
Asset.status != "deleted",
|
|
)
|
|
)
|
|
|
|
if category:
|
|
query = query.filter(Asset.category == category)
|
|
if search:
|
|
query = query.filter(Asset.name.ilike(f"%{search}%"))
|
|
|
|
total = query.count()
|
|
assets = (
|
|
query.order_by(desc(Asset.updated_at))
|
|
.offset((page - 1) * page_size)
|
|
.limit(page_size)
|
|
.all()
|
|
)
|
|
|
|
result = []
|
|
for a in assets:
|
|
result.append({
|
|
"id": a.id,
|
|
"name": a.name,
|
|
"category": a.category,
|
|
"file_type": a.file_type,
|
|
"size": a.size or 0,
|
|
"description": a.description,
|
|
"url": a.url or "",
|
|
"content": a.content or "",
|
|
"status": a.status,
|
|
"created_at": str(a.created_at) if a.created_at else None,
|
|
"updated_at": str(a.updated_at) if a.updated_at else None,
|
|
})
|
|
|
|
return {
|
|
"code": 0,
|
|
"data": {"total": total, "page": page, "page_size": page_size, "items": result},
|
|
"message": "ok",
|
|
}
|
|
|
|
|
|
@router.post("/")
|
|
def create_asset(
|
|
req: CreateAssetRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
cat = req.category if req.category in VALID_ASSET_CATEGORIES else "file"
|
|
asset = Asset(
|
|
id=gen_uuid(),
|
|
user_id=current_user.id,
|
|
name=req.name,
|
|
category=cat,
|
|
description=req.description or (req.content[:100] if req.content else None),
|
|
file_type=req.file_type or "unknown",
|
|
size=len(req.content.encode('utf-8')) if req.content else 0,
|
|
content=req.content,
|
|
status="active",
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now(),
|
|
)
|
|
db.add(asset)
|
|
db.commit()
|
|
db.refresh(asset)
|
|
|
|
LogService.log_operation(
|
|
user_id=current_user.id, username=current_user.username, action="create",
|
|
target="asset", target_id=asset.id,
|
|
detail=f"创建资产 {asset.name}",
|
|
)
|
|
|
|
return {
|
|
"code": 0,
|
|
"data": {
|
|
"id": asset.id, "name": asset.name, "category": asset.category,
|
|
"description": asset.description, "status": asset.status,
|
|
"created_at": str(asset.created_at),
|
|
},
|
|
"message": "创建成功",
|
|
}
|
|
|
|
|
|
@router.get("/{asset_id}")
|
|
def get_asset(
|
|
asset_id: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
asset = db.query(Asset).filter(
|
|
and_(Asset.id == asset_id, Asset.user_id == current_user.id)
|
|
).first()
|
|
if not asset:
|
|
raise HTTPException(status_code=404, detail="资产不存在")
|
|
|
|
return {
|
|
"code": 0,
|
|
"data": {
|
|
"id": asset.id, "name": asset.name, "category": asset.category,
|
|
"description": asset.description, "file_type": asset.file_type,
|
|
"size": asset.size or 0,
|
|
"url": asset.url or "",
|
|
"content": asset.content or "",
|
|
"status": asset.status,
|
|
"created_at": str(asset.created_at), "updated_at": str(asset.updated_at),
|
|
},
|
|
"message": "ok",
|
|
}
|
|
|
|
|
|
@router.put("/{asset_id}")
|
|
def update_asset(
|
|
asset_id: str,
|
|
req: UpdateAssetRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
asset = db.query(Asset).filter(
|
|
and_(Asset.id == asset_id, Asset.user_id == current_user.id)
|
|
).first()
|
|
if not asset:
|
|
raise HTTPException(status_code=404, detail="资产不存在")
|
|
|
|
if req.name is not None:
|
|
asset.name = req.name
|
|
if req.description is not None:
|
|
asset.description = req.description
|
|
if req.file_type is not None:
|
|
asset.file_type = req.file_type
|
|
asset.updated_at = datetime.now()
|
|
|
|
LogService.log_operation(
|
|
user_id=current_user.id, username=current_user.username, action="update",
|
|
target="asset", target_id=asset.id,
|
|
detail=f"更新资产 {asset.name}",
|
|
)
|
|
|
|
db.commit()
|
|
db.refresh(asset)
|
|
|
|
return {"code": 0, "data": {
|
|
"id": asset.id, "name": asset.name, "status": asset.status,
|
|
"updated_at": str(asset.updated_at),
|
|
}, "message": "更新成功"}
|
|
|
|
|
|
@router.delete("/{asset_id}")
|
|
def delete_asset(
|
|
asset_id: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
asset = db.query(Asset).filter(
|
|
and_(Asset.id == asset_id, Asset.user_id == current_user.id)
|
|
).first()
|
|
if not asset:
|
|
raise HTTPException(status_code=404, detail="资产不存在")
|
|
|
|
# 软删除
|
|
asset.status = "deleted"
|
|
asset.updated_at = datetime.now()
|
|
|
|
# 若资产由知识库文档同步而来,双向删除对应的知识库文档
|
|
if asset.knowledge_doc_id:
|
|
try:
|
|
kb_doc = db.query(KnowledgeDocument).filter(KnowledgeDocument.id == asset.knowledge_doc_id).first()
|
|
if kb_doc:
|
|
kb = db.query(KnowledgeBase).filter(KnowledgeBase.id == kb_doc.knowledge_base_id).first()
|
|
if kb:
|
|
kb.document_count = max(0, (kb.document_count or 0) - 1)
|
|
kb.updated_at = datetime.now()
|
|
db.delete(kb_doc)
|
|
except Exception:
|
|
pass
|
|
|
|
LogService.log_operation(
|
|
user_id=current_user.id, username=current_user.username, action="delete",
|
|
target="asset", target_id=asset.id,
|
|
detail=f"删除资产 {asset.name}",
|
|
)
|
|
|
|
db.commit()
|
|
|
|
return {"code": 0, "message": "资产已删除"}
|