初始提交: Qihu-AIFlow v2.0.1 完整项目
- 前端 React+TypeScript (源码+构建产物) - 后端 Python FastAPI - 数据库迁移 001-015 - Docker部署配置 - 微信ClawBot API频道绑定 - 项目文档 README.md
This commit is contained in:
commit
78ff261b5a
199 files changed
+69126
No files matched your search
@@ -0,0 +1,508 @@
|
||||
"""
|
||||
网站基础设置 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)}")
|
||||
Reference in new issue
Block a user