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