Merge pull request #567 from SuanmoSuanyangTechnology/release/v0.2.7

Release/v0.2.7
This commit is contained in:
Ke Sun
2026-03-16 15:47:14 +08:00
committed by GitHub
22 changed files with 580 additions and 409 deletions

View File

@@ -62,10 +62,10 @@ celery_app.conf.update(
task_serializer='json', task_serializer='json',
accept_content=['json'], accept_content=['json'],
result_serializer='json', result_serializer='json',
# 时区 # # 时区
timezone='Asia/Shanghai', # timezone='Asia/Shanghai',
enable_utc=False, # enable_utc=False,
# 任务追踪 # 任务追踪
task_track_started=True, task_track_started=True,

View File

@@ -55,6 +55,12 @@ async def get_mcp_servers(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="The paging parameter must be greater than 0" detail="The paging parameter must be greater than 0"
) )
if page * pagesize > 100:
api_logger.warning(f"Paging parameters exceed ModelScope limit: page={page}, pagesize={pagesize}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"The maximum number of MCP services can view is 100. Please visit the ModelScope MCP Plaza."
)
# 2. Query mcp market config information from the database # 2. Query mcp market config information from the database
api_logger.debug(f"Query mcp market config: {mcp_market_config_id}") api_logger.debug(f"Query mcp market config: {mcp_market_config_id}")
@@ -64,23 +70,26 @@ async def get_mcp_servers(
if not db_mcp_market_config: if not db_mcp_market_config:
api_logger.warning( api_logger.warning(
f"The mcp market config does not exist or access is denied: mcp_market_config_id={mcp_market_config_id}") f"The mcp market config does not exist or access is denied: mcp_market_config_id={mcp_market_config_id}")
raise HTTPException( return success(msg='The mcp market config does not exist or access is denied')
status_code=status.HTTP_404_NOT_FOUND,
detail="The mcp market config does not exist or access is denied"
)
# 3. Execute paged query # 3. Execute paged query
try: token = db_mcp_market_config.token
api = MCPApi() if not token:
token = db_mcp_market_config.token raise HTTPException(
api.login(token) status_code=status.HTTP_400_BAD_REQUEST,
detail="MCP market config token is not configured"
)
api = MCPApi()
api.login(token)
body = { body = {
'filter': {}, 'filter': {},
'page_number': page, 'page_number': page,
'page_size': pagesize, 'page_size': pagesize,
'search': keywords 'search': keywords
} }
try:
cookies = api.get_cookies(token) cookies = api.get_cookies(token)
r = api.session.put( r = api.session.put(
url=api.mcp_base_url, url=api.mcp_base_url,
@@ -150,14 +159,16 @@ async def get_operational_mcp_servers(
if not db_mcp_market_config: if not db_mcp_market_config:
api_logger.warning( api_logger.warning(
f"The mcp market config does not exist or access is denied: mcp_market_config_id={mcp_market_config_id}") f"The mcp market config does not exist or access is denied: mcp_market_config_id={mcp_market_config_id}")
raise HTTPException( return success(msg='The mcp market config does not exist or access is denied')
status_code=status.HTTP_404_NOT_FOUND,
detail="The mcp market config does not exist or access is denied"
)
# 2. Execute paged query # 2. Execute paged query
api = MCPApi()
token = db_mcp_market_config.token token = db_mcp_market_config.token
if not token:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="MCP market config token is not configured"
)
api = MCPApi()
api.login(token) api.login(token)
url = f'{api.mcp_base_url}/operational' url = f'{api.mcp_base_url}/operational'
@@ -208,14 +219,16 @@ async def get_mcp_server(
if not db_mcp_market_config: if not db_mcp_market_config:
api_logger.warning( api_logger.warning(
f"The mcp market config does not exist or access is denied: mcp_market_config_id={mcp_market_config_id}") f"The mcp market config does not exist or access is denied: mcp_market_config_id={mcp_market_config_id}")
raise HTTPException( return success(msg='The mcp market config does not exist or access is denied')
status_code=status.HTTP_404_NOT_FOUND,
detail="The mcp market config does not exist or access is denied"
)
# 2. Get detailed information for a specific MCP Server # 2. Get detailed information for a specific MCP Server
api = MCPApi()
token = db_mcp_market_config.token token = db_mcp_market_config.token
if not token:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="MCP market config token is not configured"
)
api = MCPApi()
api.login(token) api.login(token)
result = api.get_mcp_server(server_id=server_id) result = api.get_mcp_server(server_id=server_id)
@@ -236,7 +249,26 @@ async def create_mcp_market_config(
try: try:
api_logger.debug(f"Start creating the mcp market config: {create_data.mcp_market_id}") api_logger.debug(f"Start creating the mcp market config: {create_data.mcp_market_id}")
# 1. Check if the mcp market name already exists # 1. Validate token can access ModelScope MCP market
if not create_data.token:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Token is required to access ModelScope MCP market"
)
try:
api = MCPApi()
api.login(create_data.token)
body = {'filter': {}, 'page_number': 1, 'page_size': 1, 'search': None}
cookies = api.get_cookies(create_data.token)
r = api.session.put(url=api.mcp_base_url, headers=api.builder_headers(api.headers), json=body, cookies=cookies)
raise_for_http_status(r)
except Exception as e:
api_logger.warning(f"Token validation failed for ModelScope MCP market: {str(e)}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Unable to access ModelScope MCP market with the provided token: {str(e)}"
)
# 2. Check if the mcp market name already exists
db_mcp_market_config_exist = mcp_market_config_service.get_mcp_market_config_by_mcp_market_id(db, mcp_market_id=create_data.mcp_market_id, current_user=current_user) db_mcp_market_config_exist = mcp_market_config_service.get_mcp_market_config_by_mcp_market_id(db, mcp_market_id=create_data.mcp_market_id, current_user=current_user)
if db_mcp_market_config_exist: if db_mcp_market_config_exist:
api_logger.warning(f"The mcp market id already exists: {create_data.mcp_market_id}") api_logger.warning(f"The mcp market id already exists: {create_data.mcp_market_id}")
@@ -296,10 +328,7 @@ async def get_mcp_market_config(
db_mcp_market_config = mcp_market_config_service.get_mcp_market_config_by_id(db, mcp_market_config_id=mcp_market_config_id, current_user=current_user) db_mcp_market_config = mcp_market_config_service.get_mcp_market_config_by_id(db, mcp_market_config_id=mcp_market_config_id, current_user=current_user)
if not db_mcp_market_config: if not db_mcp_market_config:
api_logger.warning(f"The mcp market config does not exist or access is denied: mcp_market_config_id={mcp_market_config_id}") api_logger.warning(f"The mcp market config does not exist or access is denied: mcp_market_config_id={mcp_market_config_id}")
raise HTTPException( return success(msg='The mcp market config does not exist or access is denied')
status_code=status.HTTP_404_NOT_FOUND,
detail="The mcp market config does not exist or access is denied"
)
api_logger.info(f"mcp market config query successful: (ID: {db_mcp_market_config.id})") api_logger.info(f"mcp market config query successful: (ID: {db_mcp_market_config.id})")
return success(data=jsonable_encoder(mcp_market_config_schema.McpMarketConfig.model_validate(db_mcp_market_config)), return success(data=jsonable_encoder(mcp_market_config_schema.McpMarketConfig.model_validate(db_mcp_market_config)),
@@ -329,10 +358,7 @@ async def get_mcp_market_config_by_mcp_market_id(
db_mcp_market_config = mcp_market_config_service.get_mcp_market_config_by_mcp_market_id(db, mcp_market_id=mcp_market_id, current_user=current_user) db_mcp_market_config = mcp_market_config_service.get_mcp_market_config_by_mcp_market_id(db, mcp_market_id=mcp_market_id, current_user=current_user)
if not db_mcp_market_config: if not db_mcp_market_config:
api_logger.warning(f"The mcp market config does not exist or access is denied: mcp_market_id={mcp_market_id}") api_logger.warning(f"The mcp market config does not exist or access is denied: mcp_market_id={mcp_market_id}")
raise HTTPException( return success(msg='The mcp market config does not exist or access is denied')
status_code=status.HTTP_404_NOT_FOUND,
detail="The mcp market config does not exist or access is denied"
)
api_logger.info(f"mcp market config query successful: (ID: {db_mcp_market_config.id})") api_logger.info(f"mcp market config query successful: (ID: {db_mcp_market_config.id})")
return success(data=jsonable_encoder(mcp_market_config_schema.McpMarketConfig.model_validate(db_mcp_market_config)), return success(data=jsonable_encoder(mcp_market_config_schema.McpMarketConfig.model_validate(db_mcp_market_config)),
@@ -358,12 +384,25 @@ async def update_mcp_market_config(
if not db_mcp_market_config: if not db_mcp_market_config:
api_logger.warning( api_logger.warning(
f"The mcp market config does not exist or you do not have permission to access it: mcp_market_config_id={mcp_market_config_id}") f"The mcp market config does not exist or you do not have permission to access it: mcp_market_config_id={mcp_market_config_id}")
raise HTTPException( return success(msg='The mcp market config does not exist or access is denied')
status_code=status.HTTP_404_NOT_FOUND,
detail="The mcp market config does not exist or you do not have permission to access it"
)
# 2. Update fields (only update non-null fields) # 2. Validate new token if provided
if update_data.token is not None:
try:
api = MCPApi()
api.login(update_data.token)
body = {'filter': {}, 'page_number': 1, 'page_size': 1, 'search': None}
cookies = api.get_cookies(update_data.token)
r = api.session.put(url=api.mcp_base_url, headers=api.builder_headers(api.headers), json=body, cookies=cookies)
raise_for_http_status(r)
except Exception as e:
api_logger.warning(f"Token validation failed for ModelScope MCP market: {str(e)}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Unable to access ModelScope MCP market with the provided token: {str(e)}"
)
# 3. Update fields (only update non-null fields)
api_logger.debug(f"Start updating the mcp market config fields: {mcp_market_config_id}") api_logger.debug(f"Start updating the mcp market config fields: {mcp_market_config_id}")
update_dict = update_data.dict(exclude_unset=True) update_dict = update_data.dict(exclude_unset=True)
updated_fields = [] updated_fields = []
@@ -378,30 +417,6 @@ async def update_mcp_market_config(
if updated_fields: if updated_fields:
api_logger.debug(f"updated fields: {', '.join(updated_fields)}") api_logger.debug(f"updated fields: {', '.join(updated_fields)}")
# 3. verify token
db_mcp_market_config.status = 1
try:
api = MCPApi()
token = update_data.token
api.login(token)
body = {
'filter': {},
'page_number': 1,
'page_size': 20,
'search': ""
}
cookies = api.get_cookies(token)
r = api.session.put(
url=api.mcp_base_url,
headers=api.builder_headers(api.headers),
json=body,
cookies=cookies)
raise_for_http_status(r)
except requests.exceptions.RequestException as e:
api_logger.error(f"Failed to get MCP servers: {str(e)}")
db_mcp_market_config.status = 0
# 4. Save to database # 4. Save to database
try: try:
db.commit() db.commit()
@@ -439,10 +454,7 @@ async def delete_mcp_market_config(
if not db_mcp_market_config: if not db_mcp_market_config:
api_logger.warning( api_logger.warning(
f"The mcp market config does not exist or you do not have permission to access it: mcp_market_config_id={mcp_market_config_id}") f"The mcp market config does not exist or you do not have permission to access it: mcp_market_config_id={mcp_market_config_id}")
raise HTTPException( return success(msg='The mcp market config does not exist or access is denied')
status_code=status.HTTP_404_NOT_FOUND,
detail="The mcp market config does not exist or you do not have permission to access it"
)
# 2. Deleting mcp market config # 2. Deleting mcp market config
mcp_market_config_service.delete_mcp_market_config_by_id(db, mcp_market_config_id=mcp_market_config_id, current_user=current_user) mcp_market_config_service.delete_mcp_market_config_by_id(db, mcp_market_config_id=mcp_market_config_id, current_user=current_user)

View File

@@ -412,14 +412,15 @@ def get_current_user_rag_total_num(
@router.get("/rag_content", response_model=ApiResponse) @router.get("/rag_content", response_model=ApiResponse)
def get_rag_content( def get_rag_content(
end_user_id: str = Query(..., description="宿主ID"), end_user_id: str = Query(..., description="宿主ID"),
limit: int = Query(15, description="返回记录数"), page: int = Query(1, gt=0, description="页码从1开始"),
pagesize: int = Query(15, gt=0, le=100, description="每页返回记录数"),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
""" """
获取当前宿主知识库中的chunk内容 获取当前宿主知识库中的chunk内容(分页)
""" """
data = memory_dashboard_service.get_rag_content(end_user_id, limit, db, current_user) data = memory_dashboard_service.get_rag_content(end_user_id, page, pagesize, db, current_user)
return success(data=data, msg="宿主RAGchunk数据获取成功") return success(data=data, msg="宿主RAGchunk数据获取成功")

View File

@@ -53,6 +53,7 @@ class SimpleMCPClient:
else: else:
await self._connect_http() await self._connect_http()
except Exception as e: except Exception as e:
await self.disconnect()
logger.error(f"MCP连接失败: {self.server_url}, 错误: {e}") logger.error(f"MCP连接失败: {self.server_url}, 错误: {e}")
raise MCPConnectionError(f"连接失败: {e}") raise MCPConnectionError(f"连接失败: {e}")

View File

@@ -247,7 +247,6 @@ class EndUserRepository:
EndUser.user_summary: user_summary, EndUser.user_summary: user_summary,
EndUser.rag_tags: rag_tags, EndUser.rag_tags: rag_tags,
EndUser.rag_personas: rag_personas, EndUser.rag_personas: rag_personas,
EndUser.storage_type: "rag",
EndUser.rag_summary_updated_at: datetime.datetime.now(), EndUser.rag_summary_updated_at: datetime.datetime.now(),
}, },
synchronize_session=False synchronize_session=False
@@ -286,7 +285,6 @@ class EndUserRepository:
.update( .update(
{ {
EndUser.memory_insight: memory_insight, EndUser.memory_insight: memory_insight,
EndUser.storage_type: "rag",
EndUser.memory_insight_updated_at: datetime.datetime.now(), EndUser.memory_insight_updated_at: datetime.datetime.now(),
}, },
synchronize_session=False synchronize_session=False

View File

@@ -18,6 +18,7 @@ from app.models.tool_model import ToolConfig as ToolConfigModel
from app.models.workflow_model import WorkflowConfig from app.models.workflow_model import WorkflowConfig
from app.services.workflow_service import WorkflowService from app.services.workflow_service import WorkflowService
from app.core.workflow.adapters.memory_bear.memory_bear_adapter import MemoryBearAdapter from app.core.workflow.adapters.memory_bear.memory_bear_adapter import MemoryBearAdapter
from app.models.memory_config_model import MemoryConfig as MemoryConfigModel
class AppDslService: class AppDslService:
@@ -220,7 +221,7 @@ class AppDslService:
id=uuid.uuid4(), id=uuid.uuid4(),
workspace_id=workspace_id, workspace_id=workspace_id,
created_by=user_id, created_by=user_id,
name=app_meta.get("name", "导入应用"), name=self._unique_app_name(app_meta.get("name", "导入应用"), workspace_id, app_type),
description=app_meta.get("description"), description=app_meta.get("description"),
icon=app_meta.get("icon"), icon=app_meta.get("icon"),
icon_type=app_meta.get("icon_type"), icon_type=app_meta.get("icon_type"),
@@ -296,6 +297,19 @@ class AppDslService:
self.db.refresh(new_app) self.db.refresh(new_app)
return new_app, warnings return new_app, warnings
def _unique_app_name(self, name: str, workspace_id: uuid.UUID, app_type: AppType) -> str:
existing = {r[0] for r in self.db.query(App.name).filter(
App.workspace_id == workspace_id,
App.type == app_type,
App.is_active.is_(True)
).all()}
if name not in existing:
return name
counter = 1
while f"{name}({counter})" in existing:
counter += 1
return f"{name}({counter})"
def _resolve_model(self, ref: Optional[dict], tenant_id: uuid.UUID, warnings: list) -> Optional[uuid.UUID]: def _resolve_model(self, ref: Optional[dict], tenant_id: uuid.UUID, warnings: list) -> Optional[uuid.UUID]:
if not ref: if not ref:
return None return None
@@ -398,9 +412,19 @@ class AppDslService:
config_id = memory.get("memory_config_id") or memory.get("memory_content") config_id = memory.get("memory_config_id") or memory.get("memory_content")
if not config_id: if not config_id:
return memory return memory
from app.models.memory_config_model import MemoryConfig as MemoryConfigModel try:
config_uuid = uuid.UUID(str(config_id))
except (ValueError, AttributeError):
exists = self.db.query(MemoryConfigModel).filter(
MemoryConfigModel.config_id_old == int(config_id),
MemoryConfigModel.workspace_id == workspace_id
).first()
if not exists:
warnings.append(f"记忆配置 '{config_id}' 未匹配,已置空,请导入后手动配置")
return {**memory, "memory_config_id": None, "enabled": False}
return memory
exists = self.db.query(MemoryConfigModel).filter( exists = self.db.query(MemoryConfigModel).filter(
MemoryConfigModel.config_id == config_id, MemoryConfigModel.config_id == config_uuid,
MemoryConfigModel.workspace_id == workspace_id MemoryConfigModel.workspace_id == workspace_id
).first() ).first()
if not exists: if not exists:

View File

@@ -535,7 +535,8 @@ def get_users_total_chunk_batch(
def get_rag_content( def get_rag_content(
end_user_id: str, end_user_id: str,
limit: int, page: int,
pagesize: int,
db: Session, db: Session,
current_user: User current_user: User
) -> dict: ) -> dict:
@@ -543,9 +544,9 @@ def get_rag_content(
先在documents表中查询file_name=='end_user_id'+'.txt'的id和kb_id, 先在documents表中查询file_name=='end_user_id'+'.txt'的id和kb_id,
然后调用/chunks/{kb_id}/{document_id}/chunks接口的相关代码获取所有内容 然后调用/chunks/{kb_id}/{document_id}/chunks接口的相关代码获取所有内容
接着对获取的内容进行提取只要page_content的内容 接着对获取的内容进行提取只要page_content的内容
最后返回数据 最后返回分页数据
""" """
business_logger.info(f"获取RAG内容: end_user_id={end_user_id}, limit={limit}, 操作者: {current_user.username}") business_logger.info(f"获取RAG内容: end_user_id={end_user_id}, page={page}, pagesize={pagesize}, 操作者: {current_user.username}")
try: try:
from app.models.document_model import Document from app.models.document_model import Document
@@ -562,63 +563,76 @@ def get_rag_content(
if not documents: if not documents:
business_logger.warning(f"未找到文件: {file_name}") business_logger.warning(f"未找到文件: {file_name}")
return { return {
"total": 0, "page": {
"contents": [] "page": page,
"pagesize": pagesize,
"total": 0,
"hasnext": False,
},
"items": []
} }
business_logger.info(f"找到 {len(documents)} 个文档记录") business_logger.info(f"找到 {len(documents)} 个文档记录")
# 3. 获取所有chunks的page_content # 3. 按全局偏移量计算当前页数据
all_contents = [] # 全局偏移范围:[offset_start, offset_end)
total_chunks = 0 offset_start = (page - 1) * pagesize
offset_end = offset_start + pagesize
global_total = 0 # 所有文档的 chunk 总数
page_contents = [] # 当前页的内容
for document in documents: for document in documents:
try: try:
# 获取知识库信息
kb = knowledge_repository.get_knowledge_by_id(db, document.kb_id) kb = knowledge_repository.get_knowledge_by_id(db, document.kb_id)
if not kb: if not kb:
business_logger.warning(f"知识库不存在: kb_id={document.kb_id}") business_logger.warning(f"知识库不存在: kb_id={document.kb_id}")
continue continue
# 初始化向量服务
vector_service = ElasticSearchVectorFactory().init_vector(knowledge=kb) vector_service = ElasticSearchVectorFactory().init_vector(knowledge=kb)
# 获取该文档的所有chunks分页获取 # 先用 pagesize=1 获取该文档的 chunk 总数
page = 1 doc_total, _ = vector_service.search_by_segment(
pagesize = 100 # 每页100条 document_id=str(document.id),
query=None,
pagesize=1,
page=1,
asc=True
)
while True: doc_offset_start = global_total # 该文档在全局中的起始偏移
total, items = vector_service.search_by_segment( doc_offset_end = global_total + doc_total # 该文档在全局中的结束偏移
global_total += doc_total
# 当前页与该文档无交集,跳过
if doc_offset_end <= offset_start or doc_offset_start >= offset_end:
continue
# 计算需要从该文档取的局部范围
local_start = max(offset_start - doc_offset_start, 0)
local_end = min(offset_end - doc_offset_start, doc_total)
need_count = local_end - local_start
# 换算成 ES 分页参数ES page 从1开始
es_page = (local_start // pagesize) + 1
es_offset_in_page = local_start % pagesize
fetched = []
while len(fetched) < es_offset_in_page + need_count:
_, items = vector_service.search_by_segment(
document_id=str(document.id), document_id=str(document.id),
query=None, query=None,
pagesize=pagesize, pagesize=pagesize,
page=page, page=es_page,
asc=True asc=True
) )
if not items: if not items:
break break
fetched.extend(items)
# 提取page_content es_page += 1
for item in items:
all_contents.append(item.page_content)
total_chunks += 1
# # 如果达到limit限制直接返回
# if limit > 0 and total_chunks >= limit:
# business_logger.info(f"已达到limit限制: {limit}")
# return {
# "total": total_chunks,
# "contents": all_contents[:limit]
# }
# 检查是否还有下一页
if page * pagesize >= total:
break
page += 1
business_logger.info(f"文档 {document.id} 获取了 {len(items)} 个chunks") slice_items = fetched[es_offset_in_page: es_offset_in_page + need_count]
page_contents.extend([item.page_content for item in slice_items])
except Exception as e: except Exception as e:
business_logger.error(f"获取文档 {document.id} 的chunks失败: {str(e)}") business_logger.error(f"获取文档 {document.id} 的chunks失败: {str(e)}")
@@ -626,11 +640,16 @@ def get_rag_content(
# 4. 返回结果 # 4. 返回结果
result = { result = {
"total": total_chunks, "page": {
"contents": all_contents[:limit] if limit > 0 else all_contents "page": page,
"pagesize": pagesize,
"total": global_total,
"hasnext": offset_end < global_total,
},
"items": page_contents
} }
business_logger.info(f"成功获取RAG内容: total={total_chunks}, 返回={len(result['contents'])}") business_logger.info(f"成功获取RAG内容: total={global_total}, page={page}, 返回={len(page_contents)}")
return result return result
except Exception as e: except Exception as e:
@@ -730,8 +749,8 @@ async def generate_rag_profile(
if not end_user: if not end_user:
raise ValueError(f"end_user {end_user_id} 不存在") raise ValueError(f"end_user {end_user_id} 不存在")
rag_content = get_rag_content(end_user_id, limit, db, current_user) rag_content = get_rag_content(end_user_id, page=1, pagesize=limit, db=db, current_user=current_user)
chunks = rag_content.get("contents", []) chunks = rag_content.get("items", [])
if not chunks: if not chunks:
business_logger.warning(f"未找到chunk内容无法生产RAG画像: end_user_id={end_user_id}") business_logger.warning(f"未找到chunk内容无法生产RAG画像: end_user_id={end_user_id}")

View File

@@ -1,4 +1,38 @@
{ {
"v0.2.7": {
"introduction": {
"codeName": "武陵",
"releaseDate": "2026-3-13",
"upgradePosition": "🐻 应用可移植性、工具生态扩展与记忆智能精细化",
"coreUpgrades": [
"1. 应用管理与可移植性<br>* 应用导入/导出:全面支持 Agent 配置和工作流定义的导入导出,实现跨环境无缝迁移、备份和共享",
"2. 工具生态扩展 🔌<br>* MCP 广场集成:工具管理接入 MCP 广场,提供集中式工具发现、浏览和集成枢纽",
"3. 工作流增强 📝<br>* 备注节点:新增备注节点类型,支持工作流图中的内联文档和上下文说明,提升协作效率",
"4. 记忆智能精细化 🧠<br>* 隐性记忆与情绪记忆生成逻辑优化:含数据存在性校验、时间轴筛选和兴趣分布缓存校验<br>* 兴趣分布生成逻辑改进:优化算法产生更准确的用户兴趣画像",
"5. 用户体验改进 🎨<br>* 知识库分享加载状态:增加加载指示器,改善感知响应速度",
"6. 稳健性与缺陷修复 🔧<br>* 应用调试终端用户管理:修复调试会话错误创建 end_user 记录问题<br>* 知识库数据集创建流程:解决创建数据集后无法进入下一步的缺陷<br>* RAG 空间记忆生成失败:修复记忆生成失败和存储中断的关键问题<br>* 应用字符限制强制执行:增加条件校验防止过长输入<br>* 语义剪枝情绪/兴趣保留:优化剪枝逻辑防止误删情绪和兴趣片段<br>* 语义剪枝效果优化:增强算法平衡记忆压缩与信息保留",
"<br>",
"v0.2.8 及更远的未来将引入多模态记忆能力实现知识库和模型的分服务部署为应用增加语音输入支持并扩展应用能力至语音回复、BI 可视化、PPT 生成和直接生图。应用会话分享和联网搜索功能将得到修复和增强。记忆检索基准测试和情景记忆聚类算法将增强上下文召回和时序推理能力。通往真正智能、多模态、上下文感知应用的旅程仍在继续。",
"记忆熊,智慧致远 🐻✨"
]
},
"introduction_en": {
"codeName": "WuLing",
"releaseDate": "2026-3-13",
"upgradePosition": "🐻 Application portability, tool ecosystem expansion, and memory intelligence refinement",
"coreUpgrades": [
"1. Application Management & Portability<br>* Application Import/Export: Full support for importing and exporting agent configurations and workflow definitions, enabling seamless cross-environment migration, backup, and sharing",
"2. Tool Ecosystem Expansion 🔌<br>* MCP Marketplace Integration: Tool management now includes MCP Marketplace access for centralized tool discovery, browsing, and integration",
"3. Workflow Enhancements 📝<br>* Annotation Node: Introduced annotation node type for inline documentation and contextual notes within workflow graphs, improving collaboration",
"4. Memory Intelligence Refinement 🧠<br>* Implicit & Emotional Memory Generation Logic: Comprehensive optimization including data existence validation, timeline filtering, and interest distribution cache validation<br>* Interest Distribution Generation Logic: Refined algorithm for more accurate user interest profiles",
"5. User Experience Improvements 🎨<br>* Knowledge Base Sharing Loading State: Added loading indicators to improve perceived responsiveness",
"6. Robustness & Bug Fixes 🔧<br>* End User Management in App Debugging: Fixed incorrect end_user record creation during debugging sessions<br>* Knowledge Base Dataset Creation Flow: Resolved bug preventing next step after dataset creation<br>* RAG Space Memory Generation Failure: Fixed critical memory generation and storage interruption issue<br>* Application Character Limit Enforcement: Added conditional validation to prevent excessively long input<br>* Semantic Pruning Emotion/Interest Preservation: Optimized pruning logic to prevent incorrect deletion of emotional and interest fragments<br>* Semantic Pruning Effectiveness: Enhanced algorithm balance between memory compression and information retention",
"<br>",
"Looking forward to v0.2.8 and beyond, we will introduce multimodal memory capabilities with distributed service deployment for knowledge bases and models, enabling voice input for applications and expanding application capabilities with voice responses, BI visualizations, PPT generation, and direct image creation. Application conversation sharing and web search functionality will be restored and enhanced. Memory retrieval benchmarking and episodic memory clustering algorithms will enhance contextual recall and temporal reasoning. The journey toward truly intelligent, multimodal, context-aware applications continues.",
"MemoryBear, Wisdom Reaching Far 🐻✨"
]
}
},
"v0.2.6": { "v0.2.6": {
"introduction": { "introduction": {
"codeName": "听剑", "codeName": "听剑",

View File

@@ -44,6 +44,7 @@
"i18next": "^25.6.0", "i18next": "^25.6.0",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.1",
"lexical": "^0.39.0", "lexical": "^0.39.0",
"mammoth": "^1.12.0",
"mermaid": "^11.12.1", "mermaid": "^11.12.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -59,6 +60,7 @@
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"xlsx": "^0.18.5",
"zustand": "^5.0.8" "zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -123,8 +123,9 @@ export const getChunkInsight = (end_user_id: string) => {
return request.get(`/dashboard/chunk_insight`, { end_user_id }) return request.get(`/dashboard/chunk_insight`, { end_user_id })
} }
// RAG User Memory - Storage content // RAG User Memory - Storage content
export const getRagContent = (end_user_id: string) => { export const getRagContentUrl = '/dashboard/rag_content'
return request.get(`/dashboard/rag_content`, { end_user_id, limit: 20 }) export const getRagContent = (end_user_id: string, page = 1, pagesize = 20) => {
return request.get(getRagContentUrl, { end_user_id, page, pagesize })
} }
// Emotion distribution analysis // Emotion distribution analysis
export const getWordCloud = (end_user_id: string) => { export const getWordCloud = (end_user_id: string) => {

View File

@@ -6,12 +6,12 @@ export const getTools = (data: Query) => {
return request.get('/tools', data) return request.get('/tools', data)
} }
// 创建MCP工具 // 创建MCP工具
export const addTool = (values: MCPToolItem | CustomToolItem) => { export const addTool = (values: MCPToolItem | CustomToolItem, config?: { signal?: AbortSignal }) => {
return request.post('/tools', values) return request.post('/tools', values, config)
} }
// 更新工具 // 更新工具
export const updateTool = (tool_id: string, data: MCPToolItem | InnerToolItem | CustomToolItem) => { export const updateTool = (tool_id: string, data: MCPToolItem | InnerToolItem | CustomToolItem, config?: { signal?: AbortSignal }) => {
return request.put(`/tools/${tool_id}`, data) return request.put(`/tools/${tool_id}`, data, config)
} }
// 删除工具 // 删除工具
export const deleteTool = (tool_id: string) => { export const deleteTool = (tool_id: string) => {

View File

@@ -1,20 +1,18 @@
import { useState, useEffect, type FC } from 'react'; import { useState, useEffect, type FC } from 'react';
import { Spin, Alert, Button } from 'antd'; import { Spin, Alert, Button, Table } from 'antd';
import { ReloadOutlined } from '@ant-design/icons'; import { ReloadOutlined, DownloadOutlined } from '@ant-design/icons';
import RbMarkdown from '../Markdown'; import RbMarkdown from '../Markdown';
import { cookieUtils } from '@/utils/request' import { cookieUtils } from '@/utils/request';
import mammoth from 'mammoth';
type PreviewMode = 'office' | 'google'; import * as XLSX from 'xlsx';
interface DocumentPreviewProps { interface DocumentPreviewProps {
fileUrl: string; fileUrl: string;
fileName?: string; fileName?: string;
fileExt?: string; // 文件扩展名(优先使用) fileExt?: string;
width?: string | number; width?: string | number;
height?: string | number; height?: string | number;
className?: string; className?: string;
mode?: PreviewMode; // 预览模式
showModeSwitch?: boolean; // 是否显示模式切换按钮
} }
const DocumentPreview: FC<DocumentPreviewProps> = ({ const DocumentPreview: FC<DocumentPreviewProps> = ({
@@ -24,18 +22,19 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
width = '100%', width = '100%',
height = '600px', height = '600px',
className = '', className = '',
mode = 'office',
showModeSwitch = true,
}) => { }) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [currentMode, setCurrentMode] = useState<PreviewMode>(mode); const [errorMessage, setErrorMessage] = useState<string>('');
const [textContent, setTextContent] = useState<string>(''); const [textContent, setTextContent] = useState<string>('');
const [htmlContent, setHtmlContent] = useState<string>('');
const [excelData, setExcelData] = useState<{ sheetName: string; data: any[][] }[]>([]);
// 支持的文件类型 // 支持预览的文件类型
const supportedTypes = ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.pdf', '.txt', '.md', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']; const previewableTypes = ['.pdf', '.txt', '.md', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.doc', '.docx', '.xls', '.xlsx'];
// PPT 暂不支持
const downloadOnlyTypes = ['.ppt', '.pptx'];
// 获取文件扩展名(优先使用 fileExt prop
const getFileExtension = () => { const getFileExtension = () => {
if (fileExt) { if (fileExt) {
return fileExt.toLowerCase().startsWith('.') ? fileExt.toLowerCase() : `.${fileExt.toLowerCase()}`; return fileExt.toLowerCase().startsWith('.') ? fileExt.toLowerCase() : `.${fileExt.toLowerCase()}`;
@@ -45,67 +44,25 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
return match ? `.${match[1].toLowerCase()}` : ''; return match ? `.${match[1].toLowerCase()}` : '';
}; };
// 检查是否为文本文件 const isTextFile = () => getFileExtension() === '.txt';
const isTextFile = () => { const isMarkdownFile = () => getFileExtension() === '.md';
const ext = getFileExtension();
return ext === '.txt';
};
// 检查是否为 Markdown 文件
const isMarkdownFile = () => {
const ext = getFileExtension();
return ext === '.md';
};
// 检查是否为图片文件
const isImageFile = () => { const isImageFile = () => {
const ext = getFileExtension();
const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']; const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'];
return imageExts.includes(ext); return imageExts.includes(getFileExtension());
};
// 检查文件类型是否支持
const isSupportedFile = () => {
const ext = getFileExtension();
return ext && supportedTypes.includes(ext);
}; };
const isPdfFile = () => getFileExtension() === '.pdf';
const isWordFile = () => ['.doc', '.docx'].includes(getFileExtension());
const isExcelFile = () => ['.xls', '.xlsx'].includes(getFileExtension());
const isPreviewable = () => previewableTypes.includes(getFileExtension());
const isDownloadOnly = () => downloadOnlyTypes.includes(getFileExtension());
// 检查是否为 PDF 文件 const handleDownload = () => {
const isPdfFile = () => { const link = document.createElement('a');
const ext = getFileExtension(); link.href = fileUrl;
return ext === '.pdf'; link.download = fileName || 'document';
}; document.body.appendChild(link);
link.click();
// 构建预览 URL document.body.removeChild(link);
const getPreviewUrl = () => {
// 处理文件 URL如果是完整的 URL转换为代理路径
let requestUrl = fileUrl;
// 如果是完整的 https://devapi.mem.redbearai.com 开头的 URL提取路径部分
// 这样可以通过代理访问,避免 CORS 问题
if (fileUrl.includes('devapi.mem.redbearai.com')) {
const url = new URL(fileUrl);
requestUrl = url.pathname; // 只取路径部分,例如 /api/files/xxx
}
// 对于 PDF 文件,直接使用浏览器内置预览
if (isPdfFile()) {
return requestUrl;
}
// 确保 fileUrl 是完整的 URL用于第三方预览服务
let fullUrl = fileUrl;
if (!fileUrl.startsWith('http')) {
fullUrl = `${window.location.origin}${fileUrl.startsWith('/') ? '' : '/'}${fileUrl}`;
}
console.log('预览 URL:', fullUrl);
// 根据模式选择预览服务
if (currentMode === 'google') {
return `https://docs.google.com/viewer?url=${encodeURIComponent(fullUrl)}&embedded=true`;
}
// 默认使用 Microsoft Office Online Viewer
return `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(fullUrl)}`;
}; };
const handleLoad = () => { const handleLoad = () => {
@@ -113,20 +70,24 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
setError(false); setError(false);
}; };
const handleError = () => { const handleError = (msg?: string) => {
setLoading(false); setLoading(false);
setError(true); setError(true);
if (msg) setErrorMessage(msg);
}; };
const handleRetry = () => { const handleRetry = () => {
setLoading(true); setLoading(true);
setError(false); setError(false);
setErrorMessage('');
if (isTextFile() || isMarkdownFile()) { if (isTextFile() || isMarkdownFile()) {
// 重新加载文本文件
loadTextFile(); loadTextFile();
} else if (isWordFile()) {
loadWordFile();
} else if (isExcelFile()) {
loadExcelFile();
} else { } else {
// 强制重新加载 iframe
const iframe = document.querySelector(`iframe[title="${fileName || '文档预览'}"]`) as HTMLIFrameElement; const iframe = document.querySelector(`iframe[title="${fileName || '文档预览'}"]`) as HTMLIFrameElement;
if (iframe) { if (iframe) {
iframe.src = iframe.src; iframe.src = iframe.src;
@@ -134,82 +95,164 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
} }
}; };
const handleSwitchMode = () => {
setCurrentMode(prev => prev === 'office' ? 'google' : 'office');
setLoading(true);
setError(false);
};
// 加载文本文件内容
const loadTextFile = async () => { const loadTextFile = async () => {
setLoading(true); setLoading(true);
setError(false); setError(false);
setErrorMessage('');
try { try {
// 处理文件 URL如果是完整的 URL转换为代理路径
let requestUrl = fileUrl; let requestUrl = fileUrl;
// 如果是完整的 https://devapi.mem.redbearai.com 开头的 URL提取路径部分
if (fileUrl.includes('devapi.mem.redbearai.com')) { if (fileUrl.includes('devapi.mem.redbearai.com')) {
const url = new URL(fileUrl); const url = new URL(fileUrl);
requestUrl = url.pathname; // 只取路径部分,例如 /api/files/xxx requestUrl = url.pathname;
} }
const response = await fetch(requestUrl, { const response = await fetch(requestUrl, {
credentials: 'include', // 包含认证信息 credentials: 'include',
headers: { headers: {
'Authorization': `Bearer ${cookieUtils.get('authToken') || ''}`, 'Authorization': `Bearer ${cookieUtils.get('authToken') || ''}`,
}, },
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to load file'); throw new Error(`HTTP ${response.status}: ${response.statusText}`);
} }
// 检查响应的 Content-Type
const contentType = response.headers.get('Content-Type') || ''; const contentType = response.headers.get('Content-Type') || '';
console.log('文件 Content-Type:', contentType);
// 如果是图片类型,显示错误提示
if (contentType.startsWith('image/')) { if (contentType.startsWith('image/')) {
setError(true); handleError('文件实际是图片类型,但被标记为文本文件');
setTextContent('');
setLoading(false);
console.error('文件实际是图片类型,但被标记为 txt');
return; return;
} }
const text = await response.text(); const text = await response.text();
// 检查是否是二进制数据(如 PNG 文件头)
if (text.startsWith('\x89PNG') || text.startsWith('<27>PNG')) { if (text.startsWith('\x89PNG') || text.startsWith('<27>PNG')) {
setError(true); handleError('文件内容是图片,但扩展名是文本');
setTextContent('');
setLoading(false);
console.error('文件内容是 PNG 图片,但扩展名是 txt');
return; return;
} }
setTextContent(text); setTextContent(text);
setLoading(false); setLoading(false);
} catch (err) { } catch (err: any) {
console.error('加载文本文件失败:', err); console.error('加载文本文件失败:', err);
setError(true); handleError(err.message || '加载文本文件失败');
setLoading(false); }
};
const loadWordFile = async () => {
setLoading(true);
setError(false);
setErrorMessage('');
try {
let requestUrl = fileUrl;
if (fileUrl.includes('devapi.mem.redbearai.com')) {
const url = new URL(fileUrl);
requestUrl = url.pathname;
}
const response = await fetch(requestUrl, {
credentials: 'include',
headers: {
'Authorization': `Bearer ${cookieUtils.get('authToken') || ''}`,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
const result = await mammoth.convertToHtml({ arrayBuffer });
setHtmlContent(result.value);
setLoading(false);
} catch (err: any) {
console.error('加载 Word 文件失败:', err);
handleError(err.message || '加载 Word 文件失败,文件可能已损坏');
}
};
const loadExcelFile = async () => {
setLoading(true);
setError(false);
setErrorMessage('');
try {
let requestUrl = fileUrl;
if (fileUrl.includes('devapi.mem.redbearai.com')) {
const url = new URL(fileUrl);
requestUrl = url.pathname;
}
const response = await fetch(requestUrl, {
credentials: 'include',
headers: {
'Authorization': `Bearer ${cookieUtils.get('authToken') || ''}`,
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
const sheets = workbook.SheetNames.map(sheetName => {
const worksheet = workbook.Sheets[sheetName];
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
return { sheetName, data };
});
setExcelData(sheets);
setLoading(false);
} catch (err: any) {
console.error('加载 Excel 文件失败:', err);
handleError(err.message || '加载 Excel 文件失败,文件可能已损坏');
} }
}; };
// 当文件是 txt 或 md 时,加载文本内容
useEffect(() => { useEffect(() => {
if (isTextFile() || isMarkdownFile()) { if (isTextFile() || isMarkdownFile()) {
loadTextFile(); loadTextFile();
} else if (isWordFile()) {
loadWordFile();
} else if (isExcelFile()) {
loadExcelFile();
} }
}, [fileUrl]); }, [fileUrl]);
if (!isSupportedFile()) { // PPT 文件只提供下载
if (isDownloadOnly()) {
return (
<div className={`rb:relative rb:flex rb:items-center rb:justify-center rb:bg-gray-50 rb:rounded rb:border rb:border-gray-200 ${className}`} style={{ width, height }}>
<Alert
message="PowerPoint 文档预览"
description={
<div className="rb:text-center">
<p className="rb:mb-4">PPT 线</p>
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={handleDownload}
>
</Button>
</div>
}
type="info"
showIcon
/>
</div>
);
}
if (!isPreviewable()) {
return ( return (
<Alert <Alert
message="不支持的文件类型" message="不支持的文件类型"
description={`仅支持以下文件类型${supportedTypes.join(', ')}`} description={`仅支持预览${previewableTypes.join(', ')}`}
type="warning" type="warning"
showIcon showIcon
/> />
@@ -230,23 +273,26 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
message="预览失败" message="预览失败"
description={ description={
<div> <div>
<p></p> <p className="rb:mb-2"></p>
<ul className="rb:list-disc rb:pl-5 rb:mt-2"> {errorMessage && (
<li>访Office 访</li> <p className="rb:text-sm rb:text-red-600 rb:mb-3">
<li> URL 访访 URL</li> {errorMessage}
<li>Office 10MB</li> </p>
<li></li> )}
<p className="rb:text-sm rb:text-gray-600 rb:mb-3"></p>
<ul className="rb:list-disc rb:pl-5 rb:text-sm rb:text-gray-600 rb:mb-3">
<li> URL 访401/403/404</li>
<li> token </li>
<li></li>
<li></li>
</ul> </ul>
<p className="rb:mt-2 rb:text-gray-600"></p>
<div className="rb:mt-4 rb:flex rb:gap-2"> <div className="rb:mt-4 rb:flex rb:gap-2">
<Button icon={<ReloadOutlined />} onClick={handleRetry}> <Button icon={<ReloadOutlined />} onClick={handleRetry}>
</Button> </Button>
{showModeSwitch && !isPdfFile() && ( <Button icon={<DownloadOutlined />} onClick={handleDownload}>
<Button onClick={handleSwitchMode}>
{currentMode === 'office' ? 'Google' : 'Office'} </Button>
</Button>
)}
</div> </div>
</div> </div>
} }
@@ -256,26 +302,23 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
</div> </div>
)} )}
{/* 图片文件预览 */}
{isImageFile() && !error && !loading && ( {isImageFile() && !error && !loading && (
<div className="rb:w-full rb:h-full rb:overflow-auto rb:bg-gray-50 rb:flex rb:items-center rb:justify-center"> <div className="rb:w-full rb:h-full rb:overflow-auto rb:bg-gray-50 rb:flex rb:items-center rb:justify-center">
<img <img
src={fileUrl} src={fileUrl}
alt={fileName || '图片预览'} alt={fileName || '图片预览'}
className="rb:max-w-full rb:max-h-full rb:object-contain" className="rb:max-w-full rb:max-h-full rb:object-contain"
onError={() => setError(true)} onError={() => handleError('图片加载失败')}
/> />
</div> </div>
)} )}
{/* Markdown 文件预览 */}
{isMarkdownFile() && !error && !loading && ( {isMarkdownFile() && !error && !loading && (
<div className="rb:w-full rb:h-full rb:overflow-auto rb:bg-white rb:p-6 rb:rounded rb:border rb:border-gray-200"> <div className="rb:w-full rb:h-full rb:overflow-auto rb:bg-white rb:p-6 rb:rounded rb:border rb:border-gray-200">
<RbMarkdown content={textContent} /> <RbMarkdown content={textContent} />
</div> </div>
)} )}
{/* 文本文件预览 */}
{isTextFile() && !error && !loading && ( {isTextFile() && !error && !loading && (
<div className="rb:w-full rb:h-full rb:overflow-auto rb:bg-white rb:p-4 rb:rounded rb:border rb:border-gray-200"> <div className="rb:w-full rb:h-full rb:overflow-auto rb:bg-white rb:p-4 rb:rounded rb:border rb:border-gray-200">
<pre className="rb:whitespace-pre-wrap rb:text-sm rb:text-gray-800 rb:font-mono"> <pre className="rb:whitespace-pre-wrap rb:text-sm rb:text-gray-800 rb:font-mono">
@@ -284,44 +327,52 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
</div> </div>
)} )}
{/* PDF 文件预览(使用浏览器内置预览) */} {isWordFile() && !error && !loading && (
<div className="rb:w-full rb:h-full rb:overflow-auto rb:bg-white rb:p-6 rb:rounded rb:border rb:border-gray-200">
<div
className="rb:prose rb:max-w-none"
dangerouslySetInnerHTML={{ __html: htmlContent }}
/>
</div>
)}
{isExcelFile() && !error && !loading && (
<div className="rb:w-full rb:h-full rb:overflow-auto rb:bg-white rb:p-4 rb:rounded rb:border rb:border-gray-200">
{excelData.map((sheet, index) => (
<div key={index} className="rb:mb-6">
<h3 className="rb:text-lg rb:font-semibold rb:mb-3">{sheet.sheetName}</h3>
{sheet.data.length > 0 && (
<Table
dataSource={sheet.data.slice(1).map((row, idx) => ({ key: idx, ...row }))}
columns={sheet.data[0]?.map((header: any, colIdx: number) => ({
title: header || `${colIdx + 1}`,
dataIndex: colIdx,
key: colIdx,
width: 150,
})) || []}
pagination={false}
scroll={{ x: 'max-content' }}
size="small"
bordered
/>
)}
</div>
))}
</div>
)}
{isPdfFile() && !error && !loading && ( {isPdfFile() && !error && !loading && (
<iframe <iframe
src={getPreviewUrl()} src={fileUrl}
width="100%" width="100%"
height="100%" height="100%"
title={fileName || 'PDF 预览'} title={fileName || 'PDF 预览'}
className="rb:border-0" className="rb:border-0"
style={{ border: 'none' }} style={{ border: 'none' }}
onLoad={handleLoad}
onError={handleError}
/> />
)} )}
{/* Office 文件预览 */}
{!isTextFile() && !isMarkdownFile() && !isImageFile() && !isPdfFile() && (
<>
{showModeSwitch && !loading && !error && (
<div className="rb:absolute rb:top-2 rb:right-2 rb:z-20">
<Button size="small" onClick={handleSwitchMode}>
{currentMode === 'office' ? 'Google' : 'Office'}
</Button>
</div>
)}
{!error && (
<iframe
src={getPreviewUrl()}
width="100%"
height="100%"
onLoad={handleLoad}
onError={handleError}
title={fileName || '文档预览'}
className="rb:border-0"
style={{ display: loading ? 'none' : 'block', border: 'none' }}
sandbox="allow-scripts allow-same-origin allow-popups"
/>
)}
</>
)}
</div> </div>
); );
}; };

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-02 15:18:19 * @Date: 2026-02-02 15:18:19
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 15:44:42 * @Last Modified time: 2026-03-12 18:36:19
*/ */
/** /**
* PageScrollList Component * PageScrollList Component
@@ -60,8 +60,8 @@ interface PageScrollListProps<T, Q = Record<string, unknown>> {
/** Infinite scroll list component with pagination support */ /** Infinite scroll list component with pagination support */
const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({ const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
renderItem, renderItem,
query, query,
url, url,
column = 4, column = 4,
className = '', className = '',
@@ -69,68 +69,70 @@ const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
}: PageScrollListProps<T, Q>, ref: React.Ref<PageScrollListRef>) => { }: PageScrollListProps<T, Q>, ref: React.Ref<PageScrollListRef>) => {
/** Expose refresh method to parent component */ /** Expose refresh method to parent component */
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
refresh, refresh: () => {
pageRef.current = 1;
loadingRef.current = false;
setHasMore(true);
setData([]);
loadMoreData(true);
},
})); }));
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState<T[]>([]); const [data, setData] = useState<T[]>([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const pageRef = useRef(1);
const loadingRef = useRef(false);
const hasMoreRef = useRef(true);
/** Load more data from API with pagination */ /** Load more data from API with pagination */
const loadMoreData = (flag?: boolean) => { const loadMoreData = (reset?: boolean) => {
if (!flag && (loading || !hasMore)) { if (loadingRef.current || (!reset && !hasMoreRef.current)) return;
return; loadingRef.current = true;
}
setLoading(true); setLoading(true);
const currentPage = reset ? 1 : pageRef.current;
request.get(url, { request.get(url, {
page: page, page: currentPage,
pagesize: PAGE_SIZE, pagesize: PAGE_SIZE,
...(query||{}), ...(query || {}),
}) })
.then((res) => { .then((res) => {
const response = res as ApiResponse<T>; const response = res as ApiResponse<T>;
const results = Array.isArray(response.items) ? response.items : Array.isArray(response) ? response as T[] : []; const results = Array.isArray(response.items) ? response.items : Array.isArray(response) ? response as T[] : [];
// Replace data if flag is true, otherwise append pageRef.current = response.page.page + 1;
if (flag) { setData(prev => reset ? results : [...prev, ...results]);
setData(results); hasMoreRef.current = response.page?.hasnext;
} else {
setData(data.concat(results));
}
setPage(response.page.page + 1);
setHasMore(response.page?.hasnext); setHasMore(response.page?.hasnext);
setLoading(false);
console.log(`${results.length} more items loaded!`);
}) })
.catch(() => { .catch(() => {
setLoading(false); hasMoreRef.current = false;
setHasMore(false); setHasMore(false);
console.error('Failed to load data');
}) })
.finally(() => { .finally(() => {
loadingRef.current = false;
setLoading(false); setLoading(false);
// 内容不足以填满容器时,主动继续加载
setTimeout(() => {
const el = scrollRef.current;
console.log(el, el?.scrollHeight, el?.clientHeight, hasMoreRef.current)
if (el && hasMoreRef.current && el.scrollHeight <= el.clientHeight) {
loadMoreData();
}
}, 0);
}); });
}; };
/** Reset list to initial state and reload data */ /** Reset and reload when query parameters change */
const refresh = () => { const queryKey = JSON.stringify(query);
setPage(1); useEffect(() => {
pageRef.current = 1;
loadingRef.current = false;
hasMoreRef.current = true;
setHasMore(true); setHasMore(true);
setData([]); setData([]);
} loadMoreData(true);
}, [queryKey]);
/** Refresh when query parameters change */
useEffect(() => {
refresh()
}, [query]);
/** Load initial data when list is reset */
useEffect(() => {
if (page === 1 && hasMore && data.length === 0) {
loadMoreData(true);
}
}, [page, hasMore, data])
return ( return (
<> <>
<div <div
@@ -140,7 +142,7 @@ const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
> >
<InfiniteScroll <InfiniteScroll
dataLength={data.length} dataLength={data.length}
next={loadMoreData} next={() => loadMoreData()}
hasMore={hasMore} hasMore={hasMore}
loader={loading && needLoading ? <PageLoading /> : false} loader={loading && needLoading ? <PageLoading /> : false}
// endMessage={<Divider plain>It is all, nothing more 🤐</Divider>} // endMessage={<Divider plain>It is all, nothing more 🤐</Divider>}

View File

@@ -1371,6 +1371,7 @@ export const en = {
gotoList: 'Return to Application List', gotoList: 'Return to Application List',
gotoDetail: 'View Details', gotoDetail: 'View Details',
dify: 'Dify', dify: 'Dify',
pleaseUploadFile: 'Please upload file',
setting: 'Settings', setting: 'Settings',
funConfig: 'Features', funConfig: 'Features',
fileUpload: 'File Upload', fileUpload: 'File Upload',
@@ -2070,12 +2071,14 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
marketUrlPlaceholder: 'Market URL', marketUrlPlaceholder: 'Market URL',
marketCopy: 'Copy', marketCopy: 'Copy',
marketApiKeyOptional: 'Optional', marketApiKeyOptional: 'Optional',
marketApiKeyRequired: 'API Key is required',
marketApiKeyExtra: 'Some markets require an API Key to access the full service list', marketApiKeyExtra: 'Some markets require an API Key to access the full service list',
marketApiKeyPlaceholder: 'Enter API Key to access more services', marketApiKeyPlaceholder: 'Enter API Key to access more services',
marketConnectionStatus: 'Connection Status', marketConnectionStatus: 'Connection Status',
marketConnected: '● Connected', marketConnected: '● Connected',
marketDisconnected: '○ Disconnected', marketDisconnected: '○ Disconnected',
marketConnecting: 'Connecting to {{name}}...', marketConnecting: 'Connecting to {{name}}...',
marketConfigUpdated: '{{name}} configuration updated',
serverUrlInvalid: 'Must start with http:// or https://, and cannot have leading or trailing spaces', serverUrlInvalid: 'Must start with http:// or https://, and cannot have leading or trailing spaces',
requestHeaderKeyInvalid: 'Only English letters, numbers, hyphens (-), and underscores (_) are allowed, and cannot start or end with a hyphen or underscore', requestHeaderKeyInvalid: 'Only English letters, numbers, hyphens (-), and underscores (_) are allowed, and cannot start or end with a hyphen or underscore',
}, },

View File

@@ -754,7 +754,7 @@ export const zh = {
gotoList: '返回应用列表', gotoList: '返回应用列表',
gotoDetail: '查看详情', gotoDetail: '查看详情',
dify: 'Dify', dify: 'Dify',
pleaseUploadFile: '请上传工作流文件', pleaseUploadFile: '请上传文件',
setting: '设置', setting: '设置',
funConfig: '功能', funConfig: '功能',
fileUpload: '文件上传', fileUpload: '文件上传',
@@ -2067,12 +2067,14 @@ export const zh = {
marketUrlPlaceholder: '市场地址', marketUrlPlaceholder: '市场地址',
marketCopy: '复制', marketCopy: '复制',
marketApiKeyOptional: '可选', marketApiKeyOptional: '可选',
marketApiKeyRequired: '请输入 API Key',
marketApiKeyExtra: '部分市场需要 API Key 才能获取完整的服务列表', marketApiKeyExtra: '部分市场需要 API Key 才能获取完整的服务列表',
marketApiKeyPlaceholder: '输入 API Key 以获取更多服务', marketApiKeyPlaceholder: '输入 API Key 以获取更多服务',
marketConnectionStatus: '连接状态', marketConnectionStatus: '连接状态',
marketConnected: '● 已连接', marketConnected: '● 已连接',
marketDisconnected: '○ 未连接', marketDisconnected: '○ 未连接',
marketConnecting: '正在连接 {{name}}...', marketConnecting: '正在连接 {{name}}...',
marketConfigUpdated: '{{name}} 配置已更新',
serverUrlInvalid: '必须以 http:// 或 https:// 开头,且不能有前后空格', serverUrlInvalid: '必须以 http:// 或 https:// 开头,且不能有前后空格',
requestHeaderKeyInvalid: '只支持英文、数字、连字符(-)、下划线(_),不能以连字符或下划线开头结尾', requestHeaderKeyInvalid: '只支持英文、数字、连字符(-)、下划线(_),不能以连字符或下划线开头结尾',
}, },

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-28 14:08:14 * @Date: 2026-02-28 14:08:14
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-06 12:05:46 * @Last Modified time: 2026-03-12 17:19:46
*/ */
/** /**
* UploadModal Component * UploadModal Component
@@ -63,6 +63,7 @@ const UploadModal = forwardRef<UploadModalRef, UploadModalProps>(({
* Resets all states and form fields * Resets all states and form fields
*/ */
const handleClose = () => { const handleClose = () => {
refresh()
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
setCurrent(0); setCurrent(0);
@@ -211,7 +212,6 @@ const UploadModal = forwardRef<UploadModalRef, UploadModalProps>(({
fileSize={100} fileSize={100}
maxCount={1} maxCount={1}
fileType={['yml']} fileType={['yml']}
draggerHeight={200}
/> />
</Form.Item> </Form.Item>
</Form> </Form>

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-28 14:08:14 * @Date: 2026-02-28 14:08:14
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-06 12:05:46 * @Last Modified time: 2026-03-12 17:19:33
*/ */
/** /**
* UploadWorkflowModal Component * UploadWorkflowModal Component
@@ -72,6 +72,7 @@ const UploadWorkflowModal = forwardRef<UploadWorkflowModalRef, UploadWorkflowMod
setFirstFormData(null); setFirstFormData(null);
setAppId(null); setAppId(null);
setLoading(false); setLoading(false);
refresh()
}; };
/** /**

View File

@@ -255,6 +255,7 @@ const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () =>
if (!source) return; if (!source) return;
try { try {
const config: any = await getMarketConfig(sourceId); const config: any = await getMarketConfig(sourceId);
console.log('获取到的配置数据:', config);
marketConfigModalRef.current?.handleOpen({ marketConfigModalRef.current?.handleOpen({
...source, ...source,
connected: config?.status === 1, connected: config?.status === 1,
@@ -306,20 +307,8 @@ const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () =>
})); }));
setConfigIdMap(prev => ({ ...prev, [sourceId]: configId })); setConfigIdMap(prev => ({ ...prev, [sourceId]: configId }));
// 用 configId 获取第一页 MCP 列表 // 使fetchMcpList 获取完整的 MCP 列表(包含激活状态和入库状态)
try { await fetchMcpList(sourceId, 1);
const res: any = await getMarketMCPs({ mcp_market_config_id: configId, page: 1, pagesize: pageSize });
if (res?.items && Array.isArray(res.items)) {
setMcpCache(prev => ({ ...prev, [sourceId]: res.items }));
}
if (res?.page) {
setMcpTotal(res.page.total || 0);
setHasMore(!!res.page.has_next);
setCurrentPage(1);
}
} catch (error) {
console.error('获取 MCP 列表失败:', error);
}
}; };
const handleRefreshAfterAdd = async () => { const handleRefreshAfterAdd = async () => {
@@ -431,7 +420,7 @@ const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () =>
dataLength={mcpList.length} dataLength={mcpList.length}
next={loadMore} next={loadMore}
hasMore={hasMore} hasMore={hasMore}
loader={<Skeleton active paragraph={{ rows: 2 }} className="rb:mt-4" />} loader={null}
scrollableTarget="mcpScrollableDiv" scrollableTarget="mcpScrollableDiv"
> >
<div <div

View File

@@ -37,6 +37,8 @@ const MarketConfigModal = forwardRef<MarketConfigModalRef, MarketConfigModalProp
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [currentSource, setCurrentSource] = useState<MarketSource | null>(null); const [currentSource, setCurrentSource] = useState<MarketSource | null>(null);
const [showApiKey, setShowApiKey] = useState(false); const [showApiKey, setShowApiKey] = useState(false);
const [initialValues, setInitialValues] = useState<{ token: string }>({ token: '' });
const formValues = Form.useWatch([], form);
const handleClose = () => { const handleClose = () => {
setVisible(false); setVisible(false);
@@ -44,16 +46,29 @@ const MarketConfigModal = forwardRef<MarketConfigModalRef, MarketConfigModalProp
setLoading(false); setLoading(false);
setCurrentSource(null); setCurrentSource(null);
setShowApiKey(false); setShowApiKey(false);
setInitialValues({ token: '' });
}; };
const handleOpen = (source: MarketSource) => { const handleOpen = (source: MarketSource) => {
console.log('Modal 接收到的数据:', source);
setCurrentSource(source); setCurrentSource(source);
form.setFieldsValue({ setInitialValues({ token: source.token || '' });
token: source.token || '',
});
setVisible(true); setVisible(true);
}; };
const handleAfterOpenChange = (open: boolean) => {
if (open && currentSource) {
// Modal 完全打开后再设置表单值,使用 setTimeout 确保在下一个事件循环
setTimeout(() => {
form.setFieldsValue({
token: currentSource.token || '',
});
console.log('Modal 打开后设置表单值:', { token: currentSource.token || '' });
console.log('当前表单所有值:', form.getFieldsValue());
}, 100);
}
};
const handleSave = () => { const handleSave = () => {
form form
.validateFields() .validateFields()
@@ -101,6 +116,9 @@ const MarketConfigModal = forwardRef<MarketConfigModalRef, MarketConfigModalProp
} }
}; };
// 检查是否可以保存token 字段必须有值
const canSave = formValues?.token?.trim().length > 0;
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
handleOpen, handleOpen,
handleClose handleClose
@@ -113,9 +131,11 @@ const MarketConfigModal = forwardRef<MarketConfigModalRef, MarketConfigModalProp
title={t('tool.marketConfig', { name: currentSource.name })} title={t('tool.marketConfig', { name: currentSource.name })}
open={visible} open={visible}
onCancel={handleClose} onCancel={handleClose}
afterOpenChange={handleAfterOpenChange}
okText={t('tool.marketSaveAndConnect')} okText={t('tool.marketSaveAndConnect')}
onOk={handleSave} onOk={handleSave}
confirmLoading={loading} confirmLoading={loading}
okButtonProps={{ disabled: !canSave }}
width={600} width={600}
> >
<div> <div>
@@ -147,8 +167,10 @@ const MarketConfigModal = forwardRef<MarketConfigModalRef, MarketConfigModalProp
</div> </div>
<Form <Form
key={currentSource?.id || 'new'}
form={form} form={form}
layout="vertical" layout="vertical"
initialValues={initialValues}
> >
<FormItem label={t('tool.marketUrl')}> <FormItem label={t('tool.marketUrl')}>
<Space.Compact style={{ width: '100%' }}> <Space.Compact style={{ width: '100%' }}>
@@ -169,22 +191,28 @@ const MarketConfigModal = forwardRef<MarketConfigModalRef, MarketConfigModalProp
name="token" name="token"
label={ label={
<span> <span>
API Key <span className="rb:text-gray-400 rb:font-normal">({t('tool.marketApiKeyOptional')})</span> API Key
</span> </span>
} }
rules={[
{ required: true, message: t('tool.marketApiKeyRequired') },
{ whitespace: true, message: t('tool.marketApiKeyRequired') }
]}
extra={<span style={{ display: 'inline-block', marginTop: 8 }}>{t('tool.marketApiKeyExtra')}</span>} extra={<span style={{ display: 'inline-block', marginTop: 8 }}>{t('tool.marketApiKeyExtra')}</span>}
> >
<Space.Compact style={{ width: '100%' }}> <Input
<Input type={showApiKey ? 'text' : 'password'}
type={showApiKey ? 'text' : 'password'} placeholder={t('tool.marketApiKeyPlaceholder')}
placeholder={t('tool.marketApiKeyPlaceholder')} autoComplete="off"
autoComplete="off" suffix={
/> <Button
<Button type="text"
icon={showApiKey ? <EyeInvisibleOutlined /> : <EyeOutlined />} size="small"
onClick={() => setShowApiKey(!showApiKey)} icon={showApiKey ? <EyeInvisibleOutlined /> : <EyeOutlined />}
/> onClick={() => setShowApiKey(!showApiKey)}
</Space.Compact> />
}
/>
</FormItem> </FormItem>
<div className="rb:flex rb:items-center rb:gap-2 rb:p-3 rb:bg-gray-50 rb:rounded rb:text-sm"> <div className="rb:flex rb:items-center rb:gap-2 rb:p-3 rb:bg-gray-50 rb:rounded rb:text-sm">

View File

@@ -41,6 +41,7 @@ const McpServiceModal = forwardRef<McpServiceModalRef, McpServiceModalProps>(({
const values = Form.useWatch<MCPToolItem>([], form) const values = Form.useWatch<MCPToolItem>([], form)
const requestHeaderModalRef = useRef<RequestHeaderModalRef>(null) const requestHeaderModalRef = useRef<RequestHeaderModalRef>(null)
const [requestHeaderList, setRequestHeaderList] = useState<RequestHeader[]>([]) const [requestHeaderList, setRequestHeaderList] = useState<RequestHeader[]>([])
const abortControllerRef = useRef<AbortController | null>(null)
const formatTabItems = () => { const formatTabItems = () => {
return tabKeys.map(key => ({ return tabKeys.map(key => ({
@@ -54,6 +55,12 @@ const McpServiceModal = forwardRef<McpServiceModalRef, McpServiceModalProps>(({
// 封装取消方法,添加关闭弹窗逻辑 // 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => { const handleClose = () => {
// 如果有正在进行的请求,取消它
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
setVisible(false); setVisible(false);
form.resetFields(); form.resetFields();
setLoading(false); setLoading(false);
@@ -103,6 +110,10 @@ const McpServiceModal = forwardRef<McpServiceModalRef, McpServiceModalProps>(({
.validateFields() .validateFields()
.then(() => { .then(() => {
setLoading(true); setLoading(true);
// 创建 AbortController 用于取消请求
abortControllerRef.current = new AbortController();
// 创建新服务对象 // 创建新服务对象
const { config, ...rest } = values const { config, ...rest } = values
@@ -129,8 +140,13 @@ const McpServiceModal = forwardRef<McpServiceModalRef, McpServiceModalProps>(({
(newService.config as any).mcp_service_id = (editVo as any).mcp_service_id; (newService.config as any).mcp_service_id = (editVo as any).mcp_service_id;
} }
const request = editVo?.id ? updateTool(editVo.id, newService) : addTool(newService) const request = editVo?.id
? updateTool(editVo.id, newService, { signal: abortControllerRef.current.signal })
: addTool(newService, { signal: abortControllerRef.current.signal })
request.then((res: any) => { request.then((res: any) => {
// 清除 AbortController
abortControllerRef.current = null;
message.success(t('common.saveSuccess')); message.success(t('common.saveSuccess'));
setLoading(false); setLoading(false);
handleClose(); handleClose();
@@ -141,7 +157,16 @@ const McpServiceModal = forwardRef<McpServiceModalRef, McpServiceModalProps>(({
console.error('测试连接失败:', err); console.error('测试连接失败:', err);
}); });
}) })
.catch(() => { .catch((error) => {
// 清除 AbortController
abortControllerRef.current = null;
// 如果是用户主动取消,不显示错误提示
if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
console.log('请求已取消');
} else {
message.error(t('common.saveFailed'));
}
setLoading(false); setLoading(false);
}) })
}) })
@@ -171,7 +196,13 @@ const McpServiceModal = forwardRef<McpServiceModalRef, McpServiceModalProps>(({
onCancel={handleClose} onCancel={handleClose}
okText={t('tool.saveAndTest')} okText={t('tool.saveAndTest')}
onOk={handleSave} onOk={handleSave}
confirmLoading={loading} okButtonProps={{ loading: loading }}
footer={(_, { OkBtn, CancelBtn }) => (
<>
<CancelBtn />
<OkBtn />
</>
)}
> >
<Form <Form
form={form} form={form}

View File

@@ -1,8 +1,8 @@
/* /*
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 17:57:11 * @Date: 2026-02-03 17:57:11
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 17:57:11 * @Last Modified time: 2026-03-12 18:00:11
*/ */
/** /**
* RAG User Memory Detail View * RAG User Memory Detail View
@@ -150,9 +150,12 @@ const Rag: FC = () => {
}) })
} }
return ( return (
<Row gutter={[16, 16]} className="rb:pb-6"> <Row gutter={[16, 16]} className="rb:h-full!">
<Col span={8}> <Col span={8}>
<RbCard> <RbCard
className="rb:h-[calc(100vh-104px)]!"
bodyClassName="rb:overflow-y-auto! rb:h-full!"
>
<div className="rb:flex rb:items-center"> <div className="rb:flex rb:items-center">
<div className="rb:flex-[0_0_auto] rb:w-20 rb:h-20 rb:text-center rb:font-semibold rb:text-[28px] rb:leading-20 rb:rounded-lg rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name?.[0]}</div> <div className="rb:flex-[0_0_auto] rb:w-20 rb:h-20 rb:text-center rb:font-semibold rb:text-[28px] rb:leading-20 rb:rounded-lg rb:text-[#FBFDFF] rb:bg-[#155EEF]">{name?.[0]}</div>
<Flex> <Flex>

View File

@@ -1,74 +1,43 @@
/* /*
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 18:34:04 * @Date: 2026-02-03 18:34:04
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-02-03 18:34:04 * @Last Modified time: 2026-03-12 18:34:52
*/ */
/** import { type FC } from 'react'
* Conversation Memory Component
* Displays RAG conversation memory content list
*/
import { type FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { Skeleton, List } from 'antd';
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard/Card'
import Empty from '@/components/Empty'; import PageScrollList from '@/components/PageScrollList'
import Markdown from '@/components/Markdown' import Markdown from '@/components/Markdown'
import { import { getRagContentUrl } from '@/api/memory'
getRagContent
} from '@/api/memory'
const ConversationMemory:FC = () => { const ConversationMemory: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { id } = useParams() const { id } = useParams()
const [loading, setLoading] = useState<boolean>(true)
const [list, setList] = useState<string[]>([])
useEffect(() => {
if (!id) return
getList()
}, [id])
/** Fetch conversation memory list */
const getList = () => {
if (!id) return
setLoading(true)
getRagContent(id).then((res) => {
setList((res as { contents?: [] }).contents || [])
})
.finally(() => {
setLoading(false)
})
}
return ( return (
<RbCard <RbCard
title={t('userMemory.conversationMemory')} title={t('userMemory.conversationMemory')}
headerClassName="rb:text-[18px]! rb:leading-[24px]" headerClassName="rb:text-[18px]! rb:leading-[24px]"
bodyClassName="rb:h-[100%]! rb:overflow-hidden rb:py-0!" bodyClassName="rb:h-[calc(100%-56px)]! rb:overflow-hidden"
className="rb:h-[calc(100vh-104px)]!"
> >
{loading <PageScrollList<string>
? <Skeleton /> url={getRagContentUrl}
: list.length > 0 query={{ end_user_id: id }}
? <List column={1}
dataSource={list} renderItem={(item) => (
grid={{ gutter: 12, column: 1 }} <div className="rb:rounded-lg rb:border rb:border-[#DFE4ED] rb:px-4 rb:py-3 rb:bg-[#F0F3F8] rb:text-gray-800 rb:text-sm">
renderItem={(item, index) => ( <Markdown content={item} />
<List.Item> </div>
<div )}
key={index} className="rb:h-full!"
className="rb:rounded-lg rb:border rb:border-[#DFE4ED] rb:px-4 rb:py-3 rb:bg-[#F0F3F8] rb:mt-2 rb:text-gray-800 rb:text-sm" // className="rb:h-[calc(100%-24px)]!"
> />
<Markdown content={item} />
</div>
</List.Item>
)}
/>
: <Empty className="rb:h-full" />
}
</RbCard> </RbCard>
) )
} }
export default ConversationMemory
export default ConversationMemory