Merge pull request #920 from wanxunyang/feat/quota-check-decorator
feat(tenant): add public subscription plan list endpoint and enhance plan information
This commit is contained in:
@@ -1,30 +1,77 @@
|
|||||||
"""
|
"""
|
||||||
社区版默认免费套餐配置
|
社区版默认免费套餐配置
|
||||||
当无法从 SaaS 版获取 premium 模块时,使用此配置作为兜底
|
当无法从 SaaS 版获取 premium 模块时,使用此配置作为兜底
|
||||||
|
|
||||||
|
可通过环境变量覆盖配额配置,格式:QUOTA_<QUOTA_NAME>
|
||||||
|
例如:QUOTA_END_USER_QUOTA=100
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_FREE_PLAN = {
|
import os
|
||||||
"name": "记忆体验版",
|
|
||||||
"category": "saas_personal",
|
|
||||||
"tier_level": 0,
|
def _get_quota_from_env():
|
||||||
"version": "1.0",
|
"""从环境变量获取配额配置"""
|
||||||
"status": True,
|
quota_keys = [
|
||||||
"price": 0,
|
"workspace_quota",
|
||||||
"billing_cycle": "permanent_free",
|
"skill_quota",
|
||||||
"core_value": "感受永久记忆",
|
"app_quota",
|
||||||
"tech_support": "社群交流",
|
"knowledge_capacity_quota",
|
||||||
"sla_compliance": "无",
|
"memory_engine_quota",
|
||||||
"page_customization": "无",
|
"end_user_quota",
|
||||||
"theme_color": "#64748B",
|
"ontology_project_quota",
|
||||||
"quotas": {
|
"model_quota",
|
||||||
"workspace_quota": 1,
|
"api_ops_rate_limit",
|
||||||
"skill_quota": 5,
|
]
|
||||||
"app_quota": 2,
|
quotas = {}
|
||||||
"knowledge_capacity_quota": 0.3,
|
for key in quota_keys:
|
||||||
"memory_engine_quota": 1,
|
env_key = f"QUOTA_{key.upper()}"
|
||||||
"end_user_quota": 1,
|
env_value = os.getenv(env_key)
|
||||||
"ontology_project_quota": 3,
|
if env_value is not None:
|
||||||
"model_quota": 1,
|
try:
|
||||||
"api_ops_rate_limit": 50,
|
quotas[key] = float(env_value) if '.' in env_value else int(env_value)
|
||||||
},
|
except ValueError:
|
||||||
}
|
pass
|
||||||
|
return quotas
|
||||||
|
|
||||||
|
|
||||||
|
def _build_default_free_plan():
|
||||||
|
"""构建默认免费套餐配置"""
|
||||||
|
base = {
|
||||||
|
"name": "记忆体验版",
|
||||||
|
"name_en": "Memory Experience",
|
||||||
|
"category": "saas_personal",
|
||||||
|
"tier_level": 0,
|
||||||
|
"version": "1.0",
|
||||||
|
"status": True,
|
||||||
|
"price": 0,
|
||||||
|
"billing_cycle": "permanent_free",
|
||||||
|
"core_value": "感受永久记忆",
|
||||||
|
"core_value_en": "Experience Permanent Memory",
|
||||||
|
"tech_support": "社群交流",
|
||||||
|
"tech_support_en": "Community Support",
|
||||||
|
"sla_compliance": "无",
|
||||||
|
"sla_compliance_en": "None",
|
||||||
|
"page_customization": "无",
|
||||||
|
"page_customization_en": "None",
|
||||||
|
"theme_color": "#64748B",
|
||||||
|
"quotas": {
|
||||||
|
"workspace_quota": 1,
|
||||||
|
"skill_quota": 5,
|
||||||
|
"app_quota": 2,
|
||||||
|
"knowledge_capacity_quota": 0.3,
|
||||||
|
"memory_engine_quota": 1,
|
||||||
|
"end_user_quota": 1,
|
||||||
|
"ontology_project_quota": 3,
|
||||||
|
"model_quota": 1,
|
||||||
|
"api_ops_rate_limit": 50,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
env_quotas = _get_quota_from_env()
|
||||||
|
if env_quotas:
|
||||||
|
base["quotas"].update(env_quotas)
|
||||||
|
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_FREE_PLAN = _build_default_free_plan()
|
||||||
|
|||||||
@@ -100,5 +100,6 @@ manager_router.include_router(ontology_controller.router)
|
|||||||
manager_router.include_router(skill_controller.router)
|
manager_router.include_router(skill_controller.router)
|
||||||
manager_router.include_router(i18n_controller.router)
|
manager_router.include_router(i18n_controller.router)
|
||||||
manager_router.include_router(tenant_subscription_controller.router)
|
manager_router.include_router(tenant_subscription_controller.router)
|
||||||
|
manager_router.include_router(tenant_subscription_controller.public_router)
|
||||||
|
|
||||||
__all__ = ["manager_router"]
|
__all__ = ["manager_router"]
|
||||||
|
|||||||
@@ -271,6 +271,19 @@ def update_agent_config(
|
|||||||
return success(data=app_schema.AgentConfig.model_validate(cfg))
|
return success(data=app_schema.AgentConfig.model_validate(cfg))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{app_id}/model/parameters/default", summary="获取 Agent 模型参数默认配置")
|
||||||
|
@cur_workspace_access_guard()
|
||||||
|
def get_agent_model_parameters(
|
||||||
|
app_id: uuid.UUID,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
workspace_id = current_user.current_workspace_id
|
||||||
|
service = AppService(db)
|
||||||
|
model_parameters = service.get_default_model_parameters(app_id=app_id)
|
||||||
|
return success(data=model_parameters, msg="获取 Agent 模型参数默认配置")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{app_id}/config", summary="获取 Agent 配置")
|
@router.get("/{app_id}/config", summary="获取 Agent 配置")
|
||||||
@cur_workspace_access_guard()
|
@cur_workspace_access_guard()
|
||||||
def get_agent_config(
|
def get_agent_config(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from sqlalchemy.orm import Session
|
|||||||
from app.core.error_codes import BizCode
|
from app.core.error_codes import BizCode
|
||||||
from app.core.exceptions import BusinessException
|
from app.core.exceptions import BusinessException
|
||||||
from app.core.logging_config import get_business_logger
|
from app.core.logging_config import get_business_logger
|
||||||
|
from app.core.quota_manager import check_end_user_quota
|
||||||
from app.core.response_utils import success, fail
|
from app.core.response_utils import success, fail
|
||||||
from app.db import get_db, get_db_read
|
from app.db import get_db, get_db_read
|
||||||
from app.dependencies import get_share_user_id, ShareTokenData
|
from app.dependencies import get_share_user_id, ShareTokenData
|
||||||
@@ -308,6 +309,7 @@ def get_conversation(
|
|||||||
"/chat",
|
"/chat",
|
||||||
summary="发送消息(支持流式和非流式)"
|
summary="发送消息(支持流式和非流式)"
|
||||||
)
|
)
|
||||||
|
@check_end_user_quota
|
||||||
async def chat(
|
async def chat(
|
||||||
payload: conversation_schema.ChatRequest,
|
payload: conversation_schema.ChatRequest,
|
||||||
share_data: ShareTokenData = Depends(get_share_user_id),
|
share_data: ShareTokenData = Depends(get_share_user_id),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
租户套餐查询接口(普通用户可访问)
|
租户套餐查询接口(普通用户可访问)
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
from typing import Callable
|
from typing import Callable, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
@@ -19,6 +19,7 @@ from app.schemas.response_schema import ApiResponse
|
|||||||
logger = get_api_logger()
|
logger = get_api_logger()
|
||||||
|
|
||||||
router = APIRouter(prefix="/tenant", tags=["Tenant"])
|
router = APIRouter(prefix="/tenant", tags=["Tenant"])
|
||||||
|
public_router = APIRouter(tags=["Tenant"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/subscription", response_model=ApiResponse, summary="获取当前用户所属租户的套餐信息")
|
@router.get("/subscription", response_model=ApiResponse, summary="获取当前用户所属租户的套餐信息")
|
||||||
@@ -42,7 +43,41 @@ async def get_my_tenant_subscription(
|
|||||||
sub = svc.get_subscription(tenant_id)
|
sub = svc.get_subscription(tenant_id)
|
||||||
|
|
||||||
if not sub:
|
if not sub:
|
||||||
return success(data=None, msg="暂无有效套餐")
|
# 无订阅记录时,兜底返回免费套餐信息
|
||||||
|
free_plan = svc.plan_repo.get_free_plan()
|
||||||
|
if not free_plan:
|
||||||
|
return success(data=None, msg="暂无有效套餐")
|
||||||
|
return success(data={
|
||||||
|
"subscription_id": None,
|
||||||
|
"tenant_id": str(tenant_id),
|
||||||
|
"package_plan_id": str(free_plan.id),
|
||||||
|
"package_version": free_plan.version,
|
||||||
|
"package_plan": {
|
||||||
|
"id": str(free_plan.id),
|
||||||
|
"name": free_plan.name,
|
||||||
|
"name_en": free_plan.name_en,
|
||||||
|
"version": free_plan.version,
|
||||||
|
"category": free_plan.category,
|
||||||
|
"tier_level": free_plan.tier_level,
|
||||||
|
"price": float(free_plan.price) if free_plan.price is not None else 0.0,
|
||||||
|
"billing_cycle": free_plan.billing_cycle,
|
||||||
|
"core_value": free_plan.core_value,
|
||||||
|
"core_value_en": free_plan.core_value_en,
|
||||||
|
"tech_support": free_plan.tech_support,
|
||||||
|
"tech_support_en": free_plan.tech_support_en,
|
||||||
|
"sla_compliance": free_plan.sla_compliance,
|
||||||
|
"sla_compliance_en": free_plan.sla_compliance_en,
|
||||||
|
"page_customization": free_plan.page_customization,
|
||||||
|
"page_customization_en": free_plan.page_customization_en,
|
||||||
|
"theme_color": free_plan.theme_color,
|
||||||
|
},
|
||||||
|
"started_at": None,
|
||||||
|
"expired_at": None,
|
||||||
|
"status": "active",
|
||||||
|
"quota": free_plan.quotas or {},
|
||||||
|
"created_at": int(datetime.datetime.utcnow().timestamp() * 1000),
|
||||||
|
"updated_at": int(datetime.datetime.utcnow().timestamp() * 1000),
|
||||||
|
}, msg="免费套餐")
|
||||||
|
|
||||||
return success(data=svc.build_response(sub))
|
return success(data=svc.build_response(sub))
|
||||||
|
|
||||||
@@ -62,11 +97,21 @@ async def get_my_tenant_subscription(
|
|||||||
"package_plan": {
|
"package_plan": {
|
||||||
"id": None,
|
"id": None,
|
||||||
"name": plan["name"],
|
"name": plan["name"],
|
||||||
|
"name_en": plan.get("name_en"),
|
||||||
"version": plan["version"],
|
"version": plan["version"],
|
||||||
"category": plan["category"],
|
"category": plan["category"],
|
||||||
"tier_level": plan["tier_level"],
|
"tier_level": plan["tier_level"],
|
||||||
"price": float(plan["price"]),
|
"price": float(plan["price"]),
|
||||||
"billing_cycle": plan["billing_cycle"],
|
"billing_cycle": plan["billing_cycle"],
|
||||||
|
"core_value": plan.get("core_value"),
|
||||||
|
"core_value_en": plan.get("core_value_en"),
|
||||||
|
"tech_support": plan.get("tech_support"),
|
||||||
|
"tech_support_en": plan.get("tech_support_en"),
|
||||||
|
"sla_compliance": plan.get("sla_compliance"),
|
||||||
|
"sla_compliance_en": plan.get("sla_compliance_en"),
|
||||||
|
"page_customization": plan.get("page_customization"),
|
||||||
|
"page_customization_en": plan.get("page_customization_en"),
|
||||||
|
"theme_color": plan.get("theme_color"),
|
||||||
},
|
},
|
||||||
"started_at": None,
|
"started_at": None,
|
||||||
"expired_at": None,
|
"expired_at": None,
|
||||||
@@ -80,3 +125,49 @@ async def get_my_tenant_subscription(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取租户套餐信息失败: {e}", exc_info=True)
|
logger.error(f"获取租户套餐信息失败: {e}", exc_info=True)
|
||||||
return JSONResponse(status_code=500, content=fail(code=500, msg="获取套餐信息失败"))
|
return JSONResponse(status_code=500, content=fail(code=500, msg="获取套餐信息失败"))
|
||||||
|
|
||||||
|
|
||||||
|
@public_router.get("/package-plans", response_model=ApiResponse, summary="获取套餐列表(公开)")
|
||||||
|
async def list_package_plans_public(
|
||||||
|
category: Optional[str] = None,
|
||||||
|
status: Optional[bool] = None,
|
||||||
|
search: Optional[str] = None,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
公开接口,无需鉴权。
|
||||||
|
SaaS 版从数据库读取套餐列表;社区版降级返回 default_free_plan.py 中的免费套餐。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from premium.platform_admin.package_plan_service import PackagePlanService
|
||||||
|
from premium.platform_admin.package_plan_schema import PackagePlanResponse
|
||||||
|
svc = PackagePlanService(db)
|
||||||
|
result = svc.get_list(page=1, size=9999, category=category, status=status, search=search)
|
||||||
|
return success(data=[PackagePlanResponse.model_validate(p).model_dump(mode="json") for p in result["items"]])
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
from app.config.default_free_plan import DEFAULT_FREE_PLAN
|
||||||
|
plan = DEFAULT_FREE_PLAN
|
||||||
|
return success(data=[{
|
||||||
|
"id": None,
|
||||||
|
"name": plan["name"],
|
||||||
|
"name_en": plan.get("name_en"),
|
||||||
|
"version": plan["version"],
|
||||||
|
"category": plan["category"],
|
||||||
|
"tier_level": plan["tier_level"],
|
||||||
|
"price": float(plan["price"]),
|
||||||
|
"billing_cycle": plan["billing_cycle"],
|
||||||
|
"core_value": plan.get("core_value"),
|
||||||
|
"core_value_en": plan.get("core_value_en"),
|
||||||
|
"tech_support": plan.get("tech_support"),
|
||||||
|
"tech_support_en": plan.get("tech_support_en"),
|
||||||
|
"sla_compliance": plan.get("sla_compliance"),
|
||||||
|
"sla_compliance_en": plan.get("sla_compliance_en"),
|
||||||
|
"page_customization": plan.get("page_customization"),
|
||||||
|
"page_customization_en": plan.get("page_customization_en"),
|
||||||
|
"theme_color": plan.get("theme_color"),
|
||||||
|
"status": plan.get("status", True),
|
||||||
|
"quota": plan["quotas"],
|
||||||
|
}])
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取套餐列表失败: {e}", exc_info=True)
|
||||||
|
return JSONResponse(status_code=500, content=fail(code=500, msg="获取套餐列表失败"))
|
||||||
|
|||||||
@@ -55,6 +55,18 @@ def _get_tenant_id_from_kwargs(db: Session, kwargs: dict):
|
|||||||
if workspace:
|
if workspace:
|
||||||
return workspace.tenant_id
|
return workspace.tenant_id
|
||||||
|
|
||||||
|
share_data = kwargs.get("share_data")
|
||||||
|
if share_data and hasattr(share_data, 'share_token'):
|
||||||
|
from app.models.workspace_model import Workspace
|
||||||
|
from app.models.app_model import App
|
||||||
|
share_token = share_data.share_token
|
||||||
|
from app.models.release_share_model import ReleaseShare
|
||||||
|
share_record = db.query(ReleaseShare).filter(ReleaseShare.share_token == share_token).first()
|
||||||
|
if share_record:
|
||||||
|
app = db.query(App).filter(App.id == share_record.app_id, App.is_active.is_(True)).first()
|
||||||
|
if app:
|
||||||
|
return app.workspace.tenant_id
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1452,6 +1452,32 @@ class AppService:
|
|||||||
logger.debug("配置不存在,返回默认模板", extra={"app_id": str(app_id)})
|
logger.debug("配置不存在,返回默认模板", extra={"app_id": str(app_id)})
|
||||||
return self._create_default_agent_config(app_id)
|
return self._create_default_agent_config(app_id)
|
||||||
|
|
||||||
|
def get_default_model_parameters(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
app_id: uuid.UUID,
|
||||||
|
) -> "ModelParameters":
|
||||||
|
"""获取 Agent 默认模型参数(不修改数据库)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_id: 应用ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ModelParameters: 默认模型参数
|
||||||
|
"""
|
||||||
|
logger.info("获取 Agent 默认模型参数", extra={"app_id": str(app_id)})
|
||||||
|
|
||||||
|
app = self._get_app_or_404(app_id)
|
||||||
|
|
||||||
|
if app.type != "agent":
|
||||||
|
raise BusinessException("只有 Agent 类型应用支持 Agent 配置", BizCode.APP_TYPE_NOT_SUPPORTED)
|
||||||
|
|
||||||
|
from app.schemas.app_schema import ModelParameters
|
||||||
|
default_model_parameters = ModelParameters()
|
||||||
|
|
||||||
|
logger.info("获取 Agent 默认模型参数成功", extra={"app_id": str(app_id)})
|
||||||
|
return default_model_parameters
|
||||||
|
|
||||||
def _create_default_agent_config(self, app_id: uuid.UUID) -> AgentConfig:
|
def _create_default_agent_config(self, app_id: uuid.UUID) -> AgentConfig:
|
||||||
"""创建默认的 Agent 配置模板(不保存到数据库)
|
"""创建默认的 Agent 配置模板(不保存到数据库)
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ import uuid
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.models.user_model import User
|
from app.models.user_model import User
|
||||||
from app.models.knowledge_model import Knowledge
|
from app.models.knowledge_model import Knowledge
|
||||||
|
from app.models.workspace_model import Workspace
|
||||||
|
from app.models.models_model import ModelConfig
|
||||||
from app.schemas.knowledge_schema import KnowledgeCreate, KnowledgeUpdate
|
from app.schemas.knowledge_schema import KnowledgeCreate, KnowledgeUpdate
|
||||||
from app.repositories import knowledge_repository
|
from app.repositories import knowledge_repository
|
||||||
from app.core.logging_config import get_business_logger
|
from app.core.logging_config import get_business_logger
|
||||||
|
from app.repositories.model_repository import ModelConfigRepository
|
||||||
|
from app.models.models_model import ModelType
|
||||||
|
|
||||||
# Obtain a dedicated logger for business logic
|
|
||||||
business_logger = get_business_logger()
|
business_logger = get_business_logger()
|
||||||
|
|
||||||
|
|
||||||
@@ -67,6 +70,50 @@ def create_knowledge(
|
|||||||
knowledge.workspace_id = current_user.current_workspace_id
|
knowledge.workspace_id = current_user.current_workspace_id
|
||||||
if knowledge.parent_id is None:
|
if knowledge.parent_id is None:
|
||||||
knowledge.parent_id = knowledge.workspace_id
|
knowledge.parent_id = knowledge.workspace_id
|
||||||
|
|
||||||
|
workspace = db.query(Workspace).filter(Workspace.id == knowledge.workspace_id).first()
|
||||||
|
if not workspace:
|
||||||
|
raise Exception(f"Workspace {knowledge.workspace_id} not found")
|
||||||
|
|
||||||
|
tenant_id = workspace.tenant_id
|
||||||
|
|
||||||
|
if not knowledge.embedding_id:
|
||||||
|
embedding_models = ModelConfigRepository.get_by_type(
|
||||||
|
db=db, model_types=[ModelType.EMBEDDING], tenant_id=tenant_id, is_active=True
|
||||||
|
)
|
||||||
|
if embedding_models:
|
||||||
|
knowledge.embedding_id = embedding_models[0].id
|
||||||
|
business_logger.debug(f"Auto-bind embedding model: {embedding_models[0].id}")
|
||||||
|
|
||||||
|
if not knowledge.reranker_id:
|
||||||
|
rerank_models = ModelConfigRepository.get_by_type(
|
||||||
|
db=db, model_types=[ModelType.RERANK], tenant_id=tenant_id, is_active=True
|
||||||
|
)
|
||||||
|
if rerank_models:
|
||||||
|
knowledge.reranker_id = rerank_models[0].id
|
||||||
|
business_logger.debug(f"Auto-bind rerank model: {rerank_models[0].id}")
|
||||||
|
|
||||||
|
if not knowledge.llm_id:
|
||||||
|
llm_models = ModelConfigRepository.get_by_type(
|
||||||
|
db=db, model_types=[ModelType.LLM, ModelType.CHAT], tenant_id=tenant_id, is_active=True
|
||||||
|
)
|
||||||
|
if llm_models:
|
||||||
|
knowledge.llm_id = llm_models[0].id
|
||||||
|
business_logger.debug(f"Auto-bind llm model: {llm_models[0].id}")
|
||||||
|
|
||||||
|
if not knowledge.image2text_id:
|
||||||
|
image2text_models = db.query(ModelConfig).filter(
|
||||||
|
ModelConfig.tenant_id == tenant_id,
|
||||||
|
ModelConfig.type.in_([ModelType.CHAT.value]),
|
||||||
|
ModelConfig.capability.contains(["vision"]),
|
||||||
|
ModelConfig.is_active == True,
|
||||||
|
ModelConfig.is_composite == False
|
||||||
|
).order_by(ModelConfig.created_at.desc()).all()
|
||||||
|
if not image2text_models:
|
||||||
|
raise Exception("租户下没有可用的视觉模型,创建知识库失败")
|
||||||
|
knowledge.image2text_id = image2text_models[0].id
|
||||||
|
business_logger.debug(f"Auto-bind image2text model: {image2text_models[0].id}")
|
||||||
|
|
||||||
business_logger.debug(f"Start creating the knowledge base: {knowledge.name}")
|
business_logger.debug(f"Start creating the knowledge base: {knowledge.name}")
|
||||||
db_knowledge = knowledge_repository.create_knowledge(
|
db_knowledge = knowledge_repository.create_knowledge(
|
||||||
db=db, knowledge=knowledge
|
db=db, knowledge=knowledge
|
||||||
|
|||||||
Reference in New Issue
Block a user