diff --git a/backend/python/app/api/admin/settings.py b/backend/python/app/api/admin/settings.py index f9436d2..609c7ba 100644 --- a/backend/python/app/api/admin/settings.py +++ b/backend/python/app/api/admin/settings.py @@ -1,508 +1,624 @@ -""" -网站基础设置 API(管理员) -""" -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from ...database import get_db, SessionLocal -from ...models import SiteSetting, EmailConfig, SmsConfig, User, AIModel -from ...auth import get_current_admin -from ...middleware.log_service import LogService -import uuid -import smtplib -import hmac -import hashlib -import base64 -import urllib.parse -import time as time_module -from email.mime.text import MIMEText -from email.mime.multipart import MIMEMultipart -from datetime import datetime -from pydantic import BaseModel -from typing import Optional, Dict, Any -import logging - -logger = logging.getLogger("aiflow") - -router = APIRouter() - - -# ============================================================ -# Pydantic Models -# ============================================================ - -class EmailConfigUpdate(BaseModel): - smtp_host: Optional[str] = None - smtp_port: Optional[int] = None - username: Optional[str] = None - password: Optional[str] = None - from_address: Optional[str] = None - use_tls: Optional[bool] = None - - -class SmsConfigUpdate(BaseModel): - provider: Optional[str] = None - api_key: Optional[str] = None - api_secret: Optional[str] = None - sign_name: Optional[str] = None - template_code: Optional[str] = None - - -# ============================================================ -# 网站设置 -# ============================================================ - -@router.get("/site") -async def get_site_settings( - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """获取网站设置(返回所有 key-value 对)""" - settings = db.query(SiteSetting).all() - result = {} - for s in settings: - result[s.key] = s.value - return {"code": 0, "data": result, "message": "ok"} - - -@router.put("/site") -async def update_site_settings( - settings: dict, - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """批量更新网站设置""" - for key, value in settings.items(): - existing = db.query(SiteSetting).filter(SiteSetting.key == key).first() - if existing: - existing.value = str(value) if value is not None else None - existing.updated_at = datetime.now() - else: - new_setting = SiteSetting( - id=str(uuid.uuid4()), - key=key, - value=str(value) if value is not None else None, - ) - db.add(new_setting) - - db.commit() - - LogService.log_audit( - admin_id=admin_user.id, - admin_name=admin_user.username, - action="update_site_settings", - target_type="site_settings", - detail=f"更新网站设置: {list(settings.keys())}", - ) - - return {"message": "网站设置已更新"} - - -# ============================================================ -# 邮件配置 -# ============================================================ - -@router.get("/email") -async def get_email_config( - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """获取邮件配置""" - config = db.query(EmailConfig).first() - if not config: - return {"code": 0, "data": { - "smtp_host": "", - "smtp_port": 587, - "username": "", - "password": "", - "from_address": "", - "use_tls": True, - }, "message": "ok"} - - return {"code": 0, "data": { - "id": config.id, - "smtp_host": config.smtp_host, - "smtp_port": config.smtp_port, - "username": config.username, - "password": config.password, - "from_address": config.from_address, - "use_tls": config.use_tls, - }, "message": "ok"} - - -@router.put("/email") -async def update_email_config( - req: EmailConfigUpdate, - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """更新邮件配置""" - config = db.query(EmailConfig).first() - if not config: - # 必填字段校验:不允许空值 - if not req.smtp_host: - raise HTTPException(status_code=400, detail="SMTP服务器地址不能为空") - if not req.username: - raise HTTPException(status_code=400, detail="SMTP用户名不能为空") - if not req.password: - raise HTTPException(status_code=400, detail="SMTP密码不能为空") - if not req.from_address: - raise HTTPException(status_code=400, detail="发件地址不能为空") - config = EmailConfig( - id=str(uuid.uuid4()), - smtp_host=req.smtp_host or "", - smtp_port=req.smtp_port or 587, - username=req.username or "", - password=req.password or "", - from_address=req.from_address or "", - use_tls=req.use_tls if req.use_tls is not None else True, - ) - db.add(config) - else: - if req.smtp_host is not None: - if not req.smtp_host.strip(): - raise HTTPException(status_code=400, detail="SMTP服务器地址不能为空") - config.smtp_host = req.smtp_host - if req.smtp_port is not None: - config.smtp_port = req.smtp_port - if req.username is not None: - if not req.username.strip(): - raise HTTPException(status_code=400, detail="SMTP用户名不能为空") - config.username = req.username - if req.password is not None: - if not req.password.strip(): - raise HTTPException(status_code=400, detail="SMTP密码不能为空") - config.password = req.password - if req.from_address is not None: - if not req.from_address.strip(): - raise HTTPException(status_code=400, detail="发件地址不能为空") - config.from_address = req.from_address - if req.use_tls is not None: - config.use_tls = req.use_tls - config.updated_at = datetime.now() - - db.commit() - - LogService.log_audit( - admin_id=admin_user.id, - admin_name=admin_user.username, - action="update_email_config", - target_type="email_config", - detail="更新邮件配置", - ) - - return {"code": 0, "data": None, "message": "邮件配置已更新"} - - -# ============================================================ -# 短信配置 -# ============================================================ - -@router.get("/sms") -async def get_sms_config( - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """获取短信配置""" - config = db.query(SmsConfig).first() - if not config: - return {"code": 0, "data": { - "provider": "", - "api_key": "", - "api_secret": "", - "sign_name": "", - "template_code": "", - }, "message": "ok"} - - return {"code": 0, "data": { - "id": config.id, - "provider": config.provider, - "api_key": config.api_key, - "api_secret": config.api_secret, - "sign_name": config.sign_name, - "template_code": config.template_code, - }, "message": "ok"} - - -@router.put("/sms") -async def update_sms_config( - req: SmsConfigUpdate, - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """更新短信配置""" - config = db.query(SmsConfig).first() - if not config: - config = SmsConfig( - id=str(uuid.uuid4()), - provider=req.provider or "", - api_key=req.api_key or "", - api_secret=req.api_secret or "", - sign_name=req.sign_name, - template_code=req.template_code, - ) - db.add(config) - else: - if req.provider is not None: - config.provider = req.provider - if req.api_key is not None: - config.api_key = req.api_key - if req.api_secret is not None: - config.api_secret = req.api_secret - if req.sign_name is not None: - config.sign_name = req.sign_name - if req.template_code is not None: - config.template_code = req.template_code - config.updated_at = datetime.now() - - db.commit() - - LogService.log_audit( - admin_id=admin_user.id, - admin_name=admin_user.username, - action="update_sms_config", - target_type="sms_config", - detail="更新短信配置", - ) - - return {"message": "短信配置已更新"} - - -# ============================================================ -# 邮件测试 -# ============================================================ - -@router.post("/email/test") -async def test_email_config( - test_to: str = Query(default=None, description="自定义测试收件地址"), - test_subject: str = Query(default=None, description="自定义测试邮件主题"), - test_body: str = Query(default=None, description="自定义测试邮件正文"), - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """使用当前配置发送测试邮件;可传参自定义收件人、主题和正文""" - config = db.query(EmailConfig).first() - if not config or not config.smtp_host: - raise HTTPException(status_code=400, detail="邮件配置未设置,请先保存邮件配置") - if not config.username: - raise HTTPException(status_code=400, detail="SMTP用户名未填写,请检查邮件配置") - if not config.password: - raise HTTPException(status_code=400, detail="SMTP密码未填写,请检查邮件配置") - if not config.from_address: - raise HTTPException(status_code=400, detail="发件地址未填写,请检查邮件配置") - - # 优先级:传入参数 > 管理员邮箱 > 发件地址 - admin = db.query(User).filter(User.id == admin_user.id).first() - recipient = test_to or (admin.email if admin and admin.email else None) or config.from_address - if not recipient: - raise HTTPException(status_code=400, detail="无法获取接收地址: 请填写收件邮箱或设置管理员邮箱") - - subject = test_subject or "AIFlow 邮件配置测试" - body = test_body or f"这是一封来自 AIFlow 平台的测试邮件。\n\n发送时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\nSMTP服务器: {config.smtp_host}" - - logger.info(f"测试邮件: SMTP={config.smtp_host}:{config.smtp_port}, " - f"from={config.from_address}, to={recipient}") - - try: - msg = MIMEMultipart() - msg["From"] = config.from_address - msg["To"] = recipient - msg["Subject"] = subject - msg.attach(MIMEText(body, "plain")) - - if config.use_tls: - server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=10) - server.starttls() - else: - server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=10) - - server.login(config.username, config.password) - server.sendmail(config.from_address, recipient, msg.as_string()) - server.quit() - - LogService.log_audit( - admin_id=admin_user.id, - admin_name=admin_user.username, - action="test_email_config", - target_type="email_config", - detail=f"测试邮件发送成功 -> {recipient} via {config.smtp_host}", - ) - - return {"code": 0, "data": {"message": f"测试邮件已发送到 {recipient}", "recipient": recipient}, "message": "ok"} - except smtplib.SMTPAuthenticationError as e: - logger.error(f"SMTP认证失败: {e}") - raise HTTPException(status_code=400, detail=f"SMTP认证失败,请检查用户名和密码: {str(e)}") - except smtplib.SMTPConnectError as e: - logger.error(f"SMTP连接失败: {e}") - raise HTTPException(status_code=400, detail=f"无法连接到SMTP服务器 {config.smtp_host}:{config.smtp_port}: {str(e)}") - except smtplib.SMTPException as e: - logger.error(f"SMTP发送失败: {e}") - raise HTTPException(status_code=500, detail=f"邮件发送失败(SMTP): {str(e)}") - except OSError as e: - logger.error(f"邮件发送网络错误: {e}") - raise HTTPException(status_code=500, detail=f"网络连接错误: {str(e)}") - except BaseException as e: - logger.error(f"测试邮件发送失败(严重): {type(e).__name__}: {e}") - import traceback - traceback.print_exc() - raise HTTPException(status_code=500, detail=f"邮件发送失败: {str(e)}") - - -@router.post("/email/generate-content") -async def generate_email_content( - topic: str = Query(default="测试邮件"), - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """AI生成测试邮件内容:调用默认LLM生成邮件主题和正文""" - import httpx as httpx_lib - subjects = [ - "【测试】AIFlow邮件服务配置验证", - "AIFlow平台 - 邮件投递测试", - "测试邮件:您的SMTP服务已就绪", - ] - bodies = [ - f"您好!\n\n这是一封来自 AIFlow 智能自动化平台的测试邮件,用于验证邮件服务配置是否正常。\n\n如果您收到此邮件,说明SMTP邮件服务已配置成功。\n\n此致\nAIFlow 平台团队", - f"Hi,\n\nThis is a test email from AIFlow Platform.\n\nIf you received this, the SMTP service works fine.\n\nBest regards,\nAIFlow Team", - f"【系统通知】\n邮件服务连通性测试通过。\n\n发件服务器配置正确,能够成功投递邮件。\n\n—— AIFlow", - ] - - # 尝试使用默认LLM生成更好的内容 - try: - default_model = db.query(AIModel).filter( - AIModel.is_default == True, AIModel.is_enabled == True - ).first() - if default_model and default_model.endpoint: - async with httpx_lib.AsyncClient(timeout=15) as client: - headers = {} - if default_model.api_key: - headers["Authorization"] = f"Bearer {default_model.api_key}" - resp = await client.post( - default_model.endpoint, - json={ - "model": default_model.name, - "messages": [ - {"role": "system", "content": "你是一个专业的技术文档撰写者。请根据主题生成一封测试邮件的主题和正文。返回JSON格式: {\"subject\": \"邮件主题\", \"body\": \"邮件正文\"}"}, - {"role": "user", "content": f"主题: {topic}"}, - ], - "max_tokens": 400, - }, - headers=headers, - ) - if resp.status_code == 200: - content = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "") - try: - data = json.loads(content) - if data.get("subject") and data.get("body"): - return {"subject": data["subject"], "body": data["body"], "source": "ai"} - except: - # 尝试解析非JSON格式 - lines = content.strip().split("\n") - if len(lines) >= 2: - return {"subject": lines[0].replace("主题:", "").strip(), "body": "\n".join(lines[1:]).replace("正文:", "").strip(), "source": "ai_partial"} - except Exception: - pass - - import random - idx = random.randint(0, len(subjects) - 1) - return {"subject": subjects[idx], "body": bodies[idx], "source": "fallback"} - - -# ============================================================ -# 短信测试 -# ============================================================ - -def _aliyun_sign(secret: str, params: dict, method: str = "GET") -> str: - """阿里云签名机制 (HMAC-SHA1)""" - sorted_keys = sorted(params.keys()) - qs = "&".join([ - f"{urllib.parse.quote(k, safe='')}={urllib.parse.quote(str(params[k]), safe='')}" - for k in sorted_keys - ]) - string_to_sign = f"{method}&{urllib.parse.quote('/', safe='')}&{urllib.parse.quote(qs, safe='')}" - key = f"{secret}&" - h = hmac.new(key.encode('utf-8'), string_to_sign.encode('utf-8'), hashlib.sha1) - return base64.b64encode(h.digest()).decode('utf-8') - - -@router.post("/sms/test") -async def test_sms_config( - admin_user=Depends(get_current_admin), - db: Session = Depends(get_db), -): - """使用当前配置发送测试短信""" - config = db.query(SmsConfig).first() - if not config: - raise HTTPException(status_code=400, detail="短信配置未设置,请先保存短信配置") - - if not config.api_key or not config.api_secret: - raise HTTPException( - status_code=400, - detail="未配置阿里云短信 AccessKey,请填写 API Key (AccessKey ID) 和 API Secret (AccessKey Secret)" - ) - - if not config.sign_name: - raise HTTPException(status_code=400, detail="未配置短信签名") - - # 获取接收测试短信的手机号(管理员手机号) - admin = db.query(User).filter(User.id == admin_user.id).first() - phone = admin.phone if admin and admin.phone else None - if not phone: - raise HTTPException(status_code=400, detail="管理员未设置手机号,无法发送测试短信") - - logger.info(f"测试短信: provider={config.provider}, sign={config.sign_name}, phone={phone}") - - # 只有阿里云短信支持测试 - if "阿里云" not in (config.provider or ""): - raise HTTPException( - status_code=400, - detail=f"当前短信服务商 '{config.provider}' 暂不支持在线测试,请直接在生产环境测试" - ) - - try: - # 阿里云短信参数 - params = { - "AccessKeyId": config.api_key, - "Action": "SendSms", - "Format": "JSON", - "PhoneNumbers": phone, - "RegionId": "cn-hangzhou", - "SignName": config.sign_name, - "SignatureMethod": "HMAC-SHA1", - "SignatureNonce": str(uuid.uuid4()), - "SignatureVersion": "1.0", - "TemplateCode": config.template_code or "SMS_00000001", - "TemplateParam": '{"code":"123456"}', - "Timestamp": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), - "Version": "2017-05-25", - } - params["Signature"] = _aliyun_sign(config.api_secret, params) - - import requests as http_requests - resp = http_requests.post( - "https://dysmsapi.aliyuncs.com/", - data=params, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - timeout=10, - ) - result = resp.json() - - logger.info(f"阿里云短信API响应: {result}") - - if result.get("Code") == "OK": - LogService.log_audit( - admin_id=admin_user.id, - admin_name=admin_user.username, - action="test_sms_config", - target_type="sms_config", - detail=f"测试短信发送成功 -> {phone}", - ) - return {"message": f"测试短信已发送到 {phone}", "phone": phone, "biz_id": result.get("BizId")} - else: - error_msg = result.get("Message", result.get("Code", "未知错误")) - raise HTTPException(status_code=400, detail=f"阿里云短信发送失败: {error_msg}") - - except HTTPException: - raise - except Exception as e: - logger.error(f"测试短信发送失败: {e}") - raise HTTPException(status_code=500, detail=f"短信发送失败: {str(e)}") +""" +网站基础设置 API(管理员) +""" +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from ...database import get_db, SessionLocal +from ...models import SiteSetting, EmailConfig, SmsConfig, User, AIModel +from ...auth import get_current_admin +from ...middleware.log_service import LogService +import uuid +import smtplib +import hmac +import hashlib +import base64 +import urllib.parse +import time as time_module +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from datetime import datetime +from pydantic import BaseModel +from typing import Optional, Dict, Any +import logging + +logger = logging.getLogger("aiflow") + +router = APIRouter() + + +# ============================================================ +# Pydantic Models +# ============================================================ + +class EmailConfigUpdate(BaseModel): + smtp_host: Optional[str] = None + smtp_port: Optional[int] = None + username: Optional[str] = None + password: Optional[str] = None + from_address: Optional[str] = None + use_tls: Optional[bool] = None + + +class SmsConfigUpdate(BaseModel): + provider: Optional[str] = None + api_key: Optional[str] = None + api_secret: Optional[str] = None + sign_name: Optional[str] = None + template_code: Optional[str] = None + # 短信宝 (SMSBao) + username: Optional[str] = None + password: Optional[str] = None + goods_id: Optional[str] = None + + +# ============================================================ +# 网站设置 +# ============================================================ + +@router.get("/site") +async def get_site_settings( + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """获取网站设置(返回所有 key-value 对)""" + settings = db.query(SiteSetting).all() + result = {} + for s in settings: + result[s.key] = s.value + return {"code": 0, "data": result, "message": "ok"} + + +@router.put("/site") +async def update_site_settings( + settings: dict, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """批量更新网站设置""" + for key, value in settings.items(): + existing = db.query(SiteSetting).filter(SiteSetting.key == key).first() + if existing: + existing.value = str(value) if value is not None else None + existing.updated_at = datetime.now() + else: + new_setting = SiteSetting( + id=str(uuid.uuid4()), + key=key, + value=str(value) if value is not None else None, + ) + db.add(new_setting) + + db.commit() + + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="update_site_settings", + target_type="site_settings", + detail=f"更新网站设置: {list(settings.keys())}", + ) + + return {"message": "网站设置已更新"} + + +# ============================================================ +# 邮件配置 +# ============================================================ + +@router.get("/email") +async def get_email_config( + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """获取邮件配置""" + config = db.query(EmailConfig).first() + if not config: + return {"code": 0, "data": { + "smtp_host": "", + "smtp_port": 587, + "username": "", + "password": "", + "from_address": "", + "use_tls": True, + }, "message": "ok"} + + return {"code": 0, "data": { + "id": config.id, + "smtp_host": config.smtp_host, + "smtp_port": config.smtp_port, + "username": config.username, + "password": config.password, + "from_address": config.from_address, + "use_tls": config.use_tls, + }, "message": "ok"} + + +@router.put("/email") +async def update_email_config( + req: EmailConfigUpdate, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """更新邮件配置""" + config = db.query(EmailConfig).first() + if not config: + # 必填字段校验:不允许空值 + if not req.smtp_host: + raise HTTPException(status_code=400, detail="SMTP服务器地址不能为空") + if not req.username: + raise HTTPException(status_code=400, detail="SMTP用户名不能为空") + if not req.password: + raise HTTPException(status_code=400, detail="SMTP密码不能为空") + if not req.from_address: + raise HTTPException(status_code=400, detail="发件地址不能为空") + config = EmailConfig( + id=str(uuid.uuid4()), + smtp_host=req.smtp_host or "", + smtp_port=req.smtp_port or 587, + username=req.username or "", + password=req.password or "", + from_address=req.from_address or "", + use_tls=req.use_tls if req.use_tls is not None else True, + ) + db.add(config) + else: + if req.smtp_host is not None: + if not req.smtp_host.strip(): + raise HTTPException(status_code=400, detail="SMTP服务器地址不能为空") + config.smtp_host = req.smtp_host + if req.smtp_port is not None: + config.smtp_port = req.smtp_port + if req.username is not None: + if not req.username.strip(): + raise HTTPException(status_code=400, detail="SMTP用户名不能为空") + config.username = req.username + if req.password is not None: + if not req.password.strip(): + raise HTTPException(status_code=400, detail="SMTP密码不能为空") + config.password = req.password + if req.from_address is not None: + if not req.from_address.strip(): + raise HTTPException(status_code=400, detail="发件地址不能为空") + config.from_address = req.from_address + if req.use_tls is not None: + config.use_tls = req.use_tls + config.updated_at = datetime.now() + + db.commit() + + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="update_email_config", + target_type="email_config", + detail="更新邮件配置", + ) + + return {"code": 0, "data": None, "message": "邮件配置已更新"} + + +# ============================================================ +# 短信配置 +# ============================================================ + +@router.get("/sms") +async def get_sms_config( + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """获取短信配置""" + config = db.query(SmsConfig).first() + if not config: + return {"code": 0, "data": { + "provider": "", + "api_key": "", + "api_secret": "", + "sign_name": "", + "template_code": "", + "username": "", + "password": "", + "goods_id": "", + }, "message": "ok"} + + return {"code": 0, "data": { + "id": config.id, + "provider": config.provider, + "api_key": config.api_key, + "api_secret": config.api_secret, + "sign_name": config.sign_name, + "template_code": config.template_code, + "username": config.username, + "password": config.password, + "goods_id": config.goods_id, + }, "message": "ok"} + + +@router.put("/sms") +async def update_sms_config( + req: SmsConfigUpdate, + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """更新短信配置""" + config = db.query(SmsConfig).first() + if not config: + config = SmsConfig( + id=str(uuid.uuid4()), + provider=req.provider or "", + api_key=req.api_key or "", + api_secret=req.api_secret or "", + sign_name=req.sign_name, + template_code=req.template_code, + username=req.username, + password=req.password, + goods_id=req.goods_id, + ) + db.add(config) + else: + if req.provider is not None: + config.provider = req.provider + if req.api_key is not None: + config.api_key = req.api_key + if req.api_secret is not None: + config.api_secret = req.api_secret + if req.sign_name is not None: + config.sign_name = req.sign_name + if req.template_code is not None: + config.template_code = req.template_code + if req.username is not None: + config.username = req.username + if req.password is not None: + config.password = req.password + if req.goods_id is not None: + config.goods_id = req.goods_id + config.updated_at = datetime.now() + + db.commit() + + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="update_sms_config", + target_type="sms_config", + detail="更新短信配置", + ) + + return {"message": "短信配置已更新"} + + +# ============================================================ +# 邮件测试 +# ============================================================ + +@router.post("/email/test") +async def test_email_config( + test_to: str = Query(default=None, description="自定义测试收件地址"), + test_subject: str = Query(default=None, description="自定义测试邮件主题"), + test_body: str = Query(default=None, description="自定义测试邮件正文"), + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """使用当前配置发送测试邮件;可传参自定义收件人、主题和正文""" + config = db.query(EmailConfig).first() + if not config or not config.smtp_host: + raise HTTPException(status_code=400, detail="邮件配置未设置,请先保存邮件配置") + if not config.username: + raise HTTPException(status_code=400, detail="SMTP用户名未填写,请检查邮件配置") + if not config.password: + raise HTTPException(status_code=400, detail="SMTP密码未填写,请检查邮件配置") + if not config.from_address: + raise HTTPException(status_code=400, detail="发件地址未填写,请检查邮件配置") + + # 优先级:传入参数 > 管理员邮箱 > 发件地址 + admin = db.query(User).filter(User.id == admin_user.id).first() + recipient = test_to or (admin.email if admin and admin.email else None) or config.from_address + if not recipient: + raise HTTPException(status_code=400, detail="无法获取接收地址: 请填写收件邮箱或设置管理员邮箱") + + subject = test_subject or "AIFlow 邮件配置测试" + body = test_body or f"这是一封来自 AIFlow 平台的测试邮件。\n\n发送时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\nSMTP服务器: {config.smtp_host}" + + logger.info(f"测试邮件: SMTP={config.smtp_host}:{config.smtp_port}, " + f"from={config.from_address}, to={recipient}") + + try: + msg = MIMEMultipart() + msg["From"] = config.from_address + msg["To"] = recipient + msg["Subject"] = subject + msg.attach(MIMEText(body, "plain")) + + if config.use_tls: + server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=10) + server.starttls() + else: + server = smtplib.SMTP(config.smtp_host, config.smtp_port, timeout=10) + + server.login(config.username, config.password) + server.sendmail(config.from_address, recipient, msg.as_string()) + server.quit() + + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="test_email_config", + target_type="email_config", + detail=f"测试邮件发送成功 -> {recipient} via {config.smtp_host}", + ) + + return {"code": 0, "data": {"message": f"测试邮件已发送到 {recipient}", "recipient": recipient}, "message": "ok"} + except smtplib.SMTPAuthenticationError as e: + logger.error(f"SMTP认证失败: {e}") + raise HTTPException(status_code=400, detail=f"SMTP认证失败,请检查用户名和密码: {str(e)}") + except smtplib.SMTPConnectError as e: + logger.error(f"SMTP连接失败: {e}") + raise HTTPException(status_code=400, detail=f"无法连接到SMTP服务器 {config.smtp_host}:{config.smtp_port}: {str(e)}") + except smtplib.SMTPException as e: + logger.error(f"SMTP发送失败: {e}") + raise HTTPException(status_code=500, detail=f"邮件发送失败(SMTP): {str(e)}") + except OSError as e: + logger.error(f"邮件发送网络错误: {e}") + raise HTTPException(status_code=500, detail=f"网络连接错误: {str(e)}") + except BaseException as e: + logger.error(f"测试邮件发送失败(严重): {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + raise HTTPException(status_code=500, detail=f"邮件发送失败: {str(e)}") + + +@router.post("/email/generate-content") +async def generate_email_content( + topic: str = Query(default="测试邮件"), + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """AI生成测试邮件内容:调用默认LLM生成邮件主题和正文""" + import httpx as httpx_lib + subjects = [ + "【测试】AIFlow邮件服务配置验证", + "AIFlow平台 - 邮件投递测试", + "测试邮件:您的SMTP服务已就绪", + ] + bodies = [ + f"您好!\n\n这是一封来自 AIFlow 智能自动化平台的测试邮件,用于验证邮件服务配置是否正常。\n\n如果您收到此邮件,说明SMTP邮件服务已配置成功。\n\n此致\nAIFlow 平台团队", + f"Hi,\n\nThis is a test email from AIFlow Platform.\n\nIf you received this, the SMTP service works fine.\n\nBest regards,\nAIFlow Team", + f"【系统通知】\n邮件服务连通性测试通过。\n\n发件服务器配置正确,能够成功投递邮件。\n\n—— AIFlow", + ] + + # 尝试使用默认LLM生成更好的内容 + try: + default_model = db.query(AIModel).filter( + AIModel.is_default == True, AIModel.is_enabled == True + ).first() + if default_model and default_model.endpoint: + async with httpx_lib.AsyncClient(timeout=15) as client: + headers = {} + if default_model.api_key: + headers["Authorization"] = f"Bearer {default_model.api_key}" + resp = await client.post( + default_model.endpoint, + json={ + "model": default_model.name, + "messages": [ + {"role": "system", "content": "你是一个专业的技术文档撰写者。请根据主题生成一封测试邮件的主题和正文。返回JSON格式: {\"subject\": \"邮件主题\", \"body\": \"邮件正文\"}"}, + {"role": "user", "content": f"主题: {topic}"}, + ], + "max_tokens": 400, + }, + headers=headers, + ) + if resp.status_code == 200: + content = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "") + try: + data = json.loads(content) + if data.get("subject") and data.get("body"): + return {"subject": data["subject"], "body": data["body"], "source": "ai"} + except: + # 尝试解析非JSON格式 + lines = content.strip().split("\n") + if len(lines) >= 2: + return {"subject": lines[0].replace("主题:", "").strip(), "body": "\n".join(lines[1:]).replace("正文:", "").strip(), "source": "ai_partial"} + except Exception: + pass + + import random + idx = random.randint(0, len(subjects) - 1) + return {"subject": subjects[idx], "body": bodies[idx], "source": "fallback"} + + +# ============================================================ +# 短信测试 +# ============================================================ + +def _aliyun_sign(secret: str, params: dict, method: str = "GET") -> str: + """阿里云签名机制 (HMAC-SHA1)""" + sorted_keys = sorted(params.keys()) + qs = "&".join([ + f"{urllib.parse.quote(k, safe='')}={urllib.parse.quote(str(params[k]), safe='')}" + for k in sorted_keys + ]) + string_to_sign = f"{method}&{urllib.parse.quote('/', safe='')}&{urllib.parse.quote(qs, safe='')}" + key = f"{secret}&" + h = hmac.new(key.encode('utf-8'), string_to_sign.encode('utf-8'), hashlib.sha1) + return base64.b64encode(h.digest()).decode('utf-8') + + +# 短信宝 (SMSBao) 辅助 +def _smsbao_md5(password: str) -> str: + """短信宝密码:登录密码的 MD5(32位,不区分大小写)""" + return hashlib.md5(password.encode("utf-8")).hexdigest() + +SMSBAO_ERRORS = { + "0": "发送成功", + "30": "密码错误", + "40": "账号不存在", + "41": "余额不足", + "43": "IP地址限制", + "50": "内容含有敏感词", + "51": "手机号码不正确", +} + + +@router.post("/sms/test") +async def test_sms_config( + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """使用当前配置发送测试短信""" + config = db.query(SmsConfig).first() + if not config: + raise HTTPException(status_code=400, detail="短信配置未设置,请先保存短信配置") + + if not config.api_key or not config.api_secret: + raise HTTPException( + status_code=400, + detail="未配置阿里云短信 AccessKey,请填写 API Key (AccessKey ID) 和 API Secret (AccessKey Secret)" + ) + + if not config.sign_name: + raise HTTPException(status_code=400, detail="未配置短信签名") + + # 获取接收测试短信的手机号(管理员手机号) + admin = db.query(User).filter(User.id == admin_user.id).first() + phone = admin.phone if admin and admin.phone else None + if not phone: + raise HTTPException(status_code=400, detail="管理员未设置手机号,无法发送测试短信") + + logger.info(f"测试短信: provider={config.provider}, sign={config.sign_name}, phone={phone}") + + # 短信宝 (SMSBao) + if config.provider == "短信宝": + if not config.username or not config.password: + raise HTTPException(status_code=400, detail="未配置短信宝用户名或密码,请先在配置中填写") + sign = config.sign_name or "" + if "【" not in sign: + sign = f"【{sign}】" if sign else "【七虎AI】" + content = f"{sign}测试短信:您的验证码是123456,若非本人操作请忽略。" + pwd_md5 = _smsbao_md5(config.password) + params = {"u": config.username, "p": pwd_md5, "m": phone, "c": content} + if config.goods_id: + params["g"] = config.goods_id + import requests as http_requests + try: + resp = http_requests.get( + "https://api.smsbao.com/sms", params=params, timeout=10, + proxies={"http": None, "https": None}, + ) + code = resp.text.strip().split("\n")[0] + except Exception as e: + logger.error(f"短信宝请求失败: {e}") + raise HTTPException(status_code=500, detail=f"短信宝请求失败: {str(e)}") + if code == "0": + LogService.log_audit( + admin_id=admin_user.id, admin_name=admin_user.username, + action="test_sms_config", target_type="sms_config", + detail=f"短信宝测试短信发送成功 -> {phone}", + ) + return {"message": f"测试短信已发送到 {phone}", "phone": phone} + msg = SMSBAO_ERRORS.get(code, f"未知错误码 {code}") + raise HTTPException(status_code=400, detail=f"短信宝发送失败: {msg} (code={code})") + + # 只有阿里云短信支持测试 + if "阿里云" not in (config.provider or ""): + raise HTTPException( + status_code=400, + detail=f"当前短信服务商 '{config.provider}' 暂不支持在线测试,请直接在生产环境测试" + ) + + try: + # 阿里云短信参数 + params = { + "AccessKeyId": config.api_key, + "Action": "SendSms", + "Format": "JSON", + "PhoneNumbers": phone, + "RegionId": "cn-hangzhou", + "SignName": config.sign_name, + "SignatureMethod": "HMAC-SHA1", + "SignatureNonce": str(uuid.uuid4()), + "SignatureVersion": "1.0", + "TemplateCode": config.template_code or "SMS_00000001", + "TemplateParam": '{"code":"123456"}', + "Timestamp": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), + "Version": "2017-05-25", + } + params["Signature"] = _aliyun_sign(config.api_secret, params) + + import requests as http_requests + resp = http_requests.post( + "https://dysmsapi.aliyuncs.com/", + data=params, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + timeout=10, + ) + result = resp.json() + + logger.info(f"阿里云短信API响应: {result}") + + if result.get("Code") == "OK": + LogService.log_audit( + admin_id=admin_user.id, + admin_name=admin_user.username, + action="test_sms_config", + target_type="sms_config", + detail=f"测试短信发送成功 -> {phone}", + ) + return {"message": f"测试短信已发送到 {phone}", "phone": phone, "biz_id": result.get("BizId")} + else: + error_msg = result.get("Message", result.get("Code", "未知错误")) + raise HTTPException(status_code=400, detail=f"阿里云短信发送失败: {error_msg}") + + except HTTPException: + raise + except Exception as e: + logger.error(f"测试短信发送失败: {e}") + raise HTTPException(status_code=500, detail=f"短信发送失败: {str(e)}") + + +# ============================================================ +# 短信宝余额查询 +# ============================================================ + +@router.post("/sms/balance") +async def query_sms_balance( + admin_user=Depends(get_current_admin), + db: Session = Depends(get_db), +): + """查询短信宝账户余额(发送条数, 剩余条数)""" + config = db.query(SmsConfig).first() + if not config or config.provider != "短信宝": + raise HTTPException(status_code=400, detail="仅短信宝支持余额查询,请先选择短信宝并保存账号") + if not config.username or not config.password: + raise HTTPException(status_code=400, detail="未配置短信宝用户名或密码") + + import requests as http_requests + pwd_md5 = _smsbao_md5(config.password) + try: + resp = http_requests.get( + "https://www.smsbao.com/query", + params={"u": config.username, "p": pwd_md5}, + timeout=10, + proxies={"http": None, "https": None}, + ) + lines = resp.text.strip().split("\n") + except Exception as e: + logger.error(f"短信宝余额查询请求失败: {e}") + raise HTTPException(status_code=500, detail=f"短信宝余额查询失败: {str(e)}") + + code = lines[0].strip() if lines else "" + if code != "0": + msg = SMSBAO_ERRORS.get(code, f"未知错误码 {code}") + raise HTTPException(status_code=400, detail=f"查询失败: {msg}") + + sent = remain = 0 + if len(lines) >= 2: + parts = lines[1].split(",") + try: + sent = int(parts[0]) + except (ValueError, IndexError): + sent = 0 + try: + remain = int(parts[1]) + except (ValueError, IndexError): + remain = 0 + return {"code": 0, "data": {"sent": sent, "remain": remain}, "message": "ok"} diff --git a/backend/python/app/models.py b/backend/python/app/models.py index defbbc7..a0765be 100644 --- a/backend/python/app/models.py +++ b/backend/python/app/models.py @@ -1,1101 +1,1105 @@ -""" -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") - 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) - 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) - agent_endpoint = Column(String(500), default=None) - 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) - 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) +""" +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") + 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) + 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) + agent_endpoint = Column(String(500), default=None) + 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) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1856b6e..40c8c1a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -28,6 +28,7 @@ const AdminDashboard = React.lazy(() => import('@/pages/admin/Dashboard')); const AdminUsers = React.lazy(() => import('@/pages/admin/Users')); const AdminModels = React.lazy(() => import('@/pages/admin/Models')); const AdminSiteSettings = React.lazy(() => import('@/pages/admin/SiteSettings')); +const AdminSmsConfig = React.lazy(() => import('@/pages/admin/SmsConfig')); const AdminPayment = React.lazy(() => import('@/pages/admin/Payment')); const AdminServices = React.lazy(() => import('@/pages/admin/Services')); const AdminMembership = React.lazy(() => import('@/pages/admin/Membership')); @@ -130,6 +131,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index d5887ed..dc35125 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,1004 +1,2010 @@ /** + * AIFlow API 客户端 + * 统一管理所有前后端API请求 + */ + const API_BASE = '/app/api'; + + // 从localStorage获取token + function getToken(): string | null { + return localStorage.getItem('aiflow_token'); + } + + // 通用请求方法 + export async function request( + endpoint: string, + options: RequestInit = {} + ): Promise<{ code: number; data: T; message: string }> { + const token = getToken(); + const headers: Record = { + 'Content-Type': 'application/json', + ...(options.headers as Record || {}), + }; + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + const res = await fetch(`${API_BASE}${endpoint}`, { + ...options, + headers, + }); + if (res.status === 401) { + // 登录接口的401需要精确提示(用户名不存在 / 密码错误) + if (endpoint === '/auth/login') { + const err = await res.json().catch(() => ({ detail: '登录失败' })); + const error: any = new Error(err.detail || '登录失败'); + error.status = 401; + error.detail = err.detail; + throw error; + } + // 其他接口的401 = token过期/未登录,清除状态并跳转 + localStorage.removeItem('aiflow_token'); + localStorage.removeItem('aiflow_user'); + window.location.href = '/login'; + throw new Error('未登录'); + } + if (!res.ok) { + const err = await res.json().catch(() => ({ detail: '请求失败' })); + const error: any = new Error(err.detail || err.message || '请求失败'); + error.status = res.status; + error.response = { status: res.status, data: err }; + error.detail = err.detail; + throw error; + } + return res.json(); + } + + // GET请求 + function get(endpoint: string): Promise<{ code: number; data: T; message: string }> { + return request(endpoint); + } + + // POST请求 + function post(endpoint: string, body?: any): Promise<{ code: number; data: T; message: string }> { + return request(endpoint, { + method: 'POST', + body: body ? JSON.stringify(body) : undefined, + }); + } + + // PUT请求 + function put(endpoint: string, body?: any): Promise<{ code: number; data: T; message: string }> { + return request(endpoint, { + method: 'PUT', + body: body ? JSON.stringify(body) : undefined, + }); + } + + // DELETE请求 + function del(endpoint: string): Promise<{ code: number; data: T; message: string }> { + return request(endpoint, { method: 'DELETE' }); + } + + // ============================================================ + // 认证相关API + // ============================================================ + export const authAPI = { + login: (username: string, password: string) => + post('/auth/login', { username, password }), + register: (data: { username: string; password: string; email?: string; phone?: string }) => + post('/auth/register', data), + getMe: () => get('/auth/me'), + logout: () => post('/auth/logout'), + getSettings: () => get('/auth/settings'), + }; + + // ============================================================ + // 聊天相关API + // ============================================================ + export const chatAPI = { + getConversations: (status?: string) => + get(`/chat/conversations${status ? `?status=${status}` : ''}`), + createConversation: (data: { title: string; model_id?: string }) => + post('/chat/conversations', data), + updateConversation: (id: string, data: any) => + put(`/chat/conversations/${id}`, data), + deleteConversation: (id: string) => + del(`/chat/conversations/${id}`), + getMessages: (convId: string) => + get(`/chat/conversations/${convId}/messages`), + sendMessage: (convId: string, data: { content: string; model_id?: string; agent_ids?: string[]; skill_ids?: string[]; knowledge_base_ids?: string[]; use_tools?: boolean }) => + post(`/chat/conversations/${convId}/messages`, data), + clearContext: (convId: string) => + post(`/chat/conversations/${convId}/clear`), + convertToKnowledgeGraph: (convId: string) => + post(`/chat/conversations/${convId}/to-knowledge-graph/`), + getModels: () => get('/chat/models'), + }; + + // ============================================================ + // 频道相关API + // ============================================================ + export const channelAPI = { + getChannels: () => get('/channels/'), + createChannel: (data: any) => post('/channels/', data), + getChannel: (id: string) => get(`/channels/${id}`), + updateChannel: (id: string, data: any) => put(`/channels/${id}`, data), + deleteChannel: (id: string) => del(`/channels/${id}`), + connectChannel: (id: string) => post(`/channels/${id}/connect`), + disconnectChannel: (id: string) => post(`/channels/${id}/disconnect`), + getPlatformConfigs: () => get('/channels/platforms/config'), + webhookMessage: (id: string, data: { from_user: string; content: string; message_type?: string }) => + post(`/channels/${id}/webhook`, data), + getMembers: (id: string) => get(`/channels/${id}/members`), + addMember: (id: string, userId: string) => post(`/channels/${id}/members`, { user_id: userId }), + removeMember: (id: string, userId: string) => del(`/channels/${id}/members/${userId}`), + getMessages: (id: string, params?: { limit?: number; offset?: number }) => { + const sp = new URLSearchParams(); + if (params?.limit) sp.set('limit', String(params.limit)); + if (params?.offset) sp.set('offset', String(params.offset)); + const qs = sp.toString(); + return get(`/channels/${id}/messages${qs ? `?${qs}` : ''}`); + }, + sendMessage: (id: string, content: string) => post(`/channels/${id}/messages`, { content }), + authorize: (platform: string) => post('/channels/authorize', { platform }), + scanAuthorize: (platform: string, credentials?: Record) => post(`/channels/authorize/scan?platform=${platform}`, credentials ? { credentials } : {}), + scanAuthorizeStatus: (platform: string) => post(`/channels/authorize/scan/status?platform=${platform}`), + }; + + // ============================================================ + // 知识库相关API + // ============================================================ + export const knowledgeAPI = { + getBases: () => get('/knowledge/bases'), + createBase: (data: any) => post('/knowledge/bases', data), + getBase: (id: string) => get(`/knowledge/bases/${id}`), + updateBase: (id: string, data: any) => put(`/knowledge/bases/${id}`, data), + deleteBase: (id: string) => del(`/knowledge/bases/${id}`), + getDocuments: (baseId: string) => get(`/knowledge/bases/${baseId}/documents`), + getDocument: (baseId: string, docId: string) => get(`/knowledge/bases/${baseId}/documents/${docId}`), + uploadDocument: (baseId: string, data: any) => post(`/knowledge/bases/${baseId}/documents/`, data), + deleteDocument: (baseId: string, docId: string) => del(`/knowledge/bases/${baseId}/documents/${docId}`), + getDirectories: () => get('/knowledge/directories'), + createDirectory: (data: any) => post('/knowledge/directories', data), + buildGraph: (baseId: string) => post(`/knowledge/bases/${baseId}/build-graph`), + getGraph: (baseId: string) => get(`/knowledge/bases/${baseId}/graph`), + searchQA: (baseId: string, question: string) => post('/knowledge/qa', { base_id: baseId, question }), + searchDocuments: (baseId: string, keyword: string) => get(`/knowledge/bases/${baseId}/search?keyword=${encodeURIComponent(keyword)}`), + }; + + // ============================================================ + // 应用市场相关API + // ============================================================ + export const marketAPI = { + getApps: (params?: { type?: string; source?: string; search?: string; page?: number; page_size?: number }) => { + const searchParams = new URLSearchParams(); + if (params?.type) searchParams.set('type', params.type); + if (params?.source) searchParams.set('source', params.source); + if (params?.search) searchParams.set('search', params.search); + if (params?.page) searchParams.set('page', String(params.page)); + if (params?.page_size) searchParams.set('page_size', String(params.page_size)); + const qs = searchParams.toString(); + return get(`/market/apps${qs ? `?${qs}` : ''}`); + }, + getApp: (id: string) => get(`/market/apps/${id}`), + createApp: (data: any) => post('/market/apps', data), + purchaseApp: (id: string) => post(`/market/apps/${id}/purchase`), + installApp: (id: string) => post(`/market/apps/${id}/install`), + uninstallApp: (id: string) => del(`/market/apps/${id}/uninstall`), + reviewApp: (id: string, data: { rating: number; content?: string }) => + post(`/market/apps/${id}/review`, data), + getMyApps: () => get('/market/my-apps'), + getReviews: (appId: string) => get(`/market/reviews/${appId}`), + }; + + // ============================================================ + // Token相关API + // ============================================================ + export const tokenAPI = { + getOverview: () => get('/token/overview'), + getRankings: () => get('/token/rankings'), + getLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + const qs = sp.toString(); + return get(`/token/logs${qs ? `?${qs}` : ''}`); + }, + getBalance: () => get('/token/balance'), + recharge: (amount: number) => post('/token/recharge', { amount }), + }; + + // ============================================================ + // 付费相关API + // ============================================================ + export const paymentAPI = { + getPlans: () => get('/payment/plans'), + createOrder: (data: any) => post('/payment/orders', data), + getOrders: (status?: string) => get(`/payment/orders${status ? `?status=${status}` : ''}`), + getOrder: (id: string) => get(`/payment/orders/${id}`), + payOrder: (id: string, method?: string) => post(`/payment/orders/${id}/pay`, { payment_method: method }), + getCoupons: () => get('/payment/coupons'), + useCoupon: (code: string) => post('/payment/coupons/use', { code }), + getServices: () => get('/payment/services'), + createCustomOrder: (data: any) => post('/payment/custom-services', data), + createServiceRequest: (data: any) => post('/payment/service-requests', data), + }; + + // ============================================================ + // 个人中心相关API + // ============================================================ + export const profileAPI = { + getProfile: () => get('/profile/'), + updateProfile: (data: any) => put('/profile/', data), + updatePassword: (data: { old_password: string; new_password: string }) => + put('/profile/password', data), + getLoginLogs: () => get('/profile/login-logs'), + getApiKeys: () => get('/profile/api-keys'), + createApiKey: (name?: string) => post('/profile/api-keys', { name }), + updateApiKey: (id: string, status: string) => put(`/profile/api-keys/${id}`, { status }), + getMembership: () => get('/profile/membership'), + getPurchasedApps: () => get('/profile/purchased-apps'), + }; + + // ============================================================ + // 通知相关API + // ============================================================ + export const notificationAPI = { + getNotifications: (params?: { type?: string; page?: number }) => { + const sp = new URLSearchParams(); + if (params?.type) sp.set('type', params.type); + if (params?.page) sp.set('page', String(params.page)); + const qs = sp.toString(); + return get(`/notifications/${qs ? `?${qs}` : ''}`); + }, + markRead: (id: string) => put(`/notifications/${id}/read`), + markAllRead: () => put('/notifications/read-all'), + getUnreadCount: () => get('/notifications/unread-count'), + }; + + // ============================================================ + // 资产管理相关API -// ============================================================ -export const assetsAPI = { - getAssets: (category?: string) => - get(`/assets/${category ? `?category=${category}` : ''}`), - createAsset: (data: any) => post('/assets/', data), - getAsset: (id: string) => get(`/assets/${id}`), - updateAsset: (id: string, data: any) => put(`/assets/${id}`, data), - deleteAsset: (id: string) => del(`/assets/${id}`), - getCategories: () => get('/assets/categories'), - getNovelChapters: (novelId: string) => get(`/assets/novels/${novelId}/chapters`), - addChapter: (novelId: string, data: any) => post(`/assets/novels/${novelId}/chapters`, data), -}; // ============================================================ -// 后台管理API + +export const assetsAPI = { + + getAssets: (category?: string) => + + get(`/assets/${category ? `?category=${category}` : ''}`), + + createAsset: (data: any) => post('/assets/', data), + + getAsset: (id: string) => get(`/assets/${id}`), + + updateAsset: (id: string, data: any) => put(`/assets/${id}`, data), + + deleteAsset: (id: string) => del(`/assets/${id}`), + + getCategories: () => get('/assets/categories'), + + getNovelChapters: (novelId: string) => get(`/assets/novels/${novelId}/chapters`), + + addChapter: (novelId: string, data: any) => post(`/assets/novels/${novelId}/chapters`, data), + +}; + + + // ============================================================ + +// 后台管理API + +// ============================================================ + export const adminAPI = { + // 仪表盘 + getStats: () => get('/admin/dashboard/stats'), + getRecentLogs: () => get('/admin/dashboard/recent-logs'), + getRecentAudit: () => get('/admin/dashboard/recent-audit'), + getDailyStats: (days?: number) => get<{ days: number; data: Array<{ date: string; tokens: number }> }>(`/admin/dashboard/daily-stats${days ? `?days=${days}` : ''}`), + // 用户管理 + getUsers: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/users/${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + getUser: (id: string) => get(`/admin/users/${id}`), + banUser: (id: string) => put(`/admin/users/${id}/ban`), + unbanUser: (id: string) => put(`/admin/users/${id}/unban`), + getRegisterSettings: () => get('/admin/users/register-settings'), + updateRegisterSettings: (data: any) => put('/admin/users/register-settings', data), + // 模型管理 + getProviders: () => get('/admin/models/providers'), + addProvider: (data: any) => post('/admin/models/providers', data), + updateProvider: (id: string, data: any) => put(`/admin/models/providers/${id}`, data), + deleteProvider: (id: string) => del(`/admin/models/providers/${id}`), + fetchModels: (id: string) => post(`/admin/models/providers/${id}/fetch-models`), + addModels: (id: string, modelIds: string[]) => post(`/admin/models/providers/${id}/add-models`, { model_ids: modelIds }), + getProtocols: () => get('/admin/models/protocols'), + getAdminModels: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/models/${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + addModel: (data: any) => post('/admin/models/', data), + updateModel: (id: string, data: any) => put(`/admin/models/${id}`, data), + deleteModel: (id: string) => del(`/admin/models/${id}`), + toggleModel: (id: string, isEnabled: boolean) => put(`/admin/models/${id}/toggle?is_enabled=${isEnabled}`), + setDefaultModel: (id: string) => put(`/admin/models/${id}/set-default`), + // Phase 2: Enhanced model management + testModelConnection: (id: string) => post(`/admin/models/${id}/test-connection`), + testAllModels: () => post('/admin/models/test-all'), + fetchProviderModels: (id: string) => post(`/admin/models/providers/${id}/fetch-models`), + createAdminModel: (data: any) => post('/admin/models/', data), + updateAdminModel: (id: string, data: any) => put(`/admin/models/${id}`, data), + deleteAdminModel: (id: string) => del(`/admin/models/${id}`), + getModelStats: () => get('/admin/models/stats'), + // 网站设置 + getSiteSettings: () => get('/admin/settings/site'), + updateSiteSettings: (data: any) => put('/admin/settings/site', data), + getEmailConfig: () => get('/admin/settings/email'), + updateEmailConfig: (data: any) => put('/admin/settings/email', data), + testEmail: (params?: { test_to?: string; test_subject?: string; test_body?: string }) => { + const qs = new URLSearchParams(); + if (params?.test_to) qs.set('test_to', params.test_to); + if (params?.test_subject) qs.set('test_subject', params.test_subject); + if (params?.test_body) qs.set('test_body', params.test_body); + const qstr = qs.toString(); + return post(`/admin/settings/email/test${qstr ? `?${qstr}` : ''}`); + }, + generateEmailContent: (topic: string) => post(`/admin/settings/email/generate-content?topic=${encodeURIComponent(topic)}`), + getSmsConfig: () => get('/admin/settings/sms'), + updateSmsConfig: (data: any) => put('/admin/settings/sms', data), + testSms: () => post('/admin/settings/sms/test'), + + getSmsBalance: () => post('/admin/settings/sms/balance'), + // 支付配置 + getPaymentConfigs: () => get('/admin/payment/configs'), + updatePaymentConfig: (id: string, data: any) => put(`/admin/payment/configs/${id}`, data), + getAdminOrders: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/payment/orders${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + refundOrder: (id: string) => put(`/admin/payment/orders/${id}/refund`), + // 增值服务 + getServices: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/services/${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + addService: (data: any) => post('/admin/services/', data), + updateService: (id: string, data: any) => put(`/admin/services/${id}`, data), + deleteService: (id: string) => del(`/admin/services/${id}`), + getCustomOrders: (status?: string) => get(`/admin/services/custom-orders${status ? `?status=${status}` : ''}`), + updateCustomOrder: (id: string, data: any) => put(`/admin/services/custom-orders/${id}`, data), + getCustomRequests: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/services/custom-requests${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + updateCustomRequest: (id: string, data: any) => put(`/admin/services/custom-requests/${id}`, data), + getAvailableAgents: () => get('/admin/services/agents'), + assignAgentToService: (id: string, agentId: string) => put(`/admin/services/${id}/agent?agent_id=${agentId}`), + // 会员管理(增强版) + getPlans: () => get('/admin/membership/plans'), + addPlan: (params: string) => post(`/admin/membership/plans${params}`), + updatePlan: (id: string, params: string) => put(`/admin/membership/plans/${id}${params}`), + deletePlan: (id: string) => del(`/admin/membership/plans/${id}`), + getSubscriptions: (page?: number) => get(`/admin/membership/subscriptions${page ? `?page=${page}` : ''}`), + getAutomationRules: () => get('/admin/membership/automation-rules'), + getBenefitsComparison: () => get('/admin/membership/benefits-comparison'), + getMembershipConfigs: () => get('/admin/membership/global-configs'), + updateMembershipConfig: (key: string, params: string) => put(`/admin/membership/global-configs/${key}${params}`), + getUserMemberships: (page?: number, planId?: string, status?: string) => { + const p = new URLSearchParams(); + if (page) p.append('page', String(page)); + if (planId) p.append('plan_id', planId); + if (status) p.append('status', status); + const qs = p.toString(); + return get(`/admin/membership/user-memberships${qs ? `?${qs}` : ''}`); + }, + assignPlan: (userId: string, planId: string, days: number) => put(`/admin/membership/users/${userId}/assign-plan?plan_id=${planId}&duration_days=${days}`), + getPaymentLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/payment-logs${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + getAuditLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/audit-logs${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + // 新增:日志统计 + getLogStats: () => get('/admin/logs/stats'), + // 新增:API调用统计 + getApiStats: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/api-stats${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + // 新增:慢请求记录 + getSlowRequests: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/slow-requests${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + // 新增:操作类型枚举 + getActionTypes: () => get('/admin/logs/action-types'), + // 新增:审计日志导出 + exportAuditLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/export/audit-logs${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + exportOperationLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/export/operations${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + exportTokenLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/export/token-logs${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + getRiskRules: () => get('/admin/logs/risk-rules'), + addRiskRule: (data: any) => post('/admin/logs/risk-rules', data), + updateRiskRule: (id: string, data: any) => put(`/admin/logs/risk-rules/${id}`, data), + deleteRiskRule: (id: string) => del(`/admin/logs/risk-rules/${id}`), + // 技能管理 (Phase 2.2) + getAdminSkills: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); }); + return get(`/admin/skills/${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + getAdminSkill: (id: string) => get(`/admin/skills/${id}`), + createAdminSkill: (data: any) => post('/admin/skills/', data), + updateAdminSkill: (id: string, data: any) => put(`/admin/skills/${id}`, data), + deleteAdminSkill: (id: string) => del(`/admin/skills/${id}`), + changeAdminSkillState: (id: string, data: any) => put(`/admin/skills/${id}/state`, data), + getAdminSkillVersions: (id: string) => get(`/admin/skills/${id}/versions`), + rollbackAdminSkill: (id: string, data: any) => post(`/admin/skills/${id}/rollback`, data), + publishAdminSkill: (id: string, data: any) => post(`/admin/skills/${id}/publish`, data), + unpublishAdminSkill: (id: string) => post(`/admin/skills/${id}/unpublish`), + getAdminSkillsStats: () => get('/admin/skills/stats/overview'), + // 智能体管理 (Phase 2.3) + getAdminAgents: (params?: any) => { + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + return get(`/admin/agents/${query}`); + }, + getAdminAgent: (id: string) => get(`/admin/agents/${id}`), + createAdminAgent: (data: any) => post('/admin/agents/', data), + updateAdminAgent: (id: string, data: any) => put(`/admin/agents/${id}`, data), + deleteAdminAgent: (id: string) => del(`/admin/agents/${id}`), + toggleAdminAgent: (id: string, isActive: boolean) => put(`/admin/agents/${id}/toggle`, { is_active: isActive }), + getAdminAgentsStats: () => get('/admin/agents/stats/overview'), + getAdminAgentReviews: (id: string) => get(`/admin/agents/${id}/reviews`), + getAdminAvailableSkills: (params?: any) => { + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + return get(`/admin/agents/available/skills${query}`); + }, + getAdminAvailableModels: (params?: any) => { + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + return get(`/admin/agents/available/models${query}`); + }, + // 知识库管理 (Phase 2.4) + getAdminKnowledgeStats: () => get('/admin/knowledge/stats/overview'), + getAdminKnowledge: (params?: any) => { + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + return get(`/admin/knowledge/${query}`); + }, + getAdminKnowledgeDetail: (id: string) => get(`/admin/knowledge/${id}`), + createAdminKnowledge: (data: any) => post('/admin/knowledge/', data), + updateAdminKnowledge: (id: string, data: any) => put(`/admin/knowledge/${id}`, data), + deleteAdminKnowledge: (id: string) => del(`/admin/knowledge/${id}`), + toggleAdminKnowledge: (id: string, isActive: boolean) => put(`/admin/knowledge/${id}/toggle`, { is_active: isActive }), + getAdminKnowledgeDocs: (kbId: string, params?: any) => { + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + return get(`/admin/knowledge/${kbId}/documents${query}`); + }, + deleteAdminKnowledgeDoc: (kbId: string, docId: string) => del(`/admin/knowledge/${kbId}/documents/${docId}`), + getAdminKnowledgeExtServices: () => get('/admin/knowledge/external/services'), + createAdminKnowledgeExtService: (data: any) => post('/admin/knowledge/external/services', data), + updateAdminKnowledgeExtService: (id: string, data: any) => put(`/admin/knowledge/external/services/${id}`, data), + deleteAdminKnowledgeExtService: (id: string) => del(`/admin/knowledge/external/services/${id}`), + checkAdminKnowledgeExtStatus: (id: string) => get(`/admin/knowledge/external/services/${id}/status`), + syncAdminKnowledge: (kbId: string, data: any) => post(`/admin/knowledge/external/sync/${kbId}`, data), + batchDeleteAdminKnowledge: (ids: string[]) => post('/admin/knowledge/batch/delete', ids), + // OSS存储配置 (Phase 3.1) + getAssetStats: () => get('/admin/assets/stats'), + getAdminAssets: (query: string) => get(`/admin/assets/?${query}`), + deleteAdminAsset: (id: string) => del(`/admin/assets/${id}`), + getAssetActivity: () => get('/admin/assets/activity'), + getAdminStorageStats: () => get('/admin/storage/stats/overview'), + getAdminStorageConfigs: (params?: any) => { + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + return get(`/admin/storage/${query}`); + }, + getAdminStorageConfig: (id: string) => get(`/admin/storage/${id}`), + createAdminStorageConfig: (data: any) => post('/admin/storage/', data), + updateAdminStorageConfig: (id: string, data: any) => put(`/admin/storage/${id}`, data), + deleteAdminStorageConfig: (id: string) => del(`/admin/storage/${id}`), + toggleAdminStorageConfig: (id: string) => put(`/admin/storage/${id}/toggle`), + setDefaultAdminStorageConfig: (id: string) => put(`/admin/storage/${id}/set-default`), + testAdminStorageConfig: (data: any) => post('/admin/storage/test', data), + getAdminStorageUsage: (id: string) => get(`/admin/storage/${id}/usage`), + // Storage Quota APIs (Phase C5) + getStorageQuotaOverview: () => get('/admin/storage/quota/overview'), + getStorageQuotaUsers: (params?: any) => { + if (params) { + const query = Object.entries(params).map(([k, v]) => v !== undefined && v !== '' && v !== null ? `${k}=${encodeURIComponent(String(v))}` : '').filter(Boolean).join('&'); + return get(`/admin/storage/quota/users${query ? '?' + query : ''}`); + } + return get('/admin/storage/quota/users'); + }, + getStorageQuotaUser: (userId: string) => get(`/admin/storage/quota/users/${userId}`), + adjustStorageQuota: (userId: string, data: any) => put(`/admin/storage/quota/users/${userId}`, data), + recalcStorageQuota: (userId: string) => post(`/admin/storage/quota/recalculate/${userId}`, {}), + initAllStorageQuotas: () => post('/admin/storage/quota/init-all', {}), + + // 自动化任务调度 (Phase 3.2) + getAdminAutomationStats: () => get('/admin/automation/stats/overview'), + getAdminAutomationTasks: (params?: any) => { + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + return get(`/admin/automation/${query}`); + }, + getAdminAutomationTask: (id: string) => get(`/admin/automation/${id}`), + createAdminAutomationTask: (data: any) => post('/admin/automation/', data), + updateAdminAutomationTask: (id: string, data: any) => put(`/admin/automation/${id}`, data), + deleteAdminAutomationTask: (id: string) => del(`/admin/automation/${id}`), + toggleAdminAutomationTask: (id: string) => put(`/admin/automation/${id}/toggle`), + executeAdminAutomationTask: (id: string) => post(`/admin/automation/${id}/execute`), + getAdminAutomationTaskHistory: (id: string, params?: any) => { + const query = params ? '?' + Object.entries(params).filter(([_, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`).join('&') : ''; + return get(`/admin/automation/${id}/history${query}`); + }, + // MCP Server Management + getAdminMcpStats: () => get('/admin/mcp/stats/overview'), + getAdminMcpServers: (params?: any) => { + const query = new URLSearchParams(params).toString(); + return get(`/admin/mcp/${query ? '?' + query : ''}`); + }, + getAdminMcpServer: (id: string) => get(`/admin/mcp/${id}`), + createAdminMcpServer: (data: any) => post('/admin/mcp/', data), + updateAdminMcpServer: (id: string, data: any) => put(`/admin/mcp/${id}`, data), + deleteAdminMcpServer: (id: string) => del(`/admin/mcp/${id}`), + toggleAdminMcpServer: (id: string) => put(`/admin/mcp/${id}/toggle`), + testAdminMcpServer: (id: string, data?: any) => post(`/admin/mcp/${id}/test`, data || {}), + getAdminMcpServerTools: (id: string) => get(`/admin/mcp/${id}/tools`), + getAdminMcpServerResources: (id: string) => get(`/admin/mcp/${id}/resources`), + // Developer API Management + getAdminDeveloperStats: () => get('/admin/developer/stats/overview'), + getAdminApiKeys: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/developer/api-keys${sp.toString() ? '?' + sp.toString() : ''}`); + }, + createAdminApiKey: (data: any) => post('/admin/developer/api-keys/', data), + updateAdminApiKey: (id: string, data: any) => put(`/admin/developer/api-keys/${id}`, data), + revokeAdminApiKey: (id: string) => post(`/admin/developer/api-keys/${id}/revoke`), + deleteAdminApiKey: (id: string) => del(`/admin/developer/api-keys/${id}`), + getAdminOpenApiClients: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/developer/openapi-clients${sp.toString() ? '?' + sp.toString() : ''}`); + }, + createAdminOpenApiClient: (data: any) => post('/admin/developer/openapi-clients/', data), + updateAdminOpenApiClient: (id: string, data: any) => put(`/admin/developer/openapi-clients/${id}`, data), + resetAdminOpenApiClientSecret: (id: string) => post(`/admin/developer/openapi-clients/${id}/reset-secret`), + deleteAdminOpenApiClient: (id: string) => del(`/admin/developer/openapi-clients/${id}`), + getAdminCallStats: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/developer/call-stats${sp.toString() ? '?' + sp.toString() : ''}`); + }, + getAdminCallStatsOverview: () => get('/admin/developer/call-stats/overview'), + getAdminSlowLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/developer/slow-logs${sp.toString() ? '?' + sp.toString() : ''}`); + }, + clearAdminSlowLogs: () => del('/admin/developer/slow-logs'), + getAdminSlowLogsStats: () => get('/admin/developer/slow-logs/stats'), + // AI App Management + getAIAppStats: () => get('/admin/apps/stats/overview'), + getAIApps: (params?: string) => get('/admin/apps/apps' + (params ? '?' + params : '')), + getAIApp: (id: string) => get('/admin/apps/apps/' + id), + createAIApp: (data: any) => post('/admin/apps/apps', data), + updateAIApp: (id: string, data: any) => put('/admin/apps/apps/' + id, data), + deleteAIApp: (id: string) => del('/admin/apps/apps/' + id), + aiAppLifecycle: (id: string, action: string) => post('/admin/apps/apps/' + id + '/' + action, {}), + getAIAppVersions: (id: string) => get('/admin/apps/apps/' + id + '/versions'), + getAIAppAnalytics: (id: string) => get('/admin/apps/apps/' + id + '/analytics'), + getAIAppTemplates: () => get('/admin/apps/templates'), + createAIAppTemplate: (data: any) => post('/admin/apps/templates', data), + createAIAppFromTemplate: (templateId: string, data: any) => post('/admin/apps/apps/from-template/' + templateId, data), + getAIAppCategories: () => get('/admin/apps/categories'), + // Logo 上传 + uploadLogo: (formData: FormData) => { + const token = localStorage.getItem('access_token'); + return fetch(`/app/api/admin/settings/logo/upload`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` }, + body: formData, + }).then(res => res.json()); + }, + + // MCP Market (Phase G1) + getMcpMarketStats: () => + get('/admin/mcp-market/stats'), + getMcpTools: (params?: Record) => + get('/admin/mcp-market/?' + new URLSearchParams(params || {}).toString()), + getMcpToolDetail: (id: string) => + get('/admin/mcp-market/' + id), + createMcpTool: (data: any) => + post('/admin/mcp-market/', data), + updateMcpTool: (id: string, data: any) => + put('/admin/mcp-market/' + id, data), + deleteMcpTool: (id: string) => + del('/admin/mcp-market/' + id), + scanMcpTool: (id: string) => + post('/admin/mcp-market/' + id + '/security-scan'), + updateMcpToolSecurity: (id: string, data: any) => + put('/admin/mcp-market/' + id + '/security-status', data), + regenMcpDeepLink: (id: string) => + post('/admin/mcp-market/' + id + '/deep-link'), + getMcpManagedServers: () => + get('/admin/mcp-market/managed/servers'), + seedMcpTools: () => + post('/admin/mcp-market/seed'), + + // Code Execution Sandbox (Phase G2) + getCodeExecStats: () => get('/admin/code-exec/stats'), + getCodeExecRuntimes: () => get('/admin/code-exec/runtimes'), + executeCode: (data: { language: string; code: string; stdin?: string; timeout?: number }) => + post('/admin/code-exec/execute', data), + getCodeExecTasks: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/code-exec/tasks${sp.toString() ? '?' + sp.toString() : ''}`); + }, + getCodeExecTask: (id: string) => get(`/admin/code-exec/tasks/${id}`), + deleteCodeExecTask: (id: string) => del(`/admin/code-exec/tasks/${id}`), + + + // Distribution Builder (Phase G4) + getDistributionStats: () => get('/admin/distributions/stats'), + getDistributions: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]: [string, any]) => { if (v) sp.set(k, String(v)); }); + return get('/admin/distributions' + (sp.toString() ? '?' + sp.toString() : '')); + }, + getDistributionTemplates: () => get('/admin/distributions/templates'), + createDistribution: (data: any) => post('/admin/distributions', data), + getDistribution: (id: string) => get('/admin/distributions/' + id), + updateDistribution: (id: string, data: any) => put('/admin/distributions/' + id, data), + deleteDistribution: (id: string) => del('/admin/distributions/' + id), + getDistributionLicenses: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]: [string, any]) => { if (v) sp.set(k, String(v)); }); + return get('/admin/distributions/licenses' + (sp.toString() ? '?' + sp.toString() : '')); + }, + createDistributionLicense: (distroId: string, data: any) => post('/admin/distributions/' + distroId + '/licenses', data), + getDistributionLicense: (id: string) => get('/admin/distributions/licenses/' + id), + revokeDistributionLicense: (id: string) => post('/admin/distributions/licenses/' + id + '/revoke'), + // ===== Ads Management ===== + getAds: () => get('/admin/ads/'), + addAd: (data: any) => post('/admin/ads/', data), + updateAd: (id: string, data: any) => put(`/admin/ads/${id}`, data), + deleteAd: (id: string) => del(`/admin/ads/${id}`), + + // ===== Market Admin (extended) ===== + getMarketOverview: () => get('/admin/market/overview'), + getTopApps: (metric?: string, limit?: number) => get(`/admin/market/top-apps${metric ? '?metric=' + metric : ''}${limit ? (metric ? '&' : '?') + 'limit=' + limit : ''}`), + getAdminMarketApps: (params?: string) => get(`/admin/market/apps${params ? '?' + params : ''}`), + getPendingApps: () => get('/admin/market/pending'), + getCoupons: () => get('/admin/market/coupons'), + getRecentReviews: () => get('/admin/market/reviews'), + approveApp: (id: string) => post(`/admin/market/apps/${id}/approve`), + rejectApp: (id: string) => post(`/admin/market/apps/${id}/reject`), + setFeaturedApp: (id: string, featured: boolean, sortOrder?: number) => put(`/admin/market/apps/${id}/featured`, { featured, sort_order: sortOrder }), + deleteMarketApp: (id: string) => del(`/admin/market/apps/${id}`), + deleteCoupon: (id: string) => del(`/admin/market/coupons/${id}`), + updateMarketApp: (id: string, data: any) => put(`/admin/market/apps/${id}`, data), + updateCoupon: (id: string, data: any) => put(`/admin/market/coupons/${id}`, data), + addCoupon: (data: any) => post('/admin/market/coupons', data), + + // ===== Membership (extended) ===== + getMembershipStats: () => get('/admin/membership/stats'), + revokeMembership: (userId: string) => post(`/admin/membership/users/${userId}/revoke`), + + // ===== Logs (extended) ===== + getOperationLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/operation-logs${sp.toString() ? '?' + sp.toString() : ''}`); + }, + getTokenLogs: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/logs/token-logs${sp.toString() ? '?' + sp.toString() : ''}`); + }, + + // ===== Analytics ===== + getAnalyticsOverview: (days?: number) => get(`/admin/analytics/overview${days ? '?days=' + days : ''}`), + getAnalyticsApiTrends: (days?: number) => get(`/admin/analytics/trends/api-calls${days ? '?days=' + days : ''}`), + getAnalyticsTokenTrends: (days?: number) => get(`/admin/analytics/trends/tokens${days ? '?days=' + days : ''}`), + getAnalyticsUserTrends: (days?: number) => get(`/admin/analytics/trends/users${days ? '?days=' + days : ''}`), + getAnalyticsRevenueTrends: (days?: number) => get(`/admin/analytics/trends/revenue${days ? '?days=' + days : ''}`), + getAnalyticsModelUsage: (days?: number) => get(`/admin/analytics/models/usage${days ? '?days=' + days : ''}`), + + // ===== Billing ===== + getBillingRules: () => get('/admin/billing/rules'), + getBillingPackages: () => get('/admin/billing/packages'), + getBillingTransactions: (page?: number) => get(`/admin/billing/transactions${page ? '?page=' + page : ''}`), + getBillingStats: () => get('/admin/billing/stats/overview'), + + // ===== Tenant ===== + getTenants: (page?: number) => get(`/admin/tenant/list${page ? '?page=' + page : ''}`), + getTenantStats: () => get('/admin/tenant/stats/overview'), + getTenantMembers: (tenantId: string) => get(`/admin/tenant/${tenantId}/members`), + createTenant: (params: string) => post(`/admin/tenant/create${params}`), + + }; + // ===== AI自进化API ===== + export const evolutionAPI = { + // 驾驶舱面板 + getSkillTree: () => get('/evolution/dashboard/skill-tree'), + getTimeline: (eventType?: string) => + get('/evolution/dashboard/timeline' + (eventType ? `?event_type=${eventType}` : '')), + getStatistics: () => get('/evolution/dashboard/statistics'), + getProfile: () => get('/evolution/dashboard/profile'), + + // 技能管理 + getSkills: (params?: { state?: string; keyword?: string; sort_by?: string; order?: string }) => { + const query = new URLSearchParams(); + if (params?.state) query.append('state', params.state); + if (params?.keyword) query.append('keyword', params.keyword); + if (params?.sort_by) query.append('sort_by', params.sort_by); + if (params?.order) query.append('order', params.order); + const qs = query.toString(); + return get('/evolution/skills' + (qs ? `?${qs}` : '')); + }, + getSkillDetail: (name: string) => get(`/evolution/skills/${encodeURIComponent(name)}`), + updateSkill: (name: string, data: { description?: string; tags?: string[]; category?: string }) => + put(`/evolution/skills/${encodeURIComponent(name)}`, data), + deleteSkill: (name: string) => del(`/evolution/skills/${encodeURIComponent(name)}`), + freezeSkill: (name: string) => post(`/evolution/skills/${encodeURIComponent(name)}/freeze`), + unfreezeSkill: (name: string) => post(`/evolution/skills/${encodeURIComponent(name)}/unfreeze`), + getSkillHistory: (name: string) => get(`/evolution/skills/${encodeURIComponent(name)}/history`), + getSkillTrajectories: (name: string) => get(`/evolution/skills/${encodeURIComponent(name)}/trajectories`), + + // 事件 + getEvents: (params?: { event_type?: string; skill_name?: string; limit?: number }) => { + const query = new URLSearchParams(); + if (params?.event_type) query.append('event_type', params.event_type); + if (params?.skill_name) query.append('skill_name', params.skill_name); + if (params?.limit) query.append('limit', String(params.limit)); + const qs = query.toString(); + return get('/evolution/events' + (qs ? `?${qs}` : '')); + }, + + // 轨迹提交 + submitTrajectory: (data: { conversation_id: string; messages: any[] }) => + post('/evolution/trajectory/submit', data), + + // v2: 技能回滚 + rollbackSkill: (skillName: string, version: number) => + post(`/evolution/skills/${encodeURIComponent(skillName)}/rollback/${version}`), + + // v2: 发布到市场 + publishSkill: (skillName: string, data: { price: number; market_description: string }) => + post(`/evolution/skills/${encodeURIComponent(skillName)}/publish`, data), + + // v2: 团队共享 + shareToTeam: (skillName: string, teamId: string) => + post(`/evolution/skills/${encodeURIComponent(skillName)}/share-to-team`, { team_id: teamId }), + + // v2: 流水线状态 + getPipelineStatus: () => get('/evolution/pipeline/status'), + + // v2: 手动触发流水线 + triggerPipeline: () => post('/evolution/pipeline/trigger'), + + // v2: Watcher状态 + getWatcherStatus: () => get('/evolution/watcher/status'), + + // v2: 进化报告 + getReports: (reportType?: string) => { + const qs = reportType ? `?report_type=${reportType}` : ''; + return get(`/evolution/reports${qs}`); + }, + getReportDetail: (reportId: string) => get(`/evolution/reports/${reportId}`), + + + // Workflow (Phase 3.6) + getWorkflows: (params?: Record) => + get('/admin/workflows/?' + new URLSearchParams(params || {}).toString()), + getWorkflow: (id: string) => + get(`/admin/workflows/${id}`), + getWorkflowStats: () => + get('/admin/workflows/stats'), + getWorkflowTemplates: () => + get('/admin/workflows/templates'), + createWorkflow: (data: any) => + post('/admin/workflows/', data), + updateWorkflow: (id: string, data: any) => + put(`/admin/workflows/${id}`, data), + deleteWorkflow: (id: string) => + del(`/admin/workflows/${id}`), + toggleWorkflow: (id: string, isActive: boolean) => + post(`/admin/workflows/${id}/toggle?is_active=${isActive}`), + executeWorkflow: (id: string, inputText: string) => + post(`/admin/workflows/${id}/execute`, { input_text: inputText }), + + + // Privacy & Local Agent (Phase G5) + getPrivacyStats: () => get('/admin/privacy/stats'), + getPrivacyConfig: () => get('/admin/privacy/config'), + updatePrivacyConfig: (params: any) => { + const sp = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => { if (v != null) sp.append(k, String(v)); }); + return put('/admin/privacy/config?' + sp.toString()); + }, + switchPrivacyMode: (mode: string) => post('/admin/privacy/switch-mode?mode=' + encodeURIComponent(mode)), + getPrivacyLicenses: (params?: any) => { + const sp = new URLSearchParams(); + if (params?.status) sp.append('status', params.status); + return get('/admin/privacy/licenses' + (sp.toString() ? '?' + sp.toString() : '')); + }, + generateOfflineLicense: (data: any) => { + const sp = new URLSearchParams(); + if (data.agent_id) sp.append('agent_id', data.agent_id); + sp.append('max_offline_days', String(data.max_offline_days || 7)); + return post('/admin/privacy/licenses/generate?' + sp.toString()); + }, + revokePrivacyLicense: (id: number) => post('/admin/privacy/licenses/' + id + '/revoke'), + validatePrivacyLicense: (licenseKey: string) => { + const sp = new URLSearchParams(); + sp.append('license_key', licenseKey); + return post('/admin/privacy/licenses/validate?' + sp.toString()); + }, + getKbSyncStates: () => get('/admin/privacy/kb-sync'), + triggerKbSync: (kbId: number, direction: string) => { + const sp = new URLSearchParams(); + sp.append('knowledge_base_id', String(kbId)); + sp.append('sync_direction', direction); + return post('/admin/privacy/kb-sync/trigger?' + sp.toString()); + }, + getLocalAgents: () => get('/admin/privacy/local-agents'), + getPrivacyUsageStats: () => get('/admin/privacy/usage/stats'), + getBridgeConfig: () => get('/admin/privacy/bridge-config'), + + // IM Bot Management (Phase 4) + getImBotStats: () => get('/admin/im-bot/stats'), + getImBotDashboard: () => get('/admin/im-bot/dashboard'), + getImBotChannels: (params?: string) => get(`/admin/im-bot/channels${params || ''}`), + getImBotChannelDetail: (id: string) => get(`/admin/im-bot/channels/${id}`), + testImBotChannel: (id: string) => post(`/admin/im-bot/channels/${id}/test`), + activateImBotChannel: (id: string) => post(`/admin/im-bot/channels/${id}/activate`), + deactivateImBotChannel: (id: string) => post(`/admin/im-bot/channels/${id}/deactivate`), + getImBotRoutes: (params?: string) => get(`/admin/im-bot/routes${params || ''}`), + createImBotRoute: (params: string) => post(`/admin/im-bot/routes${params}`), + deleteImBotRoute: (id: string) => del(`/admin/im-bot/routes/${id}`), + // 个人微信(iLink 协议)原生适配器 + getWechatConfig: () => get('/admin/im-bot/wechat'), + wechatQrcode: () => post('/admin/im-bot/wechat/qrcode'), + wechatQrcodeStatus: (params: string) => post(`/admin/im-bot/wechat/qrcode/status${params}`), + getWechatStatus: () => get('/admin/im-bot/wechat/status'), + disconnectWechat: () => post('/admin/im-bot/wechat/disconnect'), + getImBotPlatforms: () => get('/admin/im-bot/platforms'), + getImBotWebhookUrl: (platform: string) => get(`/admin/im-bot/webhook-url/${platform}`), + getImBotMessages: (params?: string) => get(`/admin/im-bot/messages${params || ''}`), + imBotQuickSetup: (params: string) => post(`/admin/im-bot/quick-setup${params}`), + + // 推广系统 + getPromotionStats: () => get('/admin/promotion/stats'), + getPromotions: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); }); + return get(`/admin/promotion/promotions${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + createPromotion: (params: any) => { + const sp = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); }); + return post(`/admin/promotion/promotions${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + updatePromotion: (id: string, params: any) => { + const sp = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') sp.set(k, String(v)); }); + return put(`/admin/promotion/promotions/${id}${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + deletePromotion: (id: string) => del(`/admin/promotion/promotions/${id}`), + togglePromotion: (id: string) => post(`/admin/promotion/promotions/${id}/toggle`), + getReferralLinks: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/promotion/referral-links${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + generateReferralLink: (params: any) => { + const sp = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return post(`/admin/promotion/referral-links/generate${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + getReferralRewards: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/promotion/rewards${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + approveReward: (id: string) => post(`/admin/promotion/rewards/${id}/approve`), + markRewardPaid: (id: string, params: any) => { + const sp = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return post(`/admin/promotion/rewards/${id}/pay${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + getPromotionLeaderboard: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/promotion/leaderboard${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + regenerateReferralCode: (id: string) => post(`/admin/promotion/referral-links/${id}/regenerate`), + getMyReferrals: () => get('/admin/promotion/my-referrals'), + getMyRewards: () => get('/admin/promotion/my-rewards'), + + // 成就系统 + getAchievements: (params?: string) => get(`/admin/achievement/achievements${params || ''}`), + getAchievementDetail: (id: string) => get(`/admin/achievement/achievements/${id}`), + createAchievement: (params: string) => post(`/admin/achievement/achievements${params}`), + updateAchievement: (id: string, params: string) => put(`/admin/achievement/achievements/${id}${params}`), + deleteAchievement: (id: string) => del(`/admin/achievement/achievements/${id}`), + getUserAchievements: () => get('/admin/achievement/user/achievements'), + claimAchievementReward: (id: string) => post(`/admin/achievement/user/achievements/${id}/claim`), + checkAchievementProgress: () => post('/admin/achievement/check-progress'), + getAchievementLeaderboard: (params?: string) => get(`/admin/achievement/leaderboard${params || ''}`), + + // 多租户管理 + getTenants: (page?: number, keyword?: string, status?: string) => { + const sp = new URLSearchParams(); + if (page) sp.set('page', String(page)); + if (keyword) sp.set('keyword', keyword); + if (status) sp.set('status', status); + sp.set('page_size', '20'); + return get(`/admin/tenant/list${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + createTenant: (params: string) => post(`/admin/tenant/create${params}`), + getTenantDetail: (id: string) => get(`/admin/tenant/${id}`), + updateTenant: (id: string, params: string) => put(`/admin/tenant/${id}${params}`), + deleteTenant: (id: string) => del(`/admin/tenant/${id}`), + getTenantMembers: (tenantId: string, page?: number) => { + const sp = new URLSearchParams(); + sp.set('page', String(page || 1)); + sp.set('page_size', '100'); + return get(`/admin/tenant/${tenantId}/members?${sp.toString()}`); + }, + addTenantMember: (tenantId: string, params: string) => post(`/admin/tenant/${tenantId}/members/add${params}`), + updateTenantMember: (tenantId: string, memberId: string, params: string) => put(`/admin/tenant/${tenantId}/members/${memberId}${params}`), + removeTenantMember: (tenantId: string, memberId: string) => del(`/admin/tenant/${tenantId}/members/${memberId}`), + getTenantQuotas: (tenantId: string) => get(`/admin/tenant/${tenantId}/quotas`), + updateTenantQuota: (tenantId: string, params: string) => put(`/admin/tenant/${tenantId}/quotas${params}`), + getTenantInvitations: (tenantId: string) => get(`/admin/tenant/${tenantId}/invitations`), + createTenantInvitation: (tenantId: string, params: string) => post(`/admin/tenant/${tenantId}/invitations${params}`), + getTenantStats: () => get('/admin/tenant/stats/overview'), + + // 计费与支付 + getBillingRules: (page?: number) => get(`/admin/billing/rules?page=${page || 1}&page_size=100`), + createBillingRule: (params: string) => post(`/admin/billing/rules/create${params}`), + updateBillingRule: (id: string, params: string) => put(`/admin/billing/rules/${id}${params}`), + deleteBillingRule: (id: string) => del(`/admin/billing/rules/${id}`), + getBillingPackages: () => get('/admin/billing/packages'), + createBillingPackage: (params: string) => post(`/admin/billing/packages/create${params}`), + updateBillingPackage: (id: string, params: string) => put(`/admin/billing/packages/${id}${params}`), + deleteBillingPackage: (id: string) => del(`/admin/billing/packages/${id}`), + getBillingTransactions: (page?: number) => get(`/admin/billing/transactions?page=${page || 1}&page_size=20`), + getInvoices: (page?: number) => get(`/admin/billing/invoices?page=${page || 1}&page_size=20`), + generateInvoice: (params: string) => post(`/admin/billing/invoices/generate${params}`), + getPaymentConfigs: () => get('/admin/billing/payment-configs'), + updatePaymentConfig: (id: string, params: string) => put(`/admin/billing/payment-configs/${id}${params}`), + getBillingStats: () => get('/admin/billing/stats/overview'), + + // 数据分析 + getAnalyticsOverview: (days?: number) => get(`/admin/analytics/overview?days=${days || 30}`), + getAnalyticsApiTrends: (days?: number) => get(`/admin/analytics/trends/api-calls?days=${days || 7}`), + getAnalyticsTokenTrends: (days?: number) => get(`/admin/analytics/trends/tokens?days=${days || 7}`), + getAnalyticsUserTrends: (days?: number) => get(`/admin/analytics/trends/users?days=${days || 30}`), + getAnalyticsRevenueTrends: (days?: number) => get(`/admin/analytics/trends/revenue?days=${days || 30}`), + getAnalyticsModelUsage: (days?: number) => get(`/admin/analytics/models/usage?days=${days || 30}`), + getAnalyticsDashboards: () => get('/admin/analytics/dashboards'), + createAnalyticsDashboard: (params: string) => post(`/admin/analytics/dashboards/create${params}`), + deleteAnalyticsDashboard: (id: string) => del(`/admin/analytics/dashboards/${id}`), + getAnalyticsEvents: (params?: any) => { + const sp = new URLSearchParams(); + if (params) Object.entries(params).forEach(([k, v]) => { if (v) sp.set(k, String(v)); }); + return get(`/admin/analytics/events${sp.toString() ? `?${sp.toString()}` : ''}`); + }, + + }; + + + // ===== 前台用户个人微信 (iLink 协议 / 微信 ClawBot 插件) ===== + export const imUserAPI = { + getStatus: () => get('/im/wechat/status'), + qrcode: () => post('/im/wechat/qrcode'), + qrcodeStatus: (qrcode: string) => post(`/im/wechat/qrcode/status?qrcode=${encodeURIComponent(qrcode)}`), + disconnect: () => post('/im/wechat/disconnect'), + history: () => get('/im/wechat/history'), + }; + diff --git a/frontend/src/components/layout/AdminLayout.css b/frontend/src/components/layout/AdminLayout.css index 89d0fb3..41017b8 100644 --- a/frontend/src/components/layout/AdminLayout.css +++ b/frontend/src/components/layout/AdminLayout.css @@ -1,394 +1,502 @@ -/* ===== Admin Layout ===== */ -.admin-layout { - display: flex; - height: 100vh; - overflow: hidden; - background: #F1F3F7; -} - -/* Admin Sidebar */ -.admin-sidebar { - width: var(--admin-sidebar-width); - min-width: var(--admin-sidebar-width); - background: #1A1A2E; - display: flex; - flex-direction: column; - transition: width var(--transition-slow), min-width var(--transition-slow); - overflow: hidden; - z-index: var(--z-sticky); -} - -.admin-sidebar.collapsed { - width: var(--sidebar-collapsed); - min-width: var(--sidebar-collapsed); -} - -.admin-sidebar-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-lg); - height: var(--header-height); - border-bottom: 1px solid rgba(255,255,255,0.06); -} - -.admin-brand { - color: #E0E2F0 !important; - font-size: var(--text-base) !important; -} - -.admin-sidebar .collapse-btn { - color: rgba(255,255,255,0.4); -} -.admin-sidebar .collapse-btn:hover { - background: rgba(255,255,255,0.08); - color: rgba(255,255,255,0.8); -} - -/* Admin Nav */ -.admin-nav { - flex: 1; - padding: var(--space-sm); - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 2px; -} - -.admin-nav-item { - display: flex; - align-items: center; - gap: var(--space-md); - padding: var(--space-md) var(--space-md); - border-radius: var(--radius-md); - color: rgba(255,255,255,0.55); - text-decoration: none; - font-size: var(--text-sm); - font-weight: 500; - transition: all var(--transition-fast); - white-space: nowrap; - width: 100%; - text-align: left; -} - -.admin-nav-item:hover { - background: rgba(255,255,255,0.06); - color: rgba(255,255,255,0.85); -} - -.admin-nav-item.active { - background: rgba(107, 99, 255, 0.2); - color: #8B8DFF; -} - -.admin-sidebar-footer { - padding: var(--space-sm); - border-top: 1px solid rgba(255,255,255,0.06); - display: flex; - flex-direction: column; - gap: 2px; -} - -.logout-btn { - color: rgba(255,255,255,0.35) !important; -} -.logout-btn:hover { - color: #FF6B6B !important; - background: rgba(255,107,107,0.1) !important; -} - -/* Admin Main */ -.admin-main { - flex: 1; - display: flex; - flex-direction: column; - min-width: 0; - overflow: hidden; -} - -.admin-topbar { - display: flex; - align-items: center; - justify-content: space-between; - height: var(--header-height); - padding: 0 var(--space-2xl); - background: #FFFFFF; - border-bottom: 1px solid var(--color-border); - flex-shrink: 0; -} - -.admin-topbar-left { - display: flex; - align-items: center; -} - -.admin-welcome { - font-size: var(--text-sm); - color: var(--color-text-secondary); -} - -.admin-topbar-right { - display: flex; - align-items: center; - gap: var(--space-lg); -} - -.admin-user-tag { - display: flex; - align-items: center; - gap: var(--space-sm); - font-size: var(--text-sm); - font-weight: 500; - color: var(--color-text-primary); -} - -.admin-topbar-action { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 6px 14px; - border-radius: var(--radius-md); - border: 1px solid var(--color-border); - background: var(--color-bg-primary); - color: var(--color-accent); - font-size: var(--text-sm); - font-weight: 500; - text-decoration: none; - cursor: pointer; - transition: all var(--transition-fast); -} - -.admin-topbar-action:hover { - background: var(--color-accent); - color: #FFFFFF; - border-color: var(--color-accent); -} - -.admin-avatar { - background: #1A1A2E !important; -} - -/* Admin Content Area */ -.admin-content { - flex: 1; - overflow-y: auto; - padding: var(--space-2xl); -} - -/* Admin Common Components */ -.admin-page-title { - font-size: var(--text-xl); - font-weight: 700; - color: var(--color-text-primary); - margin-bottom: var(--space-2xl); -} - -.admin-card { - background: #FFFFFF; - border-radius: var(--radius-xl); - border: 1px solid var(--color-border); - padding: var(--space-2xl); - margin-bottom: var(--space-xl); -} - -.admin-card-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--space-xl); -} - -.admin-card-title { - font-size: var(--text-lg); - font-weight: 600; - color: var(--color-text-primary); -} - -.admin-table { - width: 100%; - border-collapse: collapse; -} - -.admin-table th { - text-align: left; - padding: var(--space-md) var(--space-lg); - font-size: var(--text-xs); - font-weight: 600; - color: var(--color-text-tertiary); - text-transform: uppercase; - letter-spacing: 0.05em; - border-bottom: 1px solid var(--color-border); - background: var(--color-bg-tertiary); -} - -.admin-table td { - padding: var(--space-md) var(--space-lg); - font-size: var(--text-sm); - color: var(--color-text-primary); - border-bottom: 1px solid var(--color-border-light); -} - -.admin-table tr:hover td { - background: var(--color-bg-hover); -} - -.admin-badge { - display: inline-flex; - align-items: center; - padding: 2px 10px; - border-radius: var(--radius-full); - font-size: var(--text-xs); - font-weight: 600; - white-space: nowrap; -} - -.badge-success { background: var(--color-success-light); color: var(--color-success); } -.badge-warning { background: var(--color-warning-light); color: var(--color-warning); } -.badge-error { background: var(--color-error-light); color: var(--color-error); } -.badge-info { background: var(--color-info-light); color: var(--color-info); } -.badge-neutral { background: var(--color-bg-tertiary); color: var(--color-text-secondary); } - -.admin-btn { - display: inline-flex; - align-items: center; - gap: var(--space-sm); - padding: var(--space-sm) var(--space-lg); - border-radius: var(--radius-md); - font-size: var(--text-sm); - font-weight: 600; - transition: all var(--transition-fast); - white-space: nowrap; -} - -.admin-btn-primary { - background: var(--color-accent); - color: #FFFFFF; -} -.admin-btn-primary:hover { - background: var(--color-accent-hover); -} - -.admin-btn-outline { - background: transparent; - border: 1px solid var(--color-border); - color: var(--color-text-primary); -} -.admin-btn-outline:hover { - background: var(--color-bg-hover); - border-color: var(--color-border-hover); -} - -.admin-btn-danger { - background: transparent; - border: 1px solid var(--color-error); - color: var(--color-error); -} -.admin-btn-danger:hover { - background: var(--color-error-light); -} - -.admin-btn-sm { - padding: 3px 10px; - font-size: var(--text-xs); -} - -/* Admin Stats */ -.admin-stats { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: var(--space-lg); - margin-bottom: var(--space-2xl); -} - -.admin-stat-card { - background: #FFFFFF; - border-radius: var(--radius-xl); - border: 1px solid var(--color-border); - padding: var(--space-xl); -} - -.admin-stat-label { - font-size: var(--text-xs); - color: var(--color-text-tertiary); - font-weight: 500; - margin-bottom: var(--space-sm); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.admin-stat-value { - font-size: var(--text-2xl); - font-weight: 700; - color: var(--color-text-primary); -} - -/* Admin Form */ -.admin-form-group { - margin-bottom: var(--space-xl); -} - -.admin-form-label { - display: block; - font-size: var(--text-sm); - font-weight: 600; - color: var(--color-text-primary); - margin-bottom: var(--space-sm); -} - -.admin-form-input { - width: 100%; - padding: var(--space-md) var(--space-lg); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - font-size: var(--text-sm); - color: var(--color-text-primary); - background: #FFFFFF; - transition: border-color var(--transition-fast); - outline: none; -} - -.admin-form-input:focus { - border-color: var(--color-accent); - box-shadow: 0 0 0 3px var(--color-accent-light); -} - -.admin-form-input::placeholder { - color: var(--color-text-disabled); -} - -.admin-form-textarea { - min-height: 100px; - resize: vertical; -} - -.admin-form-row { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: var(--space-lg); -} - -.admin-form-actions { - display: flex; - gap: var(--space-md); - padding-top: var(--space-lg); - border-top: 1px solid var(--color-border-light); -} - -/* Version info in sidebar */ -.admin-version-info { - padding: var(--space-sm) var(--space-md); - font-size: var(--text-xs); - color: rgba(255,255,255,0.25); - text-align: center; - white-space: nowrap; - border-top: 1px solid rgba(255,255,255,0.04); -} - -@media (max-width: 768px) { - .admin-content { - padding: var(--space-lg); - } - .admin-form-row { - grid-template-columns: 1fr; - } -} +/* ===== Admin Layout ===== */ +.admin-layout { + display: flex; + height: 100vh; + overflow: hidden; + background: #F1F3F7; +} + +/* Admin Sidebar */ +.admin-sidebar { + width: var(--admin-sidebar-width); + min-width: var(--admin-sidebar-width); + background: #1A1A2E; + display: flex; + flex-direction: column; + transition: width var(--transition-slow), min-width var(--transition-slow); + overflow: hidden; + z-index: var(--z-sticky); +} + +.admin-sidebar.collapsed { + width: var(--sidebar-collapsed); + min-width: var(--sidebar-collapsed); +} + +.admin-sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-lg); + height: var(--header-height); + border-bottom: 1px solid rgba(255,255,255,0.06); +} + +.admin-brand { + color: #E0E2F0 !important; + font-size: var(--text-base) !important; +} + +.admin-sidebar .collapse-btn { + color: rgba(255,255,255,0.4); +} +.admin-sidebar .collapse-btn:hover { + background: rgba(255,255,255,0.08); + color: rgba(255,255,255,0.8); +} + +/* Admin Nav */ +.admin-nav { + flex: 1; + padding: var(--space-sm); + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 2px; +} + +.admin-nav-item { + display: flex; + align-items: center; + gap: var(--space-md); + padding: var(--space-md) var(--space-md); + border-radius: var(--radius-md); + color: rgba(255,255,255,0.55); + text-decoration: none; + font-size: var(--text-sm); + font-weight: 500; + transition: all var(--transition-fast); + white-space: nowrap; + width: 100%; + text-align: left; +} + +.admin-nav-item:hover { + background: rgba(255,255,255,0.06); + color: rgba(255,255,255,0.85); +} + +.admin-nav-item.active { + background: rgba(107, 99, 255, 0.2); + color: #8B8DFF; +} + +.admin-sidebar-footer { + padding: var(--space-sm); + border-top: 1px solid rgba(255,255,255,0.06); + display: flex; + flex-direction: column; + gap: 2px; +} + +.logout-btn { + color: rgba(255,255,255,0.35) !important; +} +.logout-btn:hover { + color: #FF6B6B !important; + background: rgba(255,107,107,0.1) !important; +} + +/* Admin Main */ +.admin-main { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + overflow: hidden; +} + +.admin-topbar { + display: flex; + align-items: center; + justify-content: space-between; + height: var(--header-height); + padding: 0 var(--space-2xl); + background: #FFFFFF; + border-bottom: 1px solid var(--color-border); + flex-shrink: 0; +} + +.admin-topbar-left { + display: flex; + align-items: center; +} + +.admin-welcome { + font-size: var(--text-sm); + color: var(--color-text-secondary); +} + +.admin-topbar-right { + display: flex; + align-items: center; + gap: var(--space-lg); +} + +.admin-user-tag { + display: flex; + align-items: center; + gap: var(--space-sm); + font-size: var(--text-sm); + font-weight: 500; + color: var(--color-text-primary); +} + +.admin-topbar-action { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + background: var(--color-bg-primary); + color: var(--color-accent); + font-size: var(--text-sm); + font-weight: 500; + text-decoration: none; + cursor: pointer; + transition: all var(--transition-fast); +} + +.admin-topbar-action:hover { + background: var(--color-accent); + color: #FFFFFF; + border-color: var(--color-accent); +} + +.admin-avatar { + background: #1A1A2E !important; +} + +/* Admin Content Area */ +.admin-content { + flex: 1; + overflow-y: auto; + padding: var(--space-2xl); +} + +/* Admin Common Components */ +.admin-page-title { + font-size: var(--text-xl); + font-weight: 700; + color: var(--color-text-primary); + margin-bottom: var(--space-2xl); +} + +.admin-card { + background: #FFFFFF; + border-radius: var(--radius-xl); + border: 1px solid var(--color-border); + padding: var(--space-2xl); + margin-bottom: var(--space-xl); +} + +.admin-card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--space-xl); +} + +.admin-card-title { + font-size: var(--text-lg); + font-weight: 600; + color: var(--color-text-primary); +} + +.admin-table { + width: 100%; + border-collapse: collapse; +} + +.admin-table th { + text-align: left; + padding: var(--space-md) var(--space-lg); + font-size: var(--text-xs); + font-weight: 600; + color: var(--color-text-tertiary); + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid var(--color-border); + background: var(--color-bg-tertiary); +} + +.admin-table td { + padding: var(--space-md) var(--space-lg); + font-size: var(--text-sm); + color: var(--color-text-primary); + border-bottom: 1px solid var(--color-border-light); +} + +.admin-table tr:hover td { + background: var(--color-bg-hover); +} + +.admin-badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: var(--radius-full); + font-size: var(--text-xs); + font-weight: 600; + white-space: nowrap; +} + +.badge-success { background: var(--color-success-light); color: var(--color-success); } +.badge-warning { background: var(--color-warning-light); color: var(--color-warning); } +.badge-error { background: var(--color-error-light); color: var(--color-error); } +.badge-info { background: var(--color-info-light); color: var(--color-info); } +.badge-neutral { background: var(--color-bg-tertiary); color: var(--color-text-secondary); } + +.admin-btn { + display: inline-flex; + align-items: center; + gap: var(--space-sm); + padding: var(--space-sm) var(--space-lg); + border-radius: var(--radius-md); + font-size: var(--text-sm); + font-weight: 600; + transition: all var(--transition-fast); + white-space: nowrap; +} + +.admin-btn-primary { + background: var(--color-accent); + color: #FFFFFF; +} +.admin-btn-primary:hover { + background: var(--color-accent-hover); +} + +.admin-btn-outline { + background: transparent; + border: 1px solid var(--color-border); + color: var(--color-text-primary); +} +.admin-btn-outline:hover { + background: var(--color-bg-hover); + border-color: var(--color-border-hover); +} + +.admin-btn-danger { + background: transparent; + border: 1px solid var(--color-error); + color: var(--color-error); +} +.admin-btn-danger:hover { + background: var(--color-error-light); +} + +.admin-btn-sm { + padding: 3px 10px; + font-size: var(--text-xs); +} + +/* Admin Stats */ +.admin-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--space-lg); + margin-bottom: var(--space-2xl); +} + +.admin-stat-card { + background: #FFFFFF; + border-radius: var(--radius-xl); + border: 1px solid var(--color-border); + padding: var(--space-xl); +} + +.admin-stat-label { + font-size: var(--text-xs); + color: var(--color-text-tertiary); + font-weight: 500; + margin-bottom: var(--space-sm); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.admin-stat-value { + font-size: var(--text-2xl); + font-weight: 700; + color: var(--color-text-primary); +} + +/* Admin Form */ +.admin-form-group { + margin-bottom: var(--space-xl); +} + +.admin-form-label { + display: block; + font-size: var(--text-sm); + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: var(--space-sm); +} + +.admin-form-input { + width: 100%; + padding: var(--space-md) var(--space-lg); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + font-size: var(--text-sm); + color: var(--color-text-primary); + background: #FFFFFF; + transition: border-color var(--transition-fast); + outline: none; +} + +.admin-form-input:focus { + border-color: var(--color-accent); + box-shadow: 0 0 0 3px var(--color-accent-light); +} + +.admin-form-input::placeholder { + color: var(--color-text-disabled); +} + +.admin-form-textarea { + min-height: 100px; + resize: vertical; +} + +.admin-form-row { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--space-lg); +} + +.admin-form-actions { + display: flex; + gap: var(--space-md); + padding-top: var(--space-lg); + border-top: 1px solid var(--color-border-light); +} + +/* Version info in sidebar */ +.admin-version-info { + padding: var(--space-sm) var(--space-md); + font-size: var(--text-xs); + color: rgba(255,255,255,0.25); + text-align: center; + white-space: nowrap; + border-top: 1px solid rgba(255,255,255,0.04); +} + +@media (max-width: 768px) { + .admin-content { + padding: var(--space-lg); + } + .admin-form-row { + grid-template-columns: 1fr; + } +} + + +/* ===== 页面头部与提示组件(短信配置等页复用) ===== */ +.admin-page-header { + display: flex; + align-items: flex-end; + justify-content: space-between; + margin-bottom: var(--space-xl); + gap: var(--space-lg); +} +.admin-page-subtitle { + margin: 6px 0 0; + font-size: var(--text-sm); + color: var(--color-text-secondary); +} + +.admin-alert { + display: flex; + align-items: center; + gap: var(--space-sm); + padding: var(--space-md) var(--space-lg); + border-radius: var(--radius-md); + font-size: var(--text-sm); + margin-bottom: var(--space-lg); + border: 1px solid transparent; + line-height: 1.5; +} +.admin-alert-success { + background: var(--color-success-light); + border-color: var(--color-success); + color: var(--color-success); +} +.admin-alert-error { + background: var(--color-error-light); + border-color: var(--color-error); + color: var(--color-error); +} +.admin-alert-info { + background: var(--color-info-light); + border-color: var(--color-info); + color: var(--color-info); +} + +.admin-form-hint { + margin-top: var(--space-md); + font-size: var(--text-xs); + color: var(--color-text-tertiary); + line-height: 1.6; +} +.admin-form-hint code { + background: var(--color-bg-tertiary); + padding: 1px 6px; + border-radius: var(--radius-sm); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: var(--text-xs); +} + +.admin-tips { + margin: 0; + padding-left: var(--space-xl); + color: var(--color-text-secondary); + font-size: var(--text-sm); + line-height: 1.9; +} +.admin-tips li { + margin-bottom: var(--space-xs); +} + + +/* ===== 顶部栏与面包屑 ===== */ +.admin-topbar { + position: sticky; + top: 0; + z-index: 20; + display: flex; + align-items: center; + justify-content: space-between; + height: 56px; + margin: calc(-1 * var(--space-2xl)) calc(-1 * var(--space-2xl)) var(--space-2xl); + padding: 0 var(--space-2xl); + background: rgba(255, 255, 255, 0.82); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-bottom: 1px solid var(--color-border); +} +.admin-breadcrumb { + display: flex; + align-items: center; + gap: var(--space-sm); + font-size: var(--text-sm); +} +.crumb-root { color: var(--color-text-tertiary); } +.crumb-sep { color: var(--color-text-disabled); } +.crumb-group { color: var(--color-text-secondary); } +.crumb-current { color: var(--color-text-primary); font-weight: 600; } +.admin-topbar-user { + display: flex; + align-items: center; + gap: var(--space-sm); + font-size: var(--text-sm); + color: var(--color-text-secondary); +} +.admin-user-avatar.sm { + width: 28px; + height: 28px; + font-size: var(--text-xs); +} +.admin-topbar-username { font-weight: 600; color: var(--color-text-primary); } diff --git a/frontend/src/components/layout/AdminLayout.tsx b/frontend/src/components/layout/AdminLayout.tsx index 5982037..be7e91f 100644 --- a/frontend/src/components/layout/AdminLayout.tsx +++ b/frontend/src/components/layout/AdminLayout.tsx @@ -73,6 +73,7 @@ const adminNavGroups: NavGroup[] = [ icon: 'settings', items: [ { path: '/admin/settings', icon: 'settings', label: '网站设置' }, + { path: '/admin/sms', icon: 'sms', label: '短信配置' }, { path: '/admin/logs', icon: 'logs', label: '日志与风控' }, { path: '/admin/im-bots', icon: 'imbot', label: 'IM 机器人' }, { path: '/admin/privacy', icon: 'privacy', label: '隐私与本地代理' }, @@ -123,12 +124,26 @@ const adminIconPaths: Record = { apps: , assets: , chevron: , + sms: , }; const AdminLayout: React.FC = () => { const { user, logout } = useAuth(); const navigate = useNavigate(); const location = useLocation(); + const navCrumb = (() => { + for (const g of adminNavGroups) { + for (const it of g.items) { + const match = it.exact + ? location.pathname === it.path + : location.pathname === it.path || location.pathname.startsWith(it.path + '/'); + if (match) return { group: g.title, label: it.label }; + } + } + return { group: '', label: '' }; + })(); + const navGroup = navCrumb.group; + const navLabel = navCrumb.label; // Default: all groups collapsed. Auto-expand the group containing the active route. const [expandedGroups, setExpandedGroups] = useState>(() => { @@ -227,6 +242,17 @@ const AdminLayout: React.FC = () => {
+
+ +
+
{user?.username?.[0]?.toUpperCase() || 'A'}
+ {user?.username || 'admin'} +
+
diff --git a/frontend/src/pages/admin/SiteSettings.tsx b/frontend/src/pages/admin/SiteSettings.tsx index 355bd20..1372bdb 100644 --- a/frontend/src/pages/admin/SiteSettings.tsx +++ b/frontend/src/pages/admin/SiteSettings.tsx @@ -1,454 +1,379 @@ -import React, { useState, useEffect } from 'react'; -import { adminAPI } from '@/api'; - -const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => ( - - {children} - -); - -const SiteSettings: React.FC = () => { - const [brand, setBrand] = useState>({ - logo: '', siteName: '', announcement: '', icpNumber: '', contactPhone: '', - }); - const [mail, setMail] = useState({ - smtp_host: '', smtp_port: 587, username: '', password: '', from_address: '', use_tls: true, - }); - const [sms, setSms] = useState({ - provider: '', api_key: '', api_secret: '', sign_name: '', template_code: '', - }); - const [savedMsg, setSavedMsg] = useState(null); - const [loading, setLoading] = useState(true); - const [testResult, setTestResult] = useState(null); - const [showEmailTest, setShowEmailTest] = useState(false); - const [emailTestTo, setEmailTestTo] = useState(''); - const [emailTestSubject, setEmailTestSubject] = useState(''); - const [emailTestBody, setEmailTestBody] = useState(''); - const [emailGenerating, setEmailGenerating] = useState(false); - - // AI生成测试邮件内容 - const handleAIGenerateContent = async () => { - setEmailGenerating(true); - try { - const res = await adminAPI.generateEmailContent('测试邮件'); - const data = (res as any).data || res; - setEmailTestSubject(data.subject || ''); - setEmailTestBody(data.body || ''); - } catch (e) { - // 降级:使用客户端预设内容 - const subjects = [ - '【测试】AIFlow邮件服务配置验证', - 'AIFlow平台 - 邮件投递测试', - '测试邮件:您的SMTP服务已就绪', - ]; - const bodies = [ - '您好!\n\n这是一封来自 AIFlow 智能自动化平台的测试邮件。\n\n如果您收到此邮件,说明SMTP邮件服务已配置成功。\n\n此致\nAIFlow 平台团队', - 'Hi,\n\nThis is a test email from AIFlow Platform.\n\nIf you received this, the SMTP service works fine.\n\nBest regards,\nAIFlow Team', - '【系统通知】\n邮件服务连通性测试通过。\n\n发件服务器配置正确。\n\n—— AIFlow', - ]; - setEmailTestSubject(subjects[Math.floor(Math.random() * subjects.length)]); - setEmailTestBody(bodies[Math.floor(Math.random() * bodies.length)]); - } finally { - setEmailGenerating(false); - } - }; - - // 加载真实配置 - useEffect(() => { - async function loadConfig() { - try { - const [siteRes, emailRes, smsRes] = await Promise.all([ - adminAPI.getSiteSettings(), - adminAPI.getEmailConfig(), - adminAPI.getSmsConfig(), - ]); - if (siteRes?.data) { - setBrand({ - logo: siteRes.data.logo || '', - siteName: siteRes.data.site_name || siteRes.data.siteName || '', - announcement: siteRes.data.announcement || '', - icpNumber: siteRes.data.icp_number || siteRes.data.icpNumber || '', - contactPhone: siteRes.data.contact_phone || siteRes.data.contactPhone || '', - }); - } - if (emailRes?.data) { - setMail({ - smtp_host: emailRes.data.smtp_host || '', - smtp_port: emailRes.data.smtp_port || 587, - username: emailRes.data.username || '', - password: emailRes.data.password || '', - from_address: emailRes.data.from_address || '', - use_tls: emailRes.data.use_tls !== false, - }); - } - if (smsRes?.data) { - setSms({ - provider: smsRes.data.provider || '', - api_key: smsRes.data.api_key || '', - api_secret: smsRes.data.api_secret || '', - sign_name: smsRes.data.sign_name || '', - template_code: smsRes.data.template_code || '', - }); - } - } catch (e) { - console.error('加载配置失败:', e); - } finally { - setLoading(false); - } - } - loadConfig(); - }, []); - - const showSaved = (block: string) => { - setSavedMsg(block + ' 保存成功'); - setTimeout(() => setSavedMsg(null), 2000); - }; - - const showTestResult = (result: string) => { - setTestResult(result); - setTimeout(() => setTestResult(null), 5000); - }; - - // 保存品牌设置 - const saveSiteSettings = async () => { - try { - await adminAPI.updateSiteSettings({ - logo: brand.logo, - site_name: brand.siteName, - announcement: brand.announcement, - icp_number: brand.icpNumber, - contact_phone: brand.contactPhone, - }); - showSaved('品牌设置'); - } catch (e: any) { - alert('保存失败: ' + (e.detail || e.message)); - } - }; - - // 保存邮件配置 - const saveEmailConfig = async () => { - try { - await adminAPI.updateEmailConfig({ - smtp_host: mail.smtp_host, - smtp_port: mail.smtp_port, - username: mail.username, - password: mail.password, - from_address: mail.from_address, - use_tls: mail.use_tls, - }); - showSaved('邮件配置'); - } catch (e: any) { - alert('保存失败: ' + (e.detail || e.message)); - } - }; - - // 保存短信配置 - const saveSmsConfig = async () => { - try { - await adminAPI.updateSmsConfig({ - provider: sms.provider, - api_key: sms.api_key, - api_secret: sms.api_secret, - sign_name: sms.sign_name, - template_code: sms.template_code, - }); - showSaved('短信配置'); - } catch (e: any) { - alert('保存失败: ' + (e.detail || e.message)); - } - }; - - // 发送测试邮件 - 先保存配置再发送 - const sendTestEmail = async () => { - if (!emailTestTo.trim()) { - showTestResult('请输入收件邮箱地址'); - return; - } - try { - // 测试前先保存邮件配置到数据库,确保使用最新配置 - await adminAPI.updateEmailConfig({ - smtp_host: mail.smtp_host, - smtp_port: mail.smtp_port, - username: mail.username, - password: mail.password, - from_address: mail.from_address, - use_tls: mail.use_tls, - }); - const res = await adminAPI.testEmail({ - test_to: emailTestTo.trim(), - test_subject: emailTestSubject || undefined, - test_body: emailTestBody || undefined, - }); - showTestResult('测试邮件已发送至 ' + emailTestTo + ': ' + ((res as any)?.message || 'OK')); - setShowEmailTest(false); - } catch (e: any) { - showTestResult('测试邮件发送失败: ' + (e.detail || e.message)); - } - }; - - const openEmailTest = () => { - setEmailTestTo(''); - setEmailTestSubject(''); - setEmailTestBody(''); - setShowEmailTest(true); - }; - - // 发送测试短信 - const sendTestSms = async () => { - try { - const res = await adminAPI.testSms(); - showTestResult('测试短信发送成功: ' + (res?.message || 'OK')); - } catch (e: any) { - showTestResult('测试短信发送失败: ' + (e.detail || e.message)); - } - }; - - if (loading) { - return
加载中...
; - } - - return ( -
-
-

网站基础设置

- {savedMsg && ( - - - {' '}{savedMsg} - - )} -
- - {/* Brand Settings */} -
-
-

- - {' '}品牌设置 -

-
- -
-
- - setBrand({ ...brand, logo: e.target.value })} /> -
-
- - setBrand({ ...brand, siteName: e.target.value })} /> -
-
- -
- -