From f27de7df35e20d7080996bd9083ade30559ac427 Mon Sep 17 00:00:00 2001 From: Ke Sun Date: Tue, 3 Feb 2026 15:52:45 +0800 Subject: [PATCH 01/14] feat(memory): add long-term storage task routing and batching --- api/app/celery_app.py | 5 + api/app/core/agent/langchain_agent.py | 14 +- .../langgraph_graph/routing/write_router.py | 1 + .../agent/langgraph_graph/write_graph.py | 72 +++-- api/app/tasks.py | 288 ++++++++++++++++++ 5 files changed, 353 insertions(+), 27 deletions(-) diff --git a/api/app/celery_app.py b/api/app/celery_app.py index 3e7db8cb..727cd041 100644 --- a/api/app/celery_app.py +++ b/api/app/celery_app.py @@ -64,6 +64,11 @@ celery_app.conf.update( 'app.core.memory.agent.read_message': {'queue': 'memory_tasks'}, 'app.core.memory.agent.write_message': {'queue': 'memory_tasks'}, + # Long-term storage tasks → memory_tasks queue (batched write strategies) + 'app.core.memory.agent.long_term_storage.window': {'queue': 'memory_tasks'}, + 'app.core.memory.agent.long_term_storage.time': {'queue': 'memory_tasks'}, + 'app.core.memory.agent.long_term_storage.aggregate': {'queue': 'memory_tasks'}, + # Document tasks → document_tasks queue (prefork worker) 'app.core.rag.tasks.parse_document': {'queue': 'document_tasks'}, 'app.core.rag.tasks.build_graphrag_for_kb': {'queue': 'document_tasks'}, diff --git a/api/app/core/agent/langchain_agent.py b/api/app/core/agent/langchain_agent.py index 441609ac..58cf9443 100644 --- a/api/app/core/agent/langchain_agent.py +++ b/api/app/core/agent/langchain_agent.py @@ -148,6 +148,7 @@ class LangChainAgent: messages.append(HumanMessage(content=user_content)) return messages + # TODO: 移到memory module async def term_memory_save(self,long_term_messages,actual_config_id,end_user_id,type): db = next(get_db()) scope=6 @@ -307,9 +308,12 @@ class LangChainAgent: elapsed_time = time.time() - start_time if memory_flag: long_term_messages=await agent_chat_messages(message_chat,content) - # AI 回复写入(用户消息和 AI 回复配对,一次性写入完整对话) + # TODO: DUPLICATE WRITE - Remove this immediate write once batched write (term_memory_save) is verified stable. + # This writes to Neo4j immediately via Celery task, but term_memory_save also writes to Neo4j + # when the window buffer reaches scope (6 messages). This causes duplicate entities in the graph. + # Recommended: Keep only term_memory_save for batched efficiency, or only self.write for real-time. await self.write(storage_type, actual_end_user_id, message_chat, content, user_rag_memory_id, actual_end_user_id, actual_config_id) - '''长期''' + # Batched long-term memory storage (Redis buffer + Neo4j when window full) await self.term_memory_save(long_term_messages,actual_config_id,end_user_id,"chunk") response = { "content": content, @@ -441,9 +445,13 @@ class LangChainAgent: yield total_tokens break if memory_flag: - # AI 回复写入(用户消息和 AI 回复配对,一次性写入完整对话) + # TODO: DUPLICATE WRITE - Remove this immediate write once batched write (term_memory_save) is verified stable. + # This writes to Neo4j immediately via Celery task, but term_memory_save also writes to Neo4j + # when the window buffer reaches scope (6 messages). This causes duplicate entities in the graph. + # Recommended: Keep only term_memory_save for batched efficiency, or only self.write for real-time. long_term_messages = await agent_chat_messages(message_chat, full_content) await self.write(storage_type, end_user_id, message_chat, full_content, user_rag_memory_id, end_user_id, actual_config_id) + # Batched long-term memory storage (Redis buffer + Neo4j when window full) await self.term_memory_save(long_term_messages, actual_config_id, end_user_id, "chunk") except Exception as e: diff --git a/api/app/core/memory/agent/langgraph_graph/routing/write_router.py b/api/app/core/memory/agent/langgraph_graph/routing/write_router.py index d6fbbb38..6650b9b0 100644 --- a/api/app/core/memory/agent/langgraph_graph/routing/write_router.py +++ b/api/app/core/memory/agent/langgraph_graph/routing/write_router.py @@ -43,6 +43,7 @@ async def write_messages(end_user_id,langchain_messages,memory_config): for node_name, node_data in update_event.items(): if 'save_neo4j' == node_name: massages = node_data + # TODO:删除 massagesstatus = massages.get('write_result')['status'] contents = massages.get('write_result') print(contents) diff --git a/api/app/core/memory/agent/langgraph_graph/write_graph.py b/api/app/core/memory/agent/langgraph_graph/write_graph.py index d0e8a45d..84ea9381 100644 --- a/api/app/core/memory/agent/langgraph_graph/write_graph.py +++ b/api/app/core/memory/agent/langgraph_graph/write_graph.py @@ -1,18 +1,14 @@ import asyncio -import json import sys import warnings from contextlib import asynccontextmanager from langgraph.constants import END, START from langgraph.graph import StateGraph -from app.core.memory.agent.langgraph_graph.tools.write_tool import format_parsing, chat_data_format, messages_parse -from app.db import get_db from app.core.logging_config import get_agent_logger from app.core.memory.agent.utils.llm_tools import WriteState from app.core.memory.agent.langgraph_graph.nodes.write_nodes import write_node -from app.services.memory_config_service import MemoryConfigService warnings.filterwarnings("ignore", category=RuntimeWarning) logger = get_agent_logger(__name__) @@ -40,27 +36,55 @@ async def make_write_graph(): yield graph async def long_term_storage(long_term_type:str="chunk",langchain_messages:list=[],memory_config:str='',end_user_id:str='',scope:int=6): - from app.core.memory.agent.langgraph_graph.routing.write_router import memory_long_term_storage, window_dialogue,aggregate_judgment - from app.core.memory.agent.langgraph_graph.tools.write_tool import chat_data_format - from app.core.memory.agent.utils.redis_tool import write_store - write_store.save_session_write(end_user_id, await chat_data_format(langchain_messages)) - # 获取数据库会话 - db_session = next(get_db()) - config_service = MemoryConfigService(db_session) - memory_config = config_service.load_memory_config( - config_id=memory_config, # 改为整数 - service_name="MemoryAgentService" + """Dispatch long-term memory storage to Celery background tasks. + + Args: + long_term_type: Storage strategy - 'chunk' (window), 'time', or 'aggregate' + langchain_messages: List of messages to store + memory_config: Memory configuration ID (string) + end_user_id: End user identifier + scope: Window size for 'chunk' strategy (default: 6) + """ + from app.tasks import ( + long_term_storage_window_task, + # TODO: Uncomment when implemented + # long_term_storage_time_task, + # long_term_storage_aggregate_task, ) - if long_term_type=='chunk': - '''方案一:对话窗口6轮对话''' - await window_dialogue(end_user_id,langchain_messages,memory_config,scope) - if long_term_type=='time': - """时间""" - await memory_long_term_storage(end_user_id, memory_config,5) - if long_term_type=='aggregate': - - """方案三:聚合判断""" - await aggregate_judgment(end_user_id, langchain_messages, memory_config) + from app.core.logging_config import get_logger + + logger = get_logger(__name__) + + # Convert config to string if needed + config_id = str(memory_config) if memory_config else '' + + if long_term_type == 'chunk': + # Strategy 1: Window-based batching (6 rounds of dialogue) + logger.info(f"[LONG_TERM] Dispatching window task - end_user_id={end_user_id}, scope={scope}") + long_term_storage_window_task.delay( + end_user_id=end_user_id, + langchain_messages=langchain_messages, + config_id=config_id, + scope=scope + ) + # TODO: Uncomment when time-based strategy is fully implemented + # elif long_term_type == 'time': + # # Strategy 2: Time-based retrieval + # logger.info(f"[LONG_TERM] Dispatching time task - end_user_id={end_user_id}") + # long_term_storage_time_task.delay( + # end_user_id=end_user_id, + # config_id=config_id, + # time_window=5 + # ) + # TODO: Uncomment when aggregate strategy is fully implemented + # elif long_term_type == 'aggregate': + # # Strategy 3: Aggregate judgment (deduplication) + # logger.info(f"[LONG_TERM] Dispatching aggregate task - end_user_id={end_user_id}") + # long_term_storage_aggregate_task.delay( + # end_user_id=end_user_id, + # langchain_messages=langchain_messages, + # config_id=config_id + # ) # async def main(): diff --git a/api/app/tasks.py b/api/app/tasks.py index 48b41e4f..db332816 100644 --- a/api/app/tasks.py +++ b/api/app/tasks.py @@ -1066,6 +1066,7 @@ def workspace_reflection_task(self) -> Dict[str, Any]: f"工作空间 {workspace_id} 反思处理完成,处理了 {len(workspace_reflection_results)} 个任务") except Exception as e: + db.rollback() # Rollback failed transaction to allow next query api_logger.error(f"处理工作空间 {workspace_id} 反思失败: {str(e)}") all_reflection_results.append({ "workspace_id": str(workspace_id), @@ -1204,3 +1205,290 @@ def run_forgetting_cycle_task(self, config_id: Optional[uuid.UUID] = None) -> Di return result finally: loop.close() + + +# ============================================================================= +# Long-term Memory Storage Tasks (Batched Write Strategies) +# ============================================================================= + +@celery_app.task(name="app.core.memory.agent.long_term_storage.window", bind=True) +def long_term_storage_window_task( + self, + end_user_id: str, + langchain_messages: List[Dict[str, Any]], + config_id: str, + scope: int = 6 +) -> Dict[str, Any]: + """Celery task for window-based long-term memory storage. + + Accumulates messages in Redis buffer until window size (scope) is reached, + then writes batched messages to Neo4j. + + Args: + end_user_id: End user identifier + langchain_messages: List of messages [{"role": "user/assistant", "content": "..."}] + config_id: Memory configuration ID + scope: Window size (number of messages before triggering write) + + Returns: + Dict containing task status and metadata + """ + from app.core.logging_config import get_logger + logger = get_logger(__name__) + + logger.info(f"[LONG_TERM_WINDOW] Starting task - end_user_id={end_user_id}, scope={scope}") + start_time = time.time() + + async def _run() -> Dict[str, Any]: + from app.core.memory.agent.langgraph_graph.routing.write_router import window_dialogue + from app.core.memory.agent.langgraph_graph.tools.write_tool import chat_data_format + from app.core.memory.agent.utils.redis_tool import write_store + from app.services.memory_config_service import MemoryConfigService + + db = next(get_db()) + try: + # Save to Redis buffer first + write_store.save_session_write(end_user_id, await chat_data_format(langchain_messages)) + + # Load memory config + config_service = MemoryConfigService(db) + memory_config = config_service.load_memory_config( + config_id=config_id, + service_name="LongTermStorageTask" + ) + + # Execute window-based dialogue storage + await window_dialogue(end_user_id, langchain_messages, memory_config, scope) + + return {"status": "SUCCESS", "strategy": "window", "scope": scope} + finally: + db.close() + + try: + import nest_asyncio + nest_asyncio.apply() + except ImportError: + pass + + try: + loop = asyncio.get_event_loop() + if loop.is_closed(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + try: + result = loop.run_until_complete(_run()) + elapsed_time = time.time() - start_time + + logger.info(f"[LONG_TERM_WINDOW] Task completed - elapsed_time={elapsed_time:.2f}s") + + return { + **result, + "end_user_id": end_user_id, + "config_id": config_id, + "elapsed_time": elapsed_time, + "task_id": self.request.id + } + except Exception as e: + elapsed_time = time.time() - start_time + logger.error(f"[LONG_TERM_WINDOW] Task failed - error={str(e)}", exc_info=True) + + return { + "status": "FAILURE", + "strategy": "window", + "error": str(e), + "end_user_id": end_user_id, + "config_id": config_id, + "elapsed_time": elapsed_time, + "task_id": self.request.id + } + + +# @celery_app.task(name="app.core.memory.agent.long_term_storage.time", bind=True) +# def long_term_storage_time_task( +# self, +# end_user_id: str, +# config_id: str, +# time_window: int = 5 +# ) -> Dict[str, Any]: +# """Celery task for time-based long-term memory storage. + +# Retrieves recent sessions from Redis within time window and writes to Neo4j. + +# Args: +# end_user_id: End user identifier +# config_id: Memory configuration ID +# time_window: Time window in minutes for retrieving recent sessions + +# Returns: +# Dict containing task status and metadata +# """ +# from app.core.logging_config import get_logger +# logger = get_logger(__name__) + +# logger.info(f"[LONG_TERM_TIME] Starting task - end_user_id={end_user_id}, time_window={time_window}") +# start_time = time.time() + +# async def _run() -> Dict[str, Any]: +# from app.core.memory.agent.langgraph_graph.routing.write_router import memory_long_term_storage +# from app.services.memory_config_service import MemoryConfigService + +# db = next(get_db()) +# try: +# # Load memory config +# config_service = MemoryConfigService(db) +# memory_config = config_service.load_memory_config( +# config_id=config_id, +# service_name="LongTermStorageTask" +# ) + +# # Execute time-based storage +# await memory_long_term_storage(end_user_id, memory_config, time_window) + +# return {"status": "SUCCESS", "strategy": "time", "time_window": time_window} +# finally: +# db.close() + +# try: +# import nest_asyncio +# nest_asyncio.apply() +# except ImportError: +# pass + +# try: +# loop = asyncio.get_event_loop() +# if loop.is_closed(): +# loop = asyncio.new_event_loop() +# asyncio.set_event_loop(loop) +# except RuntimeError: +# loop = asyncio.new_event_loop() +# asyncio.set_event_loop(loop) + +# try: +# result = loop.run_until_complete(_run()) +# elapsed_time = time.time() - start_time + +# logger.info(f"[LONG_TERM_TIME] Task completed - elapsed_time={elapsed_time:.2f}s") + +# return { +# **result, +# "end_user_id": end_user_id, +# "config_id": config_id, +# "elapsed_time": elapsed_time, +# "task_id": self.request.id +# } +# except Exception as e: +# elapsed_time = time.time() - start_time +# logger.error(f"[LONG_TERM_TIME] Task failed - error={str(e)}", exc_info=True) + +# return { +# "status": "FAILURE", +# "strategy": "time", +# "error": str(e), +# "end_user_id": end_user_id, +# "config_id": config_id, +# "elapsed_time": elapsed_time, +# "task_id": self.request.id +# } + + +# @celery_app.task(name="app.core.memory.agent.long_term_storage.aggregate", bind=True) +# def long_term_storage_aggregate_task( +# self, +# end_user_id: str, +# langchain_messages: List[Dict[str, Any]], +# config_id: str +# ) -> Dict[str, Any]: +# """Celery task for aggregate-based long-term memory storage. + +# Uses LLM to determine if new messages describe the same event as history. +# Only writes to Neo4j if messages represent new information (not duplicates). + +# Args: +# end_user_id: End user identifier +# langchain_messages: List of messages [{"role": "user/assistant", "content": "..."}] +# config_id: Memory configuration ID + +# Returns: +# Dict containing task status, is_same_event flag, and metadata +# """ +# from app.core.logging_config import get_logger +# logger = get_logger(__name__) + +# logger.info(f"[LONG_TERM_AGGREGATE] Starting task - end_user_id={end_user_id}") +# start_time = time.time() + +# async def _run() -> Dict[str, Any]: +# from app.core.memory.agent.langgraph_graph.routing.write_router import aggregate_judgment +# from app.core.memory.agent.langgraph_graph.tools.write_tool import chat_data_format +# from app.core.memory.agent.utils.redis_tool import write_store +# from app.services.memory_config_service import MemoryConfigService + +# db = next(get_db()) +# try: +# # Save to Redis buffer first +# write_store.save_session_write(end_user_id, await chat_data_format(langchain_messages)) + +# # Load memory config +# config_service = MemoryConfigService(db) +# memory_config = config_service.load_memory_config( +# config_id=config_id, +# service_name="LongTermStorageTask" +# ) + +# # Execute aggregate judgment +# result = await aggregate_judgment(end_user_id, langchain_messages, memory_config) + +# return { +# "status": "SUCCESS", +# "strategy": "aggregate", +# "is_same_event": result.get("is_same_event", False), +# "wrote_to_neo4j": not result.get("is_same_event", False) +# } +# finally: +# db.close() + +# try: +# import nest_asyncio +# nest_asyncio.apply() +# except ImportError: +# pass + +# try: +# loop = asyncio.get_event_loop() +# if loop.is_closed(): +# loop = asyncio.new_event_loop() +# asyncio.set_event_loop(loop) +# except RuntimeError: +# loop = asyncio.new_event_loop() +# asyncio.set_event_loop(loop) + +# try: +# result = loop.run_until_complete(_run()) +# elapsed_time = time.time() - start_time + +# logger.info(f"[LONG_TERM_AGGREGATE] Task completed - is_same_event={result.get('is_same_event')}, elapsed_time={elapsed_time:.2f}s") + +# return { +# **result, +# "end_user_id": end_user_id, +# "config_id": config_id, +# "elapsed_time": elapsed_time, +# "task_id": self.request.id +# } +# except Exception as e: +# elapsed_time = time.time() - start_time +# logger.error(f"[LONG_TERM_AGGREGATE] Task failed - error={str(e)}", exc_info=True) + +# return { +# "status": "FAILURE", +# "strategy": "aggregate", +# "error": str(e), +# "end_user_id": end_user_id, +# "config_id": config_id, +# "elapsed_time": elapsed_time, +# "task_id": self.request.id +# } From 151fd3b9507adfa0dc79eeae86ef56a3294e567c Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 3 Feb 2026 17:22:58 +0800 Subject: [PATCH 02/14] fix(web): PageScrollList loading update --- web/src/components/PageScrollList/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/PageScrollList/index.tsx b/web/src/components/PageScrollList/index.tsx index bea97b04..8527554d 100644 --- a/web/src/components/PageScrollList/index.tsx +++ b/web/src/components/PageScrollList/index.tsx @@ -106,7 +106,7 @@ const PageScrollList = forwardRef(>({ dataLength={data.length} next={loadMoreData} hasMore={hasMore} - loader={needLoading ? : undefined} + loader={loading && needLoading ? : undefined} // endMessage={It is all, nothing more 🤐} scrollableTarget="scrollableDiv" className='rb:h-full!' From 72076c218fa7daf6ce3f19bfc9e9d4feffc4c9a6 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 3 Feb 2026 17:23:40 +0800 Subject: [PATCH 03/14] fix(web): PageScrollList loading update --- web/src/components/PageScrollList/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/PageScrollList/index.tsx b/web/src/components/PageScrollList/index.tsx index 8527554d..4cdfc62e 100644 --- a/web/src/components/PageScrollList/index.tsx +++ b/web/src/components/PageScrollList/index.tsx @@ -106,7 +106,7 @@ const PageScrollList = forwardRef(>({ dataLength={data.length} next={loadMoreData} hasMore={hasMore} - loader={loading && needLoading ? : undefined} + loader={loading && needLoading ? : false} // endMessage={It is all, nothing more 🤐} scrollableTarget="scrollableDiv" className='rb:h-full!' From cfcb2784062c4e558e7079af3090e91bb1ccf4a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=90=E5=8A=9B=E9=BD=90?= <162269739+lanceyq@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:42:54 +0800 Subject: [PATCH 04/14] Ontology v1 bug (#291) * [changes]Add 'id' as the secondary sorting key, and 'scene_id' now returns a UUID object * [fix]Fix the "end_user" return to be sorted by update time. * [fix]Set the default values of the memory configuration model based on the spatial model. * [fix]Remove the entity extraction check combination model, read the configuration list, and add the return of scene_id * [fix]Fix the "end_user" return to be sorted by update time. * [fix] --- api/app/controllers/ontology_controller.py | 8 -------- api/app/repositories/memory_config_repository.py | 2 ++ api/app/schemas/memory_storage_schema.py | 2 ++ api/app/services/memory_dashboard_service.py | 10 +++++++++- api/app/services/memory_storage_service.py | 7 +++++++ 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/api/app/controllers/ontology_controller.py b/api/app/controllers/ontology_controller.py index 1cf8e64e..43d3b1d2 100644 --- a/api/app/controllers/ontology_controller.py +++ b/api/app/controllers/ontology_controller.py @@ -182,14 +182,6 @@ def _get_ontology_service( detail=f"找不到指定的LLM模型: {llm_id}" ) - # 检查是否为组合模型 - if hasattr(model_config, 'is_composite') and model_config.is_composite: - logger.error(f"Model {llm_id} is a composite model, which is not supported for ontology extraction") - raise HTTPException( - status_code=400, - detail="本体提取不支持使用组合模型,请选择单个模型" - ) - # 验证模型配置了API密钥 if not model_config.api_keys: logger.error(f"Model {llm_id} has no API key configuration") diff --git a/api/app/repositories/memory_config_repository.py b/api/app/repositories/memory_config_repository.py index 568c262f..22972669 100644 --- a/api/app/repositories/memory_config_repository.py +++ b/api/app/repositories/memory_config_repository.py @@ -235,6 +235,8 @@ class MemoryConfigRepository: llm_id=params.llm_id, embedding_id=params.embedding_id, rerank_id=params.rerank_id, + reflection_model_id=params.reflection_model_id, + emotion_model_id=params.emotion_model_id, ) db.add(db_config) db.flush() # 获取自增ID但不提交事务 diff --git a/api/app/schemas/memory_storage_schema.py b/api/app/schemas/memory_storage_schema.py index 5e22d70f..11cacda0 100644 --- a/api/app/schemas/memory_storage_schema.py +++ b/api/app/schemas/memory_storage_schema.py @@ -236,6 +236,8 @@ class ConfigParamsCreate(BaseModel): # 创建配置参数模型(仅 body, llm_id: Optional[str] = Field(None, description="LLM模型配置ID") embedding_id: Optional[str] = Field(None, description="嵌入模型配置ID") rerank_id: Optional[str] = Field(None, description="重排序模型配置ID") + reflection_model_id: Optional[str] = Field(None, description="反思模型ID,默认与llm_id一致") + emotion_model_id: Optional[str] = Field(None, description="情绪分析模型ID,默认与llm_id一致") class ConfigParamsDelete(BaseModel): # 删除配置参数模型(请求体) diff --git a/api/app/services/memory_dashboard_service.py b/api/app/services/memory_dashboard_service.py index 06a94060..6fa8b228 100644 --- a/api/app/services/memory_dashboard_service.py +++ b/api/app/services/memory_dashboard_service.py @@ -53,7 +53,10 @@ def get_workspace_end_users( workspace_id: uuid.UUID, current_user: User ) -> List[EndUser]: - """获取工作空间的所有宿主(优化版本:减少数据库查询次数)""" + """获取工作空间的所有宿主(优化版本:减少数据库查询次数) + + 返回结果按 updated_at 从新到旧排序(NULL 值排在最后) + """ business_logger.info(f"获取工作空间宿主列表: workspace_id={workspace_id}, 操作者: {current_user.username}") try: @@ -68,9 +71,14 @@ def get_workspace_end_users( app_ids = [app.id for app in apps_orm] # 批量查询所有 end_users(一次查询而非循环查询) + # 按 updated_at 降序排序,NULL 值排在最后;id 作为次级排序键保证确定性 from app.models.end_user_model import EndUser as EndUserModel + from sqlalchemy import desc, nullslast end_users_orm = db.query(EndUserModel).filter( EndUserModel.app_id.in_(app_ids) + ).order_by( + nullslast(desc(EndUserModel.updated_at)), + desc(EndUserModel.id) ).all() # 转换为 Pydantic 模型(只在需要时转换) diff --git a/api/app/services/memory_storage_service.py b/api/app/services/memory_storage_service.py index eec1007b..741199c6 100644 --- a/api/app/services/memory_storage_service.py +++ b/api/app/services/memory_storage_service.py @@ -129,6 +129,12 @@ class DataConfigService: # 数据配置服务类(PostgreSQL) if not params.rerank_id: params.rerank_id = configs.get('rerank') + # reflection_model_id 和 emotion_model_id 默认与 llm_id 一致 + if not params.reflection_model_id: + params.reflection_model_id = params.llm_id + if not params.emotion_model_id: + params.emotion_model_id = params.llm_id + config = MemoryConfigRepository.create(self.db, params) self.db.commit() return {"affected": 1, "config_id": config.config_id} @@ -203,6 +209,7 @@ class DataConfigService: # 数据配置服务类(PostgreSQL) "end_user_id": config.end_user_id, "config_id_old": config_id_old, "apply_id": config.apply_id, + "scene_id": config.scene_id, "llm_id": config.llm_id, "embedding_id": config.embedding_id, "rerank_id": config.rerank_id, From 3efb3e8a35ff70295ddb3a3e157c8da4b834100c Mon Sep 17 00:00:00 2001 From: Ke Sun Date: Tue, 3 Feb 2026 18:50:59 +0800 Subject: [PATCH 05/14] fix(memory): add Redis session validation - Add macOS fork() safety configuration in celery_app.py to prevent initialization issues - Add null/False checks for Redis session queries in term_memory_save to handle missing sessions gracefully - Add null/False checks in memory_long_term_storage to prevent processing empty Redis results - Add null/False checks in aggregate_judgment before format_parsing to avoid errors on missing data - Initialize redis_messages variable in window_dialogue for consistency - Add debug logging when no existing session found in Redis for better troubleshooting - Add TODO comments for magic numbers (scope=6, time=5) to be extracted as constants - Improve error handling when Redis returns False or empty results instead of crashing --- api/app/celery_app.py | 4 ++++ api/app/core/agent/langchain_agent.py | 14 ++++++++++++++ .../agent/langgraph_graph/routing/write_router.py | 9 +++++++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/api/app/celery_app.py b/api/app/celery_app.py index 727cd041..002547f6 100644 --- a/api/app/celery_app.py +++ b/api/app/celery_app.py @@ -7,6 +7,10 @@ from celery import Celery from app.core.config import settings +# macOS fork() safety - must be set before any Celery initialization +if platform.system() == 'Darwin': + os.environ.setdefault('OBJC_DISABLE_INITIALIZE_FORK_SAFETY', 'YES') + # 创建 Celery 应用实例 # broker: 任务队列(使用 Redis DB 0) # backend: 结果存储(使用 Redis DB 10) diff --git a/api/app/core/agent/langchain_agent.py b/api/app/core/agent/langchain_agent.py index 58cf9443..7e0015ae 100644 --- a/api/app/core/agent/langchain_agent.py +++ b/api/app/core/agent/langchain_agent.py @@ -151,6 +151,7 @@ class LangChainAgent: # TODO: 移到memory module async def term_memory_save(self,long_term_messages,actual_config_id,end_user_id,type): db = next(get_db()) + #TODO: 魔法数字 scope=6 try: @@ -160,6 +161,12 @@ class LangChainAgent: from app.core.memory.agent.utils.redis_tool import write_store result = write_store.get_session_by_userid(end_user_id) + + # Handle case where no session exists in Redis (returns False) + if not result or result is False: + logger.debug(f"No existing session in Redis for user {end_user_id}, skipping short-term memory update") + return + if type=="chunk" or type=="aggregate": data = await format_parsing(result, "dict") chunk_data = data[:scope] @@ -167,7 +174,14 @@ class LangChainAgent: repo.upsert(end_user_id, chunk_data) logger.info(f'写入短长期:') else: + # TODO: This branch handles type="time" strategy, currently unused. + # Will be activated when time-based long-term storage is implemented. + # TODO: 魔法数字 - extract 5 to a constant long_time_data = write_store.find_user_recent_sessions(end_user_id, 5) + # Handle case where no session exists in Redis (returns False or empty) + if not long_time_data or long_time_data is False: + logger.debug(f"No recent sessions in Redis for user {end_user_id}") + return long_messages = await messages_parse(long_time_data) repo.upsert(end_user_id, long_messages) logger.info(f'写入短长期:') diff --git a/api/app/core/memory/agent/langgraph_graph/routing/write_router.py b/api/app/core/memory/agent/langgraph_graph/routing/write_router.py index 6650b9b0..e9de02b6 100644 --- a/api/app/core/memory/agent/langgraph_graph/routing/write_router.py +++ b/api/app/core/memory/agent/langgraph_graph/routing/write_router.py @@ -61,6 +61,7 @@ async def window_dialogue(end_user_id,langchain_messages,memory_config,scope): scope:窗口大小 ''' scope=scope + redis_messages = [] is_end_user_id = count_store.get_sessions_count(end_user_id) if is_end_user_id is not False: is_end_user_id = count_store.get_sessions_count(end_user_id)[0] @@ -92,6 +93,9 @@ async def memory_long_term_storage(end_user_id,memory_config,time): memory_config: 内存配置对象 ''' long_time_data = write_store.find_user_recent_sessions(end_user_id, time) + # Handle case where no session exists in Redis (returns False or empty) + if not long_time_data or long_time_data is False: + return format_messages = await chat_data_format(long_time_data) if format_messages!=[]: await write_messages(end_user_id, format_messages, memory_config) @@ -109,8 +113,9 @@ async def aggregate_judgment(end_user_id: str, ori_messages: list, memory_config try: # 1. 获取历史会话数据(使用新方法) result = write_store.get_all_sessions_by_end_user_id(end_user_id) - history = await format_parsing(result) - if not result: + + # Handle case where no session exists in Redis (returns False or empty) + if not result or result is False: history = [] else: history = await format_parsing(result) From f571f0688a2489e8f78a31eac1f4bf88e7d3a41f Mon Sep 17 00:00:00 2001 From: zhaoying Date: Tue, 3 Feb 2026 19:55:09 +0800 Subject: [PATCH 06/14] fix(web): PageScrollList style update --- web/src/styles/index.css | 3 --- 1 file changed, 3 deletions(-) diff --git a/web/src/styles/index.css b/web/src/styles/index.css index 53670dab..bbbe9cd9 100644 --- a/web/src/styles/index.css +++ b/web/src/styles/index.css @@ -180,7 +180,4 @@ body { .x6-node foreignObject > body { min-height: 100%; max-height: 100%; -} -#scrollableDiv .infinite-scroll-component__outerdiv { - height: 100%; } \ No newline at end of file From 24dafa7359652df27b6b4b8e25f722cc4bcdc290 Mon Sep 17 00:00:00 2001 From: Eternity <1533512157@qq.com> Date: Wed, 4 Feb 2026 11:13:28 +0800 Subject: [PATCH 07/14] fix(workflow): fix argument passing in code execution nodes --- api/app/core/workflow/nodes/code/config.py | 2 +- api/app/core/workflow/nodes/code/node.py | 2 +- sandbox/app/controllers/sandbox_controller.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/app/core/workflow/nodes/code/config.py b/api/app/core/workflow/nodes/code/config.py index 8af13f12..a47586a3 100644 --- a/api/app/core/workflow/nodes/code/config.py +++ b/api/app/core/workflow/nodes/code/config.py @@ -44,7 +44,7 @@ class CodeNodeConfig(BaseNodeConfig): description="code content" ) - language: Literal['python3', 'nodejs'] = Field( + language: Literal['python3', 'javascript'] = Field( ..., description="language" ) diff --git a/api/app/core/workflow/nodes/code/node.py b/api/app/core/workflow/nodes/code/node.py index 892708f2..019fec84 100644 --- a/api/app/core/workflow/nodes/code/node.py +++ b/api/app/core/workflow/nodes/code/node.py @@ -110,7 +110,7 @@ class CodeNode(BaseNode): code=code, inputs_variable=input_variable_dict, ) - elif self.typed_config.language == 'nodejs': + elif self.typed_config.language == 'javascript': final_script = NODEJS_SCRIPT_TEMPLATE.substitute( code=code, inputs_variable=input_variable_dict, diff --git a/sandbox/app/controllers/sandbox_controller.py b/sandbox/app/controllers/sandbox_controller.py index c5cce40c..f9bc3fc0 100644 --- a/sandbox/app/controllers/sandbox_controller.py +++ b/sandbox/app/controllers/sandbox_controller.py @@ -33,7 +33,7 @@ async def run_code(request: RunCodeRequest): """Execute code in sandbox""" if request.language == "python3": return await run_python_code(request.code, request.preload, request.options) - elif request.language == "nodejs": + elif request.language == "javascript": return await run_nodejs_code(request.code, request.preload, request.options) else: return error_response(-400, "unsupported language") From fad91b64ab48221ff90abd0ea9e361e0ba6a179f Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 4 Feb 2026 11:52:34 +0800 Subject: [PATCH 08/14] fix(web): prompt add disabled --- .../components/Editor/index.tsx | 11 ++++- .../Editor/plugin/EditablePlugin.tsx | 48 +++++++++++++++++++ web/src/views/Prompt/Prompt.tsx | 6 +-- .../Workflow/components/Editor/index.tsx | 2 +- .../Editor/plugin/InitialValuePlugin.tsx | 12 +++-- .../views/Workflow/hooks/useWorkflowGraph.ts | 4 +- 6 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 web/src/views/ApplicationConfig/components/Editor/plugin/EditablePlugin.tsx diff --git a/web/src/views/ApplicationConfig/components/Editor/index.tsx b/web/src/views/ApplicationConfig/components/Editor/index.tsx index 0c5e2a86..bd6c6733 100644 --- a/web/src/views/ApplicationConfig/components/Editor/index.tsx +++ b/web/src/views/ApplicationConfig/components/Editor/index.tsx @@ -9,6 +9,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext import InitialValuePlugin from './plugin/InitialValuePlugin' import LineBreakPlugin from './plugin/LineBreakPlugin'; import InsertTextPlugin from './plugin/InsertTextPlugin'; +import EditablePlugin from './plugin/EditablePlugin'; export interface EditorRef { insertText: (text: string) => void; @@ -23,6 +24,7 @@ interface LexicalEditorProps { value?: string; onChange?: (value: string) => void; height?: number; + disabled?: boolean; } const theme = { @@ -38,6 +40,7 @@ const EditorContent = forwardRef(({ value, placeholder = "请输入内容...", onChange, + disabled }, ref) => { const [editor] = useLexicalComposerContext(); @@ -92,7 +95,11 @@ const EditorContent = forwardRef(({ } placeholder={ @@ -105,6 +112,7 @@ const EditorContent = forwardRef(({ + ); }); @@ -114,6 +122,7 @@ const Editor = forwardRef((props, ref) => { namespace: 'Editor', theme, nodes: [], + editable: !props.disabled, onError: (error: Error) => { console.error(error); }, diff --git a/web/src/views/ApplicationConfig/components/Editor/plugin/EditablePlugin.tsx b/web/src/views/ApplicationConfig/components/Editor/plugin/EditablePlugin.tsx new file mode 100644 index 00000000..6c237f01 --- /dev/null +++ b/web/src/views/ApplicationConfig/components/Editor/plugin/EditablePlugin.tsx @@ -0,0 +1,48 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-04 11:20:49 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-02-04 11:20:49 + */ +import { useEffect } from 'react'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; + +/** + * Props for the EditablePlugin component + */ +interface EditablePluginProps { + /** Whether the editor should be disabled (read-only mode) */ + disabled?: boolean; +} + +/** + * EditablePlugin - A Lexical editor plugin that controls the editable state of the editor + * + * This plugin allows you to dynamically toggle between editable and read-only modes. + * When disabled is true, the editor becomes read-only and users cannot modify content. + * When disabled is false or undefined, the editor is fully editable. + * + * @param {EditablePluginProps} props - Component props + * @param {boolean} [props.disabled] - Controls whether the editor is in read-only mode + * @returns {null} This plugin doesn't render any UI elements + * + * @example + * ```tsx + * + * + * + * ``` + */ +export default function EditablePlugin({ disabled }: EditablePluginProps) { + // Get the editor instance from Lexical composer context + const [editor] = useLexicalComposerContext(); + + // Update editor's editable state whenever the disabled prop changes + useEffect(() => { + // Set editor to editable when disabled is false, read-only when disabled is true + editor.setEditable(!disabled); + }, [editor, disabled]); + + // This plugin doesn't render any UI, it only manages editor state + return null; +} diff --git a/web/src/views/Prompt/Prompt.tsx b/web/src/views/Prompt/Prompt.tsx index 90b1c3a6..69a597db 100644 --- a/web/src/views/Prompt/Prompt.tsx +++ b/web/src/views/Prompt/Prompt.tsx @@ -140,7 +140,6 @@ const Prompt: FC<{ editVo: HistoryItem | null; refresh: () => void; }> = ({ edit refresh() } - console.log(values) return ( <>
@@ -199,12 +198,13 @@ const Prompt: FC<{ editVo: HistoryItem | null; refresh: () => void; }> = ({ edit ref={editorRef} placeholder={t('prompt.promptPlaceholder')} className="rb:h-[calc(100vh-260px)]" + disabled={loading} // onChange={(value) => form.setFieldValue('current_prompt', value)} />
- - + +
diff --git a/web/src/views/Workflow/components/Editor/index.tsx b/web/src/views/Workflow/components/Editor/index.tsx index e37c71de..362e1c81 100644 --- a/web/src/views/Workflow/components/Editor/index.tsx +++ b/web/src/views/Workflow/components/Editor/index.tsx @@ -242,7 +242,7 @@ const Editor: FC =({ {enableLineNumbers && } { setCount(count) }} onChange={onChange} /> - + {enableLineNumbers && } diff --git a/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx index 22de9592..4021a9ee 100644 --- a/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx @@ -8,12 +8,13 @@ import { type Suggestion } from '../plugin/AutocompletePlugin' interface InitialValuePluginProps { value: string; options?: Suggestion[]; - enableJinja2?: boolean; + enableLineNumbers?: boolean; } -const InitialValuePlugin: React.FC = ({ value, options = [], enableJinja2 = false }) => { +const InitialValuePlugin: React.FC = ({ value, options = [], enableLineNumbers = false }) => { const [editor] = useLexicalComposerContext(); const prevValueRef = useRef(''); + const prevEnableLineNumbersRef = useRef(enableLineNumbers); const isUserInputRef = useRef(false); useEffect(() => { @@ -32,7 +33,7 @@ const InitialValuePlugin: React.FC = ({ value, options }, [editor]); useEffect(() => { - if (value !== prevValueRef.current && !isUserInputRef.current) { + if ((value !== prevValueRef.current || enableLineNumbers !== prevEnableLineNumbersRef.current) && !isUserInputRef.current) { queueMicrotask(() => { editor.update(() => { const root = $getRoot(); @@ -40,7 +41,7 @@ const InitialValuePlugin: React.FC = ({ value, options const parts = value.split(/(\{\{[^}]+\}\})/); - if (enableJinja2) { + if (enableLineNumbers) { // Handle newlines properly in Jinja2 mode const lines = value.split('\n'); lines.forEach((line) => { @@ -104,8 +105,9 @@ const InitialValuePlugin: React.FC = ({ value, options } prevValueRef.current = value; + prevEnableLineNumbersRef.current = enableLineNumbers; isUserInputRef.current = false; - }, [value, options, editor, enableJinja2]); + }, [value, options, editor, enableLineNumbers]); return null; }; diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index 4c010de0..48cd6652 100644 --- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts +++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts @@ -111,7 +111,7 @@ export const useWorkflowGraph = ({ nodeLibraryConfig.config[key].defaultValue = Object.entries(config[key]).map(([name, value]) => ({ name, value })) } else if (type === 'code' && key === 'code' && config[key] && nodeLibraryConfig.config && nodeLibraryConfig.config[key]) { try { - nodeLibraryConfig.config[key].defaultValue = atob(config[key] as string) + nodeLibraryConfig.config[key].defaultValue = decodeURIComponent(atob(config[key] as string)) } catch { nodeLibraryConfig.config[key].defaultValue = config[key] } @@ -851,7 +851,7 @@ export const useWorkflowGraph = ({ const code = data.config[key].defaultValue || '' itemConfig = { ...itemConfig, - code: btoa(code || '') + code: btoa(encodeURIComponent(code || '')) } } else if (key === 'memory' && data.config[key] && 'defaultValue' in data.config[key]) { const { messages, ...rest } = data.config[key].defaultValue From 9d30bc406204dda0b21264ef3bb389b06491cc35 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 4 Feb 2026 11:59:27 +0800 Subject: [PATCH 09/14] fix(web): space icon required --- web/src/views/SpaceManagement/components/SpaceModal.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/src/views/SpaceManagement/components/SpaceModal.tsx b/web/src/views/SpaceManagement/components/SpaceModal.tsx index 725db4b9..8b9dde53 100644 --- a/web/src/views/SpaceManagement/components/SpaceModal.tsx +++ b/web/src/views/SpaceManagement/components/SpaceModal.tsx @@ -85,6 +85,8 @@ const SpaceModal = forwardRef(({ }).catch(() => { handleUpdate(formData) }) + } else { + handleUpdate(formData) } } }) @@ -139,6 +141,7 @@ const SpaceModal = forwardRef(({ label={t('space.spaceIcon')} valuePropName="fileList" hidden={currentStep === 1} + rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('space.spaceIcon') }) }]} > From 21eae29bb711ca53782370082b9498c98448cb82 Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Wed, 4 Feb 2026 12:07:59 +0800 Subject: [PATCH 10/14] feat(app): modify the key of the token --- api/app/services/app_statistics_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/app/services/app_statistics_service.py b/api/app/services/app_statistics_service.py index c164924a..1b6bc3b8 100644 --- a/api/app/services/app_statistics_service.py +++ b/api/app/services/app_statistics_service.py @@ -187,7 +187,7 @@ class AppStatisticsService: daily_tokens[date_str] = 0 daily_tokens[date_str] += int(tokens) - daily_data = [{"date": date, "tokens": tokens} for date, tokens in sorted(daily_tokens.items()) if tokens != 0] + daily_data = [{"date": date, "count": tokens} for date, tokens in sorted(daily_tokens.items()) if tokens != 0] total = sum(row["tokens"] for row in daily_data) return {"daily": daily_data, "total": total} From 5694bc0230e6b8b1661cffc5d0df2505e163a5b3 Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Wed, 4 Feb 2026 12:27:14 +0800 Subject: [PATCH 11/14] fix(fix the key of the app's token): --- api/app/services/app_statistics_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/app/services/app_statistics_service.py b/api/app/services/app_statistics_service.py index 1b6bc3b8..5cfa3229 100644 --- a/api/app/services/app_statistics_service.py +++ b/api/app/services/app_statistics_service.py @@ -188,6 +188,6 @@ class AppStatisticsService: daily_tokens[date_str] += int(tokens) daily_data = [{"date": date, "count": tokens} for date, tokens in sorted(daily_tokens.items()) if tokens != 0] - total = sum(row["tokens"] for row in daily_data) + total = sum(row["count"] for row in daily_data) return {"daily": daily_data, "total": total} From bc36b791055b8892cb50c4c55c14144c8c295cac Mon Sep 17 00:00:00 2001 From: Eternity <1533512157@qq.com> Date: Wed, 4 Feb 2026 12:28:28 +0800 Subject: [PATCH 12/14] fix(workflow): switch code input encoding to base64+URL encoding --- api/app/core/workflow/nodes/code/node.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/app/core/workflow/nodes/code/node.py b/api/app/core/workflow/nodes/code/node.py index 019fec84..daee1e78 100644 --- a/api/app/core/workflow/nodes/code/node.py +++ b/api/app/core/workflow/nodes/code/node.py @@ -2,6 +2,7 @@ import base64 import json import logging import re +import urllib.parse from string import Template from textwrap import dedent from typing import Any @@ -101,6 +102,7 @@ class CodeNode(BaseNode): code = base64.b64decode( self.typed_config.code ).decode("utf-8") + code = urllib.parse.unquote(code, encoding='utf-8') input_variable_dict = base64.b64encode( json.dumps(input_variable_dict).encode("utf-8") From cbae90086662f4449d1043c181e6aa307d4ea6c4 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 4 Feb 2026 13:37:49 +0800 Subject: [PATCH 13/14] fix(web): save add session update --- web/src/views/Prompt/Prompt.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/views/Prompt/Prompt.tsx b/web/src/views/Prompt/Prompt.tsx index 69a597db..3f1ec96e 100644 --- a/web/src/views/Prompt/Prompt.tsx +++ b/web/src/views/Prompt/Prompt.tsx @@ -138,6 +138,7 @@ const Prompt: FC<{ editVo: HistoryItem | null; refresh: () => void; }> = ({ edit currentPromptValueRef.current = undefined; setChatList([]) refresh() + updateSession() } return ( From 7268886294eb65a8300c39a80f60c359a28a739c Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 4 Feb 2026 13:38:58 +0800 Subject: [PATCH 14/14] fix(web): language editor support paste --- .../Workflow/components/Editor/index.tsx | 2 +- .../components/Editor/plugin/BlurPlugin.tsx | 6 +++++ .../plugin/JavaScriptHighlightPlugin.tsx | 22 +++++++++++++++++-- .../Editor/plugin/Python3HighlightPlugin.tsx | 22 +++++++++++++++++-- .../Properties/CodeExecution/index.tsx | 10 +++++---- 5 files changed, 53 insertions(+), 9 deletions(-) diff --git a/web/src/views/Workflow/components/Editor/index.tsx b/web/src/views/Workflow/components/Editor/index.tsx index 362e1c81..4c8540a8 100644 --- a/web/src/views/Workflow/components/Editor/index.tsx +++ b/web/src/views/Workflow/components/Editor/index.tsx @@ -242,7 +242,7 @@ const Editor: FC =({ {enableLineNumbers && } { setCount(count) }} onChange={onChange} /> - + {enableLineNumbers && } diff --git a/web/src/views/Workflow/components/Editor/plugin/BlurPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/BlurPlugin.tsx index b636605b..0fb6c48f 100644 --- a/web/src/views/Workflow/components/Editor/plugin/BlurPlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/BlurPlugin.tsx @@ -16,6 +16,12 @@ export default function BlurPlugin() { return; } + // 检查是否是粘贴操作导致的焦点变化 + const relatedTarget = e.relatedTarget as HTMLElement; + if (!relatedTarget || relatedTarget === document.body) { + return; + } + editor.update(() => { $setSelection(null); }); diff --git a/web/src/views/Workflow/components/Editor/plugin/JavaScriptHighlightPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/JavaScriptHighlightPlugin.tsx index 90053646..21219139 100644 --- a/web/src/views/Workflow/components/Editor/plugin/JavaScriptHighlightPlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/JavaScriptHighlightPlugin.tsx @@ -1,6 +1,6 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { TextNode, $createTextNode, $getSelection, $isRangeSelection } from 'lexical'; +import { TextNode, $createTextNode, $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, PASTE_COMMAND } from 'lexical'; const JS_KEYWORDS = new Set([ 'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', @@ -11,13 +11,31 @@ const JS_KEYWORDS = new Set([ const JavaScriptHighlightPlugin = () => { const [editor] = useLexicalComposerContext(); + const isPastingRef = useRef(false); + + useEffect(() => { + return editor.registerCommand( + PASTE_COMMAND, + () => { + isPastingRef.current = true; + setTimeout(() => { + isPastingRef.current = false; + }, 100); + return false; + }, + COMMAND_PRIORITY_LOW + ); + }, [editor]); useEffect(() => { return editor.registerNodeTransform(TextNode, (textNode: TextNode) => { + if (isPastingRef.current) return; + const text = textNode.getTextContent(); if (textNode.hasFormat('code')) return; if (!needsHighlight(text)) return; + if (textNode.getStyle()) return; const parent = textNode.getParent(); if (!parent) return; diff --git a/web/src/views/Workflow/components/Editor/plugin/Python3HighlightPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/Python3HighlightPlugin.tsx index 387160ed..12830ffb 100644 --- a/web/src/views/Workflow/components/Editor/plugin/Python3HighlightPlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/Python3HighlightPlugin.tsx @@ -1,6 +1,6 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { TextNode, $createTextNode, $getSelection, $isRangeSelection } from 'lexical'; +import { TextNode, $createTextNode, $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, PASTE_COMMAND } from 'lexical'; const PYTHON_KEYWORDS = new Set([ 'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', @@ -11,12 +11,30 @@ const PYTHON_KEYWORDS = new Set([ const Python3HighlightPlugin = () => { const [editor] = useLexicalComposerContext(); + const isPastingRef = useRef(false); + + useEffect(() => { + return editor.registerCommand( + PASTE_COMMAND, + () => { + isPastingRef.current = true; + setTimeout(() => { + isPastingRef.current = false; + }, 100); + return false; + }, + COMMAND_PRIORITY_LOW + ); + }, [editor]); useEffect(() => { return editor.registerNodeTransform(TextNode, (textNode: TextNode) => { + if (isPastingRef.current) return; + const text = textNode.getTextContent(); if (textNode.hasFormat('code')) return; + if (textNode.getStyle()) return; if (!needsHighlight(text)) return; const parent = textNode.getParent(); diff --git a/web/src/views/Workflow/components/Properties/CodeExecution/index.tsx b/web/src/views/Workflow/components/Properties/CodeExecution/index.tsx index 7c95a4a2..8a0ea03e 100644 --- a/web/src/views/Workflow/components/Properties/CodeExecution/index.tsx +++ b/web/src/views/Workflow/components/Properties/CodeExecution/index.tsx @@ -33,7 +33,6 @@ const codeTemplate = { const CodeExecution: FC = ({ options }) => { const { t } = useTranslation() const form = Form.useFormInstance() - const values = Form.useWatch([], form) || {} const handleRefresh = () => { const code = form.getFieldValue('code') || '' @@ -66,7 +65,6 @@ const CodeExecution: FC = ({ options }) => { form.setFieldValue('code', newTemplate) } const handleChangeLanguage = (value: string) => { - form.setFieldValue('code', codeTemplate[value as keyof typeof codeTemplate]) form.setFieldsValue({ input_variables: [{ name: 'arg1' }, { name: 'arg2' }], code: codeTemplate[value as keyof typeof codeTemplate] @@ -109,8 +107,12 @@ const CodeExecution: FC = ({ options }) => { - - + prev.language !== curr.language}> + {() => ( + + + + )}