AI助手功能推送

反馈comment功能推送
This commit is contained in:
Miyamizu-MitsuhaSang 2025-10-26 01:35:38 +08:00
parent 638d9fe8f3
commit 2a96ce0a3d
14 changed files with 270 additions and 74 deletions

View File

@ -20,16 +20,17 @@ MAX_USAGE_PER = 100
CHAT_TTL = 7200
@ai_router.post("/exp")
@ai_router.post("/word/exp", deprecated=False)
async def dict_exp(
request: Request,
Q: AIQuestionRequest,
user: Tuple[User, Dict] = Depends(get_current_user)
):
"""
:param word:
:param question: 不允许question为空调用
该接口仅用于查词页面且为具有MCP功能的
:param request:
:param Q:
:param user:
:return:
"""
if user[0].token_usage > CHAT_TTL and not user[0].is_admin:
@ -95,6 +96,11 @@ async def dict_exp(
raise HTTPException(status_code=500, detail=f"AI调用失败: {str(e)}")
@ai_router.post("/univer")
async def universal_main():
pass
@ai_router.post("/clear")
async def clear_history(word: str, request: Request, user: Tuple[User, Dict] = Depends(get_current_user)):
redis = request.app.state.redis

View File

@ -8,9 +8,10 @@ CHAT_TTL = 7200
async def get_and_set_last_key(redis: Redis, word: str, user_id: str):
last_key = f"last_word:{user_id}"
last_word = await redis.get(last_key)
print(last_word)
# 如果上一次查的词和这次不同,就清空旧词的记录
if last_word and last_word.decode() != word:
if last_word and last_word != word:
await clear_chat_history(redis, user_id, last_word.decode())
# 更新当前词

View File

@ -1,28 +0,0 @@
from typing import Tuple
from fastapi import APIRouter, Depends
from app.models import User, CommentFr, CommentJp
from app.schemas.comment_schemas import CommentUpload
from app.utils.security import get_current_user
comment_router = APIRouter()
@comment_router.post("/make-comment")
async def new_word_comment(
upload: CommentUpload,
user: Tuple[User, dict] = Depends(get_current_user)
) -> None:
if upload.lang == "fr":
await CommentFr.create(
user=user[0],
comment_text=upload.comment_content,
comment_word=upload.comment_word,
)
else:
await CommentJp.create(
user=user[0],
comment_text=upload.comment_content,
comment_word=upload.comment_word,
)

View File

View File

@ -0,0 +1,34 @@
from pydantic import BaseModel, field_validator, ValidationError
class Feedback(BaseModel):
report_part: str
text: str
@classmethod
@field_validator("report_part")
def report_part_validator(cls, v):
types = (
"ui_design",
"dict_fr",
"dict_jp",
"user",
"translate",
"writting",
"ai_assist",
"pronounce",
"comment_api_test", # 该类型仅作测试使用,不对外暴露
)
if v not in types:
raise ValidationError("Invalid feedback report type")
return v
@classmethod
@field_validator("text")
def text_validator(cls, v):
if v is None:
raise ValidationError("Feedback text cannot be NULL")
return v
class Config:
from_attributes = True

View File

@ -0,0 +1,110 @@
from typing import Tuple
from fastapi import APIRouter, Depends
from app.api.make_comments.comment_schemas import Feedback
from app.core.email_utils import send_email
from app.models import User
from app.utils.security import get_current_user
comment_router = APIRouter()
@comment_router.post("/improvements")
async def new_comment(
upload: Feedback,
user: Tuple[User, dict] = Depends(get_current_user)
):
user_id = user[0].id
username = user[0].name
type = upload.report_part
mail_text = upload.text
sender = "no-reply@lexiverse.com.cn"
receivers = ["GodricTan@gmail.com"]
if type == "dict_fr":
receivers.append("aurora@lexiverse.com.cn")
content = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<title>用户反馈通知</title>
<style>
@media (prefers-color-scheme: dark) {{
body, .email-body {{ background: #0f172a !important; color: #e5e7eb !important; }}
.card {{ background: #111827 !important; border-color: #374151 !important; }}
.muted {{ color: #9ca3af !important; }}
.badge {{ background: #1f2937 !important; color: #e5e7eb !important; border-color:#374151 !important; }}
}}
</style>
</head>
<body style="margin:0;padding:0;background:#f5f7fb;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f5f7fb;">
<tr>
<td align="center" style="padding:24px;">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" class="email-body" style="width:600px;max-width:600px;background:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #e5e7eb;">
<tr>
<td style="background:linear-gradient(90deg,#4f46e5,#06b6d4);padding:22px 24px;">
<h1 style="margin:0;font-size:18px;line-height:1.4;color:#ffffff;">新的用户反馈</h1>
<p class="muted" style="margin:4px 0 0 0;font-size:12px;color:rgba(255,255,255,.85);">来自平台反馈中心</p>
</td>
</tr>
<tr>
<td style="padding:20px 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="padding:0 0 12px 0;font-size:14px;color:#111827;">
<strong>用户</strong><span>{username}</span>
</td>
</tr>
<tr>
<td style="padding:0 0 12px 0;">
<span class="badge" style="display:inline-block;padding:6px 10px;border:1px solid #e5e7eb;border-radius:999px;font-size:12px;line-height:1;color:#374151;background:#f9fafb;">
反馈板块{type}
</span>
</td>
</tr>
</table>
<div class="card" style="margin-top:8px;border:1px solid #e5e7eb;border-radius:10px;background:#ffffff;">
<div style="padding:16px 18px;">
<div style="font-size:13px;color:#6b7280;margin-bottom:8px;">反馈内容</div>
<div style="font-size:15px;line-height:1.7;color:#111827;white-space:pre-wrap;">
{mail_text}
</div>
</div>
</div>
<p class="muted" style="margin:16px 0 0 0;font-size:12px;color:#6b7280;">
您收到此邮件是因为系统检测到有新的反馈提交请在后台查看详情并进行处理
</p>
</td>
</tr>
<tr>
<td style="padding:16px 24px;border-top:1px solid #e5e7eb;">
<table width="100%" role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td align="left" class="muted" style="font-size:12px;color:#9ca3af;">
这是一封系统通知邮件请勿直接回复
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>"""
for receiver in receivers:
send_email(to_email=receiver, subject="用户反馈", content=content)
return {"massages": "feedback succeed"}

View File

View File

@ -0,0 +1,3 @@
from fastapi import APIRouter
pron_test_router = APIRouter()

View File

@ -2,9 +2,9 @@ from typing import Literal, List
from fastapi import APIRouter, Depends, HTTPException, Request
from app.api.word_comment.word_comment_schemas import CommentSet
from app.models import DefinitionJp, CommentFr, CommentJp
from app.models.fr import DefinitionFr
from app.schemas.comment_schemas import CommentSet
from app.schemas.search_schemas import SearchRequest, SearchResponse, SearchItemFr, SearchItemJp
from app.utils.all_kana import all_in_kana
from app.utils.autocomplete import suggest_autocomplete
@ -147,3 +147,5 @@ async def search_list(query_word: SearchRequest, user=Depends(get_current_user))
print(query_word.query, query_word.language, query_word.sort, query_word.order)
word_contents = await suggest_autocomplete(query=query_word)
return {"list": word_contents}
#TODO 用户搜索历史

View File

@ -39,6 +39,7 @@ async def validate_password(password: str):
detail=f"密码只能包含字母、数字和常见特殊字符 {allowed_specials}"
)
async def validate_email_exists(email: str):
user = await User.get_or_none(email=email)
if user:
@ -99,28 +100,81 @@ async def send_email_code(redis: Redis, email: str, code: str, ops_type: Literal
await save_email_code(redis, email, code)
ops_dict = {
"reg" : "用户注册",
"reset" : "密码重置",
"reg": "用户注册",
"reset": "密码重置",
}
subject = "Lexiverse 用户邮箱验证码"
content = f"""
<html>
<body style="font-family: Arial, sans-serif; line-height:1.6;">
<h2 style="color:#4CAF50;">Lexiverse 验证码</h2>
<p>您好</p>
<p>您正在进行 <b>{ops_dict[ops_type]}</b> 操作</p>
<p>
您的验证码是
<span style="font-size: 24px; font-weight: bold; color: #d9534f;">{code}</span>
</p>
<p>有效期 5 分钟请勿泄露给他人</p>
<hr>
<p style="font-size: 12px; color: #999;">
如果这不是您本人的操作请忽略此邮件
</p>
</body>
</html>
"""
content = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<title>Lexiverse 验证码</title>
<style>
@media (prefers-color-scheme: dark) {{
body, .email-body {{ background: #0f172a !important; color: #e5e7eb !important; }}
.card {{ background: #111827 !important; border-color: #374151 !important; }}
.muted {{ color: #9ca3af !important; }}
.code-box {{ background:#1f2937 !important; color:#fff !important; border-color:#374151 !important; }}
}}
</style>
</head>
<body style="margin:0;padding:0;background:#f5f7fb;font-family:'Microsoft Yahei','Arial',sans-serif;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f5f7fb;">
<tr>
<td align="center" style="padding:24px;">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" class="email-body" style="width:600px;max-width:600px;background:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #e5e7eb;">
<!-- Header -->
<tr>
<td style="background:linear-gradient(90deg,#4f46e5,#06b6d4);padding:22px 24px;">
<h1 style="margin:0;font-size:18px;line-height:1.4;color:#ffffff;">Lexiverse 验证码</h1>
<p class="muted" style="margin:4px 0 0;font-size:12px;color:rgba(255,255,255,.85);">安全身份校验</p>
</td>
</tr>
<!-- Body -->
<tr>
<td style="padding:20px 24px;">
<p style="margin-top:0;font-size:15px;color:#111827;">您好</p>
<p style="font-size:15px;color:#111827;">
您正在进行 <strong>{ops_dict[ops_type]}</strong> 操作
</p>
<div class="card" style="margin:18px 0;padding:18px;border:1px solid #e5e7eb;border-radius:10px;background:#ffffff;text-align:center;">
<div style="font-size:13px;color:#6b7280;margin-bottom:6px;">您的验证码</div>
<div class="code-box" style="display:inline-block;padding:12px 24px;border:1px solid #e5e7eb;border-radius:8px;background:#f9fafb;font-size:26px;font-weight:bold;color:#d9534f;letter-spacing:4px;">
{code}
</div>
<div class="muted" style="margin-top:10px;font-size:12px;color:#6b7280;">
有效期 5 分钟请勿泄露给他人
</div>
</div>
<p class="muted" style="margin-top:16px;font-size:12px;color:#9ca3af;">
如果这不是您本人的操作请忽略此邮件
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="padding:16px 24px;border-top:1px solid #e5e7eb;">
<table width="100%">
<tr>
<td align="left" class="muted" style="font-size:12px;color:#9ca3af;">
这是一封系统通知邮件请勿直接回复
</td>
<td align="right" class="muted" style="font-size:12px;color:#9ca3af;">
Lexiverse 安全中心
</td>
</tr>
"""
send_email(email, subject, content)

View File

@ -2,8 +2,8 @@ from typing import Literal, Tuple
from fastapi import APIRouter, Depends
from app.api.word_comment.word_comment_schemas import CommentUpload
from app.models import User, CommentFr, CommentJp
from app.schemas.comment_schemas import CommentUpload
from app.utils.security import get_current_user
word_comment_router = APIRouter()

View File

@ -21,7 +21,4 @@ class CommentSet(BaseModel):
class CommentUpload(BaseModel):
comment_word: str
comment_content: str
# lang: Literal["fr", "jp"]
class Config:
from_attributes = True
# lang: Literal["fr", "jp"]

View File

@ -11,7 +11,7 @@ from app.utils.textnorm import normalize_text
from settings import TORTOISE_ORM
async def suggest_autocomplete(query: SearchRequest, limit: int = 10) -> List[Tuple[str, str]]:
async def suggest_autocomplete(query: SearchRequest, limit: int = 10):
"""
:param query: 当前用户输入的内容
@ -22,7 +22,7 @@ async def suggest_autocomplete(query: SearchRequest, limit: int = 10) -> List[Tu
query_word = normalize_text(query.query)
exact = await (
WordlistFr
.get_or_none(text=query.query)
.get_or_none(search_text=query.query)
.values("text", "freq")
)
if exact:
@ -33,7 +33,7 @@ async def suggest_autocomplete(query: SearchRequest, limit: int = 10) -> List[Tu
qs_prefix = (
WordlistFr
.filter(Q(search_text__startswith=query_word) | Q(text__startswith=query.query))
.exclude(text=query.query)
.exclude(search_text=query.query)
.only("text", "freq")
)
prefix_objs = await qs_prefix[:limit]
@ -53,6 +53,17 @@ async def suggest_autocomplete(query: SearchRequest, limit: int = 10) -> List[Tu
contains_objs = await qs_contain[: need * 2]
contains = [(o.text, o.freq) for o in contains_objs]
seen_text, out = set(), []
for text, freq in list(exact_word) + list(prefix) + list(contains):
key = text
if key not in seen_text:
seen_text.add(key)
out.append((text, freq))
if len(out) >= limit:
break
out = sorted(out, key=lambda w: (-w[2], len(w[0]), w[0]))
return [text for text, _ in out]
else:
query_word = all_in_kana(query.query)
exact = await (
@ -89,16 +100,16 @@ async def suggest_autocomplete(query: SearchRequest, limit: int = 10) -> List[Tu
contains_objs = qs_contain[:need * 2]
contains: List[Tuple[str, str, int]] = [(o.text, o.hiragana, o.freq) for o in contains_objs]
seen_text, out = set(), []
for text, hiragana, freq in list(exact_word) + list(prefix) + list(contains):
key = (text, hiragana)
if key not in seen_text:
seen_text.add(key)
out.append((text, hiragana, freq))
if len(out) >= limit:
break
out = sorted(out, key=lambda w: (-w[2], len(w[0]), w[0]))
return [(text, hiragana) for text, hiragana, _ in out]
seen_text, out = set(), []
for text, hiragana, freq in list(exact_word) + list(prefix) + list(contains):
key = (text, hiragana)
if key not in seen_text:
seen_text.add(key)
out.append((text, hiragana, freq))
if len(out) >= limit:
break
out = sorted(out, key=lambda w: (-w[2], len(w[0]), w[0]))
return [(text, hiragana) for text, hiragana, _ in out]
async def __test():

10
main.py
View File

@ -8,6 +8,8 @@ from tortoise.contrib.fastapi import register_tortoise
import app.models.signals
from app.api.admin.router import admin_router
from app.api.ai_assist.routes import ai_router
from app.api.make_comments.routes import comment_router
from app.api.pronounciation_test.routes import pron_test_router
from app.api.redis_test import redis_test_router
from app.api.search import dict_search
from app.api.translator import translator_router
@ -15,7 +17,7 @@ from app.api.user.routes import users_router
from app.api.word_comment.routes import word_comment_router
from app.core.redis import init_redis, close_redis
from app.utils.phone_encrypt import PhoneEncrypt
from settings import ONLINE_SETTINGS
from settings import TORTOISE_ORM
@asynccontextmanager
@ -43,7 +45,7 @@ app.add_middleware(
register_tortoise(
app=app,
config=ONLINE_SETTINGS,
config=TORTOISE_ORM,
)
app.include_router(users_router, tags=["User API"], prefix="/users")
@ -56,7 +58,11 @@ app.include_router(translator_router, tags=["Translation API"])
app.include_router(ai_router, tags=["AI Assist API"], prefix="/ai_assist")
app.include_router(comment_router, tags=["Comment API"])
app.include_router(word_comment_router, tags=["Word Comment API"], prefix="/comment/word")
app.include_router(pron_test_router, tags=["Pron Test API"], prefix="/test")
if __name__ == "__main__":
uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)