初始提交: Qihu-AIFlow v2.0.1 完整项目
- 前端 React+TypeScript (源码+构建产物) - 后端 Python FastAPI - 数据库迁移 001-015 - Docker部署配置 - 微信ClawBot API频道绑定 - 项目文档 README.md
This commit is contained in:
commit
78ff261b5a
199 files changed
+69126
No files matched your search
@@ -0,0 +1,598 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, desc, asc, func
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import (
|
||||
User, MarketApp, AppPurchase, AppReview, AppInstallation, Order,
|
||||
gen_uuid
|
||||
)
|
||||
from ..auth import get_current_user, get_current_user_id, get_current_user_id_optional
|
||||
from ..middleware.log_service import LogService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class UploadAppRequest(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=200)
|
||||
description: Optional[str] = None
|
||||
icon: Optional[str] = None
|
||||
type: Optional[str] = None # skill, agent, mcp
|
||||
source: Optional[str] = None
|
||||
price: float = Field(default=0, ge=0)
|
||||
download_url: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
tags: Optional[str] = None
|
||||
is_member_free: Optional[bool] = False
|
||||
join_promotion: Optional[bool] = False
|
||||
agent_endpoint: Optional[str] = None
|
||||
|
||||
|
||||
class PurchaseAppRequest(BaseModel):
|
||||
amount: Optional[float] = None
|
||||
|
||||
|
||||
class ReviewAppRequest(BaseModel):
|
||||
rating: int = Field(ge=1, le=5)
|
||||
content: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("/apps")
|
||||
def list_apps(
|
||||
type: Optional[str] = Query(None),
|
||||
source: Optional[str] = Query(None),
|
||||
category: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
sort: str = Query(default="latest"),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
user_id: Optional[str] = Depends(get_current_user_id_optional),
|
||||
):
|
||||
query = db.query(MarketApp).filter(MarketApp.status == "published")
|
||||
|
||||
if type:
|
||||
query = query.filter(MarketApp.type == type)
|
||||
if source:
|
||||
query = query.filter(MarketApp.source == source)
|
||||
if category:
|
||||
query = query.filter(MarketApp.type == category)
|
||||
if search:
|
||||
query = query.filter(
|
||||
and_(
|
||||
MarketApp.name.ilike(f"%{search}%")
|
||||
| MarketApp.description.ilike(f"%{search}%")
|
||||
)
|
||||
)
|
||||
|
||||
if sort == "popular":
|
||||
query = query.order_by(desc(MarketApp.install_count))
|
||||
elif sort == "rating":
|
||||
query = query.order_by(desc(MarketApp.rating_avg))
|
||||
else:
|
||||
query = query.order_by(desc(MarketApp.created_at))
|
||||
|
||||
total = query.count()
|
||||
apps = query.offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
# 如果用户已登录,获取该用户已安装的应用ID集合(确保按用户隔离)
|
||||
installed_app_ids = set()
|
||||
if user_id:
|
||||
installations = db.query(AppInstallation.app_id).filter(
|
||||
AppInstallation.user_id == user_id
|
||||
).all()
|
||||
installed_app_ids = {inst[0] for inst in installations}
|
||||
|
||||
# 查询已安装状态时始终带 user_id 过滤
|
||||
|
||||
result = []
|
||||
for app in apps:
|
||||
item = {
|
||||
"id": app.id,
|
||||
"name": app.name,
|
||||
"description": app.description,
|
||||
"icon": app.icon,
|
||||
"type": app.type,
|
||||
"source": app.source,
|
||||
"publisher_id": app.publisher_id,
|
||||
"publisher_name": app.publisher_name,
|
||||
"is_member_free": app.is_member_free if app.is_member_free else False,
|
||||
"join_promotion": app.join_promotion if app.join_promotion else False,
|
||||
"status": app.status,
|
||||
"price": float(app.price) if app.price else 0,
|
||||
"install_count": app.install_count or 0,
|
||||
"rating_avg": float(app.rating_avg) if app.rating_avg else 0,
|
||||
"review_count": app.review_count or 0,
|
||||
"created_at": str(app.created_at) if app.created_at else None,
|
||||
"installed": app.id in installed_app_ids if user_id else False,
|
||||
}
|
||||
result.append(item)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"items": result,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/apps/{app_id}")
|
||||
def get_app(
|
||||
app_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Optional[User] = Depends(get_current_user),
|
||||
):
|
||||
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
# 检查当前用户是否已购买(按用户隔离)
|
||||
purchased = False
|
||||
installed = False
|
||||
if current_user:
|
||||
purchase = db.query(AppPurchase).filter(
|
||||
and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
|
||||
).first()
|
||||
if purchase:
|
||||
purchased = True
|
||||
installed = purchase.status == "installed"
|
||||
# 通过 AppInstallation 表检查是否已安装(按用户隔离)
|
||||
installation = db.query(AppInstallation).filter(
|
||||
and_(AppInstallation.app_id == app_id, AppInstallation.user_id == current_user.id)
|
||||
).first()
|
||||
if installation:
|
||||
installed = True
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": app.id, "name": app.name, "description": app.description,
|
||||
"icon": app.icon, "type": app.type, "source": app.source,
|
||||
"type": app.type,
|
||||
"price": float(app.price) if app.price else 0,
|
||||
"install_count": app.install_count or 0,
|
||||
"rating_avg": float(app.rating_avg) if app.rating_avg else 0,
|
||||
"review_count": app.review_count or 0,
|
||||
"publisher_id": app.publisher_id,
|
||||
"download_url": app.download_url,
|
||||
"detail": app.detail,
|
||||
"created_at": str(app.created_at),
|
||||
"purchased": purchased,
|
||||
"installed": installed,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/apps")
|
||||
def upload_app(
|
||||
req: UploadAppRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
app = MarketApp(
|
||||
id=gen_uuid(),
|
||||
name=req.name,
|
||||
description=req.description,
|
||||
icon=req.icon,
|
||||
type=req.type if req.type in ("skill","agent","mcp") else "skill",
|
||||
source=req.source if req.source in ("official","user") else "user",
|
||||
price=req.price,
|
||||
install_count=0,
|
||||
rating_avg=0,
|
||||
review_count=0,
|
||||
publisher_id=current_user.id,
|
||||
publisher_name=current_user.username,
|
||||
is_member_free=req.is_member_free if req.is_member_free is not None else False,
|
||||
join_promotion=req.join_promotion if req.join_promotion is not None else False,
|
||||
agent_endpoint=req.agent_endpoint,
|
||||
status="published", # 用户创建的应用直接发布
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(app)
|
||||
db.commit()
|
||||
db.refresh(app)
|
||||
|
||||
# 用户创建/导入的应用自动标记为已安装
|
||||
existing_install = db.query(AppInstallation).filter(
|
||||
AppInstallation.user_id == current_user.id,
|
||||
AppInstallation.app_id == app.id,
|
||||
).first()
|
||||
if not existing_install:
|
||||
installation = AppInstallation(
|
||||
user_id=current_user.id,
|
||||
app_id=app.id,
|
||||
installed_at=datetime.now(),
|
||||
)
|
||||
db.add(installation)
|
||||
db.commit()
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="upload_app",
|
||||
target="market_app", target_id=app.id,
|
||||
detail=f"上传应用 {app.name}",
|
||||
)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"id": app.id, "name": app.name, "status": app.status,
|
||||
"created_at": str(app.created_at),
|
||||
},
|
||||
"message": "提交成功,等待审核",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/apps/{app_id}/purchase")
|
||||
def purchase_app(
|
||||
app_id: str,
|
||||
req: PurchaseAppRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
app = db.query(MarketApp).filter(
|
||||
and_(MarketApp.id == app_id, MarketApp.status == "published")
|
||||
).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在或未上架")
|
||||
|
||||
# 检查是否已购买
|
||||
existing = db.query(AppPurchase).filter(
|
||||
and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
|
||||
).first()
|
||||
if existing and existing.status in ("purchased", "installed"):
|
||||
raise HTTPException(status_code=400, detail="您已购买该应用")
|
||||
|
||||
amount = req.amount or app.price
|
||||
|
||||
# 创建订单
|
||||
order = Order(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
order_type="app",
|
||||
item_id=app_id,
|
||||
item_name=app.name,
|
||||
amount=amount,
|
||||
status="pending",
|
||||
created_at=datetime.now(),
|
||||
)
|
||||
db.add(order)
|
||||
db.flush()
|
||||
|
||||
# 创建购买记录
|
||||
purchase = AppPurchase(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
app_id=app_id,
|
||||
order_id=order.id,
|
||||
status="purchased",
|
||||
purchased_at=datetime.now(),
|
||||
)
|
||||
db.add(purchase)
|
||||
|
||||
# 更新应用下载量
|
||||
app.install_count = (app.install_count or 0) + 1
|
||||
app.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"order_id": order.id,
|
||||
"purchase_id": purchase.id,
|
||||
"amount": amount,
|
||||
"status": "purchased",
|
||||
},
|
||||
"message": "购买成功",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/apps/{app_id}/install")
|
||||
def install_app(
|
||||
app_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
# 检查是否已安装(通过 AppInstallation 表)
|
||||
existing_install = db.query(AppInstallation).filter(
|
||||
and_(AppInstallation.app_id == app_id, AppInstallation.user_id == current_user.id)
|
||||
).first()
|
||||
if existing_install:
|
||||
raise HTTPException(status_code=400, detail="应用已安装")
|
||||
|
||||
# 检查是否已购买(付费应用需要先购买)
|
||||
purchase = db.query(AppPurchase).filter(
|
||||
and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
|
||||
).first()
|
||||
|
||||
# 对于付费应用,必须已购买才能安装
|
||||
if float(app.price or 0) > 0:
|
||||
if not purchase:
|
||||
raise HTTPException(status_code=403, detail="请先购买应用")
|
||||
if purchase.status == "installed":
|
||||
raise HTTPException(status_code=400, detail="应用已安装")
|
||||
|
||||
# 创建安装记录
|
||||
installation = AppInstallation(
|
||||
id=gen_uuid(),
|
||||
user_id=current_user.id,
|
||||
app_id=app_id,
|
||||
installed_at=datetime.now(),
|
||||
)
|
||||
db.add(installation)
|
||||
|
||||
# 如果有购买记录,更新状态为已安装
|
||||
if purchase:
|
||||
purchase.status = "installed"
|
||||
purchase.installed_at = datetime.now()
|
||||
|
||||
# 更新应用安装量
|
||||
app.install_count = (app.install_count or 0) + 1
|
||||
app.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {"installation_id": installation.id, "status": "installed"},
|
||||
"message": "安装成功",
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/apps/{app_id}/uninstall")
|
||||
def uninstall_app(
|
||||
app_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""卸载应用 - 删除当前用户的安装记录(按用户隔离)"""
|
||||
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
# 查找当前用户的安装记录(按用户隔离)
|
||||
installation = db.query(AppInstallation).filter(
|
||||
and_(AppInstallation.app_id == app_id, AppInstallation.user_id == current_user.id)
|
||||
).first()
|
||||
if not installation:
|
||||
raise HTTPException(status_code=400, detail="应用未安装")
|
||||
|
||||
# 删除安装记录
|
||||
db.delete(installation)
|
||||
|
||||
# 问题4修复: 将AppPurchase状态从installed重置为purchased
|
||||
purchase = db.query(AppPurchase).filter(
|
||||
and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
|
||||
).first()
|
||||
if purchase and purchase.status == "installed":
|
||||
purchase.status = "purchased"
|
||||
|
||||
# 更新应用安装量(减1,最小为0)
|
||||
if app.install_count and app.install_count > 0:
|
||||
app.install_count = app.install_count - 1
|
||||
app.updated_at = datetime.now()
|
||||
|
||||
db.commit()
|
||||
|
||||
LogService.log_operation(
|
||||
user_id=current_user.id, username=current_user.username, action="uninstall_app",
|
||||
target="market_app", target_id=app_id,
|
||||
detail=f"卸载应用 {app.name}",
|
||||
)
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"message": "卸载成功",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/apps/{app_id}/review")
|
||||
def review_app(
|
||||
app_id: str,
|
||||
req: ReviewAppRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="应用不存在")
|
||||
|
||||
# 检查是否已评价
|
||||
existing = db.query(AppReview).filter(
|
||||
and_(AppReview.app_id == app_id, AppReview.user_id == current_user.id)
|
||||
).first()
|
||||
if existing:
|
||||
# 更新已有评价
|
||||
old_rating = existing.rating
|
||||
existing.rating = req.rating
|
||||
existing.content = req.content
|
||||
existing.updated_at = datetime.now()
|
||||
|
||||
# 重新计算平均分 (总数不变,减去旧评分加新评分)
|
||||
if app.review_count and app.review_count > 0:
|
||||
total_rating = float(app.rating_avg or 0) * app.review_count
|
||||
total_rating = total_rating - old_rating + req.rating
|
||||
app.rating_avg = total_rating / app.review_count
|
||||
else:
|
||||
app.rating_avg = req.rating
|
||||
app.review_count = 1
|
||||
else:
|
||||
# 创建新评价
|
||||
review = AppReview(
|
||||
id=gen_uuid(),
|
||||
app_id=app_id,
|
||||
user_id=current_user.id,
|
||||
rating=req.rating,
|
||||
content=req.content,
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
db.add(review)
|
||||
|
||||
# 重新计算平均分
|
||||
app.review_count = (app.review_count or 0) + 1
|
||||
if app.review_count == 1:
|
||||
app.rating_avg = req.rating
|
||||
else:
|
||||
total_rating = float(app.rating_avg or 0) * (app.review_count - 1) + req.rating
|
||||
app.rating_avg = round(total_rating / app.review_count, 1)
|
||||
|
||||
app.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"rating_avg": float(app.rating_avg or 0),
|
||||
"review_count": app.review_count or 0,
|
||||
},
|
||||
"message": "评价成功",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/my-apps")
|
||||
def get_my_apps(
|
||||
status: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
query = db.query(AppPurchase).filter(AppPurchase.user_id == current_user.id)
|
||||
purchases = query.order_by(desc(AppPurchase.created_at)).all()
|
||||
|
||||
result = []
|
||||
for p in purchases:
|
||||
app = db.query(MarketApp).filter(MarketApp.id == p.app_id).first()
|
||||
if app:
|
||||
result.append({
|
||||
"id": p.id,
|
||||
"app_id": app.id,
|
||||
"name": app.name,
|
||||
"icon": app.icon,
|
||||
"type": app.type,
|
||||
"price": float(app.price or 0),
|
||||
"purchased_at": str(p.created_at) if p.created_at else None,
|
||||
})
|
||||
|
||||
return {"code": 0, "data": result, "message": "ok"}
|
||||
|
||||
|
||||
@router.get("/reviews/{app_id}")
|
||||
def get_reviews(
|
||||
app_id: str,
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
query = db.query(AppReview).filter(AppReview.app_id == app_id)
|
||||
total = query.count()
|
||||
|
||||
reviews = (
|
||||
query.order_by(desc(AppReview.created_at))
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
.all()
|
||||
)
|
||||
|
||||
result = []
|
||||
for r in reviews:
|
||||
user = db.query(User).filter(User.id == r.user_id).first()
|
||||
result.append({
|
||||
"id": r.id,
|
||||
"user_id": r.user_id,
|
||||
"username": user.username if user else "unknown",
|
||||
"avatar": user.avatar if user else None,
|
||||
"rating": r.rating,
|
||||
"content": r.content,
|
||||
"created_at": str(r.created_at) if r.created_at else None,
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {"total": total, "page": page, "page_size": page_size, "items": result},
|
||||
"message": "ok",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Agent部署资源查询接口
|
||||
# ============================================================
|
||||
|
||||
@router.get("/agent-resources")
|
||||
def get_agent_resources(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Agent部署时需要的资源列表:可用的应用、知识库、文档"""
|
||||
# 用户已发布的应用
|
||||
my_apps = db.query(MarketApp).filter(
|
||||
MarketApp.publisher_id == current_user.id,
|
||||
MarketApp.status == "published",
|
||||
).order_by(desc(MarketApp.updated_at)).all()
|
||||
|
||||
apps_result = []
|
||||
for app in my_apps:
|
||||
apps_result.append({
|
||||
"id": app.id,
|
||||
"name": app.name,
|
||||
"type": app.type,
|
||||
"description": app.description,
|
||||
"install_count": app.install_count or 0,
|
||||
})
|
||||
|
||||
# 用户知识库
|
||||
from ..models import KnowledgeBase, KnowledgeDocument
|
||||
kb_list = db.query(KnowledgeBase).filter(
|
||||
KnowledgeBase.user_id == current_user.id
|
||||
).order_by(desc(KnowledgeBase.updated_at)).all()
|
||||
|
||||
kb_result = []
|
||||
for kb in kb_list:
|
||||
doc_count = db.query(KnowledgeDocument).filter(
|
||||
KnowledgeDocument.knowledge_base_id == kb.id
|
||||
).count()
|
||||
kb_result.append({
|
||||
"id": kb.id,
|
||||
"name": kb.name,
|
||||
"description": kb.description,
|
||||
"document_count": doc_count,
|
||||
})
|
||||
|
||||
# 用户已安装的应用(从AppPurchase)
|
||||
installed = db.query(AppPurchase).filter(
|
||||
AppPurchase.user_id == current_user.id
|
||||
).all()
|
||||
installed_ids = [p.app_id for p in installed]
|
||||
installed_apps = []
|
||||
if installed_ids:
|
||||
iapps = db.query(MarketApp).filter(MarketApp.id.in_(installed_ids)).all()
|
||||
for ia in iapps:
|
||||
installed_apps.append({
|
||||
"id": ia.id,
|
||||
"name": ia.name,
|
||||
"type": ia.type,
|
||||
"description": ia.description,
|
||||
})
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"data": {
|
||||
"my_apps": apps_result,
|
||||
"knowledge_bases": kb_result,
|
||||
"installed_apps": installed_apps,
|
||||
},
|
||||
"message": "ok",
|
||||
}
|
||||
Reference in new issue
Block a user