269 lines
9.3 KiB
Python
269 lines
9.3 KiB
Python
import json
|
|
from datetime import datetime
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import func
|
|
from sqlmodel import Session, select
|
|
|
|
from core.database import get_session
|
|
from models.bot import BotInstance
|
|
from models.topic import TopicItem, TopicTopic
|
|
from services.topic_service import (
|
|
_TOPIC_KEY_RE,
|
|
_list_topics,
|
|
_normalize_topic_key,
|
|
_topic_item_to_dict,
|
|
_topic_to_dict,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _count_topic_items(
|
|
session: Session,
|
|
bot_id: str,
|
|
topic_key: Optional[str] = None,
|
|
unread_only: bool = False,
|
|
) -> int:
|
|
stmt = select(func.count()).select_from(TopicItem).where(TopicItem.bot_id == bot_id)
|
|
normalized_topic_key = _normalize_topic_key(topic_key or "")
|
|
if normalized_topic_key:
|
|
stmt = stmt.where(TopicItem.topic_key == normalized_topic_key)
|
|
if unread_only:
|
|
stmt = stmt.where(TopicItem.is_read == False) # noqa: E712
|
|
value = session.exec(stmt).one()
|
|
return int(value or 0)
|
|
|
|
|
|
class TopicCreateRequest(BaseModel):
|
|
topic_key: str
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
is_active: bool = True
|
|
routing: Optional[Dict[str, Any]] = None
|
|
view_schema: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
class TopicUpdateRequest(BaseModel):
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
is_active: Optional[bool] = None
|
|
routing: Optional[Dict[str, Any]] = None
|
|
view_schema: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
@router.get("/api/bots/{bot_id}/topics")
|
|
def list_bot_topics(bot_id: str, session: Session = Depends(get_session)):
|
|
bot = session.get(BotInstance, bot_id)
|
|
if not bot:
|
|
raise HTTPException(status_code=404, detail="Bot not found")
|
|
return _list_topics(session, bot_id)
|
|
|
|
|
|
@router.post("/api/bots/{bot_id}/topics")
|
|
def create_bot_topic(bot_id: str, payload: TopicCreateRequest, session: Session = Depends(get_session)):
|
|
bot = session.get(BotInstance, bot_id)
|
|
if not bot:
|
|
raise HTTPException(status_code=404, detail="Bot not found")
|
|
topic_key = _normalize_topic_key(payload.topic_key)
|
|
if not topic_key:
|
|
raise HTTPException(status_code=400, detail="topic_key is required")
|
|
if not _TOPIC_KEY_RE.fullmatch(topic_key):
|
|
raise HTTPException(status_code=400, detail="invalid topic_key")
|
|
exists = session.exec(
|
|
select(TopicTopic)
|
|
.where(TopicTopic.bot_id == bot_id)
|
|
.where(TopicTopic.topic_key == topic_key)
|
|
.limit(1)
|
|
).first()
|
|
if exists:
|
|
raise HTTPException(status_code=400, detail=f"Topic already exists: {topic_key}")
|
|
|
|
now = datetime.utcnow()
|
|
row = TopicTopic(
|
|
bot_id=bot_id,
|
|
topic_key=topic_key,
|
|
name=str(payload.name or topic_key).strip() or topic_key,
|
|
description=str(payload.description or "").strip(),
|
|
is_active=bool(payload.is_active),
|
|
is_default_fallback=False,
|
|
routing_json=json.dumps(payload.routing or {}, ensure_ascii=False),
|
|
view_schema_json=json.dumps(payload.view_schema or {}, ensure_ascii=False),
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
session.add(row)
|
|
session.commit()
|
|
session.refresh(row)
|
|
return _topic_to_dict(row)
|
|
|
|
|
|
@router.put("/api/bots/{bot_id}/topics/{topic_key}")
|
|
def update_bot_topic(bot_id: str, topic_key: str, payload: TopicUpdateRequest, session: Session = Depends(get_session)):
|
|
bot = session.get(BotInstance, bot_id)
|
|
if not bot:
|
|
raise HTTPException(status_code=404, detail="Bot not found")
|
|
normalized_key = _normalize_topic_key(topic_key)
|
|
if not normalized_key:
|
|
raise HTTPException(status_code=400, detail="topic_key is required")
|
|
row = session.exec(
|
|
select(TopicTopic)
|
|
.where(TopicTopic.bot_id == bot_id)
|
|
.where(TopicTopic.topic_key == normalized_key)
|
|
.limit(1)
|
|
).first()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Topic not found")
|
|
|
|
update_data = payload.model_dump(exclude_unset=True)
|
|
if "name" in update_data:
|
|
row.name = str(update_data.get("name") or "").strip() or row.topic_key
|
|
if "description" in update_data:
|
|
row.description = str(update_data.get("description") or "").strip()
|
|
if "is_active" in update_data:
|
|
row.is_active = bool(update_data.get("is_active"))
|
|
if "routing" in update_data:
|
|
row.routing_json = json.dumps(update_data.get("routing") or {}, ensure_ascii=False)
|
|
if "view_schema" in update_data:
|
|
row.view_schema_json = json.dumps(update_data.get("view_schema") or {}, ensure_ascii=False)
|
|
row.is_default_fallback = False
|
|
row.updated_at = datetime.utcnow()
|
|
session.add(row)
|
|
session.commit()
|
|
session.refresh(row)
|
|
return _topic_to_dict(row)
|
|
|
|
|
|
@router.delete("/api/bots/{bot_id}/topics/{topic_key}")
|
|
def delete_bot_topic(bot_id: str, topic_key: str, session: Session = Depends(get_session)):
|
|
bot = session.get(BotInstance, bot_id)
|
|
if not bot:
|
|
raise HTTPException(status_code=404, detail="Bot not found")
|
|
normalized_key = _normalize_topic_key(topic_key)
|
|
if not normalized_key:
|
|
raise HTTPException(status_code=400, detail="topic_key is required")
|
|
row = session.exec(
|
|
select(TopicTopic)
|
|
.where(TopicTopic.bot_id == bot_id)
|
|
.where(TopicTopic.topic_key == normalized_key)
|
|
.limit(1)
|
|
).first()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Topic not found")
|
|
items = session.exec(
|
|
select(TopicItem)
|
|
.where(TopicItem.bot_id == bot_id)
|
|
.where(TopicItem.topic_key == normalized_key)
|
|
).all()
|
|
for item in items:
|
|
session.delete(item)
|
|
session.delete(row)
|
|
session.commit()
|
|
return {"status": "deleted", "bot_id": bot_id, "topic_key": normalized_key}
|
|
|
|
|
|
@router.get("/api/bots/{bot_id}/topic-items")
|
|
def list_bot_topic_items(
|
|
bot_id: str,
|
|
topic_key: Optional[str] = None,
|
|
cursor: Optional[int] = None,
|
|
limit: int = 50,
|
|
session: Session = Depends(get_session),
|
|
):
|
|
bot = session.get(BotInstance, bot_id)
|
|
if not bot:
|
|
raise HTTPException(status_code=404, detail="Bot not found")
|
|
normalized_limit = max(1, min(int(limit or 50), 100))
|
|
stmt = select(TopicItem).where(TopicItem.bot_id == bot_id)
|
|
normalized_topic_key = _normalize_topic_key(topic_key or "")
|
|
if normalized_topic_key:
|
|
stmt = stmt.where(TopicItem.topic_key == normalized_topic_key)
|
|
if cursor is not None:
|
|
normalized_cursor = int(cursor)
|
|
if normalized_cursor > 0:
|
|
stmt = stmt.where(TopicItem.id < normalized_cursor)
|
|
rows = session.exec(
|
|
stmt.order_by(TopicItem.id.desc()).limit(normalized_limit + 1)
|
|
).all()
|
|
next_cursor: Optional[int] = None
|
|
if len(rows) > normalized_limit:
|
|
next_cursor = rows[-1].id
|
|
rows = rows[:normalized_limit]
|
|
return {
|
|
"bot_id": bot_id,
|
|
"topic_key": normalized_topic_key or None,
|
|
"items": [_topic_item_to_dict(row) for row in rows],
|
|
"next_cursor": next_cursor,
|
|
"unread_count": _count_topic_items(session, bot_id, normalized_topic_key, unread_only=True),
|
|
"total_unread_count": _count_topic_items(session, bot_id, unread_only=True),
|
|
}
|
|
|
|
|
|
@router.get("/api/bots/{bot_id}/topic-items/stats")
|
|
def get_bot_topic_item_stats(bot_id: str, session: Session = Depends(get_session)):
|
|
bot = session.get(BotInstance, bot_id)
|
|
if not bot:
|
|
raise HTTPException(status_code=404, detail="Bot not found")
|
|
latest_item = session.exec(
|
|
select(TopicItem)
|
|
.where(TopicItem.bot_id == bot_id)
|
|
.order_by(TopicItem.id.desc())
|
|
.limit(1)
|
|
).first()
|
|
return {
|
|
"bot_id": bot_id,
|
|
"total_count": _count_topic_items(session, bot_id),
|
|
"unread_count": _count_topic_items(session, bot_id, unread_only=True),
|
|
"latest_item_id": int(latest_item.id or 0) if latest_item and latest_item.id else None,
|
|
}
|
|
|
|
|
|
@router.post("/api/bots/{bot_id}/topic-items/{item_id}/read")
|
|
def mark_bot_topic_item_read(bot_id: str, item_id: int, session: Session = Depends(get_session)):
|
|
bot = session.get(BotInstance, bot_id)
|
|
if not bot:
|
|
raise HTTPException(status_code=404, detail="Bot not found")
|
|
row = session.exec(
|
|
select(TopicItem)
|
|
.where(TopicItem.bot_id == bot_id)
|
|
.where(TopicItem.id == item_id)
|
|
.limit(1)
|
|
).first()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Topic item not found")
|
|
if not bool(row.is_read):
|
|
row.is_read = True
|
|
session.add(row)
|
|
session.commit()
|
|
session.refresh(row)
|
|
return {
|
|
"status": "updated",
|
|
"bot_id": bot_id,
|
|
"item": _topic_item_to_dict(row),
|
|
}
|
|
|
|
|
|
@router.delete("/api/bots/{bot_id}/topic-items/{item_id}")
|
|
def delete_bot_topic_item(bot_id: str, item_id: int, session: Session = Depends(get_session)):
|
|
bot = session.get(BotInstance, bot_id)
|
|
if not bot:
|
|
raise HTTPException(status_code=404, detail="Bot not found")
|
|
row = session.exec(
|
|
select(TopicItem)
|
|
.where(TopicItem.bot_id == bot_id)
|
|
.where(TopicItem.id == item_id)
|
|
.limit(1)
|
|
).first()
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="Topic item not found")
|
|
payload = _topic_item_to_dict(row)
|
|
session.delete(row)
|
|
session.commit()
|
|
return {
|
|
"status": "deleted",
|
|
"bot_id": bot_id,
|
|
"item": payload,
|
|
}
|