Compare commits

...

14 Commits

Author SHA1 Message Date
miao
80902eb79a refactor(memory): extract memory count sync utility
- Add shared utility for syncing end user memory_count from Neo4j
2026-04-29 18:35:49 +08:00
miao
f86c023477 fix(memory): call renamed memory count sync method
- Update forgetting cycle call sites to use _sync_memory_count_to_db
2026-04-29 18:06:48 +08:00
xrzs
1d73c9e5a8 chore(migration): remove memory count revision 2026-04-29 17:46:48 +08:00
miao
89bdb9f4b5 fix(memory): allow end user id keyword search
- Match keyword against end_user_id even when other_name exists
- Keep Neo4j and RAG end user list search behavior consistent
2026-04-29 16:38:11 +08:00
miao
c57490a063 fix(migration): move memory count revision to latest head 2026-04-29 16:35:46 +08:00
miao
a7d3930f4d feat(memory): add end user memory count filtering
- Sync memory_count after Neo4j write and forgetting cycle
- Filter Neo4j end user list by memory_count > 0
- Filter RAG end user list by Memory knowledge chunk count
2026-04-29 15:02:09 +08:00
miao
d30b9224ab [add] migration script 2026-04-29 15:02:09 +08:00
yingzhao
8f6aad333f Merge pull request #1021 from SuanmoSuanyangTechnology/feature/login_ui_zy
Feature/login UI zy
2026-04-28 16:11:21 +08:00
zhaoying
72c71c1000 feat(web): login video 2026-04-28 15:57:32 +08:00
zhaoying
2c02c67e9e feat(web): login ui 2026-04-28 15:54:36 +08:00
zhaoying
03d2228d87 feat(web): login ui 2026-04-28 15:41:40 +08:00
Mark
9598bd5905 [modify] migration script 2026-04-28 13:44:05 +08:00
Mark
d85a1cb131 [add] migration script 2026-04-28 13:41:46 +08:00
Mark
a5670bfff6 Merge branch 'feature/rag2' into develop 2026-04-27 18:17:49 +08:00
20 changed files with 415 additions and 103 deletions

View File

@@ -1,4 +1,4 @@
import asyncio
import uuid
from fastapi import APIRouter, Depends, HTTPException, status, Query
from pydantic import BaseModel, Field
@@ -10,7 +10,7 @@ from app.dependencies import get_current_user
from app.models.user_model import User
from app.schemas.response_schema import ApiResponse
from app.services import memory_dashboard_service, memory_storage_service, workspace_service
from app.services import memory_dashboard_service, workspace_service
from app.services.memory_agent_service import get_end_users_connected_configs_batch
from app.services.app_statistics_service import AppStatisticsService
from app.core.logging_config import get_api_logger
@@ -48,7 +48,7 @@ def get_workspace_total_end_users(
@router.get("/end_users", response_model=ApiResponse)
async def get_workspace_end_users(
def get_workspace_end_users(
workspace_id: Optional[uuid.UUID] = Query(None, description="工作空间ID可选默认当前用户工作空间"),
keyword: Optional[str] = Query(None, description="搜索关键词(同时模糊匹配 other_name 和 id"),
page: int = Query(1, ge=1, description="页码从1开始"),
@@ -58,6 +58,15 @@ async def get_workspace_end_users(
):
"""
获取工作空间的宿主列表(分页查询,支持模糊搜索)
新增:记忆数量过滤:
Neo4j 模式:
- 使用 end_users.memory_count 过滤 memory_count > 0 的宿主
- memory_num.total 直接取 end_user.memory_count
RAG 模式:
- 使用 documents.chunk_num 聚合过滤 chunk 总数 > 0 的宿主
- memory_num.total 取聚合后的 chunk 总数
返回工作空间下的宿主列表,支持分页查询和模糊搜索。
通过 keyword 参数同时模糊匹配 other_name 和 id 字段。
@@ -80,17 +89,29 @@ async def get_workspace_end_users(
current_workspace_type = memory_dashboard_service.get_current_workspace_type(db, workspace_id, current_user)
api_logger.info(f"用户 {current_user.username} 请求获取工作空间 {workspace_id} 的宿主列表, 类型: {current_workspace_type}")
# 获取分页的 end_users
end_users_result = memory_dashboard_service.get_workspace_end_users_paginated(
db=db,
workspace_id=workspace_id,
current_user=current_user,
page=page,
pagesize=pagesize,
keyword=keyword
)
if current_workspace_type == "rag":
end_users_result = memory_dashboard_service.get_workspace_end_users_paginated_rag(
db=db,
workspace_id=workspace_id,
current_user=current_user,
page=page,
pagesize=pagesize,
keyword=keyword,
)
raw_items = end_users_result.get("items", [])
end_users = [item["end_user"] for item in raw_items]
else:
end_users_result = memory_dashboard_service.get_workspace_end_users_paginated(
db=db,
workspace_id=workspace_id,
current_user=current_user,
page=page,
pagesize=pagesize,
keyword=keyword,
)
raw_items = end_users_result.get("items", [])
end_users = raw_items
end_users = end_users_result.get("items", [])
total = end_users_result.get("total", 0)
if not end_users:
@@ -101,50 +122,19 @@ async def get_workspace_end_users(
"page": page,
"pagesize": pagesize,
"total": total,
"hasnext": (page * pagesize) < total
}
"hasnext": (page * pagesize) < total,
},
}, msg="宿主列表获取成功")
end_user_ids = [str(user.id) for user in end_users]
# 并发执行两个独立的查询任务
async def get_memory_configs():
"""获取记忆配置(在线程池中执行同步查询)"""
try:
return await asyncio.to_thread(
get_end_users_connected_configs_batch,
end_user_ids, db
)
except Exception as e:
api_logger.error(f"批量获取记忆配置失败: {str(e)}")
return {}
try:
memory_configs_map = get_end_users_connected_configs_batch(end_user_ids, db)
except Exception as e:
api_logger.error(f"批量获取记忆配置失败: {str(e)}")
memory_configs_map = {}
async def get_memory_nums():
"""获取记忆数量"""
if current_workspace_type == "rag":
# RAG 模式:批量查询
try:
chunk_map = await asyncio.to_thread(
memory_dashboard_service.get_users_total_chunk_batch,
end_user_ids, db, current_user
)
return {uid: {"total": count} for uid, count in chunk_map.items()}
except Exception as e:
api_logger.error(f"批量获取 RAG chunk 数量失败: {str(e)}")
return {uid: {"total": 0} for uid in end_user_ids}
elif current_workspace_type == "neo4j":
# Neo4j 模式批量查询简化版本只返回total
try:
batch_result = await memory_storage_service.search_all_batch(end_user_ids)
return {uid: {"total": count} for uid, count in batch_result.items()}
except Exception as e:
api_logger.error(f"批量获取 Neo4j 记忆数量失败: {str(e)}")
return {uid: {"total": 0} for uid in end_user_ids}
return {uid: {"total": 0} for uid in end_user_ids}
# 触发按需初始化:为 implicit_emotions_storage 中没有记录的用户异步生成数据
# 触发按需初始化:为 implicit_emotions_storage / interest_distribution 中没有记录的用户异步生成数据
try:
from app.celery_app import celery_app as _celery_app
_celery_app.send_task(
@@ -159,27 +149,26 @@ async def get_workspace_end_users(
except Exception as e:
api_logger.warning(f"触发按需初始化任务失败(不影响主流程): {e}")
# 并发执行配置查询和记忆数量查询
memory_configs_map, memory_nums_map = await asyncio.gather(
get_memory_configs(),
get_memory_nums()
)
# 构建结果列表
items = []
for end_user in end_users:
for index, end_user in enumerate(end_users):
user_id = str(end_user.id)
config_info = memory_configs_map.get(user_id, {})
if current_workspace_type == "rag":
memory_total = int(raw_items[index].get("memory_count", 0) or 0)
else:
memory_total = int(getattr(end_user, "memory_count", 0) or 0)
items.append({
'end_user': {
'id': user_id,
'other_name': end_user.other_name
"end_user": {
"id": user_id,
"other_name": end_user.other_name,
},
'memory_num': memory_nums_map.get(user_id, {"total": 0}),
'memory_config': {
"memory_num": {"total": memory_total},
"memory_config": {
"memory_config_id": config_info.get("memory_config_id"),
"memory_config_name": config_info.get("memory_config_name")
}
"memory_config_name": config_info.get("memory_config_name"),
},
})
# 触发社区聚类补全任务(异步,不阻塞接口响应)
@@ -407,6 +396,7 @@ def get_current_user_rag_total_num(
total_chunk = memory_dashboard_service.get_current_user_total_chunk(end_user_id, db, current_user)
return success(data=total_chunk, msg="宿主RAG知识数据获取成功")
@router.get("/rag_content", response_model=ApiResponse)
def get_rag_content(
end_user_id: str = Query(..., description="宿主ID"),

View File

@@ -20,6 +20,7 @@ from app.core.memory.storage_services.extraction_engine.knowledge_extraction.mem
memory_summary_generation
from app.core.memory.utils.llm.llm_utils import MemoryClientFactory
from app.core.memory.utils.log.logging_utils import log_time
from app.core.memory.utils.memory_count_utils import sync_end_user_memory_count_from_neo4j
from app.db import get_db_context
from app.repositories.neo4j.add_edges import add_memory_summary_statement_edges
from app.repositories.neo4j.add_nodes import add_memory_summary_nodes
@@ -313,6 +314,28 @@ async def write(
except Exception as cache_err:
logger.warning(f"[WRITE] 写入活动统计缓存失败(不影响主流程): {cache_err}", exc_info=True)
# 同步 Neo4j 记忆节点总数到 PostgreSQL end_users.memory_count
if end_user_id:
try:
memory_count_connector = Neo4jConnector()
try:
node_count = await sync_end_user_memory_count_from_neo4j(
end_user_id,
memory_count_connector,
)
finally:
await memory_count_connector.close()
logger.info(
f"[MemoryCount] 写入后同步 memory_count: "
f"end_user_id={end_user_id}, count={node_count}"
)
except Exception as e:
logger.warning(
f"[MemoryCount] 写入后同步 memory_count 失败(不影响主流程): {e}",
exc_info=True,
)
# Close LLM/Embedder underlying httpx clients to prevent
# 'RuntimeError: Event loop is closed' during garbage collection
for client_obj in (llm_client, embedder_client):
@@ -331,3 +354,4 @@ async def write(
logger.info("=== Pipeline Complete ===")
logger.info(f"Total execution time: {total_time:.2f} seconds")

View File

@@ -20,6 +20,7 @@ from uuid import UUID
from datetime import datetime
from app.core.memory.storage_services.forgetting_engine.forgetting_strategy import ForgettingStrategy
from app.core.memory.utils.memory_count_utils import sync_end_user_memory_count_from_neo4j
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
@@ -145,7 +146,22 @@ class ForgettingScheduler:
}
logger.info("没有可遗忘的节点对,遗忘周期结束")
# 同步 Neo4j 记忆节点总数到 PostgreSQL 的 end_users.memory_count
if end_user_id:
try:
node_count = await sync_end_user_memory_count_from_neo4j(
end_user_id,
self.connector,
)
logger.info(
f"[MemoryCount] 遗忘后同步 memory_count: "
f"end_user_id={end_user_id}, count={node_count}"
)
except Exception as e:
logger.warning(
f"[MemoryCount] 遗忘后同步 memory_count 失败(不影响主流程): {e}",
exc_info=True,
)
return report
# 步骤3按激活值排序激活值最低的优先
@@ -302,7 +318,22 @@ class ForgettingScheduler:
f"({reduction_rate:.2%}), "
f"耗时 {duration:.2f}"
)
# 同步 Neo4j 记忆节点总数到 PostgreSQL 的 end_users.memory_count
if end_user_id:
try:
node_count = await sync_end_user_memory_count_from_neo4j(
end_user_id,
self.connector,
)
logger.info(
f"[MemoryCount] 遗忘后同步 memory_count: "
f"end_user_id={end_user_id}, count={node_count}"
)
except Exception as e:
logger.warning(
f"[MemoryCount] 遗忘后同步 memory_count 失败(不影响主流程): {e}",
exc_info=True,
)
return report
except Exception as e:

View File

@@ -0,0 +1,36 @@
from uuid import UUID
from app.db import get_db_context
from app.models.end_user_model import EndUser
from app.repositories.memory_config_repository import MemoryConfigRepository
from app.repositories.neo4j.neo4j_connector import Neo4jConnector
async def sync_end_user_memory_count_from_neo4j(
end_user_id: str,
connector: Neo4jConnector,
) -> int:
"""
Sync one end user's Neo4j memory node count to PostgreSQL.
The caller owns the Neo4j connector lifecycle.
"""
if not end_user_id:
return 0
result = await connector.execute_query(
MemoryConfigRepository.SEARCH_FOR_ALL_BATCH,
end_user_ids=[end_user_id],
)
node_count = int(result[0]["total"]) if result else 0
with get_db_context() as db:
db.query(EndUser).filter(
EndUser.id == UUID(end_user_id)
).update(
{"memory_count": node_count},
synchronize_session=False,
)
db.commit()
return node_count

View File

@@ -1,7 +1,7 @@
import datetime
import uuid
from sqlalchemy import Column, DateTime, ForeignKey, String, Text
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
@@ -38,6 +38,15 @@ class EndUser(Base):
comment="关联的记忆配置ID"
)
memory_count = Column(
Integer,
nullable=False,
default=0,
server_default="0",
index=True,
comment="记忆节点总数",
)
# 用户摘要四个维度 - User Summary Four Dimensions
user_summary = Column(Text, nullable=True, comment="缓存的用户摘要(基本介绍)")
personality_traits = Column(Text, nullable=True, comment="性格特点")

View File

@@ -19,4 +19,6 @@ class EndUser(BaseModel):
# 用户摘要和洞察更新时间
user_summary_updated_at: Optional[datetime.datetime] = Field(description="用户摘要最后更新时间", default=None)
memory_insight_updated_at: Optional[datetime.datetime] = Field(description="洞察报告最后更新时间", default=None)
memory_insight_updated_at: Optional[datetime.datetime] = Field(description="洞察报告最后更新时间", default=None)
#用户记忆节点总数Neo4j模式
memory_count: int = Field(description="记忆节点总数", default=0)

View File

@@ -1,5 +1,5 @@
from sqlalchemy.orm import Session
from sqlalchemy import desc, nullslast, or_, and_, cast, String
from sqlalchemy import desc, nullslast, or_, cast, String, func
from typing import List, Optional, Dict, Any
import uuid
from fastapi import HTTPException
@@ -102,6 +102,7 @@ def get_workspace_end_users_paginated(
"""获取工作空间的宿主列表(分页版本,支持模糊搜索)
返回结果按 created_at 从新到旧排序NULL 值排在最后)
固定过滤 memory_count > 0 的宿主,保证分页基于“有记忆宿主”集合计算。
支持通过 keyword 参数同时模糊搜索 other_name 和 id 字段
Args:
@@ -120,7 +121,8 @@ def get_workspace_end_users_paginated(
try:
# 构建基础查询
base_query = db.query(EndUserModel).filter(
EndUserModel.workspace_id == workspace_id
EndUserModel.workspace_id == workspace_id,
EndUserModel.memory_count > 0 , # 只查询有记忆的宿主
)
# 构建搜索条件过滤空字符串和None
@@ -128,20 +130,13 @@ def get_workspace_end_users_paginated(
if keyword:
keyword_pattern = f"%{keyword}%"
# other_name 匹配始终生效id 匹配仅对 other_name 为空的记录生效
base_query = base_query.filter(
or_(
EndUserModel.other_name.ilike(keyword_pattern),
and_(
or_(
EndUserModel.other_name.is_(None),
EndUserModel.other_name == "",
),
cast(EndUserModel.id, String).ilike(keyword_pattern),
),
cast(EndUserModel.id, String).ilike(keyword_pattern),
)
)
business_logger.info(f"应用模糊搜索: keyword={keyword}(匹配 other_nameother_name 为空时匹配 id")
business_logger.info(f"应用模糊搜索: keyword={keyword}(匹配 other_name id")
# 获取总记录数
total = base_query.count()
@@ -169,6 +164,98 @@ def get_workspace_end_users_paginated(
business_logger.error(f"获取工作空间宿主列表(分页)失败: workspace_id={workspace_id} - {str(e)}")
raise
def get_workspace_end_users_paginated_rag(
db: Session,
workspace_id: uuid.UUID,
current_user: User,
page: int,
pagesize: int,
keyword: Optional[str] = None,
) -> Dict[str, Any]:
"""RAG 模式宿主列表分页。
RAG 记忆数量以 documents.chunk_num 为准:
- file_name = end_user_id + ".txt"
- 只统计当前 workspace 下 permission_id="Memory" 的用户记忆知识库
- 在 SQL 层过滤 chunk 总数为 0 的宿主,保证分页准确
"""
business_logger.info(
f"获取 RAG 宿主列表(分页): workspace_id={workspace_id}, "
f"keyword={keyword}, page={page}, pagesize={pagesize}, 操作者: {current_user.username}"
)
try:
from app.models.document_model import Document
from app.models.knowledge_model import Knowledge
chunk_subquery = (
db.query(
Document.file_name.label("file_name"),
func.coalesce(func.sum(Document.chunk_num), 0).label("memory_count"),
)
.join(Knowledge, Document.kb_id == Knowledge.id)
.filter(
Knowledge.workspace_id == workspace_id,
Knowledge.status == 1,
Knowledge.permission_id == "Memory",
Document.status == 1,
)
.group_by(Document.file_name)
.subquery()
)
base_query = (
db.query(
EndUserModel,
chunk_subquery.c.memory_count.label("memory_count"),
)
.join(
chunk_subquery,
chunk_subquery.c.file_name == func.concat(cast(EndUserModel.id, String), ".txt"),
)
.filter(
EndUserModel.workspace_id == workspace_id,
chunk_subquery.c.memory_count > 0,
)
)
keyword = keyword.strip() if keyword else None
if keyword:
keyword_pattern = f"%{keyword}%"
base_query = base_query.filter(
or_(
EndUserModel.other_name.ilike(keyword_pattern),
cast(EndUserModel.id, String).ilike(keyword_pattern),
)
)
total = base_query.count()
if total == 0:
business_logger.info("RAG 模式下没有符合条件的宿主")
return {"items": [], "total": 0}
rows = base_query.order_by(
nullslast(desc(EndUserModel.created_at)),
desc(EndUserModel.id),
).offset((page - 1) * pagesize).limit(pagesize).all()
items = []
for end_user_orm, memory_count in rows:
items.append({
"end_user": EndUserSchema.model_validate(end_user_orm),
"memory_count": int(memory_count or 0),
})
business_logger.info(f"成功获取 RAG 宿主记录 {len(items)} 条,总计 {total}")
return {"items": items, "total": total}
except HTTPException:
raise
except Exception as e:
business_logger.error(
f"获取 RAG 宿主列表(分页)失败: workspace_id={workspace_id} - {str(e)}"
)
raise
def get_workspace_memory_increment(
db: Session,

View File

@@ -0,0 +1,47 @@
"""202604271530
Revision ID: 1f85dce125e5
Revises: 4e89970f9e7c
Create Date: 2026-04-27 15:30:35.614679
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '1f85dce125e5'
down_revision: Union[str, None] = '4e89970f9e7c'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('files', sa.Column('file_key', sa.String(length=512), nullable=True, comment='storage file key for FileStorageService'))
op.create_index(op.f('ix_files_file_key'), 'files', ['file_key'], unique=False)
op.alter_column('model_configs', 'capability',
existing_type=postgresql.ARRAY(sa.VARCHAR()),
comment="模型能力列表(如['vision', 'audio', 'video', 'thinking']",
existing_comment="模型能力列表(如['vision', 'audio', 'video']",
existing_nullable=False)
# ### end Alembic commands ###
op.execute("""
UPDATE files
SET file_key = 'kb/' || kb_id::text || '/' || parent_id::text || '/' || id::text || file_ext
WHERE file_ext != 'folder' AND file_key IS NULL
""")
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('model_configs', 'capability',
existing_type=postgresql.ARRAY(sa.VARCHAR()),
comment="模型能力列表(如['vision', 'audio', 'video']",
existing_comment="模型能力列表(如['vision', 'audio', 'video', 'thinking']",
existing_nullable=False)
op.drop_index(op.f('ix_files_file_key'), table_name='files')
op.drop_column('files', 'file_key')
# ### end Alembic commands ###

View File

@@ -0,0 +1,34 @@
"""202604281230
Revision ID: e2d60c6d1a1a
Revises: 1f85dce125e5
Create Date: 2026-04-28 12:32:01.643954
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = 'e2d60c6d1a1a'
down_revision: Union[str, None] = '1f85dce125e5'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('tenants', 'api_ops_rate_limit')
op.drop_column('tenants', 'plan')
op.drop_column('tenants', 'plan_expired_at')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('tenants', sa.Column('plan_expired_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
op.add_column('tenants', sa.Column('plan', sa.VARCHAR(length=50), autoincrement=False, nullable=True))
op.add_column('tenants', sa.Column('api_ops_rate_limit', sa.VARCHAR(length=100), autoincrement=False, nullable=True))
# ### end Alembic commands ###

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 B

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>勾选</title>
<g id="空间外层页面优化" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="登录页面" transform="translate(-64, -611)" fill="#FFFFFF" fill-rule="nonzero">
<g id="编组-8" transform="translate(64, 608)">
<g id="勾选" transform="translate(0, 3)">
<path d="M12,0 C14.209139,0 16,1.790861 16,4 L16,12 C16,14.209139 14.209139,16 12,16 L4,16 C1.790861,16 0,14.209139 0,12 L0,4 C0,1.790861 1.790861,4.4408921e-16 4,0 L12,0 Z M11.9182266,4.80024782 C11.7273831,4.80024782 11.5444062,4.87629473 11.4097812,5.0115625 L6.552,9.86932813 L4.4284375,7.74489063 C4.29381317,7.60962766 4.11083967,7.53358379 3.92,7.53358379 C3.72916033,7.53358379 3.54618683,7.60962766 3.4115625,7.74489063 C3.27602096,7.87955071 3.19979999,8.06271883 3.19979999,8.25378125 C3.19979999,8.44484367 3.27602096,8.62801179 3.4115625,8.76267188 L6.0453125,11.3946719 C6.17993745,11.5299396 6.3629143,11.6059866 6.55375781,11.6059866 C6.74460132,11.6059866 6.92757818,11.5299396 7.06220312,11.3946719 L12.4311094,6.02667188 C12.5659036,5.89187668 12.6412595,5.70881589 12.6404302,5.51818919 C12.639587,5.3275625 12.5626279,5.14516989 12.4266562,5.0115625 C12.2920469,4.87629473 12.1090701,4.80024782 11.9182266,4.80024782 Z" id="形状结合"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -467,4 +467,29 @@ input:-webkit-autofill:active {
animation-name: onAutoFillStart;
animation-duration: 1ms;
}
@keyframes onAutoFillStart { from {} to {} }
@keyframes onAutoFillStart { from {} to {} }
/* Login input placeholder */
.login-input input::placeholder {
color: #A8A9AA !important;
}
.login-input {
border-color: #A8A9AA;
}
/* Login input hover/focus border */
.login-input:hover,
.login-input:focus-within {
border-color: #FFFFFF !important;
box-shadow: none !important;
}
/* Override browser autofill styles */
.login-input input:-webkit-autofill,
.login-input input:-webkit-autofill:hover,
.login-input input:-webkit-autofill:focus,
.login-input input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0px 1000px #0A0A0A inset !important;
-webkit-text-fill-color: #FFFFFF !important;
transition: background-color 5000s ease-in-out 0s !important;
}

View File

@@ -102,7 +102,7 @@ const Index = () => {
<Flex gap={12} wrap="nowrap" className="rb:w-full! rb:h-full! rb:overflow-y-auto">
<div className="rb:flex-1 rb:min-w-0">
<Flex vertical>
<div className='rb:w-full rb:h-26 rb:p-4 rb:bg-cover rb:bg-[url("@/assets/images/index/index_bg@2x.png")] rb:rounded-xl rb:overflow-hidden'>
<div className='rb:w-full rb:h-26 rb:p-4 rb:bg-cover rb:bg-[url("@/assets/images/index/index_bg.png")] rb:rounded-xl rb:overflow-hidden'>
<div className="rb:font-[MiSans-Bold] rb:font-bold rb:text-white rb:text-[18px] rb:leading-7">
{t('index.spaceTitle')}
</div>

View File

@@ -14,27 +14,33 @@ import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Input, Form, App } from 'antd';
import type { FormProps } from 'antd';
import clsx from 'clsx';
import { useUser, type LoginInfo } from '@/store/user';
import { login } from '@/api/user'
import loginBg from '@/assets/images/login/loginBg.png'
import check from '@/assets/images/login/check.png'
import loginBg from '@/assets/images/login/bg.mp4'
import check from '@/assets/images/login/check.svg'
import email from '@/assets/images/login/email.svg'
import lock from '@/assets/images/login/lock.svg'
import type { LoginForm } from './types';
import { useI18n } from '@/store/locale'
/**
* Input field styling
*/
const inputClassName = "rb:rounded-[8px]! rb:p-[12px]! rb:h-[44px]!"
const inputClassName = "login-input rb:rounded-[8px]! rb:p-[12px]! rb:h-[44px]! rb:bg-transparent! rb:text-[#FFFFFF]! [&_input]:rb:text-[#FFFFFF]! [&_input]:rb:caret-[#FFFFFF]!"
/**
* Login page component
*/const LoginPage: React.FC = () => {
const { t } = useTranslation();
const { clearUserInfo, updateLoginInfo, getUserInfo } = useUser();
const { language } = useI18n()
const [loading, setLoading] = useState(false);
const [form] = Form.useForm<LoginForm>();
const emailVal = Form.useWatch('email', form);
const passwordVal = Form.useWatch('password', form);
const canLogin = !!(emailVal && passwordVal);
const { message } = App.useApp();
useEffect(() => {
@@ -43,6 +49,7 @@ const inputClassName = "rb:rounded-[8px]! rb:p-[12px]! rb:h-[44px]!"
/** Handle login form submission */
const handleLogin: FormProps<LoginForm>['onFinish'] = async (values) => {
if (!canLogin) return;
if (!values.email) {
message.warning(t('login.emailPlaceholder'));
return;
@@ -64,42 +71,45 @@ const inputClassName = "rb:rounded-[8px]! rb:p-[12px]! rb:h-[44px]!"
return (
<div className="rb:min-h-screen rb:flex rb:h-screen">
<div className="rb:min-h-screen rb:flex rb:h-screen rb:bg-[#0A0A0A] rb:text-[#FFFFFF]">
<div className="rb:relative rb:w-1/2 rb:h-screen rb:overflow-hidden">
<img src={loginBg} alt="loginBg" className="rb:w-full rb:h-full rb:object-cover rb:absolute rb:top-1/2 rb:-translate-y-1/2 rb:left-0" />
<div className="rb:absolute rb:top-14 rb:left-16">
<div className="rb:text-[28px] rb:leading-8.25 rb:font-bold rb:font-[AlimamaShuHeiTi,AlimamaShuHeiTi] rb:mb-4">{t('login.title')}</div>
<div className="rb:text-[18px] rb:leading-6.25 rb:font-regular">{t('login.subTitle')}</div>
<video src={loginBg} loop autoPlay playsInline muted className="rb:w-full rb:h-full rb:object-cover"></video>
<div className="rb:absolute rb:top-10 rb:left-12">
<div className={clsx("rb:h-8.25 rb:bg-cover", {
"rb:w-89 rb:bg-[url('@/assets/images/login/title_en.png')]": language !== 'zh',
"rb:w-42 rb:bg-[url('@/assets/images/login/title_zh.png')]": language === 'zh'
})}></div>
<div className="rb:text-[18px] rb:text-[rgba(255,255,255,0.7)] rb:leading-6.25 rb:font-regular rb:mt-3">{t('login.subTitle')}</div>
</div>
<div className="rb:absolute rb:bottom-20.25 rb:left-16 rb:grid rb:grid-cols-2 rb:gap-x-30 rb:gap-y-10.75">
{['intelligentMemory', 'instantRecall', 'knowledgeAssociation'].map(key => (
<div key={key} className="rb:flex">
<div className="rb:absolute rb:bottom-14 rb:left-12 rb:right-12 rb:grid rb:grid-cols-2 rb:gap-x-30 rb:gap-y-10.75">
{['intelligentMemory', 'instantRecall', 'knowledgeAssociation'].map((key, index) => (
<div key={key} className={`rb:flex${index === 0 ? ' rb:col-span-2' : ''}`}>
<img src={check} className="rb:w-4 rb:h-4 rb:mr-2 rb:mt-0.75" />
<div className="rb:text-[16px] rb:leading-5.5">
<div className="rb:font-medium">{t(`login.${key}`)}</div>
<div className="rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:font-regular! rb:mt-2">{t(`login.${key}Desc`)}</div>
<div className="rb:text-[14px] rb:text-[rgba(255,255,255,0.7)] rb:leading-5 rb:font-regular! rb:mt-2">{t(`login.${key}Desc`)}</div>
</div>
</div>
))}
</div>
</div>
<div className="rb:bg-[#FFFFFF] rb:flex rb:items-center rb:justify-center rb:flex-[1_1_auto]">
<div className="rb:w-100 rb:mx-auto">
<div className="rb:text-center rb:text-[28px] rb:font-semibold rb:leading-8 rb:mb-12">{t('login.welcome')}</div>
<div className="rb:flex rb:items-center rb:justify-center rb:flex-[1_1_auto]">
<div className="rb:w-110 rb:mx-auto">
<div className="rb:text-center rb:text-[24px] rb:font-[MiSans-Bold] rb:font-bold rb:leading-8 rb:mb-12">{t('login.welcome')}</div>
<Form
form={form}
onFinish={handleLogin}
>
<Form.Item name="email" className="rb:mb-5!">
<Form.Item name="email" className="rb:mb-6!">
<Input
prefix={<img src={email} className="rb:w-5 rb:h-5 rb:mr-2" />}
placeholder={t('login.emailPlaceholder')}
className={inputClassName}
/>
</Form.Item>
<Form.Item name="password">
<Form.Item name="password" className="rb:mb-0!">
<Input.Password
prefix={<img src={lock} className="rb:w-5 rb:h-5 rb:mr-2" />}
placeholder={t('login.passwordPlaceholder')}
@@ -111,7 +121,11 @@ const inputClassName = "rb:rounded-[8px]! rb:p-[12px]! rb:h-[44px]!"
block
loading={loading}
htmlType="submit"
className="rb:h-10! rb:rounded-lg! rb:mt-4"
disabled={!canLogin}
className={clsx("rb:h-11.5! rb:rounded-lg! rb:mt-12", {
'rb:hover:bg-[#2d6ef1]! rb:bg-[#155EEF]! rb:border-[#155EEF]!': canLogin,
'rb:bg-[#171719]! rb:border-[#171719]!': !canLogin
})}
>
{t('login.loginIn')}
</Button>

View File

@@ -361,7 +361,7 @@ const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () =>
)}
</Flex>
<div>
<div className="rb:font-[MiSans Bold] rb:font-bold rb:text-[16px] rb:leading-5.5">{source.name}</div>
<div className="rb:font-[MiSans-Bold] rb:font-bold rb:text-[16px] rb:leading-5.5">{source.name}</div>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5">{t('tool.availableMcp')} ({mcpTotal})</div>
</div>
</Flex>