From 3f52e7bd61d5731aa7eaa20ce729d5e2e6a128b5 Mon Sep 17 00:00:00 2001 From: liubiren Date: Mon, 27 Jul 2026 08:41:54 +0800 Subject: [PATCH] 1 --- agent/alembic.ini | 149 ++++++++++++++++++++++++ agent/alembic/README | 1 + agent/alembic/env.py | 78 +++++++++++++ agent/alembic/script.py.mako | 28 +++++ agent/alembic/versions/a7968da75b72_.py | 98 ++++++++++++++++ agent/application/application.py | 2 +- agent/application/domain_models.py | 4 +- agent/application/pages/index.py | 53 ++++----- agent/application/states/auth.py | 2 +- agent/database.db | Bin 118784 -> 77824 bytes 10 files changed, 385 insertions(+), 30 deletions(-) create mode 100644 agent/alembic.ini create mode 100644 agent/alembic/README create mode 100644 agent/alembic/env.py create mode 100644 agent/alembic/script.py.mako create mode 100644 agent/alembic/versions/a7968da75b72_.py diff --git a/agent/alembic.ini b/agent/alembic.ini new file mode 100644 index 0000000..807ded2 --- /dev/null +++ b/agent/alembic.ini @@ -0,0 +1,149 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts. +# this is typically a path given in POSIX (e.g. forward slashes) +# format, relative to the token %(here)s which refers to the location of this +# ini file +script_location = %(here)s/alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s +# Or organize into date-based subdirectories (requires recursive_version_locations = true) +# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. for multiple paths, the path separator +# is defined by "path_separator" below. +prepend_sys_path = . + + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the tzdata library which can be installed by adding +# `alembic[tz]` to the pip requirements. +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to /versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "path_separator" +# below. +# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions + +# path_separator; This indicates what character is used to split lists of file +# paths, including version_locations and prepend_sys_path within configparser +# files such as alembic.ini. +# The default rendered in new alembic.ini files is "os", which uses os.pathsep +# to provide os-dependent path splitting. +# +# Note that in order to support legacy alembic.ini files, this default does NOT +# take place if path_separator is not present in alembic.ini. If this +# option is omitted entirely, fallback logic is as follows: +# +# 1. Parsing of the version_locations option falls back to using the legacy +# "version_path_separator" key, which if absent then falls back to the legacy +# behavior of splitting on spaces and/or commas. +# 2. Parsing of the prepend_sys_path option falls back to the legacy +# behavior of splitting on spaces, commas, or colons. +# +# Valid values for path_separator are: +# +# path_separator = : +# path_separator = ; +# path_separator = space +# path_separator = newline +# +# Use os.pathsep. Default configuration used for new projects. +path_separator = os + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# database URL. This is consumed by the user-maintained env.py script only. +# other means of configuring database URLs may be customized within the env.py +# file. +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module +# hooks = ruff +# ruff.type = module +# ruff.module = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Alternatively, use the exec runner to execute a binary found on your PATH +# hooks = ruff +# ruff.type = exec +# ruff.executable = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Logging configuration. This is also consumed by the user-maintained +# env.py script only. +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/agent/alembic/README b/agent/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/agent/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/agent/alembic/env.py b/agent/alembic/env.py new file mode 100644 index 0000000..36112a3 --- /dev/null +++ b/agent/alembic/env.py @@ -0,0 +1,78 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = None + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/agent/alembic/script.py.mako b/agent/alembic/script.py.mako new file mode 100644 index 0000000..1101630 --- /dev/null +++ b/agent/alembic/script.py.mako @@ -0,0 +1,28 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + """Upgrade schema.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Downgrade schema.""" + ${downgrades if downgrades else "pass"} diff --git a/agent/alembic/versions/a7968da75b72_.py b/agent/alembic/versions/a7968da75b72_.py new file mode 100644 index 0000000..d079021 --- /dev/null +++ b/agent/alembic/versions/a7968da75b72_.py @@ -0,0 +1,98 @@ +"""empty message + +Revision ID: a7968da75b72 +Revises: +Create Date: 2026-07-26 23:23:19.558164 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + +# revision identifiers, used by Alembic. +revision: str = 'a7968da75b72' +down_revision: Union[str, Sequence[str], None] = None +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('captcharecord', + 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_verified', sa.Boolean(), nullable=False), + sa.Column('expired_at', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('email', 'captcha', 'expired_at') + ) + with op.batch_alter_table('captcharecord', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_captcharecord_is_valid'), ['is_valid'], unique=False) + batch_op.create_index(batch_op.f('ix_captcharecord_is_verified'), ['is_verified'], unique=False) + + op.create_table('conversationrecord', + sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('user_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('is_deleted', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('conversationrecord', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_conversationrecord_is_deleted'), ['is_deleted'], unique=False) + batch_op.create_index(batch_op.f('ix_conversationrecord_user_id'), ['user_id'], unique=False) + + op.create_table('dialogrecord', + sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('conversation_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('user_prompt', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('thoughts', sa.JSON(), nullable=False), + sa.Column('result_output', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('dialogrecord', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_dialogrecord_conversation_id'), ['conversation_id'], unique=False) + + op.create_table('resultrecord', + sa.Column('conversation_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('dialog_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('new_messages', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.PrimaryKeyConstraint('conversation_id', 'dialog_id') + ) + op.create_table('userrecord', + 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('userrecord', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_userrecord_email'), ['email'], unique=False) + + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('userrecord', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_userrecord_email')) + + op.drop_table('userrecord') + op.drop_table('resultrecord') + with op.batch_alter_table('dialogrecord', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_dialogrecord_conversation_id')) + + op.drop_table('dialogrecord') + with op.batch_alter_table('conversationrecord', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_conversationrecord_user_id')) + batch_op.drop_index(batch_op.f('ix_conversationrecord_is_deleted')) + + op.drop_table('conversationrecord') + with op.batch_alter_table('captcharecord', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_captcharecord_is_verified')) + batch_op.drop_index(batch_op.f('ix_captcharecord_is_valid')) + + op.drop_table('captcharecord') + # ### end Alembic commands ### diff --git a/agent/application/application.py b/agent/application/application.py index eba6182..af2e2fa 100644 --- a/agent/application/application.py +++ b/agent/application/application.py @@ -6,7 +6,7 @@ import reflex as rx from application.pages.index import index app = rx.App( - theme=rx.theme(), + theme=rx.theme(color_mode="light"), stylesheets=["/styles.css"], style={ "box-shadow": "none", diff --git a/agent/application/domain_models.py b/agent/application/domain_models.py index 2864271..2ceae48 100644 --- a/agent/application/domain_models.py +++ b/agent/application/domain_models.py @@ -25,7 +25,7 @@ class Dialog(BaseModel): id: str = Field(default_factory=lambda: str(uuid7()), description="对话唯一标识") user_prompt: str = Field(..., description="用户提示词") - thoughts: List[Thought] = Field(default_factory=List, description="思考列表") + thoughts: List[Thought] = Field(default_factory=list, description="思考列表") result_output: str = Field(default="", description="结果输出") is_thinking: bool = Field( default=False, description="正在思考,True 表示正在思考,False 表示未正在思考" @@ -45,7 +45,7 @@ class Conversation(BaseModel): is_running: bool = Field( default=False, description="正在运行,True 表示正在运行,False 表示未正在运行" ) - dialogs: Dict[str, Dialog] = Field(default_factory=Dict, description="对话列表") + dialogs: Dict[str, Dialog] = Field(default_factory=dict, description="对话列表") created_at: datetime = Field(default_factory=datetime.now, description="创建时间") diff --git a/agent/application/pages/index.py b/agent/application/pages/index.py index c9dde18..24c590a 100644 --- a/agent/application/pages/index.py +++ b/agent/application/pages/index.py @@ -210,76 +210,78 @@ def sidebar() -> rx.Component: ) -def login_window() -> rx.Component: +def login_popup() -> rx.Component: """ - 登录窗 + 登录弹窗 :return: Component """ return rx.center( rx.hstack( rx.vstack( + # 标题 rx.text( "登录解锁更多功能", + margin_bottom="24px", line_height="32px", font_size="var(--prismui-font-size-3)", font_weight="var(--prismui-font-weight-3)", - margin_bottom="24px", ), + # 邮箱输入框 rx.input( - placeholder="请输入邮箱地址", + placeholder="请输入邮箱", + # 值绑定邮箱 value=AuthState.email, - width="100%", - height="45px", - margin_bottom="24px", - padding="4px", - border_radius="var(--prismui-border-radius-4)", - line_height="16px", # 值变化事件:设置邮箱 on_change=AuthState.set_email, # 获取焦点事件:重置登录错误信息 - on_focus=AuthState.reset_error_message, + on_focus=AuthState.reset_login_error_message, + width="100%", + height="45px", + padding="4px", + margin_bottom="24px", + border_radius="var(--prismui-border-radius-4)", + line_height="16px", ), - # 自定义验证码输入框 + # 验证码输入框 rx.hstack( rx.input( placeholder="请输入验证码", - max_length=6, + # 值绑定验证码 value=AuthState.captcha, + max_length=6, # 值变化事件:设置验证码 on_change=AuthState.set_captcha, # 获取焦点事件:重置登录错误信息 - on_focus=AuthState.reset_error_message, + on_focus=AuthState.reset_login_error_message, flex=1, height="100%", padding="4px", + border_radius="var(--prismui-border-radius-4)", line_height="16px", - border="none", - style={ - "&": { - "border": "none !important", - }, # 清除父元素边框 - }, + border="none !important", ), rx.button( - # 若验证码发送倒计时大于0则显示倒计时,否则显示获取验证码 + # 若再次发送验证码倒计时大于0则显示倒计时,否则显示获取验证码 rx.cond( AuthState.resend_captcha_countdown > 0, f"{AuthState.resend_captcha_countdown} s", "获取验证码", ), + # 点击事件:发送验证码 + on_click=AuthState.send_captcha, + # 是否禁用绑定验证码发送禁用状态 + disabled=AuthState.is_captcha_sending_disabled, margin="0 8px", background_color="var(--prismui-background-color-1)", line_height="16px", + # 若验证码发送禁用状态为是则颜色设置为 color-4,否则设置为 color-5(主题色) color=rx.cond( AuthState.is_captcha_sending_disabled, "var(--prismui-color-4)", "var(--prismui-color-5)", ), cursor="pointer", - # 点击事件:发送验证码 - on_click=AuthState.send_captcha, - disabled=AuthState.is_captcha_sending_disabled, ), width="100%", height="45px", @@ -379,7 +381,6 @@ def login_window() -> rx.Component: height="100%", padding="40px 48px 0 48px", align_items="center", - background_color="var(--prismui-background-color-1)", ), rx.vstack( rx.image( @@ -447,6 +448,6 @@ def index() -> rx.Component: rx.cond( AuthState.user_id, rx.fragment(), - login_window(), + login_popup(), ), ) diff --git a/agent/application/states/auth.py b/agent/application/states/auth.py index 6373624..27b4b46 100644 --- a/agent/application/states/auth.py +++ b/agent/application/states/auth.py @@ -51,7 +51,7 @@ class AuthState(rx.State): activated_nav_button: str = "conversation" @rx.event - def reset_error_message(self) -> None: + def reset_login_error_message(self) -> None: """ 重置登录错误信息 :return: None diff --git a/agent/database.db b/agent/database.db index 022cca33c1414f15bfc67f7bc518aae5c4f85c46..b062bcd75a1696a1d1911440b7f57accbff29a55 100644 GIT binary patch literal 77824 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