This commit is contained in:
parent
6eb839fd9d
commit
b74fa0ca34
|
|
@ -0,0 +1,67 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 1595751865c1
|
||||||
|
Revises: 51f049b860bb
|
||||||
|
Create Date: 2026-07-17 16:28:58.025029
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlmodel
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '1595751865c1'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = '51f049b860bb'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('captchas',
|
||||||
|
sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
|
sa.Column('captcha', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
|
sa.Column('is_valid', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('is_unused', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('expired_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('email', 'expired_at')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('captchas', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_captchas_captcha'), ['captcha'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_captchas_is_unused'), ['is_unused'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_captchas_is_valid'), ['is_valid'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('users',
|
||||||
|
sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
|
sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_users_email'), ['email'], unique=False)
|
||||||
|
|
||||||
|
with op.batch_alter_table('conversations', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_conversations_is_deleted'), ['is_deleted'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('conversations', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_conversations_is_deleted'))
|
||||||
|
|
||||||
|
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_users_email'))
|
||||||
|
|
||||||
|
op.drop_table('users')
|
||||||
|
with op.batch_alter_table('captchas', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_captchas_is_valid'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_captchas_is_unused'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_captchas_captcha'))
|
||||||
|
|
||||||
|
op.drop_table('captchas')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 32268fce88c5
|
||||||
|
Revises: 1595751865c1
|
||||||
|
Create Date: 2026-07-17 16:31:01.794850
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '32268fce88c5'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = '1595751865c1'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('captchas', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_captchas_captcha'))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('captchas', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_captchas_captcha'), ['captcha'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
@ -20,16 +20,16 @@ app = rx.App(
|
||||||
"*::before, *::after": {
|
"*::before, *::after": {
|
||||||
"box_sizing": "border-box",
|
"box_sizing": "border-box",
|
||||||
},
|
},
|
||||||
|
"--prismui-background-1": "linear-gradient(to bottom, #D0C9FF 0%, #E6D6F0 8%, #F1DBEA 12%, #C8DCFB 40%, #ABC6F6 60%, #87AEFE 90%)",
|
||||||
"--prismui-background-color-1": "#ffffff", # 纯白背景色
|
"--prismui-background-color-1": "#ffffff", # 纯白背景色
|
||||||
"--prismui-background-color-2": "rgb(235, 235, 235)",
|
"--prismui-background-color-2": "#ebebeb",
|
||||||
"--prismui-background-color-3": "#f6f6f8",
|
"--prismui-background-color-3": "#f6f6f8",
|
||||||
"--prismui-background-color-4": "#babbc0",
|
"--prismui-background-color-4": "#babbc0",
|
||||||
"--prismui-background-1": "linear-gradient(to bottom, #D0C9FF 0%, #E6D6F0 8%, #F1DBEA 12%, #C8DCFB 40%, #ABC6F6 60%, #87AEFE 90%)",
|
"--mc-float-block-shadow": "#d5d5d5",
|
||||||
"--mc-float-block-shadow": "rgba(213, 213, 213, 0.25)",
|
|
||||||
"--devui-global-bg": "#f6f6f8",
|
"--devui-global-bg": "#f6f6f8",
|
||||||
"--mc-global-bg": "linear-gradient(to bottom, #D0C9FF 0%, #E6D6F0 8%, #F1DBEA 12%, #C8DCFB 40%, #ABC6F6 60%, #87AEFE 90%)",
|
"--mc-global-bg": "linear-gradient(to bottom, #D0C9FF 0%, #E6D6F0 8%, #F1DBEA 12%, #C8DCFB 40%, #ABC6F6 60%, #87AEFE 90%)",
|
||||||
"--mc-box-shadow": "rgba(25, 25, 25, .06)",
|
"--mc-box-shadow": "#191919",
|
||||||
"--devui-shadow-length-connected-overlay": "0px 0px 12px 0px rgba(0, 0, 0, 0.1)",
|
"--devui-shadow-length-connected-overlay": "0px 0px 12px 0px #000000",
|
||||||
"--prismui-box-shadow-1": "0 4px 12px #0000001a", # 导航按钮阴影
|
"--prismui-box-shadow-1": "0 4px 12px #0000001a", # 导航按钮阴影
|
||||||
"--devui-icon-fill-weak": "#babbc0",
|
"--devui-icon-fill-weak": "#babbc0",
|
||||||
"--prismui-background-color-weak": "#babbc0",
|
"--prismui-background-color-weak": "#babbc0",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from application.components.sidebar import sidebar
|
|
||||||
|
|
||||||
__all__ = ["sidebar"]
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,21 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from application.models.datatables import Conversations, Dialogs, RunResults
|
from application.models.tables import (
|
||||||
|
Captchas,
|
||||||
|
Conversations,
|
||||||
|
Dialogs,
|
||||||
|
RunResults,
|
||||||
|
Users,
|
||||||
|
)
|
||||||
from application.models.memories import Conversation, Dialog, ThoughtNode
|
from application.models.memories import Conversation, Dialog, ThoughtNode
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"Captchas",
|
||||||
"Conversation",
|
"Conversation",
|
||||||
"Conversations",
|
"Conversations",
|
||||||
"Dialog",
|
"Dialog",
|
||||||
"Dialogs",
|
"Dialogs",
|
||||||
"RunResults",
|
"RunResults",
|
||||||
"ThoughtNode",
|
"ThoughtNode",
|
||||||
|
"Users",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,47 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
数据表模型
|
表
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from pydantic_ai._uuid import uuid7
|
from pydantic_ai._uuid import uuid7
|
||||||
from sqlmodel import Field, JSON, SQLModel
|
from sqlmodel import Field, JSON, SQLModel
|
||||||
|
from random import choices
|
||||||
|
|
||||||
|
|
||||||
# 会话数据表
|
# 验证码表
|
||||||
|
class Captchas(SQLModel, table=True):
|
||||||
|
email: str = Field(..., primary_key=True, description="邮箱")
|
||||||
|
captcha: str = Field(
|
||||||
|
default_factory=lambda: "".join(choices("0123456789", k=6)),
|
||||||
|
description="验证码",
|
||||||
|
)
|
||||||
|
is_valid: bool = Field(
|
||||||
|
default=True, index=True, description="是否有效,True 表示有效,False 表示无效"
|
||||||
|
)
|
||||||
|
is_unused: bool = Field(
|
||||||
|
default=True,
|
||||||
|
index=True,
|
||||||
|
description="是否未使用,True 表示未使用,False 表示已使用。",
|
||||||
|
)
|
||||||
|
expired_at: datetime = Field(
|
||||||
|
default_factory=lambda: datetime.now() + timedelta(minutes=30),
|
||||||
|
primary_key=True,
|
||||||
|
description="过期时间",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 用户表
|
||||||
|
class Users(SQLModel, table=True):
|
||||||
|
id: str = Field(
|
||||||
|
default_factory=lambda: str(uuid7()),
|
||||||
|
primary_key=True,
|
||||||
|
description="用户唯一标识",
|
||||||
|
)
|
||||||
|
email: str = Field(..., index=True, description="邮箱")
|
||||||
|
|
||||||
|
|
||||||
|
# 会话表
|
||||||
class Conversations(SQLModel, table=True):
|
class Conversations(SQLModel, table=True):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid7()),
|
default_factory=lambda: str(uuid7()),
|
||||||
|
|
@ -17,18 +50,18 @@ class Conversations(SQLModel, table=True):
|
||||||
)
|
)
|
||||||
user_id: str = Field(..., index=True, description="用户唯一标识")
|
user_id: str = Field(..., index=True, description="用户唯一标识")
|
||||||
description: str = Field(..., description="会话描述")
|
description: str = Field(..., description="会话描述")
|
||||||
is_deleted: bool = Field(default=False, description="是否删除")
|
is_deleted: bool = Field(default=False, index=True, description="是否删除")
|
||||||
created_at: datetime = Field(default_factory=datetime.now, description="创建时间")
|
created_at: datetime = Field(default_factory=datetime.now, description="创建时间")
|
||||||
|
|
||||||
|
|
||||||
# 思考节点数据表(仅用于定义)
|
# 思考节点表(仅定义)
|
||||||
class ThoughtNodes(SQLModel):
|
class ThoughtNodes(SQLModel):
|
||||||
|
|
||||||
kind: str = Field(description="思考节点类型")
|
kind: str = Field(description="思考节点类型")
|
||||||
content: str = Field(default="", description="思考节点内容")
|
content: str = Field(default="", description="思考节点内容")
|
||||||
|
|
||||||
|
|
||||||
# 对话数据表
|
# 对话表
|
||||||
class Dialogs(SQLModel, table=True):
|
class Dialogs(SQLModel, table=True):
|
||||||
id: str = Field(
|
id: str = Field(
|
||||||
default_factory=lambda: str(uuid7()),
|
default_factory=lambda: str(uuid7()),
|
||||||
|
|
@ -43,7 +76,7 @@ class Dialogs(SQLModel, table=True):
|
||||||
answer: str = Field(default="", description="回答")
|
answer: str = Field(default="", description="回答")
|
||||||
|
|
||||||
|
|
||||||
# 运行结果数据表
|
# 运行结果表
|
||||||
class RunResults(SQLModel, table=True):
|
class RunResults(SQLModel, table=True):
|
||||||
conversation_id: str = Field(primary_key=True, description="会话唯一标识")
|
conversation_id: str = Field(primary_key=True, description="会话唯一标识")
|
||||||
dialog_id: str = Field(primary_key=True, description="对话唯一标识")
|
dialog_id: str = Field(primary_key=True, description="对话唯一标识")
|
||||||
|
|
@ -5,11 +5,7 @@
|
||||||
import reflex as rx
|
import reflex as rx
|
||||||
|
|
||||||
from application.models import Conversation, Dialog, ThoughtNode
|
from application.models import Conversation, Dialog, ThoughtNode
|
||||||
from application.states import (
|
from application.states import ConversationState
|
||||||
ConversationHistoryCollapseState,
|
|
||||||
ConversationState,
|
|
||||||
CreateConversationState,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def render_conversation_history_item(
|
def render_conversation_history_item(
|
||||||
|
|
@ -602,8 +598,8 @@ def render_create_conversation_button() -> rx.Component:
|
||||||
),
|
),
|
||||||
background_color=rx.color("mauve", 1),
|
background_color=rx.color("mauve", 1),
|
||||||
),
|
),
|
||||||
open=CreateConversationState.is_modal_open,
|
open=ConversationState.is_creating_conversation,
|
||||||
on_open_change=CreateConversationState.toggle_modal,
|
on_open_change=ConversationState.toggle_creating_conversation,
|
||||||
),
|
),
|
||||||
display="flex",
|
display="flex",
|
||||||
width="100%",
|
width="100%",
|
||||||
|
|
@ -781,7 +777,7 @@ def render_conversation_history_collapse_button(
|
||||||
transition="all 0.3s ease-in-out",
|
transition="all 0.3s ease-in-out",
|
||||||
cursor="pointer",
|
cursor="pointer",
|
||||||
# 点击事件:切换会话历史折叠面板展开状态
|
# 点击事件:切换会话历史折叠面板展开状态
|
||||||
on_click=ConversationHistoryCollapseState.toggle_expanded,
|
on_click=ConversationState.toggle_conversation_history_shown,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -790,7 +786,7 @@ def render_conversation() -> rx.Component:
|
||||||
渲染会话页面:布局参考 MetaChat,左侧为会话历史,右侧为对话历史和输入框
|
渲染会话页面:布局参考 MetaChat,左侧为会话历史,右侧为对话历史和输入框
|
||||||
"""
|
"""
|
||||||
# 会话历史折叠面板的展开状态
|
# 会话历史折叠面板的展开状态
|
||||||
is_expanded = ConversationHistoryCollapseState.is_expanded
|
is_expanded = ConversationState.is_conversation_history_shown
|
||||||
|
|
||||||
return rx.box(
|
return rx.box(
|
||||||
# 渲染折叠面板
|
# 渲染折叠面板
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import reflex as rx
|
||||||
|
|
||||||
from application.pages import render_conversation, render_library
|
from application.pages import render_conversation, render_library
|
||||||
from application.pages.conversation import render_conversation
|
from application.pages.conversation import render_conversation
|
||||||
from application.states import AuthState, SidebarState
|
from application.states import AuthState
|
||||||
|
|
||||||
|
|
||||||
# 导航按钮字典
|
# 导航按钮字典
|
||||||
|
|
@ -30,7 +30,7 @@ def render_nav_button(
|
||||||
:return : Component
|
:return : Component
|
||||||
"""
|
"""
|
||||||
# 导航按钮激活状态
|
# 导航按钮激活状态
|
||||||
is_actived = nav_button == SidebarState.activated_nav_button
|
is_actived = nav_button == AuthState.activated_nav_button
|
||||||
|
|
||||||
return rx.vstack(
|
return rx.vstack(
|
||||||
# 渲染图标
|
# 渲染图标
|
||||||
|
|
@ -56,8 +56,8 @@ def render_nav_button(
|
||||||
align_items="center",
|
align_items="center",
|
||||||
gap="4px",
|
gap="4px",
|
||||||
cursor="pointer",
|
cursor="pointer",
|
||||||
# 点击事件:将指定导航按钮设置为侧边栏中激活的导航按钮
|
# 点击事件:将指定导航按钮设置为激活的导航按钮
|
||||||
on_click=SidebarState.switch_nav_button(nav_button),
|
on_click=AuthState.switch_activated_nav_button(nav_button),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -116,9 +116,9 @@ def render_sidebar() -> rx.Component:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def render_login_modal() -> rx.Component:
|
def render_login_window() -> rx.Component:
|
||||||
"""
|
"""
|
||||||
渲染登录模态窗
|
渲染登录窗
|
||||||
:return: Component
|
:return: Component
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -145,23 +145,24 @@ def render_login_modal() -> rx.Component:
|
||||||
flex=1,
|
flex=1,
|
||||||
),
|
),
|
||||||
rx.button(
|
rx.button(
|
||||||
"获取验证码",
|
rx.cond(
|
||||||
|
AuthState.captcha_sending_countdown > 0,
|
||||||
|
f"{AuthState.captcha_sending_countdown} s111111",
|
||||||
|
"获取验证码",
|
||||||
|
),
|
||||||
variant="outline",
|
variant="outline",
|
||||||
width="120px",
|
width="120px",
|
||||||
on_click=AuthState.send_email_code,
|
on_click=AuthState.send_captcha,
|
||||||
|
disabled=AuthState.captcha_sending_countdown > 0,
|
||||||
),
|
),
|
||||||
gap="8px",
|
gap="8px",
|
||||||
width="100%",
|
width="100%",
|
||||||
),
|
),
|
||||||
rx.cond(
|
rx.text(AuthState.notification, color="red", font_size="12px"),
|
||||||
AuthState.email == "",
|
|
||||||
rx.text("请输入邮箱地址", color="red", font_size="12px"),
|
|
||||||
rx.box(height="16px"),
|
|
||||||
),
|
|
||||||
rx.hstack(
|
rx.hstack(
|
||||||
rx.checkbox(
|
rx.checkbox(
|
||||||
checked=AuthState.agree_policy,
|
checked=AuthState.is_policies_agreed,
|
||||||
on_change=AuthState.toggle_agree,
|
on_change=AuthState.toggle_policies_agreed,
|
||||||
),
|
),
|
||||||
rx.text("我已阅读并同意 ", font_size="13px"),
|
rx.text("我已阅读并同意 ", font_size="13px"),
|
||||||
rx.link("用户协议、隐私政策", font_size="13px", href="#"),
|
rx.link("用户协议、隐私政策", font_size="13px", href="#"),
|
||||||
|
|
@ -173,7 +174,9 @@ def render_login_modal() -> rx.Component:
|
||||||
bg="#0a0e29",
|
bg="#0a0e29",
|
||||||
color="white",
|
color="white",
|
||||||
margin_top="12px",
|
margin_top="12px",
|
||||||
on_click=AuthState.login_by_email_code,
|
on_click=AuthState.login,
|
||||||
|
loading=AuthState.is_logging_in,
|
||||||
|
disabled=AuthState.is_logging_in,
|
||||||
),
|
),
|
||||||
rx.text(
|
rx.text(
|
||||||
"登录有问题?去反馈",
|
"登录有问题?去反馈",
|
||||||
|
|
@ -222,7 +225,7 @@ def render_login_modal() -> rx.Component:
|
||||||
|
|
||||||
def render_index() -> rx.Component:
|
def render_index() -> rx.Component:
|
||||||
"""
|
"""
|
||||||
首页
|
渲染首页
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return rx.fragment(
|
return rx.fragment(
|
||||||
|
|
@ -231,7 +234,7 @@ def render_index() -> rx.Component:
|
||||||
render_sidebar(),
|
render_sidebar(),
|
||||||
# 根据侧边栏状态中激活的导航按钮渲染相应页面(本项目采用卡片布局)
|
# 根据侧边栏状态中激活的导航按钮渲染相应页面(本项目采用卡片布局)
|
||||||
rx.match(
|
rx.match(
|
||||||
SidebarState.activated_nav_button,
|
AuthState.activated_nav_button,
|
||||||
# 渲染知识库页面
|
# 渲染知识库页面
|
||||||
("library", render_library()),
|
("library", render_library()),
|
||||||
# 渲染会话页面
|
# 渲染会话页面
|
||||||
|
|
@ -245,9 +248,10 @@ def render_index() -> rx.Component:
|
||||||
# 挂载事件:
|
# 挂载事件:
|
||||||
on_mount=AuthState.check,
|
on_mount=AuthState.check,
|
||||||
),
|
),
|
||||||
|
# 若用户唯一标识为空则渲染登录窗
|
||||||
rx.cond(
|
rx.cond(
|
||||||
AuthState.user_id == "",
|
AuthState.user_id == "",
|
||||||
render_login_modal(),
|
render_login_window(),
|
||||||
rx.fragment(),
|
rx.fragment(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,20 @@
|
||||||
"""
|
"""
|
||||||
认证状态
|
认证状态
|
||||||
"""
|
"""
|
||||||
|
from asyncio import sleep, create_task
|
||||||
|
from email.header import Header
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
import re
|
||||||
|
from typing import Any, AsyncGenerator, Coroutine, cast
|
||||||
|
from aiosmtplib import SMTP, SMTPException
|
||||||
import reflex as rx
|
import reflex as rx
|
||||||
|
|
||||||
import re
|
from application.states.database import DatabaseState
|
||||||
from sqlmodel import select
|
|
||||||
from application.models.user import EmailVerifyCode
|
# 验证码HTML模板
|
||||||
from application.database.base import get_async_db
|
CAPTCHA_HTML_TEMPLATE = """
|
||||||
|
<p>验证码:<strong>{captcha}</strong></p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class AuthState(rx.State):
|
class AuthState(rx.State):
|
||||||
|
|
@ -15,17 +23,34 @@ class AuthState(rx.State):
|
||||||
认证状态
|
认证状态
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 当前登录的用户唯一标识(通过本地存储同步)
|
# 用户唯一标识(通过本地存储同步)
|
||||||
user_id: str = rx.LocalStorage("user_id", sync=True)
|
user_id: str = rx.LocalStorage("user_id", sync=True)
|
||||||
|
|
||||||
|
# 邮箱
|
||||||
email: str = ""
|
email: str = ""
|
||||||
|
# 验证码
|
||||||
captcha: str = ""
|
captcha: str = ""
|
||||||
|
# 协议同意状态
|
||||||
is_policies_agreed: bool = False
|
is_policies_agreed: bool = False
|
||||||
|
# 提示信息
|
||||||
|
notification: str = ""
|
||||||
|
|
||||||
|
# 验证码发送倒计时
|
||||||
|
captcha_sending_countdown: int = 0
|
||||||
|
|
||||||
|
# 验证码发送中状态
|
||||||
|
is_captcha_sending: bool = False
|
||||||
|
|
||||||
|
# 登录中状态
|
||||||
|
is_logging_in: bool = False
|
||||||
|
|
||||||
|
# 激活的导航按钮
|
||||||
|
activated_nav_button: str = "conversation"
|
||||||
|
|
||||||
@rx.event
|
@rx.event
|
||||||
async def check(self) -> None:
|
async def check(self) -> None:
|
||||||
"""
|
"""
|
||||||
检查认证状态
|
检查
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.user_id = ""
|
self.user_id = ""
|
||||||
|
|
@ -56,94 +81,142 @@ class AuthState(rx.State):
|
||||||
"""
|
"""
|
||||||
self.is_policies_agreed = not self.is_policies_agreed
|
self.is_policies_agreed = not self.is_policies_agreed
|
||||||
|
|
||||||
@rx.event
|
def validate_email(self) -> bool:
|
||||||
async def send_email_code(self):
|
"""
|
||||||
"""生成并存储邮箱验证码(异步数据库,不阻塞事件循环)"""
|
验证邮箱
|
||||||
|
:return: 验证是否通过,True 表示校验通过,False 表示校验不通过
|
||||||
email_rule = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
"""
|
||||||
if not re.fullmatch(email_rule, self.email):
|
self.notification = ""
|
||||||
return rx.toast("请输入合法邮箱地址", color="red")
|
# 非空校验
|
||||||
|
|
||||||
# 异步数据库会话
|
|
||||||
async for db in get_async_db():
|
|
||||||
# 清理该邮箱旧验证码(await 异步查询)
|
|
||||||
old_codes = await db.exec(
|
|
||||||
select(EmailVerifyCode).where(EmailVerifyCode.email == self.email)
|
|
||||||
)
|
|
||||||
old_list = old_codes.all()
|
|
||||||
for item in old_list:
|
|
||||||
await db.delete(item)
|
|
||||||
|
|
||||||
# 6位验证码,5分钟有效期
|
|
||||||
import random
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
code_str = "".join(random.choices("0123456789", k=6))
|
|
||||||
expire_time = datetime.utcnow() + timedelta(minutes=5)
|
|
||||||
db.add(
|
|
||||||
EmailVerifyCode(email=self.email, code=code_str, expire_at=expire_time)
|
|
||||||
)
|
|
||||||
await db.commit()
|
|
||||||
|
|
||||||
# 本地调试打印验证码
|
|
||||||
print(f"邮箱 {self.email} 测试验证码:{code_str}")
|
|
||||||
return rx.toast("验证码已发送(控制台查看测试码)", color="green")
|
|
||||||
|
|
||||||
@rx.event
|
|
||||||
async def login_by_email_code(self):
|
|
||||||
"""邮箱验证码登录,不存在用户自动注册"""
|
|
||||||
if not self.email:
|
if not self.email:
|
||||||
return rx.toast("请填写邮箱", color="red")
|
self.notification = "请输入邮箱"
|
||||||
if not self.code:
|
return False
|
||||||
return rx.toast("请填写验证码", color="red")
|
# 格式校验
|
||||||
if not self.agree_policy:
|
if not re.fullmatch(
|
||||||
return rx.toast("请勾选同意用户协议与隐私政策", color="red")
|
r"^[a-zA-Z0-9]+([_\-+.][a-zA-Z0-9_\-+]+)*@[a-zA-Z0-9]+(\-[a-zA-Z0-9\-]+)*(\.[a-zA-Z0-9]+(\-[a-zA-Z0-9\-]+)*)*\.[a-zA-Z0-9]{2,}$",
|
||||||
|
self.email,
|
||||||
|
):
|
||||||
|
self.notification = "邮箱格式不正确,请重新输入"
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
db = get_db_session()
|
def validate_captcha(self) -> bool:
|
||||||
# 查询有效验证码
|
|
||||||
code_record = db.exec(
|
|
||||||
select(EmailVerifyCode).where(
|
|
||||||
EmailVerifyCode.email == self.email, EmailVerifyCode.code == self.code
|
|
||||||
)
|
|
||||||
).first()
|
|
||||||
if not code_record or code_record.is_expired():
|
|
||||||
return rx.toast("验证码错误或已过期", color="red")
|
|
||||||
|
|
||||||
# 一次性验证码使用后删除
|
|
||||||
db.delete(code_record)
|
|
||||||
|
|
||||||
# 查找用户,无则新建
|
|
||||||
user = db.exec(select(User).where(User.email == self.email)).first()
|
|
||||||
if not user:
|
|
||||||
user = User(email=self.email, nickname=self.email.split("@")[0])
|
|
||||||
db.add(user)
|
|
||||||
user.last_login_at = datetime.utcnow()
|
|
||||||
db.commit()
|
|
||||||
db.refresh(user)
|
|
||||||
|
|
||||||
# 登录成功,弹窗自动消失
|
|
||||||
self.user_id = str(user.id)
|
|
||||||
self.email = ""
|
|
||||||
self.code = ""
|
|
||||||
self.agree_policy = False
|
|
||||||
return rx.toast("登录成功", color="green")
|
|
||||||
|
|
||||||
@rx.var
|
|
||||||
async def storage_key(self) -> str:
|
|
||||||
"""
|
"""
|
||||||
LocalStorage 键
|
验证验证码
|
||||||
|
:return: 验证是否通过,True 表示校验通过,False 表示校验不通过
|
||||||
"""
|
"""
|
||||||
# 获取认证状态
|
self.notification = ""
|
||||||
auth = await self.get_state(AuthState)
|
# 非空校验
|
||||||
# 获取用户唯一标识
|
if not self.captcha:
|
||||||
user_id = auth.user_id or "default"
|
self.notification = "请输入验证码"
|
||||||
return f"sidebar_{user_id}"
|
return False
|
||||||
|
# 格式校验
|
||||||
|
if not re.fullmatch(r"^\d{6}$", self.captcha):
|
||||||
|
self.notification = "验证码格式不正确,请重新输入"
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
# 激活的导航按钮
|
@rx.event(background=True)
|
||||||
activated_nav_button: str = rx.LocalStorage(storage_key, sync=True)
|
async def resend_captcha_countdown(self) -> None:
|
||||||
|
"""
|
||||||
|
重新发送验证码倒计时
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
async with self:
|
||||||
|
self.captcha_sending_countdown = 59
|
||||||
|
|
||||||
|
while self.captcha_sending_countdown > 0:
|
||||||
|
await sleep(1)
|
||||||
|
async with self:
|
||||||
|
self.captcha_sending_countdown -= 1
|
||||||
|
|
||||||
|
@rx.event(background=True)
|
||||||
|
async def send_captcha_(self) -> None:
|
||||||
|
"""
|
||||||
|
发送验证码
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
async with self:
|
||||||
|
self.notification = "验证码已发送"
|
||||||
|
# 创建验证码记录并赋值
|
||||||
|
database_state = await self.get_state(DatabaseState)
|
||||||
|
captcha = await database_state.create_captchas_record(email=self.email)
|
||||||
|
|
||||||
|
# 构建邮件
|
||||||
|
message = MIMEText(
|
||||||
|
CAPTCHA_HTML_TEMPLATE.format(captcha=captcha),
|
||||||
|
"html",
|
||||||
|
"utf-8",
|
||||||
|
)
|
||||||
|
message["Subject"] = Header("验证码", "utf-8").encode()
|
||||||
|
message["From"] = "office@liubiren.cloud"
|
||||||
|
message["To"] = self.email
|
||||||
|
try:
|
||||||
|
async with SMTP(
|
||||||
|
hostname="smtp.feishu.cn",
|
||||||
|
port=465,
|
||||||
|
username="office@liubiren.cloud",
|
||||||
|
password="cx1dM5tBcbyWftpS",
|
||||||
|
use_tls=True,
|
||||||
|
local_hostname="localhost",
|
||||||
|
) as smtp:
|
||||||
|
await smtp.send_message(message)
|
||||||
|
|
||||||
|
except SMTPException:
|
||||||
|
async with self:
|
||||||
|
self.notification = f"发送失败,请稍后重试"
|
||||||
|
self.captcha_sending_countdown = 0
|
||||||
|
|
||||||
@rx.event
|
@rx.event
|
||||||
def switch_nav_button(self, button: str):
|
async def send_captcha(self) -> AsyncGenerator:
|
||||||
|
"""
|
||||||
|
发送验证码
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.notification = ""
|
||||||
|
|
||||||
|
# 验证邮箱
|
||||||
|
if not self.validate_email():
|
||||||
|
return
|
||||||
|
|
||||||
|
# 将重新发送验证码倒计时和发送验证码事件添加至后台任务队列
|
||||||
|
yield type(self).resend_captcha_countdown
|
||||||
|
yield type(self).send_captcha_
|
||||||
|
|
||||||
|
@rx.event
|
||||||
|
async def login(self) -> None:
|
||||||
|
"""
|
||||||
|
登录
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.is_logging_in = True
|
||||||
|
|
||||||
|
# 验证邮箱
|
||||||
|
if not self.validate_email():
|
||||||
|
return
|
||||||
|
|
||||||
|
# 验证验证码
|
||||||
|
if not self.validate_captcha():
|
||||||
|
return
|
||||||
|
|
||||||
|
# 创建验证码记录并赋值验证码
|
||||||
|
database_state = await self.get_state(DatabaseState)
|
||||||
|
|
||||||
|
if not await database_state.update_captcha(
|
||||||
|
email=self.email, captcha=self.captcha
|
||||||
|
):
|
||||||
|
self.notification = "验证码错误"
|
||||||
|
return
|
||||||
|
|
||||||
|
# 创建用户记录
|
||||||
|
user_id = await database_state.create_users_record(email=self.email)
|
||||||
|
self.user_id = user_id
|
||||||
|
self.notification = "登录成功"
|
||||||
|
self.is_logging_in = False
|
||||||
|
|
||||||
|
@rx.event
|
||||||
|
def switch_activated_nav_button(self, button: str):
|
||||||
"""
|
"""
|
||||||
将指定导航按钮设置为激活的导航按钮
|
将指定导航按钮设置为激活的导航按钮
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -65,12 +65,12 @@ class ConversationState(rx.State):
|
||||||
会话状态
|
会话状态
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 用户唯一标识
|
# 当前用户唯一标识
|
||||||
user_id: str = ""
|
user_id: str = ""
|
||||||
|
|
||||||
# 会话字典
|
# 会话字典
|
||||||
conversations: Dict[str, Conversation] = {}
|
conversations: Dict[str, Conversation] = {}
|
||||||
# 会话唯一标识
|
# 当前会话唯一标识
|
||||||
conversation_id: str = ""
|
conversation_id: str = ""
|
||||||
|
|
||||||
# 会话历史展示状态,True表示展示,False表示隐藏
|
# 会话历史展示状态,True表示展示,False表示隐藏
|
||||||
|
|
@ -84,7 +84,7 @@ class ConversationState(rx.State):
|
||||||
"""
|
"""
|
||||||
页面挂载时初始化会话列表和会话唯一标识
|
页面挂载时初始化会话列表和会话唯一标识
|
||||||
"""
|
"""
|
||||||
# 获取登录用户唯一标识
|
# 获取当前用户唯一标识
|
||||||
auth_state = await self.get_state(AuthState)
|
auth_state = await self.get_state(AuthState)
|
||||||
self.user_id = auth_state.user_id
|
self.user_id = auth_state.user_id
|
||||||
if not self.user_id:
|
if not self.user_id:
|
||||||
|
|
@ -93,21 +93,28 @@ class ConversationState(rx.State):
|
||||||
# 获取数据库状态
|
# 获取数据库状态
|
||||||
database_state = await self.get_state(DatabaseState)
|
database_state = await self.get_state(DatabaseState)
|
||||||
|
|
||||||
# 获取会话字典
|
# 检索会话字典并赋值
|
||||||
self.conversations = await database_state.get_conversations(
|
self.conversations = await database_state.retrieve_conversations(
|
||||||
user_id=self.user_id
|
user_id=self.user_id
|
||||||
)
|
)
|
||||||
# 若会话字典为空则新建会话
|
# 若会话字典为空则先创建会话记录再在会话字典中添加默认会话
|
||||||
if not self.conversations:
|
if not self.conversations:
|
||||||
# 先在数据库新建会话,再在会话字典新建会话
|
conversation_id, created_at = (
|
||||||
conversation_id, created_at = await database_state.create_conversation(
|
await database_state.create_conversations_record(user_id=self.user_id)
|
||||||
user_id=self.user_id
|
|
||||||
)
|
)
|
||||||
self.conversations[conversation_id] = Conversation(created_at=created_at)
|
self.conversations[conversation_id] = Conversation(created_at=created_at)
|
||||||
|
|
||||||
# 默认将最后一个会话的唯一标识设置为当前会话唯一标识
|
# 将最后一个会话的唯一标识设置为当前会话唯一标识
|
||||||
self.conversation_id = next(reversed(self.conversations.keys()))
|
self.conversation_id = next(reversed(self.conversations.keys()))
|
||||||
|
|
||||||
|
@rx.event
|
||||||
|
def toggle_conversation_history_shown(self) -> None:
|
||||||
|
"""
|
||||||
|
切换会话历史展示状态
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.is_conversation_history_shown = not self.is_conversation_history_shown
|
||||||
|
|
||||||
@rx.var
|
@rx.var
|
||||||
def conversation_history(self) -> Dict[str, Conversation]:
|
def conversation_history(self) -> Dict[str, Conversation]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -126,15 +133,16 @@ class ConversationState(rx.State):
|
||||||
# 获取会话描述
|
# 获取会话描述
|
||||||
description = form_data["description"].strip() or "新会话"
|
description = form_data["description"].strip() or "新会话"
|
||||||
|
|
||||||
# 先在数据库中创建会话记录,再在会话字典中创建会话记录
|
# 先创建会话记录再在会话字典中添加会话
|
||||||
database_state = await self.get_state(DatabaseState)
|
database_state = await self.get_state(DatabaseState)
|
||||||
conversation_id, created_at = await database_state.create_conversation(
|
conversation_id, created_at = await database_state.create_conversations_record(
|
||||||
user_id=self.user_id, description=description
|
user_id=self.user_id, description=description
|
||||||
)
|
)
|
||||||
self.conversations[conversation_id] = Conversation(
|
self.conversations[conversation_id] = Conversation(
|
||||||
description=description, created_at=created_at
|
description=description, created_at=created_at
|
||||||
)
|
)
|
||||||
# 将新建会话的唯一标识设置为当前会话唯一标识
|
|
||||||
|
# 将所添加的会话唯一标识设置为当前会话唯一标识
|
||||||
self.conversation_id = conversation_id
|
self.conversation_id = conversation_id
|
||||||
# 将正在新建会话状态设置为已完成
|
# 将正在新建会话状态设置为已完成
|
||||||
self.is_creating_conversation = False
|
self.is_creating_conversation = False
|
||||||
|
|
@ -149,15 +157,15 @@ class ConversationState(rx.State):
|
||||||
if conversation_id not in self.conversations:
|
if conversation_id not in self.conversations:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 先在数据库删除会话,再在会话字典删除会话
|
# 先逻辑删除会话记录再在会话字典中删除会话
|
||||||
database_state = await self.get_state(DatabaseState)
|
database_state = await self.get_state(DatabaseState)
|
||||||
await database_state.delete_conversation(conversation_id=conversation_id)
|
await database_state.delete_conversations_record(conversation_id=conversation_id)
|
||||||
del self.conversations[conversation_id]
|
del self.conversations[conversation_id]
|
||||||
|
|
||||||
# 删除后,若会话字典为空则先在数据库新建会话,再在会话字典新建会话
|
# 删除后,若会话字典为空则先创建会话记录再在会话字典中添加默认会话
|
||||||
if not self.conversations:
|
if not self.conversations:
|
||||||
conversation_id, created_at = await database_state.create_conversation(
|
conversation_id, created_at = (
|
||||||
user_id=self.user_id
|
await database_state.create_conversations_record(user_id=self.user_id)
|
||||||
)
|
)
|
||||||
self.conversations[conversation_id] = Conversation(created_at=created_at)
|
self.conversations[conversation_id] = Conversation(created_at=created_at)
|
||||||
|
|
||||||
|
|
@ -168,7 +176,7 @@ class ConversationState(rx.State):
|
||||||
@rx.event
|
@rx.event
|
||||||
def switch_conversation(self, conversation_id: str) -> None:
|
def switch_conversation(self, conversation_id: str) -> None:
|
||||||
"""
|
"""
|
||||||
将指定会话唯一标识设置为当前会话唯一标识
|
将指定会话的唯一标识设置为当前会话唯一标识
|
||||||
:param conversation_id: 指定会话唯一标识
|
:param conversation_id: 指定会话唯一标识
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
|
@ -176,6 +184,13 @@ class ConversationState(rx.State):
|
||||||
return
|
return
|
||||||
self.conversation_id = conversation_id
|
self.conversation_id = conversation_id
|
||||||
|
|
||||||
|
@rx.event
|
||||||
|
def toggle_creating_conversation(self) -> None:
|
||||||
|
"""
|
||||||
|
切换正在新建会话状态
|
||||||
|
"""
|
||||||
|
self.is_creating_conversation = not self.is_creating_conversation
|
||||||
|
|
||||||
@rx.var
|
@rx.var
|
||||||
def dialog_history(self) -> Dict[str, Dialog]:
|
def dialog_history(self) -> Dict[str, Dialog]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -221,13 +236,13 @@ class ConversationState(rx.State):
|
||||||
# 获取数据库状态
|
# 获取数据库状态
|
||||||
database_state = await self.get_state(DatabaseState)
|
database_state = await self.get_state(DatabaseState)
|
||||||
|
|
||||||
# 获取消息历史
|
# 检索运行结果记录并作为消息历史
|
||||||
message_history = await database_state.get_message_history(
|
message_history = await database_state.retrieve_message_history(
|
||||||
conversation_id=self.conversation_id
|
conversation_id=self.conversation_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# 先在数据库新建对话,再在当前会话的对话字典新增对话
|
# 先创建对话记录再在当前对话字典中新增对话
|
||||||
dialog_id = await database_state.create_dialog(
|
dialog_id = await database_state.create_dialogs_record(
|
||||||
conversation_id=self.conversation_id, question=question
|
conversation_id=self.conversation_id, question=question
|
||||||
)
|
)
|
||||||
dialog = conversation.dialogs.setdefault(dialog_id, Dialog(question=question))
|
dialog = conversation.dialogs.setdefault(dialog_id, Dialog(question=question))
|
||||||
|
|
@ -383,14 +398,14 @@ class ConversationState(rx.State):
|
||||||
|
|
||||||
# ========== 智能体运行结果事件 ==========
|
# ========== 智能体运行结果事件 ==========
|
||||||
case AgentRunResultEvent(result=result):
|
case AgentRunResultEvent(result=result):
|
||||||
# 完成对话
|
# 更新对话记录
|
||||||
await database_state.complete_dialog(
|
await database_state.update_dialog_record(
|
||||||
dialog_id=dialog_id,
|
dialog_id=dialog_id,
|
||||||
thought_nodes=dialog.thought_nodes,
|
thought_nodes=dialog.thought_nodes,
|
||||||
answer=dialog.answer,
|
answer=dialog.answer,
|
||||||
)
|
)
|
||||||
# 保存新增消息
|
# 创建运行结果记录
|
||||||
await database_state.save_new_messages(
|
await database_state.create_run_result_record(
|
||||||
conversation_id=self.conversation_id,
|
conversation_id=self.conversation_id,
|
||||||
dialog_id=dialog_id,
|
dialog_id=dialog_id,
|
||||||
new_messages=result.new_messages(),
|
new_messages=result.new_messages(),
|
||||||
|
|
@ -407,18 +422,3 @@ class ConversationState(rx.State):
|
||||||
# 指定运行
|
# 指定运行
|
||||||
dialog = self.conversations[self.conversation_id].dialogs[dialog_id]
|
dialog = self.conversations[self.conversation_id].dialogs[dialog_id]
|
||||||
dialog.is_collapse_expanded = not dialog.is_collapse_expanded
|
dialog.is_collapse_expanded = not dialog.is_collapse_expanded
|
||||||
|
|
||||||
@rx.event
|
|
||||||
def toggle_conversation_history_shown(self) -> None:
|
|
||||||
"""
|
|
||||||
切换会话历史展示状态
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.is_conversation_history_shown = not self.is_conversation_history_shown
|
|
||||||
|
|
||||||
@rx.event
|
|
||||||
def toggle_creating_conversation(self) -> None:
|
|
||||||
"""
|
|
||||||
切换正在新建会话状态
|
|
||||||
"""
|
|
||||||
self.is_creating_conversation = not self.is_creating_conversation
|
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,17 @@ from pydantic import TypeAdapter
|
||||||
from pydantic_ai import ModelMessage, ModelMessagesTypeAdapter
|
from pydantic_ai import ModelMessage, ModelMessagesTypeAdapter
|
||||||
import reflex as rx
|
import reflex as rx
|
||||||
from sqlalchemy import ColumnElement, desc
|
from sqlalchemy import ColumnElement, desc
|
||||||
from sqlmodel import select
|
from sqlmodel import select, update
|
||||||
|
|
||||||
from application.models import (
|
from application.models import (
|
||||||
|
Captchas,
|
||||||
Conversation,
|
Conversation,
|
||||||
Conversations,
|
Conversations,
|
||||||
Dialog,
|
Dialog,
|
||||||
Dialogs,
|
Dialogs,
|
||||||
RunResults,
|
RunResults,
|
||||||
ThoughtNode,
|
ThoughtNode,
|
||||||
|
Users,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -30,13 +32,88 @@ class DatabaseState(rx.State):
|
||||||
数据库状态
|
数据库状态
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def get_conversations(self, user_id: str) -> Dict[str, Conversation]:
|
async def create_captchas_record(self, email: str) -> str:
|
||||||
|
"""
|
||||||
|
创建验证码记录
|
||||||
|
:param email: 邮箱
|
||||||
|
:return: 验证码
|
||||||
|
"""
|
||||||
|
async with rx.asession() as session:
|
||||||
|
# 前置处理:将该邮箱有效且未使用的验证码记录的是否有效设置为无效
|
||||||
|
await session.exec(
|
||||||
|
update(Captchas)
|
||||||
|
.where(
|
||||||
|
cast(ColumnElement[bool], Captchas.email == email),
|
||||||
|
cast(
|
||||||
|
ColumnElement[bool],
|
||||||
|
Captchas.is_valid == True,
|
||||||
|
),
|
||||||
|
cast(
|
||||||
|
ColumnElement[bool],
|
||||||
|
Captchas.is_unused == True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.values(is_valid=False)
|
||||||
|
)
|
||||||
|
await session.flush()
|
||||||
|
# 创建验证码记录
|
||||||
|
record = Captchas(
|
||||||
|
email=email,
|
||||||
|
)
|
||||||
|
session.add(record)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(record)
|
||||||
|
return record.captcha
|
||||||
|
|
||||||
|
async def update_captcha(self, email: str, captcha: str) -> bool:
|
||||||
|
"""
|
||||||
|
更新验证码
|
||||||
|
:param email: 邮箱
|
||||||
|
:param captcha: 验证码
|
||||||
|
:return: 是否更新成功,True 表示更新成功,False 表示更新失败
|
||||||
|
"""
|
||||||
|
async with rx.asession() as session:
|
||||||
|
result = await session.exec(
|
||||||
|
select(Captchas).where(
|
||||||
|
Captchas.email == email,
|
||||||
|
Captchas.captcha == captcha,
|
||||||
|
Captchas.is_valid == True,
|
||||||
|
Captchas.is_unused == True,
|
||||||
|
Captchas.expired_at > datetime.now(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
record = result.first()
|
||||||
|
if not record:
|
||||||
|
return False
|
||||||
|
record.is_unused = False
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(record)
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def create_users_record(self, email: str) -> str:
|
||||||
|
"""
|
||||||
|
创建用户记录
|
||||||
|
:param email: 邮箱
|
||||||
|
:return: 创建用户记录的唯一标识
|
||||||
|
"""
|
||||||
|
async with rx.asession() as session:
|
||||||
|
result = await session.exec(select(Users).where(Users.email == email))
|
||||||
|
record = result.first()
|
||||||
|
if record:
|
||||||
|
return record.id
|
||||||
|
new_user = Users(email=email)
|
||||||
|
session.add(new_user)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(new_user)
|
||||||
|
return new_user.id
|
||||||
|
|
||||||
|
async def retrieve_conversations(self, user_id: str) -> Dict[str, Conversation]:
|
||||||
"""
|
"""
|
||||||
获取会话字典
|
获取会话字典
|
||||||
:param user_id: 用户唯一标识
|
:param user_id: 用户唯一标识
|
||||||
:return: 会话字典
|
:return: 会话字典
|
||||||
"""
|
"""
|
||||||
conversations: Dict[str, Conversation] = {}
|
records: Dict[str, Conversation] = {}
|
||||||
async with rx.asession() as session:
|
async with rx.asession() as session:
|
||||||
result = await session.exec(
|
result = await session.exec(
|
||||||
select(Conversations, Dialogs)
|
select(Conversations, Dialogs)
|
||||||
|
|
@ -54,121 +131,101 @@ class DatabaseState(rx.State):
|
||||||
.order_by(Conversations.id, Dialogs.id)
|
.order_by(Conversations.id, Dialogs.id)
|
||||||
)
|
)
|
||||||
for conversation, dialog in result.all():
|
for conversation, dialog in result.all():
|
||||||
conversation_memory = conversations.setdefault(
|
record = records.setdefault(
|
||||||
conversation.id,
|
conversation.id,
|
||||||
Conversation(
|
Conversation(
|
||||||
description=conversation.description,
|
description=conversation.description,
|
||||||
created_at=conversation.created_at,
|
created_at=conversation.created_at,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not dialog:
|
if not dialog:
|
||||||
continue
|
continue
|
||||||
conversation_memory.dialogs[dialog.id] = Dialog(
|
record.dialogs[dialog.id] = Dialog(
|
||||||
question=dialog.question,
|
question=dialog.question,
|
||||||
thought_nodes=ThoughtNodeTypeAdapter.validate_python(
|
thought_nodes=ThoughtNodeTypeAdapter.validate_python(
|
||||||
dialog.thought_nodes
|
dialog.thought_nodes
|
||||||
), # 反序列化为思考节点内存实例字典
|
),
|
||||||
answer=dialog.answer,
|
answer=dialog.answer,
|
||||||
)
|
)
|
||||||
return conversations
|
return records
|
||||||
|
|
||||||
async def create_conversation(
|
async def create_conversations_record(
|
||||||
self, user_id: str, description: str = "新会话"
|
self, user_id: str, description: str = "新会话"
|
||||||
) -> tuple[str, datetime]:
|
) -> tuple[str, datetime]:
|
||||||
"""
|
"""
|
||||||
新建会话
|
创建会话记录
|
||||||
:param user_id: 用户唯一标识
|
:param user_id: 用户唯一标识
|
||||||
:param description: 会话描述
|
:param description: 会话描述
|
||||||
:return: 新建会话唯一标识、创建时间
|
:return: 创建会话记录的唯一标识和创建时间
|
||||||
"""
|
"""
|
||||||
async with rx.asession() as session:
|
async with rx.asession() as session:
|
||||||
conversation = Conversations(user_id=user_id, description=description)
|
record = Conversations(user_id=user_id, description=description)
|
||||||
session.add(conversation)
|
session.add(record)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(conversation)
|
await session.refresh(record)
|
||||||
return conversation.id, conversation.created_at
|
return record.id, record.created_at
|
||||||
|
|
||||||
async def delete_conversation(self, conversation_id: str) -> None:
|
async def delete_conversations_record(self, conversation_id: str) -> None:
|
||||||
"""
|
"""
|
||||||
删除指定会话(逻辑删除)
|
逻辑删除会话记录
|
||||||
:param conversation_id: 指定会话唯一标识
|
:param conversation_id: 指定会话记录唯一标识
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
async with rx.asession() as session:
|
async with rx.asession() as session:
|
||||||
conversation = await session.get(Conversations, conversation_id)
|
record = await session.get(Conversations, conversation_id)
|
||||||
if not conversation:
|
if not record:
|
||||||
return
|
return
|
||||||
conversation.is_deleted = True
|
record.is_deleted = True
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
async def create_dialog(self, conversation_id: str, question: str) -> str:
|
async def create_dialogs_record(self, conversation_id: str, question: str) -> str:
|
||||||
"""
|
"""
|
||||||
新建对话
|
创建对话记录
|
||||||
:param conversation_id: 会话唯一标识
|
:param conversation_id: 会话唯一标识
|
||||||
:param question: 提问
|
:param question: 问题
|
||||||
:return: 新建对话唯一标识
|
:return: 创建对话记录的唯一标识
|
||||||
"""
|
"""
|
||||||
async with rx.asession() as session:
|
async with rx.asession() as session:
|
||||||
dialog = Dialogs(
|
record = Dialogs(
|
||||||
conversation_id=conversation_id,
|
conversation_id=conversation_id,
|
||||||
question=question,
|
question=question,
|
||||||
)
|
)
|
||||||
session.add(dialog)
|
session.add(record)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(dialog)
|
await session.refresh(record)
|
||||||
return dialog.id
|
return record.id
|
||||||
|
|
||||||
async def complete_dialog(
|
async def update_dialog_record(
|
||||||
self,
|
self,
|
||||||
dialog_id: str,
|
dialog_id: str,
|
||||||
thought_nodes: dict[int, ThoughtNode],
|
thought_nodes: dict[int, ThoughtNode],
|
||||||
answer: str,
|
answer: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
完成对话
|
更新对话记录
|
||||||
:param dialog_id: 对话唯一标识
|
:param dialog_id: 对话唯一标识
|
||||||
:param thought_nodes: 思考节点列表
|
:param thought_nodes: 思考节点列表
|
||||||
:param answer: 回答
|
:param answer: 回答
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
async with rx.asession() as session:
|
async with rx.asession() as session:
|
||||||
dialog = await session.get(Dialogs, dialog_id)
|
record = await session.get(Dialogs, dialog_id)
|
||||||
if not dialog:
|
if not record:
|
||||||
return
|
return
|
||||||
dialog.thought_nodes = ThoughtNodeTypeAdapter.dump_python(
|
record.thought_nodes = ThoughtNodeTypeAdapter.dump_python(
|
||||||
{k: v for k, v in thought_nodes.items()}
|
{k: v for k, v in thought_nodes.items()}
|
||||||
)
|
)
|
||||||
dialog.answer = answer
|
record.answer = answer
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
async def get_message_history(self, conversation_id: str) -> List[ModelMessage]:
|
async def create_run_result_record(
|
||||||
"""
|
|
||||||
获取消息历史
|
|
||||||
:param conversation_id: 会话唯一标识
|
|
||||||
:return: 消息历史
|
|
||||||
"""
|
|
||||||
message_history: List[ModelMessage] = []
|
|
||||||
async with rx.asession() as session:
|
|
||||||
result = await session.exec(
|
|
||||||
select(RunResults)
|
|
||||||
.where(RunResults.conversation_id == conversation_id)
|
|
||||||
.order_by(desc(RunResults.dialog_id))
|
|
||||||
)
|
|
||||||
for record in result.all():
|
|
||||||
message_history.extend(
|
|
||||||
ModelMessagesTypeAdapter.validate_json(record.new_messages)
|
|
||||||
)
|
|
||||||
return message_history
|
|
||||||
|
|
||||||
async def save_new_messages(
|
|
||||||
self,
|
self,
|
||||||
conversation_id: str,
|
conversation_id: str,
|
||||||
dialog_id: str,
|
dialog_id: str,
|
||||||
new_messages: List[ModelMessage],
|
new_messages: List[ModelMessage],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
保存新增消息
|
创建运行结果记录
|
||||||
:param conversation_id: 会话唯一标识
|
:param conversation_id: 会话唯一标识
|
||||||
:param dialog_id: 对话唯一标识
|
:param dialog_id: 对话唯一标识
|
||||||
:param new_messages: 新增消息
|
:param new_messages: 新增消息
|
||||||
|
|
@ -187,3 +244,24 @@ class DatabaseState(rx.State):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
async def retrieve_message_history(
|
||||||
|
self, conversation_id: str
|
||||||
|
) -> List[ModelMessage]:
|
||||||
|
"""
|
||||||
|
获取消息历史
|
||||||
|
:param conversation_id: 会话唯一标识
|
||||||
|
:return: 消息历史
|
||||||
|
"""
|
||||||
|
records: List[ModelMessage] = []
|
||||||
|
async with rx.asession() as session:
|
||||||
|
result = await session.exec(
|
||||||
|
select(RunResults)
|
||||||
|
.where(RunResults.conversation_id == conversation_id)
|
||||||
|
.order_by(desc(RunResults.dialog_id))
|
||||||
|
)
|
||||||
|
for record in result.all():
|
||||||
|
records.extend(
|
||||||
|
ModelMessagesTypeAdapter.validate_json(record.new_messages)
|
||||||
|
)
|
||||||
|
return records
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -11,6 +11,7 @@
|
||||||
"isbot": "5.1.40",
|
"isbot": "5.1.40",
|
||||||
"lucide-react": "1.14.0",
|
"lucide-react": "1.14.0",
|
||||||
"react": "19.2.6",
|
"react": "19.2.6",
|
||||||
|
"react-debounce-input": "3.3.0",
|
||||||
"react-dom": "19.2.6",
|
"react-dom": "19.2.6",
|
||||||
"react-error-boundary": "6.1.1",
|
"react-error-boundary": "6.1.1",
|
||||||
"react-helmet": "6.1.0",
|
"react-helmet": "6.1.0",
|
||||||
|
|
@ -660,6 +661,8 @@
|
||||||
|
|
||||||
"lodash": ["lodash@4.18.1", "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
|
"lodash": ["lodash@4.18.1", "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
|
||||||
|
|
||||||
|
"lodash.debounce": ["lodash.debounce@4.0.8", "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
|
||||||
|
|
||||||
"longest-streak": ["longest-streak@3.1.0", "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
"longest-streak": ["longest-streak@3.1.0", "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||||
|
|
||||||
"loose-envify": ["loose-envify@1.4.0", "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
"loose-envify": ["loose-envify@1.4.0", "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
|
|
@ -814,6 +817,8 @@
|
||||||
|
|
||||||
"react": ["react@19.2.6", "https://registry.npmmirror.com/react/-/react-19.2.6.tgz", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
|
"react": ["react@19.2.6", "https://registry.npmmirror.com/react/-/react-19.2.6.tgz", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
|
||||||
|
|
||||||
|
"react-debounce-input": ["react-debounce-input@3.3.0", "https://registry.npmmirror.com/react-debounce-input/-/react-debounce-input-3.3.0.tgz", { "dependencies": { "lodash.debounce": "^4", "prop-types": "^15.8.1" }, "peerDependencies": { "react": "^15.3.0 || 16 || 17 || 18" } }, "sha512-VEqkvs8JvY/IIZvh71Z0TC+mdbxERvYF33RcebnodlsUZ8RSgyKe2VWaHXv4+/8aoOgXLxWrdsYs2hDhcwbUgA=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.2.6", "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.6.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
|
"react-dom": ["react-dom@19.2.6", "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.6.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
|
||||||
|
|
||||||
"react-error-boundary": ["react-error-boundary@6.1.1", "https://registry.npmmirror.com/react-error-boundary/-/react-error-boundary-6.1.1.tgz", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w=="],
|
"react-error-boundary": ["react-error-boundary@6.1.1", "https://registry.npmmirror.com/react-error-boundary/-/react-error-boundary-6.1.1.tgz", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w=="],
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
"isbot": "5.1.40",
|
"isbot": "5.1.40",
|
||||||
"lucide-react": "1.14.0",
|
"lucide-react": "1.14.0",
|
||||||
"react": "19.2.6",
|
"react": "19.2.6",
|
||||||
|
"react-debounce-input": "3.3.0",
|
||||||
"react-dom": "19.2.6",
|
"react-dom": "19.2.6",
|
||||||
"react-error-boundary": "6.1.1",
|
"react-error-boundary": "6.1.1",
|
||||||
"react-helmet": "6.1.0",
|
"react-helmet": "6.1.0",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue