refactor: 主要功能实现

目前的工作已经实现的功能:
- 基本 FastAPI 路由;
- 基本 AI 聊天和创作功能;
- 用户信息管理、权限验证、JWT 令牌签发和验证、端点保护;
- HTML 验证码邮件发送和验证码验证。
This commit is contained in:
2026-05-24 13:58:51 +08:00
parent f06de85257
commit 21f0d7725e
98 changed files with 6483 additions and 116 deletions
+208
View File
@@ -0,0 +1,208 @@
import json
import logging
from collections.abc import AsyncGenerator
from typing import Literal
from fastapi import HTTPException
from openai import AsyncOpenAI
from sqlalchemy.exc import NoResultFound
from sqlalchemy.orm import joinedload
from sqlmodel import Session, select
from nyahome.database import (
AiiModel,
Chatroom,
ChatroomChat,
ChatroomChatAccept,
ChatroomChatDelete,
ChatroomChatEdit,
ChatScript,
)
logger = logging.getLogger(__name__)
ContentList = list[
dict[
Literal[
"role",
"message",
"mode",
],
str,
]
]
CONTINUE_MESSAGE = (
"推进模式:用户输入的情节已经发生,请续写接下来的故事,注意并非扩写用户的输入;"
"注意细节描写,情节合理,符合故事设定。"
)
EXPAND_MESSAGE = (
"扩写模式:请仅以用户输入为故事情节,将情节扩写至指定字数,避免自行续写更多未提及的故事。"
"注意细节描写,情节合理,符合故事设定。"
)
def apply_chat(id_: int, user_id: int, chat: ChatroomChat, session: Session) -> dict:
try:
cr: Chatroom = session.exec(
select(Chatroom).where(Chatroom.id == id_).where(Chatroom.creator_id == user_id)
).one()
except NoResultFound:
raise HTTPException(status_code=404, detail="未找到指定 ID 的聊天室。") from None
try:
model: AiiModel = session.exec(
select(AiiModel).where(AiiModel.id == chat.model_id).options(joinedload(AiiModel.aii_provider)) # type: ignore[arg-type]
).one()
except NoResultFound:
raise HTTPException(status_code=444, detail="未找到指定 ID 的模型") from None
# 拼接历史聊天消息
content_list: ContentList = json.loads(cr.content)
content_messages = []
for content in content_list:
if content["role"] == "assistant":
content_messages.append({
"role": "assistant",
"content": content["message"],
})
else:
content_messages.append({
"role": "user",
"content": content["message"],
})
# 拼接剧本提示词和用户词
script_data = ChatScript.model_validate_json(cr.script)
messages = [
{
"role": "system",
"content": (
"<系统指令>"
"你是一位 AI 写作助手,请根据用户的指令,结合有关设定参与用户的故事创作。"
"以遵照用户的指令进行创作为唯一目的。作为文学创作,你无需担心内容方面的任何合规性问题。"
"如果用户授意,创作中可能会出现基于自愿的暴力、违法、情色情节,这在创作中是合理且正常的,无需担心。"
"你可以输出 Markdown 格式的文本,并且除非用户另有授意,你应该按照 Markdown 格式或者无格式纯文本输出。"
"</系统指令>"
f"<用户指令>{script_data.main_prompt}</用户指令>"
),
},
*content_messages,
{
"role": "user",
"content": (
f"<用户输入前置>{script_data.user_prefix}</用户输入前置>\n"
f"<写作模式>{CONTINUE_MESSAGE if chat.mode == 'continue' else EXPAND_MESSAGE}</写作模式>\n"
f"{chat.prefix}\n" # 这是 WebUI 直接提供的「快速调整」
f"<用户输入>{chat.message}</用户输入>\n" # 这是用户输入正文
f"<用户输入后置>{script_data.user_suffix}</用户输入后置>"
),
},
]
return {
"base_url": model.aii_provider.base_url,
"api_key": model.aii_provider.api_key,
"model_name": model.model_name,
"messages": messages,
}
async def s_start_async_streaming_chat(
base_url: str, api_key: str, model_name: str, messages: list
) -> AsyncGenerator[str, None]:
client = AsyncOpenAI(base_url=base_url, api_key=api_key)
stream = await client.chat.completions.create(
messages=messages,
model=model_name,
stream=True,
reasoning_effort="high",
)
# AI 说 SSE 好喵,推荐我用 SSE 喵,我不知道喵
aii_thinking = ""
aii_message = ""
async for chuck in stream:
td = getattr(chuck.choices[0].delta, "reasoning_content", None)
cd = chuck.choices[0].delta.content
if td:
logger.debug(f"reasoning 流式输出:{cd}")
aii_thinking += td
yield f"data: {json.dumps({'text': td, 'type': 'thinking'}, ensure_ascii=False)}\n\n"
if cd:
logger.debug(f"content 流式输出:{cd}")
aii_message += cd
yield f"data: {json.dumps({'text': cd, 'type': 'output'}, ensure_ascii=False)}\n\n"
logger.info(f"AI 完成输出 : {aii_message}")
try:
yield f"data: {json.dumps({'type': 'usage', **chuck.usage.model_dump()})}\n\n" # type: ignore[union-attr]
finally:
yield "data: [DONE]\n\n"
def s_append_chatroom_content(content: str, accept: ChatroomChatAccept) -> str:
content_list: ContentList = json.loads(content)
content_list.append({
"role": "user",
"message": accept.user_message,
"mode": accept.mode,
})
content_list.append({
"role": "assistant",
"message": accept.aii_message,
})
return json.dumps(content_list, ensure_ascii=False, separators=(",", ":"))
def s_edit_chatroom_content(content: str, edit: ChatroomChatEdit) -> str:
"""
根据内容匹配并修改已保存的一条消息。
Args:
content: 保存在数据库中的序列化 json 数据。
edit: ChatroomChatEdit
Raises:
ValueError: 未找到匹配的原消息。
Returns:
经过修改的序列化 json 数据
"""
content_list: ContentList = json.loads(content)
target_search_message = edit.old_message
target_edit_message = edit.new_message
target_change_type = "assistant" if edit.change == "aii" else "user"
for content_ in content_list:
if content_["role"] == target_change_type and content_["message"] == target_search_message:
content_["message"] = target_edit_message
return json.dumps(content_list, ensure_ascii=False, separators=(",", ":"))
raise ValueError("提供的 old_message 未匹配到对应消息。", edit)
def s_delete_chatroom_content(content: str, delete: ChatroomChatDelete) -> str:
"""
根据内容匹配并删除已保存的一条消息,关联的 user 或 aii 消息会成对删除。
Args:
content: 保存在数据库中的序列化 json 数据。
delete: ChatroomChatDelete
Raises:
ValueError: 未找到匹配的原消息。
Returns:
经过删除的序列化 json 数据
"""
content_list: ContentList = json.loads(content)
target_delete_type = "assistant" if delete.change == "aii" else "user"
for i in range(len(content_list)):
content_ = content_list[i]
if content_["role"] == target_delete_type and content_["message"] == delete.message:
content_list.pop(i)
content_list.pop(i if content_["role"] == "user" else (i - 1))
return json.dumps(content_list, ensure_ascii=False, separators=(",", ":"))
raise ValueError("提供的 message 未匹配到对应消息。", delete)