This commit is contained in:
liubiren 2026-10-20 20:52:18 +08:00
parent 0b4b02a43d
commit d9153b13bb
11 changed files with 169 additions and 129 deletions

View File

@ -23,7 +23,7 @@ app = rx.App(
}
})
"""
)
) # 点击链接在新标签页打开
],
stylesheets=["/styles.css"],
style={

View File

@ -61,8 +61,8 @@ def login_page() -> rx.Component:
rx.button(
rx.text(
rx.cond(
AuthState.resend_captcha_code_countdown > 0,
f"{AuthState.resend_captcha_code_countdown} s",
AuthState.countdown > 0,
f"{AuthState.countdown} s",
"获取验证码",
),
line_height="16px",

View File

@ -587,8 +587,8 @@ def message_history() -> rx.Component:
height="28px",
width="100%",
cursor="pointer",
# 点击事件:显示当前任务删除确认
on_select=TaskState.toggle_task_delete_confirm(
# 点击事件:显示任务删除确认
on_click=TaskState.toggle_task_delete_confirm_open(
True
).stop_propagation,
style={
@ -652,9 +652,9 @@ def message_history() -> rx.Component:
width="240px",
padding="16px",
),
# 显示绑定:显示当前任务删除确认
open=TaskState.is_task_delete_confirm_shown,
on_open_change=TaskState.toggle_task_delete_confirm,
# 显示绑定:显示任务删除确认
open=TaskState.is_task_delete_confirm_open,
on_open_change=TaskState.toggle_task_delete_confirm_open,
),
align_items="center",
gap="4px",
@ -856,6 +856,4 @@ def task_page() -> rx.Component:
# 渲染...
width="100%",
height="100%",
# 挂载事件:恢复任务状态
on_mount=AuthState.resume_task_state,
)

View File

@ -159,13 +159,64 @@ def sidebar() -> rx.Component:
rx.alert_dialog.root(
rx.alert_dialog.trigger(rx.box(height="0", width="100%")),
rx.alert_dialog.content(
rx.alert_dialog.title("确认退出登录?"),
rx.alert_dialog.description(
"退出登录不会丢失任何数据,你仍可以登录此账号。"
rx.vstack(
rx.text(
"确认退出登录?",
line_height="24px",
font_size="var(--prismui-font-size-3)",
font_weight="var(--prismui-font-weight-3)",
color="var(--prismui-color-1)",
margin_bottom="16px",
),
rx.text(
"退出登录不会丢失任何数据,你仍可以登录此账号。",
line_height="20px",
color="var(--prismui-color-2)",
margin_bottom="24px",
),
rx.hstack(
rx.spacer(),
rx.hstack(
rx.alert_dialog.cancel(
rx.button(
"取消",
style={
"_hover": {
"background": "var(--prismui-background-color-9)"
}
},
)
),
rx.alert_dialog.action(
rx.button(
"退出登录",
color="var(--prismui-color-9)",
width="80px",
background_color="var(--prismui-background-color-6)",
style={
"_hover": {
"background": "var(--prismui-background-color-7)"
}
},
),
# 点击事件:退出登录
on_click=AuthState.logout,
),
),
justify_content="space-between",
width="100%",
),
gap="0",
width="100%",
),
rx.alert_dialog.cancel("取消"),
rx.alert_dialog.action("退出登录"),
width="480px",
padding="24px",
border_radius="var(--prismui-border-radius-7)",
),
# 打开绑定退出登录确认打开状态
open=SidebarState.is_logout_confirm_open,
# 当打开状态变动时绑定切换退出登录确认打开状态
on_open_change=SidebarState.toggle_logout_confirm_open,
),
# 渲染系统图标和名称
rx.hstack(
@ -221,6 +272,7 @@ def sidebar() -> rx.Component:
width="100%", background_color="var(--prismui-background-color-3)"
),
rx.hstack(
# 渲染用户中心
rx.popover.root(
rx.popover.trigger(
rx.hstack(
@ -274,6 +326,7 @@ def sidebar() -> rx.Component:
border_radius="var(--prismui-border-radius-3)",
),
rx.vstack(
# 渲染退出登录
rx.popover.close(
rx.hstack(
rx.icon(
@ -290,12 +343,17 @@ def sidebar() -> rx.Component:
padding="5px 8px",
border_radius="var(--prismui-border-radius-3)",
cursor="pointer",
# 点击事件:切换退出登录确认显示
on_click=SidebarState.toggle_logout_confirm_open(
True
).stop_propagation,
style={
"_hover": {
"background": "var(--prismui-background-color-2)",
"background": "var(--prismui-background-color-9)",
},
},
),
width="100%",
),
width="100%",
padding="8px",
@ -308,6 +366,7 @@ def sidebar() -> rx.Component:
background_color="var(--prismui-background-color-9)",
),
),
# 渲染消息中心
rx.tooltip(
rx.box(
rx.icon(
@ -376,4 +435,6 @@ def workbench() -> rx.Component:
width="100%",
height="100vh",
),
# 挂载事件:恢复认证状态
on_mount=AuthState.resume,
)

View File

@ -6,14 +6,14 @@ from asyncio import sleep
from email.header import Header
from email.mime.text import MIMEText
import re
from typing import AsyncGenerator, Optional
from typing import AsyncGenerator
from aiosmtplib import SMTP, SMTPException
import reflex as rx
from reflex.event import EventCallback
from application.states.task import TaskState
from application.states.database import DatabaseState
from application.states.task import TaskState
# 验证码HTML模板
CAPTCHA_HTML_TEMPLATE = """
@ -35,8 +35,8 @@ class AuthState(rx.State):
captcha_code: str = ""
# 验证码已发送
is_captcha_code_sent: bool = False
# 再次发送验证码倒计时
resend_captcha_code_countdown: int = 0
# 倒计时
countdown: int = 0
# 协议同意状态
is_policies_agreed: bool = True
# 错误信息
@ -44,22 +44,6 @@ class AuthState(rx.State):
# 登录中状态
is_logging_in: bool = False
# 设置悬停卡片打开状态
is_settings_hover_card_open: bool = False
# 数据库状态
_db_state: Optional[DatabaseState] = None
async def get_db_state(self) -> DatabaseState:
"""
获取当前数据库状态
:return: 当前数据库状态
"""
# 若数据库状态为空则获取数据库状态,否则直接返回当前数据库状态
if not self._db_state:
self._db_state = await self.get_state(DatabaseState)
return self._db_state
@rx.event
def reset_error(self) -> None:
"""
@ -112,34 +96,34 @@ class AuthState(rx.State):
邮箱为空或再次发送验证码倒计时大于0时禁用
:return: bool
"""
return not self.email or self.resend_captcha_code_countdown > 0
return not self.email or self.countdown > 0
@rx.event(background=True)
async def countdown_background(self) -> None:
async def background_countdown(self) -> None:
"""
倒计时(后台任务)
后台运行倒计时
:return: None
"""
async with self:
self.resend_captcha_code_countdown = 59
self.countdown = 59
while self.resend_captcha_code_countdown > 0:
while self.countdown > 0:
await sleep(1)
async with self:
self.resend_captcha_code_countdown -= 1
self.countdown -= 1
@rx.event(background=True)
async def send_captcha_code_background(self) -> None:
async def background_send_captcha_code(self) -> None:
"""
发送验证码(后台任务)
后台运行发送验证码
:return: None
"""
async with self:
# 获取当前数据库状态
db_state = await self.get_db_state()
# 获取数据库状态
database_state = await self.get_state(DatabaseState)
# 创建验证记录
captcha_code = await db_state.create_captcha_record(email=self.email)
captcha_code = await database_state.create_captcha_record(email=self.email)
# 构建验证码邮件
message = MIMEText(
@ -174,15 +158,16 @@ class AuthState(rx.State):
if not self.validate_email():
return
if self.resend_captcha_code_countdown > 0:
# 若倒计时未结束则不允许再次发送验证码
if self.countdown > 0:
return
# 标记验证码已发送
self.is_captcha_code_sent = True
# 同时触发发送验证码和倒计时后台事件
yield type(self).send_captcha_code_background()
yield type(self).countdown_background()
# 并行处理:后台运行倒计时与后台运行发送验证码
yield type(self).background_countdown()
yield type(self).background_send_captcha_code()
@rx.event
def set_captcha_code(self, captcha_code: str) -> None:
@ -202,14 +187,14 @@ class AuthState(rx.State):
self.is_policies_agreed = not self.is_policies_agreed
@rx.event
async def resume_task_state(self):
async def resume(self):
"""
恢复任务状态
恢复认证状态
"""
# 获取任务状态
task_state = await self.get_state(TaskState)
# 恢复任务状态
await task_state.resume_task_state(user_id=self.user_id)
await task_state.resume(user_id=self.user_id)
@rx.event
async def login(self) -> None:
@ -238,11 +223,11 @@ class AuthState(rx.State):
self.is_logging_in = True
# 获取当前数据库状态
db_state = await self.get_db_state()
# 获取数据库状态
database_state = await self.get_state(DatabaseState)
# 核验验证码
if not await db_state.verify_captcha_code(
if not await database_state.verify_captcha_code(
email=self.email, captcha_code=self.captcha_code
):
self.is_logging_in = False
@ -250,41 +235,32 @@ class AuthState(rx.State):
return
# 创建用户记录
user_id = await db_state.create_user_record(email=self.email)
user_id = await database_state.create_user_record(email=self.email)
# 获取任务状态
task_state = await self.get_state(TaskState)
# 恢复当前用户的任务状态
await task_state.resume_task_state(user_id=user_id)
# 恢复任务状态
await task_state.resume(user_id=user_id)
self.user_id = user_id
self.email = ""
self.captcha_code = ""
self.is_captcha_code_sent = False
self.resend_captcha_code_countdown = 0
self.countdown = 0
self.is_policies_agreed = True
self.error = ""
self.is_logging_in = False
@rx.event
def set_settings_hover_card_open(self, is_settings_hover_card_open: bool) -> None:
"""
设置设置悬停卡片打开
:param is_settings_hover_card_open: 设置悬停卡片打开状态,True 表示打开,False 表示关闭
:return: None
"""
self.is_settings_hover_card_open = is_settings_hover_card_open
@rx.event
async def logout(self) -> None:
"""
退出登录
:return: None
"""
self.is_settings_hover_card_open = False # 设置设置悬停卡片关闭
task_state = await self.get_state(TaskState)
task_state.user_id = ""
task_state.tasks = {}
task_state.task_id = ""
self.user_id = ""
# 获取任务状态
task_state = await self.get_state(TaskState)
# 重置任务状态
task_state.reset()

View File

@ -167,7 +167,7 @@ class DatabaseState(rx.State):
"""
核验验证码
:param email: 邮箱
:param code: 验证码
:param captcha_code: 验证码
:return: 核验是否成功,True 表示核验成功,False 表示核验失败
"""
async with rx.asession() as session:

View File

@ -15,6 +15,9 @@ class SidebarState(rx.State):
# 选中的导航按钮,默认为创建任务
nav_button: str = "create_task"
# 退出登录确认打开状态
is_logout_confirm_open: bool = False
@rx.event
async def set_nav_button(self, nav_button: str):
"""
@ -32,7 +35,7 @@ class SidebarState(rx.State):
@rx.event
async def set_task_id(self, task_id: str):
"""
将指定任务唯一标识设置为当前选中任务
将指定任务的唯一标识设置为当前选中任务的唯一标识
"""
self.nav_button = ""
@ -40,3 +43,12 @@ class SidebarState(rx.State):
task_state = await self.get_state(TaskState)
# 将指定任务唯一标识设置为当前任务唯一标识
task_state.set_task_id(task_id)
@rx.event
def toggle_logout_confirm_open(self, is_open: bool) -> None:
"""
切换退出登录确认打开状态
:param is_open: 退出登录确认打开状态
:return: None
"""
self.is_logout_confirm_open = is_open

View File

@ -51,26 +51,12 @@ class TaskState(rx.State):
# 临时用户提示词
temporary_user_prompt: str = ""
# 当前任务删除确认显示
is_task_delete_confirm_shown: bool = False
# 上一次任务删除确认显示日期时间
last_task_delete_confirm_show_at: float = 0
# 任务删除确认打开状态
is_task_delete_confirm_open: bool = False
# 任务删除确认打开日期时间
task_delete_confirm_open_at: float = 0
# 当前数据库状态(私有变量)
# 私有变量:reflex 约定以 _ 开头的变量为私有变量,后端不序列化,前端不可使用
_db_state: DatabaseState | None = None
async def get_db_state(self) -> DatabaseState:
"""
获取当前数据库状态
:return: 当前数据库状态
"""
# 若数据库状态为 None 则获取数据库状态,否则直接返回当前数据库状态
if not self._db_state:
self._db_state = await self.get_state(DatabaseState)
return self._db_state
async def resume_task_state(self, user_id: str) -> None:
async def resume(self, user_id: str) -> None:
"""
恢复任务状态
:param user_id: 用户唯一标识
@ -80,12 +66,11 @@ class TaskState(rx.State):
if not self.user_id:
return
# 获取当前数据库状态
db_state = await self.get_db_state()
# 获取数据库状态
database_state = await self.get_state(DatabaseState)
# 查询当前用户的任务字典
self.tasks = await db_state.read_tasks(self.user_id)
self.tasks = await database_state.read_tasks(self.user_id)
# 遍历所有任务若最后一条消息未审批则将该任务和该消息正在运行设置为是
for task in self.tasks.values():
# 最后一个消息
message = next(iter(reversed(task.messages.values())), None)
@ -157,20 +142,20 @@ class TaskState(rx.State):
return f"更新于 {task.last_run_at.strftime("%Y-%m-%d %H:%M:%S")}"
@rx.event
async def toggle_task_delete_confirm(self, is_shown: bool) -> None:
async def toggle_task_delete_confirm_open(self, is_open: bool) -> None:
"""
切换当前任务删除确认
:param is_shown: 显示当前任务删除确认
切换任务删除确认打开状态
:param is_open: 任务删除确认打开状态
:return: None
"""
# 当前日期时间
now = datetime.now().timestamp()
# 根据当前时间与上一次任务删除确认显示日期时间间隔防抖
if now - self.last_task_delete_confirm_show_at < 0.2:
current = datetime.now().timestamp()
# 根据当前时间与任务删除确认打开日期时间间隔防抖
if current - self.task_delete_confirm_open_at < 0.2:
return
self.is_task_delete_confirm_shown = is_shown
self.last_task_delete_confirm_show_at = now
self.is_task_delete_confirm_open = is_open
self.task_delete_confirm_open_at = current
@rx.event
async def delete_task(self) -> EventSpec | None:
@ -185,20 +170,20 @@ class TaskState(rx.State):
if not task:
return
# 获取当前数据库状态
db_state = await self.get_db_state()
# 获取数据库状态
database_state = await self.get_state(DatabaseState)
# 更新任务记录
await db_state.update_task_record(task.id, is_deleted=True)
await database_state.update_task_record(task.id, is_deleted=True)
del self.tasks[task.id]
# 获取当前侧边栏状态
# 获取侧边栏状态
sidebar_state = await self.get_state(SidebarState)
# 将创建任务设置为导航按钮
sidebar_state.nav_button = ""
# 将空设置为当前任务唯一标识
self.task_id = ""
# 将任务删除确认显示设置为否
self.is_task_delete_confirm_shown = False
self.is_task_delete_confirm_open = False
return rx.toast.success("删除成功")
@rx.event
@ -289,19 +274,18 @@ class TaskState(rx.State):
"""
from application.states import SidebarState
# 获取当前数据库状态
db_state = await self.get_db_state()
# 若当前任务唯一标识为空则创建任务
# 获取数据库状态
database_state = await self.get_state(DatabaseState)
# 若当前任务的唯一标识为空则创建任务
if not self.task_id:
# 获取当前侧边栏状态
# 获取侧边栏状态
sidebar_state = await self.get_state(SidebarState)
# 将空设置为导航按钮
sidebar_state.nav_button = ""
# 创建任务记录再添加任务
self.tasks.update(
await db_state.create_task_record(
await database_state.create_task_record(
self.user_id, self.temporary_user_prompt
)
)
@ -354,7 +338,7 @@ class TaskState(rx.State):
task.awaiting_stream = True
# 先创建消息记录再添加消息
task.messages.update(
await db_state.create_message_record(
await database_state.create_message_record(
task_id=task.id,
message=Message(
type=MessageType.USER_PROMPT,
@ -368,7 +352,9 @@ class TaskState(rx.State):
index_map_to_message_id: dict[int, str] = {}
# 查询消息历史列表
message_history = await db_state.read_message_history(task_id=self.task_id)
message_history = await database_state.read_message_history(
task_id=self.task_id
)
# 使用量
usage = task.usage
@ -644,7 +630,7 @@ class TaskState(rx.State):
# 更新使用量
task.usage = result.usage
# 更新任务记录
await db_state.update_task_record(
await database_state.update_task_record(
id=task.id,
last_run_at=task.last_run_at,
usage=task.usage,
@ -652,13 +638,13 @@ class TaskState(rx.State):
)
# 创建运行记录
await db_state.create_run_record(
await database_state.create_run_record(
task_id=task.id,
new_messages=result.new_messages(),
)
# 批量创建消息记录
await db_state.create_message_records(
await database_state.create_message_records(
task_id=task.id,
messages=messages,
)
@ -780,9 +766,9 @@ class TaskState(rx.State):
# 设置工作流
task.workflow = workflow
# 获取数据库状态
db_state = await self.get_db_state()
database_state = await self.get_state(DatabaseState)
# 更新任务记录
await db_state.update_task_record(
await database_state.update_task_record(
id=task.id,
workflow=task.workflow,
)

View File

@ -178,7 +178,7 @@ instruction = """
无需联网:基础常识、数学计算、代码编写与解释、用户已提供完整材料的分析任务、无时间依赖的静态问题。
【检索硬约束】
1. 最多**2轮联网检索**,严禁发起第3轮。一轮定义:单次思考决策发起1条检索Query;**单次思考步骤,最多只能生成1条检索query,禁止同一思考周期并行发起多条联网检索调用**。必须串行执行:执行1次检索,拿到结果评估之后,才允许在后续思考中决定是否发起下一轮检索。不允许一步同时调用多次web_search。
1. 最多**2轮联网检索**,严禁发起第3轮。一轮定义:单次思考决策发起1条检索Query;**单次思考步骤,最多只能生成1条检索query,禁止同一思考周期并行发起多条联网检索调用**。必须串行执行:执行1次检索,拿到结果评估之后,才允许在后续思考中决定是否发起下一轮检索。不允许一步同时调用多次web_retrieve。
2. 发起新一轮检索前,必须评估已获取的检索摘要,现有信息足以回答问题则立即停止调用工具。
3. 禁止仅改写同义词、做语义几乎等价的重复检索;若存在局部信息缺失,优先从已有结果中提取可用内容,不要直接发起新检索。
4. 默认只执行1次真实联网检索;拿到检索结果后评估证据,满足以下任意条件,必须停止检索:
@ -187,7 +187,7 @@ instruction = """
✅仅在以下4种场景,才允许执行第2轮检索:
①第一轮无有效检索结果;②回答问题必需的关键事实完全缺失;③权威信源之间事实存在明显冲突;④第一轮检索结果严重偏题。
第二轮检索必须瞄准明确的信息缺口,禁止全盘复刻上一轮查询。
> 重要:第二轮检索执行完成后,无论信息是否补齐,都必须终止检索,不再发起任何web_search调用。
> 重要:第二轮检索执行完成后,无论信息是否补齐,都必须终止检索,不再发起任何web_retrieve调用。
## 二、时间与Query规则
时效判断严格以运行时传入的当前日期、年份、时区为准,禁止依靠模型内置记忆猜测时间。
@ -217,7 +217,7 @@ instruction = """
## 六、引用规范
引用编号区分检索轮次:S1‑1、S1‑2 代表第1轮检索结果;S2‑1、S2‑2 代表第2轮检索结果。
只引用回答中实际用到的资料。
引用格式:`[参考 S1‑1] https://xxx`,链接必须来源于 **web_search** 工具返回结果,禁止编造链接。
引用格式:`[参考 S1‑1] https://xxx`,链接必须来源于 **web_retrieve** 工具返回结果,禁止编造链接。
## 七、输出规则
简单事实类问题:优先给出结论,附带关键证据与来源,回答简洁,避免冗余。

View File

@ -131,6 +131,12 @@ pre, pre code {
color: var(--prismui-color-7) !important;
}
.rt-AlertDialogContent {
font-family: var(--prismui-font-family) !important;
font-size: var(--prismui-font-size-2) !important;
}
/* DropdownMenuItem */
.rt-DropdownMenuItem .rt-Text {
font-family: var(--prismui-font-family);
@ -150,6 +156,7 @@ pre, pre code {
font-family: var(--prismui-font-family);
font-size: var(--prismui-font-size-2);
color: var(--prismui-color-1);
white-space: nowrap;
height: 32px;
width: 68px;
background-color: var(--prismui-background-color-1);

Binary file not shown.