From 6f3fb400a4c9ca4678718770153fd903e90fb8d6 Mon Sep 17 00:00:00 2001 From: QihuAI Design Date: Fri, 17 Jul 2026 12:09:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(market):=20skills/mcp=20=E8=83=BD=E5=8A=9B?= =?UTF-8?q?=E5=88=86=E7=B1=BB=20+=20AI=E9=AA=8C=E8=AF=81=E6=9C=9F=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E8=AF=86=E5=88=AB=20+=20=E5=AF=BC=E5=85=A5=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=85=A5=E5=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/python/app/api/market.py | 86 +- backend/python/app/models.py | 2247 ++++++++++--------- frontend/src/api/index.ts | 6 +- frontend/src/pages/front/MarketPage.css | 49 + frontend/src/pages/front/MarketPage.tsx | 148 +- frontend/src/types/index.ts | 1 + migrations/017_add_marketapp_capability.sql | 10 + 7 files changed, 1407 insertions(+), 1140 deletions(-) create mode 100644 migrations/017_add_marketapp_capability.sql diff --git a/backend/python/app/api/market.py b/backend/python/app/api/market.py index f4feb57..c185bcf 100644 --- a/backend/python/app/api/market.py +++ b/backend/python/app/api/market.py @@ -14,10 +14,11 @@ import os from ..database import get_db from ..models import ( User, MarketApp, AppPurchase, AppReview, AppInstallation, Order, - AdBanner, MCPServer, gen_uuid + AdBanner, MCPServer, ModelProvider, gen_uuid ) from ..auth import get_current_user, get_current_user_id, get_current_user_id_optional from ..middleware.log_service import LogService +from ..capability_registry import CAPABILITY_TYPES, resolve_auto_model as _resolve_auto_model from app.api.admin.mcp_admin import _test_mcp_server, parse_mcp_config router = APIRouter() @@ -206,6 +207,53 @@ def verify_agent_connectivity(platform: str, endpoint: str, api_key: str) -> dic return {"ok": False, "verified": False, "detail": f"地址不可达:{_short_err(e)}"} +# ===== 能力分类(AI 自动归类,任何失败都返回 None,不阻断导入) ===== +def _classify_capability(name: str, description: Optional[str], content: Optional[str], db: Session) -> Optional[str]: + """用平台文本模型把应用归类到 11 种能力之一;失败返回 None(不阻断导入)。""" + try: + text_model = _resolve_auto_model("text", db) + if not text_model: + return None + provider = None + if getattr(text_model, "provider_id", None): + provider = db.query(ModelProvider).filter(ModelProvider.id == text_model.provider_id).first() + api_key = text_model.api_key or (provider.api_key if provider else None) + if not api_key: + return None + api_url = text_model.endpoint or ( + f"{provider.api_base_url}/v1/chat/completions" if provider and provider.api_base_url else None + ) + if not api_url: + return None + options = "、".join(f"{c['id']}({c['label']})" for c in CAPABILITY_TYPES) + sys_prompt = ( + "你是应用市场的能力分类器。根据应用名称、描述与内容,把它精确归类到下列能力之一:" + f"{options}。\n只返回一个 JSON 对象:{{\"capability\": \"\"}},不要任何额外解释或 Markdown。" + ) + user_text = f"名称:{name}\n描述:{description or ''}\n内容摘要:{(content or '')[:2000]}" + messages = [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": user_text}, + ] + proxy = _skill_http_proxy() + with httpx.Client(timeout=25, follow_redirects=True, trust_env=False, proxy=proxy) as c: + r = c.post( + api_url, + headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, + json={"model": text_model.name, "messages": messages, "max_tokens": 64, "temperature": 0, "enable_thinking": False}, + ) + if r.status_code != 200: + return None + data = r.json() + text_out = data.get("choices", [{}])[0].get("message", {}).get("content", "") + m = re.search(r'"capability"\s*:\s*"([a-z_]+)"', text_out) + if m and m.group(1) in {c["id"] for c in CAPABILITY_TYPES}: + return m.group(1) + return None + except Exception: + return None + + class VerifyAgentRequest(BaseModel): platform: Optional[str] = None endpoint: str @@ -222,6 +270,25 @@ def verify_agent( return {"code": 0, "data": result, "message": "ok"} +class DetectCapabilityRequest(BaseModel): + name: str + description: Optional[str] = None + skill_content: Optional[str] = None + type: Optional[str] = None + + +@router.post("/apps/detect-capability") +def detect_capability( + req: DetectCapabilityRequest, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """导入前 AI 验证阶段:根据名称/描述/内容识别能力分类,供前台预填并允许用户修改。""" + cap = _classify_capability(req.name, req.description, req.skill_content, db) + label = next((c["label"] for c in CAPABILITY_TYPES if c["id"] == cap), None) + return {"code": 0, "data": {"capability": cap, "label": label}, "message": "ok"} + + @router.get("/ads") def list_public_ads(db: Session = Depends(get_db)): """ @@ -260,6 +327,7 @@ class UploadAppRequest(BaseModel): download_url: Optional[str] = None skill_content: Optional[str] = None category: Optional[str] = None + capability: Optional[str] = None tags: Optional[str] = None is_member_free: Optional[bool] = False join_promotion: Optional[bool] = False @@ -281,7 +349,7 @@ class ReviewAppRequest(BaseModel): def list_apps( type: Optional[str] = Query(None), source: Optional[str] = Query(None), - category: Optional[str] = Query(None), + capability: Optional[str] = Query(None), search: Optional[str] = Query(None), sort: str = Query(default="latest"), page: int = Query(default=1, ge=1), @@ -295,8 +363,8 @@ def list_apps( query = query.filter(MarketApp.type == type) if source: query = query.filter(MarketApp.source == source) - if category: - query = query.filter(MarketApp.type == category) + if capability: + query = query.filter(MarketApp.capability == capability) if search: query = query.filter( and_( @@ -346,6 +414,7 @@ def list_apps( "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, + "capability": app.capability or None, "installed": (app.id in installed_app_ids or is_user_owned) if user_id else False, } result.append(item) @@ -400,8 +469,9 @@ def get_app( "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, + "download_url": getattr(app, "download_url", None), + "detail": getattr(app, "detail", None), + "capability": app.capability or None, "created_at": str(app.created_at), "purchased": purchased, "installed": installed, @@ -456,6 +526,9 @@ def upload_app( created_at=datetime.now(), updated_at=datetime.now(), ) + # 能力分类:前端显式指定优先;否则导入阶段由 AI 自动归类;都不行则留空(不阻断导入) + _classify_content = req.skill_content or req.download_url or "" + app.capability = req.capability or _classify_capability(req.name, req.description, _classify_content, db) or None db.add(app) db.commit() db.refresh(app) @@ -536,6 +609,7 @@ def upload_app( "code": 0, "data": { "id": app.id, "name": app.name, "status": app.status, + "capability": app.capability, "created_at": str(app.created_at), }, "message": "提交成功,等待审核", diff --git a/backend/python/app/models.py b/backend/python/app/models.py index 8fa005e..6cf665d 100644 --- a/backend/python/app/models.py +++ b/backend/python/app/models.py @@ -1,1123 +1,1124 @@ -""" -AIFlow 数据库模型定义 - SQLAlchemy ORM -所有用户数据查询必须带 user_id 过滤实现物理隔离 -""" -import uuid -from datetime import datetime, date -from sqlalchemy import ( - Column, String, Integer, BigInteger, Float, Boolean, - DateTime, Date, Text, Enum, ForeignKey, Index, JSON, - DECIMAL, UniqueConstraint -) -from sqlalchemy.orm import relationship -from .database import Base - - -def gen_uuid() -> str: - return str(uuid.uuid4()) - - -# ============================================================ -# 用户与权限 -# ============================================================ - -class User(Base): - __tablename__ = "users" - - id = Column(String(36), primary_key=True, default=gen_uuid) - username = Column(String(50), unique=True, nullable=False) - email = Column(String(100), default=None) - phone = Column(String(20), default=None) - password_hash = Column(String(255), nullable=False) - avatar = Column(String(500), default=None) - role = Column(String(10), nullable=False, default="user") # user / admin - status = Column(String(10), nullable=False, default="active") # active / banned - real_name_status = Column(String(20), nullable=False, default="unverified") - real_name = Column(String(50), default=None) - id_card = Column(String(20), default=None) - membership_plan_id = Column(String(36), default=None) - membership_level = Column(Integer, default=0) - membership_expire_at = Column(DateTime, default=None) - auto_renew = Column(Boolean, default=False) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class LoginLog(Base): - __tablename__ = "login_logs" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - ip = Column(String(45), default=None) - user_agent = Column(String(500), default=None) - login_at = Column(DateTime, default=datetime.now) - - -class ApiKey(Base): - __tablename__ = "api_keys" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - api_key = Column(String(64), unique=True, nullable=False) - secret = Column(String(128), nullable=False) - name = Column(String(100), default=None) - status = Column(String(10), nullable=False, default="active") # active / revoked - last_used_at = Column(DateTime, default=None) - created_at = Column(DateTime, default=datetime.now) - expired_at = Column(DateTime, default=None) - description = Column(String(500), default=None, comment="密钥描述") - rate_limit = Column(Integer, default=60, comment="速率限制(请求/分钟)") - total_calls = Column(BigInteger, default=0, comment="总调用次数") - - -# ============================================================ -# AI聊天与频道 -# ============================================================ - -class Conversation(Base): - __tablename__ = "conversations" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - title = Column(String(200), nullable=False, default="新对话") - model_id = Column(String(36), default=None) - status = Column(String(10), nullable=False, default="active") # active / archived - pinned = Column(Boolean, default=False) - knowledge_graph_count = Column(Integer, default=0) - knowledge_graph_base_id = Column(String(64), default=None, comment="已生成的知识图谱知识库ID") - source = Column(String(32), default=None, comment="对话来源,如 wechat_bot") - ext_agent_conversation_id = Column(String(128), default=None, comment="外部Agent(Dify/Coze)会话ID,用于维持多轮上下文") - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class Message(Base): - __tablename__ = "messages" - - id = Column(String(36), primary_key=True, default=gen_uuid) - conversation_id = Column(String(36), ForeignKey("conversations.id"), nullable=False) - role = Column(String(10), nullable=False) # user / assistant / system - content = Column(Text, nullable=False) - token_used = Column(Integer, default=0) - model_id = Column(String(36), default=None) - knowledge_ids = Column(JSON, default=None) - agent_ids = Column(JSON, default=None) - skill_ids = Column(JSON, default=None) - mcp_ids = Column(JSON, default=None, comment="该轮对话选择的MCP应用ID列表") - attachments = Column(JSON, default=None, comment="该轮对话附带的文件附件列表") - used_skills = Column(JSON, default=None, comment="该轮对话调用的进化技能名列表") - created_at = Column(DateTime, default=datetime.now) - - -class Channel(Base): - __tablename__ = "channels" - - id = Column(String(36), primary_key=True, default=gen_uuid) - owner_id = Column(String(36), ForeignKey("users.id"), nullable=False) - name = Column(String(100), nullable=False) - description = Column(Text, default=None) - platform = Column(String(20), nullable=False) - webhook_url = Column(String(500), default=None) - credential = Column(JSON, default=None) - oauth_state = Column(String(500), default=None) - settings = Column(JSON, default=None) - account_name = Column(String(100), default=None) - default_agent_id = Column(String(36), default=None) - default_skill_id = Column(String(36), default=None) - knowledge_base_id = Column(String(36), default=None) - bound_model_id = Column(String(36), default=None) - features = Column(JSON, default=None) - status = Column(String(20), nullable=False, default="pending") - is_active = Column(Boolean, default=True) - last_active = Column(DateTime, default=None) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class ChannelMember(Base): - __tablename__ = "channel_members" - __table_args__ = (UniqueConstraint("channel_id", "user_id"),) - - id = Column(String(36), primary_key=True, default=gen_uuid) - channel_id = Column(String(36), ForeignKey("channels.id"), nullable=False) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - role = Column(String(10), nullable=False, default="member") # owner / member - joined_at = Column(DateTime, default=datetime.now) - - -class ChannelMessage(Base): - __tablename__ = "channel_messages" - - id = Column(String(36), primary_key=True, default=gen_uuid) - channel_id = Column(String(36), ForeignKey("channels.id"), nullable=False) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - role = Column(String(10), nullable=False) - content = Column(Text, nullable=False) - token_used = Column(Integer, default=0) - created_at = Column(DateTime, default=datetime.now) - - -# ============================================================ -# 知识库 -# ============================================================ - -class KnowledgeBase(Base): - __tablename__ = "knowledge_bases" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - name = Column(String(200), nullable=False) - description = Column(Text, default=None) - directory_path = Column(String(500), default="/") - document_count = Column(Integer, default=0) - knowledge_graph_count = Column(Integer, default=0) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - is_official = Column(Boolean, default=False) - status = Column(String(20), default="active") - external_service_id = Column(String(36), default=None) - external_id = Column(String(100), default=None) - external_sync_status = Column(String(20), default="none") - external_config = Column(JSON, default=None) - - -class KnowledgeDirectory(Base): - __tablename__ = "knowledge_directories" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - knowledge_base_id = Column(String(36), ForeignKey("knowledge_bases.id"), default=None) - parent_id = Column(String(36), default=None) - name = Column(String(200), nullable=False) - created_at = Column(DateTime, default=datetime.now) - - -class KnowledgeDocument(Base): - __tablename__ = "knowledge_documents" - - id = Column(String(36), primary_key=True, default=gen_uuid) - knowledge_base_id = Column(String(36), ForeignKey("knowledge_bases.id"), nullable=False) - directory_id = Column(String(36), ForeignKey("knowledge_directories.id"), default=None) - filename = Column(String(255), nullable=False) - file_type = Column(String(20), nullable=False) - file_size = Column(BigInteger, default=0) - file_path = Column(String(500), default=None) - status = Column(String(20), nullable=False, default="uploading") - chunk_count = Column(Integer, default=0) - error_message = Column(Text, default=None) - content = Column(Text, default=None) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - - - -class KnowledgeDocumentChunk(Base): - """知识库文档分段表 — Stage C.1""" - __tablename__ = "knowledge_document_chunks" - - id = Column(String(36), primary_key=True, default=gen_uuid) - document_id = Column(String(36), ForeignKey("knowledge_documents.id"), nullable=False) - knowledge_base_id = Column(String(36), ForeignKey("knowledge_bases.id"), nullable=False) - chunk_index = Column(Integer, nullable=False, default=0, comment="分段序号") - content = Column(Text, nullable=False, comment="分段文本内容") - start_char = Column(Integer, default=0, comment="起始字符位置") - end_char = Column(Integer, default=0, comment="结束字符位置") - chunk_metadata = Column(JSON, default=None, comment="分段元数据(标题、关键词等)") - token_count = Column(Integer, default=0, comment="分段token数估算") - created_at = Column(DateTime, default=datetime.now) - - __table_args__ = ( - Index("idx_kdc_doc_id", "document_id"), - Index("idx_kdc_kb_id", "knowledge_base_id"), - ) - -class KnowledgeGraphNode(Base): - __tablename__ = "knowledge_graph_nodes" - - id = Column(String(36), primary_key=True, default=gen_uuid) - knowledge_base_id = Column(String(36), ForeignKey("knowledge_bases.id"), nullable=False) - source_doc_id = Column(String(36), ForeignKey("knowledge_documents.id"), default=None) - label = Column(String(500), nullable=False) - type = Column(String(50), nullable=False, comment="entity/attribute/concept/event") - properties = Column(JSON, default=None) - description = Column(Text, default=None) - created_at = Column(DateTime, default=datetime.now) - - -class KnowledgeGraphEdge(Base): - __tablename__ = "knowledge_graph_edges" - - id = Column(String(36), primary_key=True, default=gen_uuid) - knowledge_base_id = Column(String(36), ForeignKey("knowledge_bases.id"), nullable=False) - source_node_id = Column(String(36), ForeignKey("knowledge_graph_nodes.id"), nullable=False) - target_node_id = Column(String(36), ForeignKey("knowledge_graph_nodes.id"), nullable=False) - relation = Column(String(200), nullable=False) - weight = Column(Float, default=1.0) - description = Column(Text, default=None) - created_at = Column(DateTime, default=datetime.now) - - -class KnowledgeGraphBuildLog(Base): - __tablename__ = "knowledge_graph_build_logs" - - id = Column(String(36), primary_key=True, default=gen_uuid) - knowledge_base_id = Column(String(36), ForeignKey("knowledge_bases.id"), nullable=False) - document_id = Column(String(36), ForeignKey("knowledge_documents.id"), nullable=False) - node_count = Column(Integer, default=0) - edge_count = Column(Integer, default=0) - status = Column(String(20), default="completed") - created_at = Column(DateTime, default=datetime.now) - - -# ============================================================ -# 应用市场 -# ============================================================ - -class MarketApp(Base): - __tablename__ = "market_apps" - - id = Column(String(36), primary_key=True, default=gen_uuid) - name = Column(String(200), nullable=False) - type = Column(String(10), nullable=False) # skill / agent / mcp - source = Column(String(10), nullable=False, default="user") # official / user - publisher_id = Column(String(36), ForeignKey("users.id"), default=None) - publisher_name = Column(String(100), default=None) - description = Column(Text, default=None) - icon = Column(String(500), default=None) - tutorial = Column(Text, default=None) - price = Column(DECIMAL(10, 2), nullable=False, default=0.00) - is_member_free = Column(Boolean, default=False) - join_promotion = Column(Boolean, default=False) - status = Column(String(10), nullable=False, default="pending") # pending / published / removed - rating_avg = Column(DECIMAL(3, 2), default=0.00) - review_count = Column(Integer, default=0) - install_count = Column(Integer, default=0) - skill_content = Column(Text, default=None) - mcp_transport = Column(String(20), default=None) - platform = Column(String(20), default=None, comment="Agent 接入平台:dify/n8n/coze") - agent_endpoint = Column(String(500), default=None) - agent_api_key = Column(String(500), default=None, comment="单个Agent接入的API Key") - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class AppPurchase(Base): - __tablename__ = "app_purchases" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - app_id = Column(String(36), ForeignKey("market_apps.id"), nullable=False) - price = Column(DECIMAL(10, 2), nullable=False, default=0.00) - order_id = Column(String(36), default=None) - status = Column(String(20), nullable=False, default="purchased") # purchased / installed - installed_at = Column(DateTime, default=None) - purchased_at = Column(DateTime, default=None) - created_at = Column(DateTime, default=datetime.now) - - -class AppReview(Base): - __tablename__ = "app_reviews" - __table_args__ = (UniqueConstraint("user_id", "app_id"),) - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - app_id = Column(String(36), ForeignKey("market_apps.id"), nullable=False) - rating = Column(Integer, nullable=False) - content = Column(Text, default=None) - created_at = Column(DateTime, default=datetime.now) - - -class AppInstallation(Base): - __tablename__ = "app_installations" - __table_args__ = (UniqueConstraint("user_id", "app_id"),) - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - app_id = Column(String(36), ForeignKey("market_apps.id"), nullable=False) - installed_at = Column(DateTime, default=datetime.now) - - -# ============================================================ -# 付费与订单 -# ============================================================ - -class MembershipPlan(Base): - __tablename__ = "membership_plans" - - id = Column(String(36), primary_key=True, default=gen_uuid) - name = Column(String(100), nullable=False) - level = Column(Integer, nullable=False, default=0) - duration_type = Column(String(10), nullable=False) # month / quarter / year - price = Column(DECIMAL(10, 2), nullable=False) - daily_free_tokens = Column(Integer, default=0) - features = Column(JSON, default=None) - free_app_ids = Column(JSON, default=None) - free_model_ids = Column(JSON, default=None) - # 配额字段 - max_agents = Column(Integer, default=5) - max_workflows = Column(Integer, default=3) - max_knowledge_bases = Column(Integer, default=2) - max_storage_mb = Column(Integer, default=1024) - api_calls_per_day = Column(Integer, default=100) - api_calls_per_min = Column(Integer, default=10) - # 权限字段 - can_publish_apps = Column(Boolean, default=False) - can_create_api_keys = Column(Boolean, default=False) - can_use_workflow = Column(Boolean, default=False) - can_use_mcp = Column(Boolean, default=False) - can_use_code_exec = Column(Boolean, default=False) - can_use_privacy_mode = Column(Boolean, default=False) - can_use_dag = Column(Boolean, default=False) - allowed_model_tiers = Column(JSON, default=None) - permissions = Column(JSON, default=None) - is_active = Column(Boolean, default=True) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class MembershipSubscription(Base): - __tablename__ = "membership_subscriptions" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - plan_id = Column(String(36), ForeignKey("membership_plans.id"), nullable=False) - order_id = Column(String(36), default=None) - start_at = Column(DateTime, nullable=False) - end_at = Column(DateTime, nullable=False) - auto_renew = Column(Boolean, default=False) - status = Column(String(15), nullable=False, default="active") # active / expired / downgraded - created_at = Column(DateTime, default=datetime.now) - - -class Order(Base): - __tablename__ = "orders" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - order_type = Column(String(15), nullable=False) # membership / service / app - item_id = Column(String(36), default=None) - item_name = Column(String(200), default=None) - amount = Column(DECIMAL(10, 2), nullable=False) - coupon_id = Column(String(36), default=None) - discount_amount = Column(DECIMAL(10, 2), default=0.00) - payment_method = Column(String(10), default=None) # wechat / alipay - payment_id = Column(String(200), default=None) - status = Column(String(15), nullable=False, default="pending") # pending / paid / refunded / cancelled - paid_at = Column(DateTime, default=None) - receipt_number = Column(String(100), default=None) - receipt_type = Column(String(20), default=None) - receipt_title = Column(String(200), default=None) - receipt_tax_number = Column(String(50), default=None) - receipt_issued_at = Column(DateTime, default=None) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class Coupon(Base): - __tablename__ = "coupons" - - id = Column(String(36), primary_key=True, default=gen_uuid) - code = Column(String(50), unique=True, nullable=False) - type = Column(String(10), nullable=False) # fixed / percent - value = Column(DECIMAL(10, 2), nullable=False) - min_amount = Column(DECIMAL(10, 2), default=0.00) - valid_from = Column(DateTime, nullable=False) - valid_to = Column(DateTime, nullable=False) - total_count = Column(Integer, default=None) - used_count = Column(Integer, default=0) - status = Column(String(10), nullable=False, default="active") # active / expired / disabled - created_at = Column(DateTime, default=datetime.now) - - -class UserCoupon(Base): - __tablename__ = "user_coupons" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - coupon_id = Column(String(36), ForeignKey("coupons.id"), nullable=False) - status = Column(String(10), nullable=False, default="unused") # unused / used / expired - used_at = Column(DateTime, default=None) - created_at = Column(DateTime, default=datetime.now) - - -class ValueAddedService(Base): - __tablename__ = "value_added_services" - - id = Column(String(36), primary_key=True, default=gen_uuid) - name = Column(String(200), nullable=False) - description = Column(Text, default=None) - price = Column(DECIMAL(10, 2), nullable=False) - type = Column(String(20), nullable=False) # app_custom / platform_proxy / data_custom / private_project - features = Column(JSON, default=None) # 服务特性 - status = Column(String(10), nullable=False, default="active") # active / inactive / deleted - assigned_agent_id = Column(String(36), default=None) # 应用定制服务绑定的Agent(市场应用ID) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class CustomServiceOrder(Base): - __tablename__ = "custom_service_orders" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - service_id = Column(String(36), default=None) - service_type = Column(String(10), nullable=False) - description = Column(Text, default=None) - status = Column(String(15), nullable=False, default="pending") - price = Column(DECIMAL(10, 2), default=0.00) - order_id = Column(String(36), default=None) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class CustomServiceRequest(Base): - """增值服务工单表 - 用于数据定制服务和私有化项目服务""" - __tablename__ = "custom_service_requests" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - service_id = Column(String(36), ForeignKey("value_added_services.id"), nullable=False) - request_type = Column(String(20), nullable=False) # data_custom / private_project - title = Column(String(200), nullable=False) - description = Column(Text, default=None) - requirements = Column(JSON, default=None) # 需求详情JSON - status = Column(String(20), nullable=False, default="pending") # pending / processing / completed / rejected - assigned_agent_id = Column(String(36), default=None) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -# ============================================================ -# Token与计费 -# ============================================================ - -class ModelProvider(Base): - __tablename__ = "model_providers" - - id = Column(String(36), primary_key=True, default=gen_uuid) - name = Column(String(100), nullable=False) - protocol = Column(String(30), nullable=False, default="openai_compatible") - api_key = Column(String(255), default=None) - api_base_url = Column(String(500), default=None) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class AIModel(Base): - __tablename__ = "models" - - id = Column(String(36), primary_key=True, default=gen_uuid) - provider_id = Column(String(36), ForeignKey("model_providers.id"), nullable=False) - name = Column(String(100), nullable=False) - display_name = Column(String(200), default=None, comment="display name") - api_key = Column(String(255), default=None) - endpoint = Column(String(500), default=None) - capabilities = Column(JSON, default=None) - timeout = Column(Integer, default=60) - status = Column(String(10), nullable=False, default="paid") # free / paid - price_per_million_tokens = Column(DECIMAL(10, 4), nullable=False, default=0.0000) - is_enabled = Column(Boolean, default=True) - is_default = Column(Boolean, default=False) # 全局默认模型 - total_calls = Column(BigInteger, default=0) - total_tokens = Column(BigInteger, default=0) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class TokenUsageLog(Base): - __tablename__ = "token_usage_logs" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - model_id = Column(String(36), default=None) - model_name = Column(String(100), default=None) - app_id = Column(String(36), default=None) - app_name = Column(String(200), default=None) - conversation_id = Column(String(36), default=None) - prompt_tokens = Column(Integer, default=0) - completion_tokens = Column(Integer, default=0) - total_tokens = Column(Integer, default=0) - cost = Column(DECIMAL(10, 6), default=0.000000) - created_at = Column(DateTime, default=datetime.now) - - -class TokenBalance(Base): - __tablename__ = "token_balances" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), unique=True, nullable=False) - total_consumed = Column(BigInteger, default=0) - balance = Column(BigInteger, default=0) - daily_free_used = Column(BigInteger, default=0) - daily_free_date = Column(Date, default=None) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -# ============================================================ -# 通知与日志 -# ============================================================ - -class Notification(Base): - __tablename__ = "notifications" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - type = Column(String(15), nullable=False) - title = Column(String(200), nullable=False) - content = Column(Text, default=None) - is_read = Column(Boolean, default=False) - created_at = Column(DateTime, default=datetime.now) - - -class OperationLog(Base): - __tablename__ = "operation_logs" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), nullable=False) - username = Column(String(50), default=None) - action = Column(String(100), nullable=False) - target = Column(String(100), default=None) - target_id = Column(String(36), default=None) - detail = Column(Text, default=None) - ip = Column(String(45), default=None) - created_at = Column(DateTime, default=datetime.now) - - -class AuditLog(Base): - __tablename__ = "audit_logs" - - id = Column(String(36), primary_key=True, default=gen_uuid) - admin_id = Column(String(36), nullable=False) - admin_name = Column(String(50), default=None) - action = Column(String(100), nullable=False) - target_type = Column(String(50), default=None) - target_id = Column(String(36), default=None) - detail = Column(Text, default=None) - created_at = Column(DateTime, default=datetime.now) - - -class RiskRule(Base): - __tablename__ = "risk_rules" - - id = Column(String(36), primary_key=True, default=gen_uuid) - name = Column(String(100), nullable=False) - type = Column(String(15), nullable=False) - threshold = Column(Integer, default=0) - duration = Column(Integer, default=3600) - is_enabled = Column(Boolean, default=True) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -# ============================================================ -# 系统配置 -# ============================================================ - -class SiteSetting(Base): - __tablename__ = "site_settings" - - id = Column(String(36), primary_key=True, default=gen_uuid) - key = Column(String(100), unique=True, nullable=False) - value = Column(Text, default=None) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class EmailConfig(Base): - __tablename__ = "email_configs" - - id = Column(String(36), primary_key=True, default=gen_uuid) - smtp_host = Column(String(200), nullable=False) - smtp_port = Column(Integer, nullable=False, default=587) - username = Column(String(200), nullable=False) - password = Column(String(200), nullable=False) - from_address = Column(String(200), nullable=False) - use_tls = Column(Boolean, default=True) - use_ssl = Column(Boolean, default=False) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class SmsConfig(Base): - __tablename__ = "sms_configs" - - id = Column(String(36), primary_key=True, default=gen_uuid) - provider = Column(String(50), nullable=False) - api_key = Column(String(200), nullable=False) - api_secret = Column(String(200), nullable=False) - sign_name = Column(String(100), default=None) - template_code = Column(String(100), default=None) - # 短信宝 (SMSBao) 字段 - username = Column(String(100), default=None) # 短信宝用户名 u - password = Column(String(200), default=None) # 短信宝密码(明文存储,发送时 MD5 32位) - goods_id = Column(String(100), default=None) # 短信宝专属通道产品 ID g - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class PaymentConfig(Base): - __tablename__ = "payment_configs" - - id = Column(String(36), primary_key=True, default=gen_uuid) - type = Column(String(10), nullable=False) # wechat / alipay - app_id = Column(String(100), nullable=False) - merchant_id = Column(String(100), default=None) # 微信: 商户号 - api_key = Column(String(255), nullable=False) # 微信: APIv2密钥 / 支付宝: 应用私钥 - api_v3_key = Column(String(255), default=None) # 微信: APIv3密钥 - cert_path = Column(String(500), default=None) # 微信: 证书路径 - cert_serial_no = Column(String(200), default=None) # 微信: 证书序列号 - alipay_public_key = Column(Text, default=None) # 支付宝: 支付宝公钥 - app_private_key = Column(Text, default=None) # 支付宝: 应用私钥 - gateway_url = Column(String(500), default=None) # 支付宝: 网关地址 - notify_url = Column(String(500), default=None) # 支付回调通知地址 - return_url = Column(String(500), default=None) # 支付宝: 同步跳转地址 - status = Column(String(10), nullable=False, default="inactive") # active / inactive - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class RegisterSetting(Base): - __tablename__ = "register_settings" - - id = Column(String(36), primary_key=True, default=gen_uuid) - phone_required = Column(Boolean, default=True) - phone_sms_required = Column(Boolean, default=True) - email_required = Column(Boolean, default=True) - email_verify_required = Column(Boolean, default=True) - real_name_required = Column(Boolean, default=False) - aliyun_access_key = Column(String(200), default=None) - aliyun_secret_key = Column(String(200), default=None) - aliyun_endpoint = Column(String(200), default=None) - sms_sign_name = Column(String(100), default=None) - sms_template_code = Column(String(50), default=None) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -# ============================================================ -# 广告位 -# ============================================================ - -class AdBanner(Base): - __tablename__ = "ad_banners" - - id = Column(String(36), primary_key=True, default=gen_uuid) - title = Column(String(200), nullable=False) - image_url = Column(String(500), default=None) - link_app_id = Column(String(36), default=None) - link_app_name = Column(String(200), default=None) - sort_order = Column(Integer, default=0) - click_count = Column(Integer, default=0) - status = Column(String(10), nullable=False, default="active") # active / inactive - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -# ============================================================ -# 资产管理 -# ============================================================ - -class AssetCategory(Base): - __tablename__ = "asset_categories" - - id = Column(String(36), primary_key=True, default=gen_uuid) - name = Column(String(100), nullable=False) - type = Column(String(10), nullable=False) - icon = Column(String(200), default=None) - color = Column(String(20), default=None) - is_default = Column(Boolean, default=False) - user_id = Column(String(36), default=None) - created_at = Column(DateTime, default=datetime.now) - - -class Asset(Base): - __tablename__ = "assets" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - name = Column(String(200), nullable=False) - category = Column(String(10), nullable=False) - sub_category = Column(String(50), default=None) - file_type = Column(String(50), default=None) - size = Column(BigInteger, default=0) - description = Column(Text, default=None) - cover_url = Column(String(500), default=None) - url = Column(String(500), default=None) - content = Column(Text, default=None) - novel_meta = Column(JSON, default=None) - status = Column(String(10), nullable=False, default="active") # active / deleted - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class NovelChapter(Base): - __tablename__ = "novel_chapters" - - id = Column(String(36), primary_key=True, default=gen_uuid) - asset_id = Column(String(36), ForeignKey("assets.id"), nullable=False) - title = Column(String(200), nullable=False) - word_count = Column(Integer, default=0) - sort_order = Column(Integer, default=0) - content = Column(Text(length=4294967295), default=None) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -# ============================================================ -# 预留扩展 -# ============================================================ - -class Team(Base): - __tablename__ = "teams" - - id = Column(String(36), primary_key=True, default=gen_uuid) - owner_id = Column(String(36), ForeignKey("users.id"), nullable=False) - name = Column(String(100), nullable=False) - description = Column(Text, default=None) - status = Column(String(10), nullable=False, default="active") - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class Referral(Base): - __tablename__ = "referrals" - - id = Column(String(36), primary_key=True, default=gen_uuid) - referrer_id = Column(String(36), ForeignKey("users.id"), nullable=False) - referee_id = Column(String(36), ForeignKey("users.id"), nullable=False) - referral_code = Column(String(50), default=None) - status = Column(String(10), nullable=False, default="pending") - commission_rate = Column(DECIMAL(5, 2), default=0.00) - created_at = Column(DateTime, default=datetime.now) - - -class OpenApiClient(Base): - __tablename__ = "open_api_clients" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - name = Column(String(100), nullable=False) - client_id = Column(String(64), unique=True, nullable=False) - client_secret = Column(String(128), nullable=False) - allowed_models = Column(JSON, default=None) - rate_limit = Column(Integer, default=60) - status = Column(String(10), nullable=False, default="active") - created_at = Column(DateTime, default=datetime.now) - total_calls = Column(BigInteger, default=0, comment="总调用次数") - last_used_at = Column(DateTime, default=None, comment="最后使用时间") - - -# ============================================================ -# API调用统计 -# ============================================================ - -class ApiCallStat(Base): - """API调用统计表 - 按端点聚合""" - __tablename__ = "api_call_stats" - - id = Column(String(36), primary_key=True, default=gen_uuid) - endpoint = Column(String(200), nullable=False) - method = Column(String(10), nullable=False) - call_count = Column(BigInteger, default=0) - total_response_time = Column(Float, default=0.0) - error_count = Column(BigInteger, default=0) - last_called_at = Column(DateTime, default=None) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - __table_args__ = ( - UniqueConstraint("endpoint", "method", name="uk_endpoint_method"), - ) - - -class SlowRequestLog(Base): - """慢请求记录表""" - __tablename__ = "slow_request_logs" - - id = Column(String(36), primary_key=True, default=gen_uuid) - request_id = Column(String(64), default=None) - method = Column(String(10), nullable=False) - endpoint = Column(String(500), nullable=False) - status_code = Column(Integer, default=0) - response_time = Column(Float, default=0.0) - threshold = Column(Float, default=3.0) - created_at = Column(DateTime, default=datetime.now) - - __table_args__ = ( - Index("idx_srl_created", "created_at"), - ) - - -# ============================================================ -# AI自进化引擎 - 用户进化配置 -# ============================================================ -class UserEvolutionConfig(Base): - __tablename__ = "user_evolution_config" - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), unique=True, nullable=False, index=True) - skill_dir_path = Column(String(500), nullable=True, comment="用户Skill仓库根目录") - trajectory_dir_path = Column(String(500), nullable=True, comment="用户对话轨迹目录") - watcher_status = Column(String(20), default="stopped", comment="运行中/已停止/错误") - watcher_error = Column(String(500), nullable=True, comment="watcher异常信息") - pipeline_status = Column(String(20), default="idle", comment="流水线状态: running/idle/error") - pipeline_processed = Column(Integer, default=0, comment="已处理轨迹数") - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class EvolutionSkill(Base): - __tablename__ = "evolution_skills" - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True) - skill_name = Column(String(200), nullable=False, comment="技能名称") - description = Column(Text, nullable=True) - tags = Column(JSON, nullable=True, comment="标签列表") - category = Column(String(100), nullable=True, comment="技能分类") - state = Column(String(20), default="baby", comment="baby/main/staging/frozen") - use_count = Column(Integer, default=0, comment="使用次数") - version = Column(Integer, default=1) - skill_md_path = Column(String(500), nullable=True, comment="SKILL.md文件路径") - git_commit_sha = Column(String(40), nullable=True) - last_used_at = Column(DateTime, nullable=True) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class EvolutionEvent(Base): - __tablename__ = "evolution_events" - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True) - skill_name = Column(String(200), nullable=False) - event_type = Column(String(50), nullable=False, comment="created/promoted/rejected/frozen/unfrozen/edited/deleted/milestone") - description = Column(Text, nullable=True) - version_before = Column(Integer, nullable=True) - version_after = Column(Integer, nullable=True) - created_at = Column(DateTime, default=datetime.now) - - -class EvolutionTrajectory(Base): - __tablename__ = "evolution_trajectories" - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True) - conversation_id = Column(String(36), nullable=True, comment="关联的对话ID") - file_path = Column(String(500), nullable=True, comment="轨迹文件路径") - status = Column(String(20), default="discovered", comment="discovered/splitting/split_done/indexed/clustering/done/error/filtered") - atom_count = Column(Integer, default=0) - token_used = Column(Integer, default=0) - error_msg = Column(String(500), nullable=True, comment="轨迹处理失败的错误信息") - retry_count = Column(Integer, default=0, comment="失败重试次数") - tasks_extracted = Column(Integer, default=0, comment="拆分的AtomTask数量") - created_at = Column(DateTime, default=datetime.now) - processed_at = Column(DateTime, nullable=True) - - -class SkillTrajectoryLink(Base): - """技能-轨迹关联表:记录技能与对话轨迹的关联关系""" - __tablename__ = "skill_trajectory_links" - id = Column(String(36), primary_key=True, default=gen_uuid) - skill_name = Column(String(200), nullable=False, index=True, comment="关联的技能名称") - user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True) - trajectory_id = Column(String(36), nullable=True, comment="关联的轨迹ID") - atom_id = Column(String(36), nullable=True, comment="原子任务ID") - weightscore = Column(Integer, default=0, comment="关联权重分") - created_at = Column(DateTime, default=datetime.now) - - -# ============================================================ -# AI自进化v2 - 新增表 -# ============================================================ - -class SkillVersion(Base): - """技能版本历史表:存储每个版本的SKILL.md完整内容,支持回滚""" - __tablename__ = "skill_versions" - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - skill_name = Column(String(200), nullable=False) - version = Column(Integer, nullable=False, default=1) - skill_md_content = Column(Text(4294967295), nullable=True, comment="该版本的SKILL.md完整内容") - git_commit_sha = Column(String(40), nullable=True) - description = Column(Text, nullable=True, comment="版本变更说明") - created_at = Column(DateTime, default=datetime.now) - - __table_args__ = ( - Index("idx_sv_user_skill", "user_id", "skill_name"), - Index("idx_sv_version", "version"), - ) - - -class EvolutionReport(Base): - """进化报告表:存储周报/月报""" - __tablename__ = "evolution_reports" - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - report_type = Column(String(20), nullable=False, comment="weekly / monthly") - period_start = Column(Date, nullable=False) - period_end = Column(Date, nullable=False) - report_data = Column(JSON, nullable=True, comment="报告详细数据(JSON)") - created_at = Column(DateTime, default=datetime.now) - - __table_args__ = ( - Index("idx_er_user_type", "user_id", "report_type"), - Index("idx_er_period", "period_start", "period_end"), - ) - - -class SkillPublishRecord(Base): - """技能市场发布记录表""" - __tablename__ = "skill_publish_records" - id = Column(String(36), primary_key=True, default=gen_uuid) - skill_id = Column(String(36), nullable=False, comment="关联evolution_skills.id") - skill_name = Column(String(200), nullable=False) - user_id = Column(String(36), ForeignKey("users.id"), nullable=False) - market_app_id = Column(String(36), nullable=True, comment="关联market_apps.id") - price = Column(DECIMAL(10, 2), default=0.00) - market_description = Column(Text, nullable=True) - status = Column(String(20), nullable=False, default="published", comment="published/unpublished") - published_at = Column(DateTime, default=datetime.now) - created_at = Column(DateTime, default=datetime.now) - - __table_args__ = ( - Index("idx_spr_skill_id", "skill_id"), - Index("idx_spr_user_id", "user_id"), - Index("idx_spr_status", "status"), - ) - - -class TeamSharedSkill(Base): - """团队共享技能表""" - __tablename__ = "team_shared_skills" - id = Column(String(36), primary_key=True, default=gen_uuid) - team_id = Column(String(36), ForeignKey("teams.id"), nullable=False) - skill_id = Column(String(36), nullable=False, comment="关联evolution_skills.id") - skill_name = Column(String(200), nullable=False) - shared_by = Column(String(36), ForeignKey("users.id"), nullable=False) - shared_by_name = Column(String(100), nullable=True, comment="分享者昵称") - is_public = Column(Boolean, default=True, comment="团队内是否可见") - created_at = Column(DateTime, default=datetime.now) - - __table_args__ = ( - UniqueConstraint("team_id", "skill_id", name="uk_tss_team_skill"), - ) - - -class ExternalKBService(Base): - """外部知识库服务配置""" - __tablename__ = "external_kb_services" - - id = Column(String(36), primary_key=True, default=gen_uuid) - name = Column(String(200), nullable=False) - service_type = Column(String(20), nullable=False) - api_url = Column(String(500), nullable=False) - api_key = Column(String(255), default="") - config = Column(JSON, default=None) - is_enabled = Column(Boolean, default=True) - status = Column(String(20), default="disconnected") - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - -class OSSStorageConfig(Base): - __tablename__ = "oss_storage_configs" - - id = Column(String(36), primary_key=True, default=gen_uuid) - name = Column(String(200), nullable=False) - provider = Column(String(20), nullable=False) - endpoint = Column(String(500), nullable=False) - access_key = Column(String(255), nullable=False) - secret_key = Column(String(255), nullable=False) - bucket = Column(String(200), nullable=False) - region = Column(String(100), default="") - base_path = Column(String(300), default="") - is_default = Column(Boolean, default=False) - is_enabled = Column(Boolean, default=True) - status = Column(String(20), default="disconnected") - config = Column(JSON, default=None) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - - -class UserStorageQuota(Base): - __tablename__ = "user_storage_quotas" - - id = Column(String(36), primary_key=True, default=gen_uuid) - user_id = Column(String(36), nullable=False, unique=True) - quota_bytes = Column(BigInteger, nullable=False, default=1073741824) - used_bytes = Column(BigInteger, nullable=False, default=0) - quota_type = Column(String(20), nullable=False, default="membership") - membership_level = Column(Integer, default=0) - plan_id = Column(String(36), default=None) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class AutomatedTask(Base): - __tablename__ = "automated_tasks" - - id = Column(String(36), primary_key=True, default=gen_uuid) - name = Column(String(200), nullable=False) - description = Column(Text, default="") - trigger_type = Column(String(20), nullable=False) - cron_expression = Column(String(100), default=None) - event_condition = Column(JSON, default=None) - action_type = Column(String(30), nullable=False) - action_config = Column(JSON, default=None) - is_enabled = Column(Boolean, default=True) - status = Column(String(20), default="idle") - max_retries = Column(Integer, default=3) - timeout_seconds = Column(Integer, default=300) - last_run_at = Column(DateTime, default=None) - next_run_at = Column(DateTime, default=None) - total_runs = Column(Integer, default=0) - success_runs = Column(Integer, default=0) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - -class TaskExecutionLog(Base): - __tablename__ = "task_execution_logs" - - id = Column(String(36), primary_key=True, default=gen_uuid) - task_id = Column(String(36), nullable=False) - task_name = Column(String(200), default="") - status = Column(String(20), default="running") - trigger_type = Column(String(20), default="manual") - started_at = Column(DateTime, default=None) - finished_at = Column(DateTime, default=None) - duration_ms = Column(Integer, default=0) - result_summary = Column(Text, default="") - error_message = Column(Text, default="") - created_at = Column(DateTime, default=datetime.now) - - - -class MCPServer(Base): - __tablename__ = "mcp_servers" - - id = Column(String(36), primary_key=True, default=gen_uuid) - name = Column(String(100), nullable=False) - description = Column(Text, default="") - transport_type = Column(String(20), default="stdio") - command = Column(String(500), default="") - args = Column(Text, default="") - env_vars = Column(Text, default="") - url = Column(String(500), default="") - headers = Column(Text, default="") - is_enabled = Column(Boolean, default=True) - status = Column(String(20), default="disconnected") - last_connected_at = Column(DateTime, default=None) - tools_count = Column(Integer, default=0) - resources_count = Column(Integer, default=0) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -class SmsVerificationCode(Base): - __tablename__ = "sms_verification_codes" - - id = Column(String(36), primary_key=True, default=gen_uuid) - phone = Column(String(20), nullable=False, index=True) - code = Column(String(10), nullable=False) - purpose = Column(String(30), nullable=False) # register / reset_password - expires_at = Column(DateTime, nullable=False) - used = Column(Boolean, default=False) - created_at = Column(DateTime, default=datetime.now) - +""" +AIFlow 数据库模型定义 - SQLAlchemy ORM +所有用户数据查询必须带 user_id 过滤实现物理隔离 +""" +import uuid +from datetime import datetime, date +from sqlalchemy import ( + Column, String, Integer, BigInteger, Float, Boolean, + DateTime, Date, Text, Enum, ForeignKey, Index, JSON, + DECIMAL, UniqueConstraint +) +from sqlalchemy.orm import relationship +from .database import Base + + +def gen_uuid() -> str: + return str(uuid.uuid4()) + + +# ============================================================ +# 用户与权限 +# ============================================================ + +class User(Base): + __tablename__ = "users" + + id = Column(String(36), primary_key=True, default=gen_uuid) + username = Column(String(50), unique=True, nullable=False) + email = Column(String(100), default=None) + phone = Column(String(20), default=None) + password_hash = Column(String(255), nullable=False) + avatar = Column(String(500), default=None) + role = Column(String(10), nullable=False, default="user") # user / admin + status = Column(String(10), nullable=False, default="active") # active / banned + real_name_status = Column(String(20), nullable=False, default="unverified") + real_name = Column(String(50), default=None) + id_card = Column(String(20), default=None) + membership_plan_id = Column(String(36), default=None) + membership_level = Column(Integer, default=0) + membership_expire_at = Column(DateTime, default=None) + auto_renew = Column(Boolean, default=False) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class LoginLog(Base): + __tablename__ = "login_logs" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + ip = Column(String(45), default=None) + user_agent = Column(String(500), default=None) + login_at = Column(DateTime, default=datetime.now) + + +class ApiKey(Base): + __tablename__ = "api_keys" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + api_key = Column(String(64), unique=True, nullable=False) + secret = Column(String(128), nullable=False) + name = Column(String(100), default=None) + status = Column(String(10), nullable=False, default="active") # active / revoked + last_used_at = Column(DateTime, default=None) + created_at = Column(DateTime, default=datetime.now) + expired_at = Column(DateTime, default=None) + description = Column(String(500), default=None, comment="密钥描述") + rate_limit = Column(Integer, default=60, comment="速率限制(请求/分钟)") + total_calls = Column(BigInteger, default=0, comment="总调用次数") + + +# ============================================================ +# AI聊天与频道 +# ============================================================ + +class Conversation(Base): + __tablename__ = "conversations" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + title = Column(String(200), nullable=False, default="新对话") + model_id = Column(String(36), default=None) + status = Column(String(10), nullable=False, default="active") # active / archived + pinned = Column(Boolean, default=False) + knowledge_graph_count = Column(Integer, default=0) + knowledge_graph_base_id = Column(String(64), default=None, comment="已生成的知识图谱知识库ID") + source = Column(String(32), default=None, comment="对话来源,如 wechat_bot") + ext_agent_conversation_id = Column(String(128), default=None, comment="外部Agent(Dify/Coze)会话ID,用于维持多轮上下文") + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class Message(Base): + __tablename__ = "messages" + + id = Column(String(36), primary_key=True, default=gen_uuid) + conversation_id = Column(String(36), ForeignKey("conversations.id"), nullable=False) + role = Column(String(10), nullable=False) # user / assistant / system + content = Column(Text, nullable=False) + token_used = Column(Integer, default=0) + model_id = Column(String(36), default=None) + knowledge_ids = Column(JSON, default=None) + agent_ids = Column(JSON, default=None) + skill_ids = Column(JSON, default=None) + mcp_ids = Column(JSON, default=None, comment="该轮对话选择的MCP应用ID列表") + attachments = Column(JSON, default=None, comment="该轮对话附带的文件附件列表") + used_skills = Column(JSON, default=None, comment="该轮对话调用的进化技能名列表") + created_at = Column(DateTime, default=datetime.now) + + +class Channel(Base): + __tablename__ = "channels" + + id = Column(String(36), primary_key=True, default=gen_uuid) + owner_id = Column(String(36), ForeignKey("users.id"), nullable=False) + name = Column(String(100), nullable=False) + description = Column(Text, default=None) + platform = Column(String(20), nullable=False) + webhook_url = Column(String(500), default=None) + credential = Column(JSON, default=None) + oauth_state = Column(String(500), default=None) + settings = Column(JSON, default=None) + account_name = Column(String(100), default=None) + default_agent_id = Column(String(36), default=None) + default_skill_id = Column(String(36), default=None) + knowledge_base_id = Column(String(36), default=None) + bound_model_id = Column(String(36), default=None) + features = Column(JSON, default=None) + status = Column(String(20), nullable=False, default="pending") + is_active = Column(Boolean, default=True) + last_active = Column(DateTime, default=None) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class ChannelMember(Base): + __tablename__ = "channel_members" + __table_args__ = (UniqueConstraint("channel_id", "user_id"),) + + id = Column(String(36), primary_key=True, default=gen_uuid) + channel_id = Column(String(36), ForeignKey("channels.id"), nullable=False) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + role = Column(String(10), nullable=False, default="member") # owner / member + joined_at = Column(DateTime, default=datetime.now) + + +class ChannelMessage(Base): + __tablename__ = "channel_messages" + + id = Column(String(36), primary_key=True, default=gen_uuid) + channel_id = Column(String(36), ForeignKey("channels.id"), nullable=False) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + role = Column(String(10), nullable=False) + content = Column(Text, nullable=False) + token_used = Column(Integer, default=0) + created_at = Column(DateTime, default=datetime.now) + + +# ============================================================ +# 知识库 +# ============================================================ + +class KnowledgeBase(Base): + __tablename__ = "knowledge_bases" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + name = Column(String(200), nullable=False) + description = Column(Text, default=None) + directory_path = Column(String(500), default="/") + document_count = Column(Integer, default=0) + knowledge_graph_count = Column(Integer, default=0) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + is_official = Column(Boolean, default=False) + status = Column(String(20), default="active") + external_service_id = Column(String(36), default=None) + external_id = Column(String(100), default=None) + external_sync_status = Column(String(20), default="none") + external_config = Column(JSON, default=None) + + +class KnowledgeDirectory(Base): + __tablename__ = "knowledge_directories" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + knowledge_base_id = Column(String(36), ForeignKey("knowledge_bases.id"), default=None) + parent_id = Column(String(36), default=None) + name = Column(String(200), nullable=False) + created_at = Column(DateTime, default=datetime.now) + + +class KnowledgeDocument(Base): + __tablename__ = "knowledge_documents" + + id = Column(String(36), primary_key=True, default=gen_uuid) + knowledge_base_id = Column(String(36), ForeignKey("knowledge_bases.id"), nullable=False) + directory_id = Column(String(36), ForeignKey("knowledge_directories.id"), default=None) + filename = Column(String(255), nullable=False) + file_type = Column(String(20), nullable=False) + file_size = Column(BigInteger, default=0) + file_path = Column(String(500), default=None) + status = Column(String(20), nullable=False, default="uploading") + chunk_count = Column(Integer, default=0) + error_message = Column(Text, default=None) + content = Column(Text, default=None) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + + + +class KnowledgeDocumentChunk(Base): + """知识库文档分段表 — Stage C.1""" + __tablename__ = "knowledge_document_chunks" + + id = Column(String(36), primary_key=True, default=gen_uuid) + document_id = Column(String(36), ForeignKey("knowledge_documents.id"), nullable=False) + knowledge_base_id = Column(String(36), ForeignKey("knowledge_bases.id"), nullable=False) + chunk_index = Column(Integer, nullable=False, default=0, comment="分段序号") + content = Column(Text, nullable=False, comment="分段文本内容") + start_char = Column(Integer, default=0, comment="起始字符位置") + end_char = Column(Integer, default=0, comment="结束字符位置") + chunk_metadata = Column(JSON, default=None, comment="分段元数据(标题、关键词等)") + token_count = Column(Integer, default=0, comment="分段token数估算") + created_at = Column(DateTime, default=datetime.now) + + __table_args__ = ( + Index("idx_kdc_doc_id", "document_id"), + Index("idx_kdc_kb_id", "knowledge_base_id"), + ) + +class KnowledgeGraphNode(Base): + __tablename__ = "knowledge_graph_nodes" + + id = Column(String(36), primary_key=True, default=gen_uuid) + knowledge_base_id = Column(String(36), ForeignKey("knowledge_bases.id"), nullable=False) + source_doc_id = Column(String(36), ForeignKey("knowledge_documents.id"), default=None) + label = Column(String(500), nullable=False) + type = Column(String(50), nullable=False, comment="entity/attribute/concept/event") + properties = Column(JSON, default=None) + description = Column(Text, default=None) + created_at = Column(DateTime, default=datetime.now) + + +class KnowledgeGraphEdge(Base): + __tablename__ = "knowledge_graph_edges" + + id = Column(String(36), primary_key=True, default=gen_uuid) + knowledge_base_id = Column(String(36), ForeignKey("knowledge_bases.id"), nullable=False) + source_node_id = Column(String(36), ForeignKey("knowledge_graph_nodes.id"), nullable=False) + target_node_id = Column(String(36), ForeignKey("knowledge_graph_nodes.id"), nullable=False) + relation = Column(String(200), nullable=False) + weight = Column(Float, default=1.0) + description = Column(Text, default=None) + created_at = Column(DateTime, default=datetime.now) + + +class KnowledgeGraphBuildLog(Base): + __tablename__ = "knowledge_graph_build_logs" + + id = Column(String(36), primary_key=True, default=gen_uuid) + knowledge_base_id = Column(String(36), ForeignKey("knowledge_bases.id"), nullable=False) + document_id = Column(String(36), ForeignKey("knowledge_documents.id"), nullable=False) + node_count = Column(Integer, default=0) + edge_count = Column(Integer, default=0) + status = Column(String(20), default="completed") + created_at = Column(DateTime, default=datetime.now) + + +# ============================================================ +# 应用市场 +# ============================================================ + +class MarketApp(Base): + __tablename__ = "market_apps" + + id = Column(String(36), primary_key=True, default=gen_uuid) + name = Column(String(200), nullable=False) + type = Column(String(10), nullable=False) # skill / agent / mcp + source = Column(String(10), nullable=False, default="user") # official / user + publisher_id = Column(String(36), ForeignKey("users.id"), default=None) + publisher_name = Column(String(100), default=None) + description = Column(Text, default=None) + icon = Column(String(500), default=None) + tutorial = Column(Text, default=None) + price = Column(DECIMAL(10, 2), nullable=False, default=0.00) + is_member_free = Column(Boolean, default=False) + join_promotion = Column(Boolean, default=False) + status = Column(String(10), nullable=False, default="pending") # pending / published / removed + rating_avg = Column(DECIMAL(3, 2), default=0.00) + review_count = Column(Integer, default=0) + install_count = Column(Integer, default=0) + skill_content = Column(Text, default=None) + mcp_transport = Column(String(20), default=None) + platform = Column(String(20), default=None, comment="Agent 接入平台:dify/n8n/coze") + capability = Column(String(20), default=None, comment="能力分类:text/thinking/vision/tool/image/audio/asr/ocr/embedding/rerank/video") + agent_endpoint = Column(String(500), default=None) + agent_api_key = Column(String(500), default=None, comment="单个Agent接入的API Key") + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class AppPurchase(Base): + __tablename__ = "app_purchases" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + app_id = Column(String(36), ForeignKey("market_apps.id"), nullable=False) + price = Column(DECIMAL(10, 2), nullable=False, default=0.00) + order_id = Column(String(36), default=None) + status = Column(String(20), nullable=False, default="purchased") # purchased / installed + installed_at = Column(DateTime, default=None) + purchased_at = Column(DateTime, default=None) + created_at = Column(DateTime, default=datetime.now) + + +class AppReview(Base): + __tablename__ = "app_reviews" + __table_args__ = (UniqueConstraint("user_id", "app_id"),) + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + app_id = Column(String(36), ForeignKey("market_apps.id"), nullable=False) + rating = Column(Integer, nullable=False) + content = Column(Text, default=None) + created_at = Column(DateTime, default=datetime.now) + + +class AppInstallation(Base): + __tablename__ = "app_installations" + __table_args__ = (UniqueConstraint("user_id", "app_id"),) + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + app_id = Column(String(36), ForeignKey("market_apps.id"), nullable=False) + installed_at = Column(DateTime, default=datetime.now) + + +# ============================================================ +# 付费与订单 +# ============================================================ + +class MembershipPlan(Base): + __tablename__ = "membership_plans" + + id = Column(String(36), primary_key=True, default=gen_uuid) + name = Column(String(100), nullable=False) + level = Column(Integer, nullable=False, default=0) + duration_type = Column(String(10), nullable=False) # month / quarter / year + price = Column(DECIMAL(10, 2), nullable=False) + daily_free_tokens = Column(Integer, default=0) + features = Column(JSON, default=None) + free_app_ids = Column(JSON, default=None) + free_model_ids = Column(JSON, default=None) + # 配额字段 + max_agents = Column(Integer, default=5) + max_workflows = Column(Integer, default=3) + max_knowledge_bases = Column(Integer, default=2) + max_storage_mb = Column(Integer, default=1024) + api_calls_per_day = Column(Integer, default=100) + api_calls_per_min = Column(Integer, default=10) + # 权限字段 + can_publish_apps = Column(Boolean, default=False) + can_create_api_keys = Column(Boolean, default=False) + can_use_workflow = Column(Boolean, default=False) + can_use_mcp = Column(Boolean, default=False) + can_use_code_exec = Column(Boolean, default=False) + can_use_privacy_mode = Column(Boolean, default=False) + can_use_dag = Column(Boolean, default=False) + allowed_model_tiers = Column(JSON, default=None) + permissions = Column(JSON, default=None) + is_active = Column(Boolean, default=True) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class MembershipSubscription(Base): + __tablename__ = "membership_subscriptions" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + plan_id = Column(String(36), ForeignKey("membership_plans.id"), nullable=False) + order_id = Column(String(36), default=None) + start_at = Column(DateTime, nullable=False) + end_at = Column(DateTime, nullable=False) + auto_renew = Column(Boolean, default=False) + status = Column(String(15), nullable=False, default="active") # active / expired / downgraded + created_at = Column(DateTime, default=datetime.now) + + +class Order(Base): + __tablename__ = "orders" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + order_type = Column(String(15), nullable=False) # membership / service / app + item_id = Column(String(36), default=None) + item_name = Column(String(200), default=None) + amount = Column(DECIMAL(10, 2), nullable=False) + coupon_id = Column(String(36), default=None) + discount_amount = Column(DECIMAL(10, 2), default=0.00) + payment_method = Column(String(10), default=None) # wechat / alipay + payment_id = Column(String(200), default=None) + status = Column(String(15), nullable=False, default="pending") # pending / paid / refunded / cancelled + paid_at = Column(DateTime, default=None) + receipt_number = Column(String(100), default=None) + receipt_type = Column(String(20), default=None) + receipt_title = Column(String(200), default=None) + receipt_tax_number = Column(String(50), default=None) + receipt_issued_at = Column(DateTime, default=None) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class Coupon(Base): + __tablename__ = "coupons" + + id = Column(String(36), primary_key=True, default=gen_uuid) + code = Column(String(50), unique=True, nullable=False) + type = Column(String(10), nullable=False) # fixed / percent + value = Column(DECIMAL(10, 2), nullable=False) + min_amount = Column(DECIMAL(10, 2), default=0.00) + valid_from = Column(DateTime, nullable=False) + valid_to = Column(DateTime, nullable=False) + total_count = Column(Integer, default=None) + used_count = Column(Integer, default=0) + status = Column(String(10), nullable=False, default="active") # active / expired / disabled + created_at = Column(DateTime, default=datetime.now) + + +class UserCoupon(Base): + __tablename__ = "user_coupons" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + coupon_id = Column(String(36), ForeignKey("coupons.id"), nullable=False) + status = Column(String(10), nullable=False, default="unused") # unused / used / expired + used_at = Column(DateTime, default=None) + created_at = Column(DateTime, default=datetime.now) + + +class ValueAddedService(Base): + __tablename__ = "value_added_services" + + id = Column(String(36), primary_key=True, default=gen_uuid) + name = Column(String(200), nullable=False) + description = Column(Text, default=None) + price = Column(DECIMAL(10, 2), nullable=False) + type = Column(String(20), nullable=False) # app_custom / platform_proxy / data_custom / private_project + features = Column(JSON, default=None) # 服务特性 + status = Column(String(10), nullable=False, default="active") # active / inactive / deleted + assigned_agent_id = Column(String(36), default=None) # 应用定制服务绑定的Agent(市场应用ID) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class CustomServiceOrder(Base): + __tablename__ = "custom_service_orders" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + service_id = Column(String(36), default=None) + service_type = Column(String(10), nullable=False) + description = Column(Text, default=None) + status = Column(String(15), nullable=False, default="pending") + price = Column(DECIMAL(10, 2), default=0.00) + order_id = Column(String(36), default=None) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class CustomServiceRequest(Base): + """增值服务工单表 - 用于数据定制服务和私有化项目服务""" + __tablename__ = "custom_service_requests" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + service_id = Column(String(36), ForeignKey("value_added_services.id"), nullable=False) + request_type = Column(String(20), nullable=False) # data_custom / private_project + title = Column(String(200), nullable=False) + description = Column(Text, default=None) + requirements = Column(JSON, default=None) # 需求详情JSON + status = Column(String(20), nullable=False, default="pending") # pending / processing / completed / rejected + assigned_agent_id = Column(String(36), default=None) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +# ============================================================ +# Token与计费 +# ============================================================ + +class ModelProvider(Base): + __tablename__ = "model_providers" + + id = Column(String(36), primary_key=True, default=gen_uuid) + name = Column(String(100), nullable=False) + protocol = Column(String(30), nullable=False, default="openai_compatible") + api_key = Column(String(255), default=None) + api_base_url = Column(String(500), default=None) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class AIModel(Base): + __tablename__ = "models" + + id = Column(String(36), primary_key=True, default=gen_uuid) + provider_id = Column(String(36), ForeignKey("model_providers.id"), nullable=False) + name = Column(String(100), nullable=False) + display_name = Column(String(200), default=None, comment="display name") + api_key = Column(String(255), default=None) + endpoint = Column(String(500), default=None) + capabilities = Column(JSON, default=None) + timeout = Column(Integer, default=60) + status = Column(String(10), nullable=False, default="paid") # free / paid + price_per_million_tokens = Column(DECIMAL(10, 4), nullable=False, default=0.0000) + is_enabled = Column(Boolean, default=True) + is_default = Column(Boolean, default=False) # 全局默认模型 + total_calls = Column(BigInteger, default=0) + total_tokens = Column(BigInteger, default=0) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class TokenUsageLog(Base): + __tablename__ = "token_usage_logs" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + model_id = Column(String(36), default=None) + model_name = Column(String(100), default=None) + app_id = Column(String(36), default=None) + app_name = Column(String(200), default=None) + conversation_id = Column(String(36), default=None) + prompt_tokens = Column(Integer, default=0) + completion_tokens = Column(Integer, default=0) + total_tokens = Column(Integer, default=0) + cost = Column(DECIMAL(10, 6), default=0.000000) + created_at = Column(DateTime, default=datetime.now) + + +class TokenBalance(Base): + __tablename__ = "token_balances" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), unique=True, nullable=False) + total_consumed = Column(BigInteger, default=0) + balance = Column(BigInteger, default=0) + daily_free_used = Column(BigInteger, default=0) + daily_free_date = Column(Date, default=None) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +# ============================================================ +# 通知与日志 +# ============================================================ + +class Notification(Base): + __tablename__ = "notifications" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + type = Column(String(15), nullable=False) + title = Column(String(200), nullable=False) + content = Column(Text, default=None) + is_read = Column(Boolean, default=False) + created_at = Column(DateTime, default=datetime.now) + + +class OperationLog(Base): + __tablename__ = "operation_logs" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), nullable=False) + username = Column(String(50), default=None) + action = Column(String(100), nullable=False) + target = Column(String(100), default=None) + target_id = Column(String(36), default=None) + detail = Column(Text, default=None) + ip = Column(String(45), default=None) + created_at = Column(DateTime, default=datetime.now) + + +class AuditLog(Base): + __tablename__ = "audit_logs" + + id = Column(String(36), primary_key=True, default=gen_uuid) + admin_id = Column(String(36), nullable=False) + admin_name = Column(String(50), default=None) + action = Column(String(100), nullable=False) + target_type = Column(String(50), default=None) + target_id = Column(String(36), default=None) + detail = Column(Text, default=None) + created_at = Column(DateTime, default=datetime.now) + + +class RiskRule(Base): + __tablename__ = "risk_rules" + + id = Column(String(36), primary_key=True, default=gen_uuid) + name = Column(String(100), nullable=False) + type = Column(String(15), nullable=False) + threshold = Column(Integer, default=0) + duration = Column(Integer, default=3600) + is_enabled = Column(Boolean, default=True) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +# ============================================================ +# 系统配置 +# ============================================================ + +class SiteSetting(Base): + __tablename__ = "site_settings" + + id = Column(String(36), primary_key=True, default=gen_uuid) + key = Column(String(100), unique=True, nullable=False) + value = Column(Text, default=None) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class EmailConfig(Base): + __tablename__ = "email_configs" + + id = Column(String(36), primary_key=True, default=gen_uuid) + smtp_host = Column(String(200), nullable=False) + smtp_port = Column(Integer, nullable=False, default=587) + username = Column(String(200), nullable=False) + password = Column(String(200), nullable=False) + from_address = Column(String(200), nullable=False) + use_tls = Column(Boolean, default=True) + use_ssl = Column(Boolean, default=False) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class SmsConfig(Base): + __tablename__ = "sms_configs" + + id = Column(String(36), primary_key=True, default=gen_uuid) + provider = Column(String(50), nullable=False) + api_key = Column(String(200), nullable=False) + api_secret = Column(String(200), nullable=False) + sign_name = Column(String(100), default=None) + template_code = Column(String(100), default=None) + # 短信宝 (SMSBao) 字段 + username = Column(String(100), default=None) # 短信宝用户名 u + password = Column(String(200), default=None) # 短信宝密码(明文存储,发送时 MD5 32位) + goods_id = Column(String(100), default=None) # 短信宝专属通道产品 ID g + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class PaymentConfig(Base): + __tablename__ = "payment_configs" + + id = Column(String(36), primary_key=True, default=gen_uuid) + type = Column(String(10), nullable=False) # wechat / alipay + app_id = Column(String(100), nullable=False) + merchant_id = Column(String(100), default=None) # 微信: 商户号 + api_key = Column(String(255), nullable=False) # 微信: APIv2密钥 / 支付宝: 应用私钥 + api_v3_key = Column(String(255), default=None) # 微信: APIv3密钥 + cert_path = Column(String(500), default=None) # 微信: 证书路径 + cert_serial_no = Column(String(200), default=None) # 微信: 证书序列号 + alipay_public_key = Column(Text, default=None) # 支付宝: 支付宝公钥 + app_private_key = Column(Text, default=None) # 支付宝: 应用私钥 + gateway_url = Column(String(500), default=None) # 支付宝: 网关地址 + notify_url = Column(String(500), default=None) # 支付回调通知地址 + return_url = Column(String(500), default=None) # 支付宝: 同步跳转地址 + status = Column(String(10), nullable=False, default="inactive") # active / inactive + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class RegisterSetting(Base): + __tablename__ = "register_settings" + + id = Column(String(36), primary_key=True, default=gen_uuid) + phone_required = Column(Boolean, default=True) + phone_sms_required = Column(Boolean, default=True) + email_required = Column(Boolean, default=True) + email_verify_required = Column(Boolean, default=True) + real_name_required = Column(Boolean, default=False) + aliyun_access_key = Column(String(200), default=None) + aliyun_secret_key = Column(String(200), default=None) + aliyun_endpoint = Column(String(200), default=None) + sms_sign_name = Column(String(100), default=None) + sms_template_code = Column(String(50), default=None) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +# ============================================================ +# 广告位 +# ============================================================ + +class AdBanner(Base): + __tablename__ = "ad_banners" + + id = Column(String(36), primary_key=True, default=gen_uuid) + title = Column(String(200), nullable=False) + image_url = Column(String(500), default=None) + link_app_id = Column(String(36), default=None) + link_app_name = Column(String(200), default=None) + sort_order = Column(Integer, default=0) + click_count = Column(Integer, default=0) + status = Column(String(10), nullable=False, default="active") # active / inactive + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +# ============================================================ +# 资产管理 +# ============================================================ + +class AssetCategory(Base): + __tablename__ = "asset_categories" + + id = Column(String(36), primary_key=True, default=gen_uuid) + name = Column(String(100), nullable=False) + type = Column(String(10), nullable=False) + icon = Column(String(200), default=None) + color = Column(String(20), default=None) + is_default = Column(Boolean, default=False) + user_id = Column(String(36), default=None) + created_at = Column(DateTime, default=datetime.now) + + +class Asset(Base): + __tablename__ = "assets" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + name = Column(String(200), nullable=False) + category = Column(String(10), nullable=False) + sub_category = Column(String(50), default=None) + file_type = Column(String(50), default=None) + size = Column(BigInteger, default=0) + description = Column(Text, default=None) + cover_url = Column(String(500), default=None) + url = Column(String(500), default=None) + content = Column(Text, default=None) + novel_meta = Column(JSON, default=None) + status = Column(String(10), nullable=False, default="active") # active / deleted + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class NovelChapter(Base): + __tablename__ = "novel_chapters" + + id = Column(String(36), primary_key=True, default=gen_uuid) + asset_id = Column(String(36), ForeignKey("assets.id"), nullable=False) + title = Column(String(200), nullable=False) + word_count = Column(Integer, default=0) + sort_order = Column(Integer, default=0) + content = Column(Text(length=4294967295), default=None) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +# ============================================================ +# 预留扩展 +# ============================================================ + +class Team(Base): + __tablename__ = "teams" + + id = Column(String(36), primary_key=True, default=gen_uuid) + owner_id = Column(String(36), ForeignKey("users.id"), nullable=False) + name = Column(String(100), nullable=False) + description = Column(Text, default=None) + status = Column(String(10), nullable=False, default="active") + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class Referral(Base): + __tablename__ = "referrals" + + id = Column(String(36), primary_key=True, default=gen_uuid) + referrer_id = Column(String(36), ForeignKey("users.id"), nullable=False) + referee_id = Column(String(36), ForeignKey("users.id"), nullable=False) + referral_code = Column(String(50), default=None) + status = Column(String(10), nullable=False, default="pending") + commission_rate = Column(DECIMAL(5, 2), default=0.00) + created_at = Column(DateTime, default=datetime.now) + + +class OpenApiClient(Base): + __tablename__ = "open_api_clients" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + name = Column(String(100), nullable=False) + client_id = Column(String(64), unique=True, nullable=False) + client_secret = Column(String(128), nullable=False) + allowed_models = Column(JSON, default=None) + rate_limit = Column(Integer, default=60) + status = Column(String(10), nullable=False, default="active") + created_at = Column(DateTime, default=datetime.now) + total_calls = Column(BigInteger, default=0, comment="总调用次数") + last_used_at = Column(DateTime, default=None, comment="最后使用时间") + + +# ============================================================ +# API调用统计 +# ============================================================ + +class ApiCallStat(Base): + """API调用统计表 - 按端点聚合""" + __tablename__ = "api_call_stats" + + id = Column(String(36), primary_key=True, default=gen_uuid) + endpoint = Column(String(200), nullable=False) + method = Column(String(10), nullable=False) + call_count = Column(BigInteger, default=0) + total_response_time = Column(Float, default=0.0) + error_count = Column(BigInteger, default=0) + last_called_at = Column(DateTime, default=None) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + __table_args__ = ( + UniqueConstraint("endpoint", "method", name="uk_endpoint_method"), + ) + + +class SlowRequestLog(Base): + """慢请求记录表""" + __tablename__ = "slow_request_logs" + + id = Column(String(36), primary_key=True, default=gen_uuid) + request_id = Column(String(64), default=None) + method = Column(String(10), nullable=False) + endpoint = Column(String(500), nullable=False) + status_code = Column(Integer, default=0) + response_time = Column(Float, default=0.0) + threshold = Column(Float, default=3.0) + created_at = Column(DateTime, default=datetime.now) + + __table_args__ = ( + Index("idx_srl_created", "created_at"), + ) + + +# ============================================================ +# AI自进化引擎 - 用户进化配置 +# ============================================================ +class UserEvolutionConfig(Base): + __tablename__ = "user_evolution_config" + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), unique=True, nullable=False, index=True) + skill_dir_path = Column(String(500), nullable=True, comment="用户Skill仓库根目录") + trajectory_dir_path = Column(String(500), nullable=True, comment="用户对话轨迹目录") + watcher_status = Column(String(20), default="stopped", comment="运行中/已停止/错误") + watcher_error = Column(String(500), nullable=True, comment="watcher异常信息") + pipeline_status = Column(String(20), default="idle", comment="流水线状态: running/idle/error") + pipeline_processed = Column(Integer, default=0, comment="已处理轨迹数") + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class EvolutionSkill(Base): + __tablename__ = "evolution_skills" + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True) + skill_name = Column(String(200), nullable=False, comment="技能名称") + description = Column(Text, nullable=True) + tags = Column(JSON, nullable=True, comment="标签列表") + category = Column(String(100), nullable=True, comment="技能分类") + state = Column(String(20), default="baby", comment="baby/main/staging/frozen") + use_count = Column(Integer, default=0, comment="使用次数") + version = Column(Integer, default=1) + skill_md_path = Column(String(500), nullable=True, comment="SKILL.md文件路径") + git_commit_sha = Column(String(40), nullable=True) + last_used_at = Column(DateTime, nullable=True) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class EvolutionEvent(Base): + __tablename__ = "evolution_events" + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True) + skill_name = Column(String(200), nullable=False) + event_type = Column(String(50), nullable=False, comment="created/promoted/rejected/frozen/unfrozen/edited/deleted/milestone") + description = Column(Text, nullable=True) + version_before = Column(Integer, nullable=True) + version_after = Column(Integer, nullable=True) + created_at = Column(DateTime, default=datetime.now) + + +class EvolutionTrajectory(Base): + __tablename__ = "evolution_trajectories" + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True) + conversation_id = Column(String(36), nullable=True, comment="关联的对话ID") + file_path = Column(String(500), nullable=True, comment="轨迹文件路径") + status = Column(String(20), default="discovered", comment="discovered/splitting/split_done/indexed/clustering/done/error/filtered") + atom_count = Column(Integer, default=0) + token_used = Column(Integer, default=0) + error_msg = Column(String(500), nullable=True, comment="轨迹处理失败的错误信息") + retry_count = Column(Integer, default=0, comment="失败重试次数") + tasks_extracted = Column(Integer, default=0, comment="拆分的AtomTask数量") + created_at = Column(DateTime, default=datetime.now) + processed_at = Column(DateTime, nullable=True) + + +class SkillTrajectoryLink(Base): + """技能-轨迹关联表:记录技能与对话轨迹的关联关系""" + __tablename__ = "skill_trajectory_links" + id = Column(String(36), primary_key=True, default=gen_uuid) + skill_name = Column(String(200), nullable=False, index=True, comment="关联的技能名称") + user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True) + trajectory_id = Column(String(36), nullable=True, comment="关联的轨迹ID") + atom_id = Column(String(36), nullable=True, comment="原子任务ID") + weightscore = Column(Integer, default=0, comment="关联权重分") + created_at = Column(DateTime, default=datetime.now) + + +# ============================================================ +# AI自进化v2 - 新增表 +# ============================================================ + +class SkillVersion(Base): + """技能版本历史表:存储每个版本的SKILL.md完整内容,支持回滚""" + __tablename__ = "skill_versions" + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + skill_name = Column(String(200), nullable=False) + version = Column(Integer, nullable=False, default=1) + skill_md_content = Column(Text(4294967295), nullable=True, comment="该版本的SKILL.md完整内容") + git_commit_sha = Column(String(40), nullable=True) + description = Column(Text, nullable=True, comment="版本变更说明") + created_at = Column(DateTime, default=datetime.now) + + __table_args__ = ( + Index("idx_sv_user_skill", "user_id", "skill_name"), + Index("idx_sv_version", "version"), + ) + + +class EvolutionReport(Base): + """进化报告表:存储周报/月报""" + __tablename__ = "evolution_reports" + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + report_type = Column(String(20), nullable=False, comment="weekly / monthly") + period_start = Column(Date, nullable=False) + period_end = Column(Date, nullable=False) + report_data = Column(JSON, nullable=True, comment="报告详细数据(JSON)") + created_at = Column(DateTime, default=datetime.now) + + __table_args__ = ( + Index("idx_er_user_type", "user_id", "report_type"), + Index("idx_er_period", "period_start", "period_end"), + ) + + +class SkillPublishRecord(Base): + """技能市场发布记录表""" + __tablename__ = "skill_publish_records" + id = Column(String(36), primary_key=True, default=gen_uuid) + skill_id = Column(String(36), nullable=False, comment="关联evolution_skills.id") + skill_name = Column(String(200), nullable=False) + user_id = Column(String(36), ForeignKey("users.id"), nullable=False) + market_app_id = Column(String(36), nullable=True, comment="关联market_apps.id") + price = Column(DECIMAL(10, 2), default=0.00) + market_description = Column(Text, nullable=True) + status = Column(String(20), nullable=False, default="published", comment="published/unpublished") + published_at = Column(DateTime, default=datetime.now) + created_at = Column(DateTime, default=datetime.now) + + __table_args__ = ( + Index("idx_spr_skill_id", "skill_id"), + Index("idx_spr_user_id", "user_id"), + Index("idx_spr_status", "status"), + ) + + +class TeamSharedSkill(Base): + """团队共享技能表""" + __tablename__ = "team_shared_skills" + id = Column(String(36), primary_key=True, default=gen_uuid) + team_id = Column(String(36), ForeignKey("teams.id"), nullable=False) + skill_id = Column(String(36), nullable=False, comment="关联evolution_skills.id") + skill_name = Column(String(200), nullable=False) + shared_by = Column(String(36), ForeignKey("users.id"), nullable=False) + shared_by_name = Column(String(100), nullable=True, comment="分享者昵称") + is_public = Column(Boolean, default=True, comment="团队内是否可见") + created_at = Column(DateTime, default=datetime.now) + + __table_args__ = ( + UniqueConstraint("team_id", "skill_id", name="uk_tss_team_skill"), + ) + + +class ExternalKBService(Base): + """外部知识库服务配置""" + __tablename__ = "external_kb_services" + + id = Column(String(36), primary_key=True, default=gen_uuid) + name = Column(String(200), nullable=False) + service_type = Column(String(20), nullable=False) + api_url = Column(String(500), nullable=False) + api_key = Column(String(255), default="") + config = Column(JSON, default=None) + is_enabled = Column(Boolean, default=True) + status = Column(String(20), default="disconnected") + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + +class OSSStorageConfig(Base): + __tablename__ = "oss_storage_configs" + + id = Column(String(36), primary_key=True, default=gen_uuid) + name = Column(String(200), nullable=False) + provider = Column(String(20), nullable=False) + endpoint = Column(String(500), nullable=False) + access_key = Column(String(255), nullable=False) + secret_key = Column(String(255), nullable=False) + bucket = Column(String(200), nullable=False) + region = Column(String(100), default="") + base_path = Column(String(300), default="") + is_default = Column(Boolean, default=False) + is_enabled = Column(Boolean, default=True) + status = Column(String(20), default="disconnected") + config = Column(JSON, default=None) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + + +class UserStorageQuota(Base): + __tablename__ = "user_storage_quotas" + + id = Column(String(36), primary_key=True, default=gen_uuid) + user_id = Column(String(36), nullable=False, unique=True) + quota_bytes = Column(BigInteger, nullable=False, default=1073741824) + used_bytes = Column(BigInteger, nullable=False, default=0) + quota_type = Column(String(20), nullable=False, default="membership") + membership_level = Column(Integer, default=0) + plan_id = Column(String(36), default=None) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class AutomatedTask(Base): + __tablename__ = "automated_tasks" + + id = Column(String(36), primary_key=True, default=gen_uuid) + name = Column(String(200), nullable=False) + description = Column(Text, default="") + trigger_type = Column(String(20), nullable=False) + cron_expression = Column(String(100), default=None) + event_condition = Column(JSON, default=None) + action_type = Column(String(30), nullable=False) + action_config = Column(JSON, default=None) + is_enabled = Column(Boolean, default=True) + status = Column(String(20), default="idle") + max_retries = Column(Integer, default=3) + timeout_seconds = Column(Integer, default=300) + last_run_at = Column(DateTime, default=None) + next_run_at = Column(DateTime, default=None) + total_runs = Column(Integer, default=0) + success_runs = Column(Integer, default=0) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + +class TaskExecutionLog(Base): + __tablename__ = "task_execution_logs" + + id = Column(String(36), primary_key=True, default=gen_uuid) + task_id = Column(String(36), nullable=False) + task_name = Column(String(200), default="") + status = Column(String(20), default="running") + trigger_type = Column(String(20), default="manual") + started_at = Column(DateTime, default=None) + finished_at = Column(DateTime, default=None) + duration_ms = Column(Integer, default=0) + result_summary = Column(Text, default="") + error_message = Column(Text, default="") + created_at = Column(DateTime, default=datetime.now) + + + +class MCPServer(Base): + __tablename__ = "mcp_servers" + + id = Column(String(36), primary_key=True, default=gen_uuid) + name = Column(String(100), nullable=False) + description = Column(Text, default="") + transport_type = Column(String(20), default="stdio") + command = Column(String(500), default="") + args = Column(Text, default="") + env_vars = Column(Text, default="") + url = Column(String(500), default="") + headers = Column(Text, default="") + is_enabled = Column(Boolean, default=True) + status = Column(String(20), default="disconnected") + last_connected_at = Column(DateTime, default=None) + tools_count = Column(Integer, default=0) + resources_count = Column(Integer, default=0) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + + +class SmsVerificationCode(Base): + __tablename__ = "sms_verification_codes" + + id = Column(String(36), primary_key=True, default=gen_uuid) + phone = Column(String(20), nullable=False, index=True) + code = Column(String(10), nullable=False) + purpose = Column(String(30), nullable=False) # register / reset_password + expires_at = Column(DateTime, nullable=False) + used = Column(Boolean, default=False) + created_at = Column(DateTime, default=datetime.now) + diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index f0fbdac..f4ef003 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -289,7 +289,7 @@ export const knowledgeAPI = { export const marketAPI = { - getApps: (params?: { type?: string; source?: string; search?: string; page?: number; page_size?: number }) => { + getApps: (params?: { type?: string; source?: string; capability?: string; search?: string; page?: number; page_size?: number }) => { const searchParams = new URLSearchParams(); @@ -297,6 +297,8 @@ export const marketAPI = { if (params?.source) searchParams.set('source', params.source); + if (params?.capability) searchParams.set('capability', params.capability); + if (params?.search) searchParams.set('search', params.search); if (params?.page) searchParams.set('page', String(params.page)); @@ -315,6 +317,8 @@ export const marketAPI = { verifyAgent: (data: any) => post('/market/apps/verify-agent', data), + detectCapability: (data: any) => post('/market/apps/detect-capability', data), + purchaseApp: (id: string) => post(`/market/apps/${id}/purchase`), installApp: (id: string) => post(`/market/apps/${id}/install`), diff --git a/frontend/src/pages/front/MarketPage.css b/frontend/src/pages/front/MarketPage.css index 2c51cfc..907fcbf 100644 --- a/frontend/src/pages/front/MarketPage.css +++ b/frontend/src/pages/front/MarketPage.css @@ -1739,3 +1739,52 @@ border: 1px solid var(--color-border-light); border-radius: var(--radius-md); } + +/* ===== 能力分类筛选 & 徽章 ===== */ +.capability-filter { + display: flex; + flex-wrap: wrap; + gap: var(--space-xs); + padding: var(--space-sm) 0 0; +} + +.cap-chip { + padding: 4px 12px; + font-size: var(--text-xs); + font-weight: 500; + border-radius: 999px; + border: 1px solid var(--color-border-light); + background: var(--color-bg-secondary); + color: var(--color-text-secondary); + cursor: pointer; + transition: all var(--transition-fast); + white-space: nowrap; +} + +.cap-chip:hover { + border-color: var(--color-primary); + color: var(--color-primary); +} + +.cap-chip.active { + color: #fff; + border-color: transparent; +} + +.capability-badge { + display: inline-flex; + align-items: center; + padding: 1px 8px; + font-size: 11px; + font-weight: 600; + line-height: 18px; + border-radius: 6px; + border: 1px solid transparent; + white-space: nowrap; +} + +.cap-hint { + font-size: 11px; + font-weight: 400; + color: var(--color-text-tertiary); +} diff --git a/frontend/src/pages/front/MarketPage.tsx b/frontend/src/pages/front/MarketPage.tsx index f8363ac..cb27304 100644 --- a/frontend/src/pages/front/MarketPage.tsx +++ b/frontend/src/pages/front/MarketPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useEffect } from 'react'; +import React, { useState, useMemo, useEffect, useRef } from 'react'; import { marketAPI } from '@/api'; import type { MarketApp, AppType, AppReview } from '@/types'; import './MarketPage.css'; @@ -182,6 +182,36 @@ const typeIcons: Record = { mcp: , }; +// ===== 能力分类(与后端 11 种能力保持一致) ===== +const CAPABILITIES: { id: string; label: string; color: string }[] = [ + { id: 'text', label: '文本对话', color: '#5B5FED' }, + { id: 'thinking', label: '推理思考', color: '#8B5CF6' }, + { id: 'vision', label: '视觉理解', color: '#219B5E' }, + { id: 'tool', label: '工具调用', color: '#3B82F6' }, + { id: 'image', label: '图像生成', color: '#D9383A' }, + { id: 'audio', label: '语音合成', color: '#E8922C' }, + { id: 'asr', label: '语音识别', color: '#0EA5E9' }, + { id: 'ocr', label: 'OCR识别', color: '#F59E0B' }, + { id: 'embedding', label: '嵌入向量', color: '#10B981' }, + { id: 'rerank', label: '重排序', color: '#EC4899' }, + { id: 'video', label: '视频生成', color: '#EF4444' }, +]; + +function CapabilityBadge({ capability }: { capability?: string | null }) { + if (!capability) return null; + const cap = CAPABILITIES.find(c => c.id === capability); + if (!cap) return null; + return ( + + {cap.label} + + ); +} + // ===== Hub Platforms (修改3 & 修改5) ===== const skillsHubPlatforms = [ { id: 'skills.sh', name: 'skills.sh', url: 'https://www.skills.sh' }, @@ -351,6 +381,7 @@ const AppDetailModal: React.FC = ({ app, onClose, onAppChan {typeIcons[app.type]} {typeLabels[app.type]} + {app.publisherName} @@ -470,6 +501,8 @@ const CreateToolModal: React.FC = ({ appType, onClose, onS const [mcpCode, setMcpCode] = useState(''); const [mcpCodeVerifyResult, setMcpCodeVerifyResult] = useState<'idle' | 'checking' | 'pass' | 'fail'>('idle'); const [mcpCodeVerifyMsg, setMcpCodeVerifyMsg] = useState(''); + // 能力分类:AI验证后自动识别,可手动修改;空字符串表示交由后端 AI 自动归类 + const [capability, setCapability] = useState(''); const handleCheckFormat = () => { const content = skillsFile || skillsContent; @@ -483,6 +516,7 @@ const CreateToolModal: React.FC = ({ appType, onClose, onS if (hasMinLength && (hasMarkdownHeader || hasValidStructure)) { setFormatCheckResult('pass'); setFormatCheckMsg('格式检测通过:符合Skills标准规范(工具定义 + 使用说明 + 参数配置)'); + detectCap(content); } else { setFormatCheckResult('fail'); setFormatCheckMsg('格式不符合Skills标准:请确保包含工具定义、功能描述、使用说明和参数配置'); @@ -500,6 +534,7 @@ const CreateToolModal: React.FC = ({ appType, onClose, onS if (hasMinLength && hasKeywords) { setMcpCodeVerifyResult('pass'); setMcpCodeVerifyMsg('AI验证通过:代码包含必要的传输协议和端点配置'); + detectCap(mcpCode); } else { setMcpCodeVerifyResult('fail'); setMcpCodeVerifyMsg('AI验证失败:代码中缺少必要的关键词(transport/endpoint),请检查代码完整性'); @@ -519,6 +554,18 @@ const CreateToolModal: React.FC = ({ appType, onClose, onS } }; + // 调用后端 detect-capability,AI 验证阶段自动识别能力分类并预填 + const detectCap = (content: string) => { + if (!name.trim()) return; + marketAPI.detectCapability({ name: name.trim(), description: description.trim(), skill_content: content }) + .then((res: any) => { + if (res && res.code === 0 && res.data && res.data.capability) { + setCapability(res.data.capability); + } + }) + .catch(() => {}); + }; + const handleSubmit = () => { if (!name.trim() || !description.trim()) return; // Skills requires format check pass @@ -532,6 +579,7 @@ const CreateToolModal: React.FC = ({ appType, onClose, onS onSubmit({ name: name.trim(), type: appType, + capability: capability || undefined, description: description.trim(), price: isPaid ? parseFloat(price) || 0 : 0, isMemberFree: !isPaid, @@ -697,6 +745,19 @@ const CreateToolModal: React.FC = ({ appType, onClose, onS /> + {/* 能力分类(AI验证后自动识别,可手动修改) */} + {appType !== 'agent' && ( +
+ + +
+ )} + {/* MCP-specific: Transport Type (修改5) */} {appType === 'mcp' && (
@@ -879,7 +940,7 @@ interface HubImportModalProps { appType: 'skill' | 'mcp'; platforms: typeof skillsHubPlatforms; onClose: () => void; - onImport: (platformName: string, url: string, skillName: string, price?: number, description?: string) => void; + onImport: (platformName: string, url: string, skillName: string, price?: number, description?: string, capability?: string) => void; } const HubImportModal: React.FC = ({ appType, platforms, onClose, onImport }) => { @@ -900,6 +961,10 @@ const HubImportModal: React.FC = ({ appType, platforms, onC const [hubCodeVerifyMsgMap, setHubCodeVerifyMsgMap] = useState>({}); const [hubName, setHubName] = useState(''); const [hubDescription, setHubDescription] = useState(''); + // 能力分类:AI验证后自动识别,可手动修改;用 ref 避免导入点击时的陈旧闭包 + const [capability, setCapability] = useState(''); + const capabilityRef = useRef(''); + const setCap = (v: string) => { setCapability(v); capabilityRef.current = v; }; const handleVisit = (url: string) => { window.open(url, '_blank'); @@ -915,6 +980,7 @@ const HubImportModal: React.FC = ({ appType, platforms, onC if (hasMinLength && hasKeywords) { setHubCodeVerifyResult('pass'); setHubCodeVerifyMsg('AI验证通过:代码包含必要的传输协议和端点配置,可以导入'); + detectCap(hubName || 'MCP应用', hubCode); } else { setHubCodeVerifyResult('fail'); setHubCodeVerifyMsg('AI验证失败:代码中缺少必要的关键词(transport/endpoint),请检查代码完整性'); @@ -934,6 +1000,7 @@ const HubImportModal: React.FC = ({ appType, platforms, onC if (hasMinLength && hasKeywords) { setHubCodeVerifyResultMap(prev => ({ ...prev, [platformId]: 'pass' })); setHubCodeVerifyMsgMap(prev => ({ ...prev, [platformId]: 'AI验证通过:代码包含必要的传输协议和端点配置,可以导入' })); + detectCap(hubName || 'MCP应用', trimmed); } else { setHubCodeVerifyResultMap(prev => ({ ...prev, [platformId]: 'fail' })); setHubCodeVerifyMsgMap(prev => ({ ...prev, [platformId]: 'AI验证失败:代码中缺少必要的关键词(transport/endpoint),请检查代码完整性' })); @@ -941,6 +1008,17 @@ const HubImportModal: React.FC = ({ appType, platforms, onC }, 1200); }; + const detectCap = (name: string, content: string, description?: string) => { + if (!name.trim()) return; + marketAPI.detectCapability({ name: name.trim(), description: description || '', skill_content: content }) + .then((res: any) => { + if (res && res.code === 0 && res.data && res.data.capability) { + setCap(res.data.capability); + } + }) + .catch(() => {}); + }; + const handleImport = (platformId: string) => { const url = importUrl[platformId]; const isCustomImport = platformId === 'custom-import'; @@ -955,6 +1033,10 @@ const HubImportModal: React.FC = ({ appType, platforms, onC } if (!hubName.trim()) return; } + // Skills Hub 导入无独立AI验证步骤:在导入时触发一次能力识别(异步,导入完成前会回填到 ref) + if (appType === 'skill' && !capabilityRef.current && url?.trim()) { + detectCap(hubName || extractSkillName(url, platformId), url, hubDescription); + } setImportingId(platformId); setImportStatus('verifying'); setStatusMsg(appType === 'mcp' ? '正在验证代码...' : '正在验证URL链接...'); @@ -976,7 +1058,7 @@ const HubImportModal: React.FC = ({ appType, platforms, onC setTimeout(() => { const name = appType === 'skill' ? (hubName || extractSkillName(url || '', platformId)) : hubName; const importPayload = appType === 'mcp' ? platformCode : (url || ''); - onImport(platformId, importPayload, name, importPrice, hubDescription); + onImport(platformId, importPayload, name, importPrice, hubDescription, capabilityRef.current); setImportingId(null); setImportStatus('idle'); setImportUrl(prev => ({ ...prev, [platformId]: '' })); @@ -1047,6 +1129,17 @@ const HubImportModal: React.FC = ({ appType, platforms, onC />
+ {/* 能力分类(AI验证后自动识别,可手动修改) */} +
+ + +
+ {/* MCP Hub Import: Code Editor + AI验证 */} {appType === 'mcp' && (
@@ -1267,6 +1360,8 @@ interface AgentConnectModalProps { const AgentConnectModal: React.FC = ({ onClose, onConnect }) => { const [platform, setPlatform] = useState('dify'); + // 能力分类(可选,未指定时由后端 AI 自动归类) + const [capability, setCapability] = useState(''); const [name, setName] = useState(''); const [intro, setIntro] = useState(''); const [apiKey, setApiKey] = useState(''); @@ -1316,7 +1411,7 @@ const AgentConnectModal: React.FC = ({ onClose, onConnec if (!ok) return; setConnecting(true); try { - await onConnect(platform, { apiKey: apiKey.trim(), endpoint: endpoint.trim() }, price, name.trim(), intro.trim()); + await onConnect(platform, { apiKey: apiKey.trim(), endpoint: endpoint.trim() }, price, name.trim(), intro.trim(), capability); } finally { setConnecting(false); } @@ -1361,6 +1456,15 @@ const AgentConnectModal: React.FC = ({ onClose, onConnec onChange={e => setIntro(e.target.value)} />
+
+ + +
{ const [localApps, setLocalApps] = useState([]); const [loading, setLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(''); + const [capabilityFilter, setCapabilityFilter] = useState(null); const [installToast, setInstallToast] = useState(null); // Install toast @@ -1441,7 +1546,7 @@ const MarketPage: React.FC = () => { // 从后端加载应用列表 const loadApps = (search?: string) => { setLoading(true); - marketAPI.getApps(search ? { search, page_size: 100 } : { page_size: 100 }).then((res: any) => { + marketAPI.getApps({ ...(search ? { search } : {}), ...(capabilityFilter ? { capability: capabilityFilter } : {}), page_size: 100 }).then((res: any) => { if (res.code === 0 && res.data) { const items = (res.data.items || []).map((item: any) => ({ id: item.id, @@ -1479,8 +1584,11 @@ const MarketPage: React.FC = () => { let base = activeTab === 'all' ? localApps : localApps.filter(a => a.type === activeTab); + if (capabilityFilter) { + base = base.filter(a => a.capability === capabilityFilter); + } return base; - }, [activeTab, localApps]); + }, [activeTab, localApps, capabilityFilter]); const tabs: { key: AppType | 'all'; label: string }[] = [ { key: 'all', label: '全部' }, @@ -1524,7 +1632,7 @@ const MarketPage: React.FC = () => { }; // Handle hub import - 调用后端 API 创建应用 - const handleHubImport = async (platformName: string, url: string, skillName: string, price?: number, description?: string) => { + const handleHubImport = async (platformName: string, url: string, skillName: string, price?: number, description?: string, capability?: string) => { try { const isMcp = showHubImport === 'mcp'; const desc = description || `从 ${platformName} 导入的${isMcp ? 'MCP应用' : 'Skills工具'}${!isMcp ? ` (来源: ${url})` : ''}`; @@ -1537,6 +1645,7 @@ const MarketPage: React.FC = () => { join_promotion: false, source: 'user', }; + if (capability) payload.capability = capability; if (isMcp && url) { payload.download_url = url; } @@ -1554,14 +1663,14 @@ const MarketPage: React.FC = () => { }; // Handle agent connect - 调用后端 API 创建应用(单个Agent方式接入) - const handleAgentConnect = async (platformId: string, config: Record, price?: number, name?: string, description?: string) => { + const handleAgentConnect = async (platformId: string, config: Record, price?: number, name?: string, description?: string, capability?: string) => { const platform = agentPlatforms.find(p => p.id === platformId); const finalName = (name && name.trim()) ? name.trim() : `${platform?.name || '外部'} Agent`; const finalDesc = (description && description.trim()) ? description.trim() : `从 ${platform?.name || '外部平台'} 对接导入的智能Agent`; try { - await marketAPI.createApp({ + const payload: any = { name: finalName, type: 'agent', description: finalDesc, @@ -1572,7 +1681,9 @@ const MarketPage: React.FC = () => { agent_endpoint: config.endpoint || '', api_key: config.apiKey || '', platform: platformId || 'dify', - }); + }; + if (capability) payload.capability = capability; + await marketAPI.createApp(payload); // 重新加载列表 loadApps(); } catch (e: any) { @@ -1688,6 +1799,22 @@ const MarketPage: React.FC = () => {
)} + {/* 能力分类筛选 */} +
+ + {CAPABILITIES.map(c => ( + + ))} +
+ {/* Search Bar */}
@@ -1738,6 +1865,7 @@ const MarketPage: React.FC = () => { )} +

diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 6397ea0..3c05725 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -107,6 +107,7 @@ export interface MarketApp { reviewCount: number; installCount: number; installed?: boolean; + capability?: string; createdAt: string; } diff --git a/migrations/017_add_marketapp_capability.sql b/migrations/017_add_marketapp_capability.sql new file mode 100644 index 0000000..0d454f9 --- /dev/null +++ b/migrations/017_add_marketapp_capability.sql @@ -0,0 +1,10 @@ +-- 017: 为 market_apps 增加 capability 列(能力分类:text/thinking/vision/tool/image/audio/asr/ocr/embedding/rerank/video) +-- 幂等:列已存在则跳过 +SET @col = (SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'market_apps' AND column_name = 'capability'); +SET @sql = IF(@col = 0, + 'ALTER TABLE market_apps ADD COLUMN capability VARCHAR(20) NULL COMMENT \'能力分类:text/thinking/vision/tool/image/audio/asr/ocr/embedding/rerank/video\'', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt;