feat: add FastAPI routers for channels, notify, and history

This commit is contained in:
OpenClaw Agent 2026-02-07 18:13:24 +00:00
parent 53bc48fee6
commit 7f581e3fbb
11 changed files with 255 additions and 0 deletions

Binary file not shown.

Binary file not shown.

49
backend/main.py Normal file
View File

@ -0,0 +1,49 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
import os
from backend.database import init_db
from backend.routers import channels, notify, history
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
await init_db()
yield
# Shutdown
app = FastAPI(
title="Apprise Notify Center",
description="多通道通知中心 - 支持 80+ 种通知服务",
version="1.0.0",
lifespan=lifespan
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 注册路由
app.include_router(channels.router)
app.include_router(notify.router)
app.include_router(history.router)
# 静态文件(前端)
frontend_path = os.path.join(os.path.dirname(__file__), "..", "frontend")
if os.path.exists(frontend_path):
app.mount("/", StaticFiles(directory=frontend_path, html=True), name="frontend")
@app.get("/api/health")
async def health_check():
return {"status": "ok"}
if __name__ == "__main__":
import uvicorn
uvicorn.run("backend.main:app", host="0.0.0.0", port=8000, reload=True)

View File

@ -0,0 +1 @@
# routers package

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,82 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from backend.database import get_db
from backend import crud
from backend.schemas import Channel, ChannelCreate, ChannelUpdate, ChannelList
router = APIRouter(prefix="/api/channels", tags=["channels"])
@router.get("", response_model=ChannelList)
async def list_channels(
skip: int = 0,
limit: int = 100,
db: AsyncSession = Depends(get_db)
):
"""获取所有通道列表"""
channels = await crud.get_channels(db, skip=skip, limit=limit)
total = len(channels)
return {"channels": channels, "total": total}
@router.get("/{channel_id}", response_model=Channel)
async def get_channel(channel_id: int, db: AsyncSession = Depends(get_db)):
"""获取单个通道详情"""
channel = await crud.get_channel(db, channel_id)
if not channel:
raise HTTPException(status_code=404, detail="Channel not found")
return channel
@router.post("", response_model=Channel, status_code=201)
async def create_channel(
channel: ChannelCreate,
db: AsyncSession = Depends(get_db)
):
"""创建新通道"""
existing = await crud.get_channel_by_name(db, channel.name)
if existing:
raise HTTPException(status_code=400, detail="Channel name already exists")
return await crud.create_channel(db, channel.model_dump())
@router.put("/{channel_id}", response_model=Channel)
async def update_channel(
channel_id: int,
channel: ChannelUpdate,
db: AsyncSession = Depends(get_db)
):
"""更新通道"""
if channel.name:
existing = await crud.get_channel_by_name(db, channel.name)
if existing and existing.id != channel_id:
raise HTTPException(status_code=400, detail="Channel name already exists")
updated = await crud.update_channel(db, channel_id, channel.model_dump(exclude_unset=True))
if not updated:
raise HTTPException(status_code=404, detail="Channel not found")
return updated
@router.delete("/{channel_id}")
async def delete_channel(channel_id: int, db: AsyncSession = Depends(get_db)):
"""删除通道"""
success = await crud.delete_channel(db, channel_id)
if not success:
raise HTTPException(status_code=404, detail="Channel not found")
return {"message": "Channel deleted successfully"}
@router.post("/{channel_id}/test")
async def test_channel(channel_id: int, db: AsyncSession = Depends(get_db)):
"""测试通道配置"""
from backend.notify_service import NotifyService
service = NotifyService()
result = await service.send_notification(
db, channel_id, "测试通知", "这是一条测试消息", "normal"
)
if result["status"] == "sent":
return {"success": True, "message": "Test notification sent successfully"}
else:
raise HTTPException(
status_code=400,
detail=f"Test failed: {result.get('error_msg', 'Unknown error')}"
)

View File

@ -0,0 +1,41 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from backend.database import get_db
from backend import crud
from backend.schemas import Notification
router = APIRouter(prefix="/api/notifications", tags=["history"])
@router.get("")
async def list_notifications(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
channel_id: Optional[int] = None,
status: Optional[str] = Query(None, enum=["pending", "sent", "failed"]),
db: AsyncSession = Depends(get_db)
):
"""获取通知历史记录"""
notifications = await crud.get_notifications(db, skip, limit, channel_id, status)
return {
"notifications": notifications,
"skip": skip,
"limit": limit
}
@router.get("/{notification_id}")
async def get_notification(notification_id: int, db: AsyncSession = Depends(get_db)):
"""获取单条通知详情"""
from sqlalchemy import select
from backend.models import Notification
result = await db.execute(
select(Notification).where(Notification.id == notification_id)
)
notification = result.scalar_one_or_none()
if not notification:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Notification not found")
return notification

82
backend/routers/notify.py Normal file
View File

@ -0,0 +1,82 @@
from typing import Optional, List
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from backend.database import get_db
from backend.schemas import NotifyRequest, NotifyResponse
from backend.notify_service import NotifyService
router = APIRouter(prefix="/api", tags=["notify"])
@router.post("/notify", response_model=NotifyResponse)
async def send_notification(
request: NotifyRequest,
db: AsyncSession = Depends(get_db)
):
"""发送通知到指定通道或按标签发送"""
if not request.channels and not request.tags:
raise HTTPException(
status_code=400,
detail="Either 'channels' or 'tags' must be provided"
)
service = NotifyService()
results = await service.send_to_channels(
db,
request.channels,
request.tags,
request.title,
request.body,
request.priority
)
total = len(results)
sent = sum(1 for r in results if r["status"] == "sent")
failed = sum(1 for r in results if r["status"] == "failed")
return {
"success": sent == total,
"results": results,
"total": total,
"sent": sent,
"failed": failed
}
@router.get("/notify")
async def send_notification_get(
channels: Optional[str] = Query(None, description="通道名称,多个用逗号分隔"),
tags: Optional[str] = Query(None, description="标签,多个用逗号分隔"),
title: Optional[str] = Query(None),
body: str = Query(..., description="消息内容"),
priority: str = Query("normal"),
db: AsyncSession = Depends(get_db)
):
"""通过 GET 请求发送通知(方便脚本调用)"""
channel_list = channels.split(",") if channels else None
tag_list = tags.split(",") if tags else None
if not channel_list and not tag_list:
raise HTTPException(
status_code=400,
detail="Either 'channels' or 'tags' must be provided"
)
service = NotifyService()
results = await service.send_to_channels(
db,
channel_list,
tag_list,
title,
body,
priority
)
total = len(results)
sent = sum(1 for r in results if r["status"] == "sent")
return {
"success": sent == total,
"results": results,
"total": total,
"sent": sent,
"failed": total - sent
}