diff --git a/api/app/config/default_free_plan.py b/api/app/config/default_free_plan.py index 23a3a10e..4b357236 100644 --- a/api/app/config/default_free_plan.py +++ b/api/app/config/default_free_plan.py @@ -5,6 +5,7 @@ DEFAULT_FREE_PLAN = { "name": "记忆体验版", + "name_en": "Memory Experience", "category": "saas_personal", "tier_level": 0, "version": "1.0", @@ -12,9 +13,13 @@ DEFAULT_FREE_PLAN = { "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, diff --git a/api/app/controllers/__init__.py b/api/app/controllers/__init__.py index 377205c4..e9417d68 100644 --- a/api/app/controllers/__init__.py +++ b/api/app/controllers/__init__.py @@ -100,5 +100,6 @@ manager_router.include_router(ontology_controller.router) manager_router.include_router(skill_controller.router) manager_router.include_router(i18n_controller.router) manager_router.include_router(tenant_subscription_controller.router) +manager_router.include_router(tenant_subscription_controller.public_router) __all__ = ["manager_router"] diff --git a/api/app/controllers/tenant_subscription_controller.py b/api/app/controllers/tenant_subscription_controller.py index c3fde572..141d48a8 100644 --- a/api/app/controllers/tenant_subscription_controller.py +++ b/api/app/controllers/tenant_subscription_controller.py @@ -2,7 +2,7 @@ 租户套餐查询接口(普通用户可访问) """ import datetime -from typing import Callable +from typing import Callable, Optional from fastapi import APIRouter, Depends from fastapi.responses import JSONResponse @@ -19,6 +19,7 @@ from app.schemas.response_schema import ApiResponse logger = get_api_logger() router = APIRouter(prefix="/tenant", tags=["Tenant"]) +public_router = APIRouter(tags=["Tenant"]) @router.get("/subscription", response_model=ApiResponse, summary="获取当前用户所属租户的套餐信息") @@ -42,7 +43,41 @@ async def get_my_tenant_subscription( sub = svc.get_subscription(tenant_id) 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)) @@ -62,11 +97,21 @@ async def get_my_tenant_subscription( "package_plan": { "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"), }, "started_at": None, "expired_at": None, @@ -80,3 +125,49 @@ async def get_my_tenant_subscription( except Exception as e: logger.error(f"获取租户套餐信息失败: {e}", exc_info=True) 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="获取套餐列表失败")) diff --git a/api/app/services/knowledge_service.py b/api/app/services/knowledge_service.py index bac02e96..46108efd 100644 --- a/api/app/services/knowledge_service.py +++ b/api/app/services/knowledge_service.py @@ -2,11 +2,14 @@ import uuid from sqlalchemy.orm import Session from app.models.user_model import User 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.repositories import knowledge_repository 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() @@ -60,13 +63,57 @@ def create_knowledge( db: Session, knowledge: KnowledgeCreate, current_user: User ) -> Knowledge: business_logger.info(f"Create a knowledge base: {knowledge.name}, creator: {current_user.username}") - + try: knowledge.created_by = current_user.id if knowledge.workspace_id is None: knowledge.workspace_id = current_user.current_workspace_id if knowledge.parent_id is None: 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, ModelType.IMAGE.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}") db_knowledge = knowledge_repository.create_knowledge( db=db, knowledge=knowledge