Merge pull request #132 from SuanmoSuanyangTechnology/develop

Develop
This commit is contained in:
yingzhao
2026-01-15 20:59:53 +08:00
committed by GitHub
25 changed files with 367 additions and 280 deletions

View File

@@ -74,7 +74,7 @@ def get_multi_agent_configs(
"app_id": str(app_id), "app_id": str(app_id),
"default_model_config_id": None, "default_model_config_id": None,
"model_parameters": None, "model_parameters": None,
"orchestration_mode": "conditional", "orchestration_mode": "supervisor",
"sub_agents": [], "sub_agents": [],
"routing_rules": [], "routing_rules": [],
"execution_config": { "execution_config": {

View File

@@ -516,8 +516,16 @@ class ConversationService:
conversation_messages = self.get_conversation_history( conversation_messages = self.get_conversation_history(
conversation_id=conversation_id, conversation_id=conversation_id,
max_history=30 max_history=20
) )
if len(conversation_messages) == 0:
return ConversationOut(
theme="",
question=[],
summary="",
takeaways=[],
info_score=0,
)
with open('app/services/prompt/conversation_summary_system.jinja2', 'r', encoding='utf-8') as f: with open('app/services/prompt/conversation_summary_system.jinja2', 'r', encoding='utf-8') as f:
system_prompt = f.read() system_prompt = f.read()
@@ -536,6 +544,7 @@ class ConversationService:
] ]
logger.info(f"Invoking LLM for conversation_id={conversation_id}") logger.info(f"Invoking LLM for conversation_id={conversation_id}")
model_resp = await llm.ainvoke(messages) model_resp = await llm.ainvoke(messages)
try: try:
if isinstance(model_resp.content, str): if isinstance(model_resp.content, str):
result = json_repair.repair_json(model_resp.content, return_objects=True) result = json_repair.repair_json(model_resp.content, return_objects=True)

View File

@@ -9,7 +9,7 @@ import os
import re import re
import time import time
import uuid import uuid
from threading import Lock
from typing import Any, AsyncGenerator, Dict, List, Optional from typing import Any, AsyncGenerator, Dict, List, Optional
import redis import redis
@@ -51,9 +51,7 @@ _neo4j_connector = Neo4jConnector()
class MemoryAgentService: class MemoryAgentService:
"""Service for memory agent operations""" """Service for memory agent operations"""
def __init__(self):
self.user_locks: Dict[str, Lock] = {}
self.locks_lock = Lock()
def writer_messages_deal(self,messages,start_time,group_id,config_id,message): def writer_messages_deal(self,messages,start_time,group_id,config_id,message):
messages = str(messages).replace("'", '"').replace('\\n', '').replace('\n', '').replace('\\', '') messages = str(messages).replace("'", '"').replace('\\n', '').replace('\n', '').replace('\\', '')
@@ -83,12 +81,7 @@ class MemoryAgentService:
raise ValueError(f"写入失败: {messages}") raise ValueError(f"写入失败: {messages}")
def get_group_lock(self, group_id: str) -> Lock:
"""Get lock for specific group to prevent concurrent processing"""
with self.locks_lock:
if group_id not in self.user_locks:
self.user_locks[group_id] = Lock()
return self.user_locks[group_id]
def extract_tool_call_info(self, event: Dict) -> bool: def extract_tool_call_info(self, event: Dict) -> bool:
"""Extract tool call information from event""" """Extract tool call information from event"""
@@ -417,241 +410,236 @@ class MemoryAgentService:
except ImportError: except ImportError:
audit_logger = None audit_logger = None
# Get group lock to prevent concurrent processing try:
group_lock = self.get_group_lock(group_id) config_service = MemoryConfigService(db)
memory_config = config_service.load_memory_config(
config_id=config_id,
service_name="MemoryAgentService"
)
logger.info(f"Configuration loaded successfully: {memory_config.config_name}")
except ConfigurationError as e:
error_msg = f"Failed to load configuration for config_id: {config_id}: {e}"
logger.error(error_msg)
with group_lock: # Log failed operation
# Step 1: Load configuration from database only if audit_logger:
try: duration = time.time() - start_time
config_service = MemoryConfigService(db) audit_logger.log_operation(
memory_config = config_service.load_memory_config( operation="READ",
config_id=config_id, config_id=config_id,
service_name="MemoryAgentService" group_id=group_id,
success=False,
duration=duration,
error=error_msg
) )
logger.info(f"Configuration loaded successfully: {memory_config.config_name}")
except ConfigurationError as e:
error_msg = f"Failed to load configuration for config_id: {config_id}: {e}"
logger.error(error_msg)
# Log failed operation raise ValueError(error_msg)
if audit_logger:
duration = time.time() - start_time
audit_logger.log_operation(
operation="READ",
config_id=config_id,
group_id=group_id,
success=False,
duration=duration,
error=error_msg
)
raise ValueError(error_msg) # Step 2: Prepare history
history.append({"role": "user", "content": message})
logger.debug(f"Group ID:{group_id}, Message:{message}, History:{history}, Config ID:{config_id}")
# Step 2: Prepare history # Step 3: Initialize MCP client and execute read workflow
history.append({"role": "user", "content": message}) mcp_config = get_mcp_server_config()
logger.debug(f"Group ID:{group_id}, Message:{message}, History:{history}, Config ID:{config_id}") client = MultiServerMCPClient(mcp_config)
# Step 3: Initialize MCP client and execute read workflow async with client.session('data_flow') as session:
mcp_config = get_mcp_server_config() session_start = time.time()
client = MultiServerMCPClient(mcp_config) logger.debug("Connected to MCP Server: data_flow")
async with client.session('data_flow') as session: tools_start = time.time()
session_start = time.time() tools = await load_mcp_tools(session)
logger.debug("Connected to MCP Server: data_flow") tools_time = time.time() - tools_start
logger.info(f"[PERF] MCP tools loading took: {tools_time:.4f}s")
tools_start = time.time()
tools = await load_mcp_tools(session)
tools_time = time.time() - tools_start
logger.info(f"[PERF] MCP tools loading took: {tools_time:.4f}s")
outputs = []
intermediate_outputs = []
seen_intermediates = set() # Track seen intermediate outputs to avoid duplicates
# Pass memory_config to the graph workflow outputs = []
graph_start = time.time() intermediate_outputs = []
async with make_read_graph(group_id, tools, search_switch, group_id, group_id, memory_config=memory_config, storage_type=storage_type, user_rag_memory_id=user_rag_memory_id) as graph: seen_intermediates = set() # Track seen intermediate outputs to avoid duplicates
graph_init_time = time.time() - graph_start
logger.info(f"[PERF] Graph initialization took: {graph_init_time:.4f}s")
start = time.time()
config = {"configurable": {"thread_id": group_id}}
workflow_errors = [] # Track errors from workflow
event_count = 0
async for event in graph.astream(
{"messages": history, "memory_config": memory_config, "errors": []},
stream_mode="values",
config=config
):
event_count += 1
event_start = time.time()
messages = event.get('messages')
# Capture any errors from the state
if event.get('errors'):
workflow_errors.extend(event.get('errors', []))
for msg in messages: # Pass memory_config to the graph workflow
msg_content = msg.content graph_start = time.time()
msg_role = msg.__class__.__name__.lower().replace("message", "") async with make_read_graph(group_id, tools, search_switch, group_id, group_id, memory_config=memory_config, storage_type=storage_type, user_rag_memory_id=user_rag_memory_id) as graph:
outputs.append({ graph_init_time = time.time() - graph_start
"role": msg_role, logger.info(f"[PERF] Graph initialization took: {graph_init_time:.4f}s")
"content": msg_content
})
# Extract intermediate outputs start = time.time()
if hasattr(msg, 'content'): config = {"configurable": {"thread_id": group_id}}
try: workflow_errors = [] # Track errors from workflow
# Handle MCP content format: [{'type': 'text', 'text': '...'}]
content_to_parse = msg_content
if isinstance(msg_content, list):
for block in msg_content:
if isinstance(block, dict) and block.get('type') == 'text':
content_to_parse = block.get('text', '')
break
else:
continue # No text block found
# Try to parse content as JSON event_count = 0
if isinstance(content_to_parse, str): async for event in graph.astream(
try: {"messages": history, "memory_config": memory_config, "errors": []},
parsed = json.loads(content_to_parse) stream_mode="values",
if isinstance(parsed, dict): config=config
# Check for single intermediate output ):
if '_intermediate' in parsed: event_count += 1
intermediate_data = parsed['_intermediate'] event_start = time.time()
messages = event.get('messages')
# Capture any errors from the state
if event.get('errors'):
workflow_errors.extend(event.get('errors', []))
for msg in messages:
msg_content = msg.content
msg_role = msg.__class__.__name__.lower().replace("message", "")
outputs.append({
"role": msg_role,
"content": msg_content
})
# Extract intermediate outputs
if hasattr(msg, 'content'):
try:
# Handle MCP content format: [{'type': 'text', 'text': '...'}]
content_to_parse = msg_content
if isinstance(msg_content, list):
for block in msg_content:
if isinstance(block, dict) and block.get('type') == 'text':
content_to_parse = block.get('text', '')
break
else:
continue # No text block found
# Try to parse content as JSON
if isinstance(content_to_parse, str):
try:
parsed = json.loads(content_to_parse)
if isinstance(parsed, dict):
# Check for single intermediate output
if '_intermediate' in parsed:
intermediate_data = parsed['_intermediate']
output_key = self._create_intermediate_key(intermediate_data)
if output_key not in seen_intermediates:
seen_intermediates.add(output_key)
intermediate_outputs.append(self._format_intermediate_output(intermediate_data))
# Check for multiple intermediate outputs (from Retrieve)
if '_intermediates' in parsed:
for intermediate_data in parsed['_intermediates']:
output_key = self._create_intermediate_key(intermediate_data) output_key = self._create_intermediate_key(intermediate_data)
if output_key not in seen_intermediates: if output_key not in seen_intermediates:
seen_intermediates.add(output_key) seen_intermediates.add(output_key)
intermediate_outputs.append(self._format_intermediate_output(intermediate_data)) intermediate_outputs.append(self._format_intermediate_output(intermediate_data))
except (json.JSONDecodeError, ValueError):
pass
except Exception as e:
logger.debug(f"Failed to extract intermediate output: {e}")
# Check for multiple intermediate outputs (from Retrieve) event_time = time.time() - event_start
if '_intermediates' in parsed: logger.info(f"[PERF] Event {event_count} processing took: {event_time:.4f}s")
for intermediate_data in parsed['_intermediates']:
output_key = self._create_intermediate_key(intermediate_data)
if output_key not in seen_intermediates: workflow_duration = time.time() - start
seen_intermediates.add(output_key) session_duration = time.time() - session_start
intermediate_outputs.append(self._format_intermediate_output(intermediate_data)) logger.info(f"[PERF] Read graph workflow completed in {workflow_duration}s")
except (json.JSONDecodeError, ValueError): logger.info(f"[PERF] Total session duration: {session_duration:.4f}s")
pass logger.info(f"[PERF] Total events processed: {event_count}")
except Exception as e: # Extract final answer
logger.debug(f"Failed to extract intermediate output: {e}") final_answer = ""
for messages in outputs:
event_time = time.time() - event_start if messages['role'] == 'tool':
logger.info(f"[PERF] Event {event_count} processing took: {event_time:.4f}s") message = messages['content']
workflow_duration = time.time() - start # Handle MCP content format: [{'type': 'text', 'text': '...'}]
session_duration = time.time() - session_start if isinstance(message, list):
logger.info(f"[PERF] Read graph workflow completed in {workflow_duration}s") # Extract text from MCP content blocks
logger.info(f"[PERF] Total session duration: {session_duration:.4f}s") for block in message:
logger.info(f"[PERF] Total events processed: {event_count}") if isinstance(block, dict) and block.get('type') == 'text':
# Extract final answer message = block.get('text', '')
final_answer = "" break
for messages in outputs: else:
if messages['role'] == 'tool': continue # No text block found
message = messages['content']
# Handle MCP content format: [{'type': 'text', 'text': '...'}] try:
if isinstance(message, list): parsed = json.loads(message) if isinstance(message, str) else message
# Extract text from MCP content blocks if isinstance(parsed, dict):
for block in message: if parsed.get('status') == 'success':
if isinstance(block, dict) and block.get('type') == 'text': summary_result = parsed.get('summary_result')
message = block.get('text', '') if summary_result:
break final_answer = summary_result
else: except (json.JSONDecodeError, ValueError):
continue # No text block found pass
try: # 记录成功的操作
parsed = json.loads(message) if isinstance(message, str) else message total_duration = time.time() - start_time
if isinstance(parsed, dict):
if parsed.get('status') == 'success':
summary_result = parsed.get('summary_result')
if summary_result:
final_answer = summary_result
except (json.JSONDecodeError, ValueError):
pass
# 记录成功的操作 # Check for workflow errors
total_duration = time.time() - start_time if workflow_errors:
error_details = "; ".join([f"{e['tool']}: {e['error']}" for e in workflow_errors])
logger.warning(f"Read workflow completed with errors: {error_details}")
# Check for workflow errors if audit_logger:
if workflow_errors:
error_details = "; ".join([f"{e['tool']}: {e['error']}" for e in workflow_errors])
logger.warning(f"Read workflow completed with errors: {error_details}")
if audit_logger:
audit_logger.log_operation(
operation="READ",
config_id=config_id,
group_id=group_id,
success=False,
duration=total_duration,
error=error_details,
details={
"search_switch": search_switch,
"history_length": len(history),
"intermediate_outputs_count": len(intermediate_outputs),
"has_answer": bool(final_answer),
"errors": workflow_errors
}
)
# Raise error if no answer was produced
if not final_answer:
raise ValueError(f"Read workflow failed: {error_details}")
if audit_logger and not workflow_errors:
audit_logger.log_operation( audit_logger.log_operation(
operation="READ", operation="READ",
config_id=config_id, config_id=config_id,
group_id=group_id, group_id=group_id,
success=True, success=False,
duration=total_duration, duration=total_duration,
error=error_details,
details={ details={
"search_switch": search_switch, "search_switch": search_switch,
"history_length": len(history), "history_length": len(history),
"intermediate_outputs_count": len(intermediate_outputs), "intermediate_outputs_count": len(intermediate_outputs),
"has_answer": bool(final_answer) "has_answer": bool(final_answer),
"errors": workflow_errors
} }
) )
retrieved_content=[]
repo = ShortTermMemoryRepository(db) # Raise error if no answer was produced
if str(search_switch)!="2": if not final_answer:
for intermediate in intermediate_outputs: raise ValueError(f"Read workflow failed: {error_details}")
print(intermediate)
intermediate_type=intermediate['type'] if audit_logger and not workflow_errors:
if intermediate_type=="search_result": audit_logger.log_operation(
query=intermediate['query'] operation="READ",
raw_results=intermediate['raw_results'] config_id=config_id,
reranked_results=raw_results.get('reranked_results',[]) group_id=group_id,
try: success=True,
statements=[statement['statement'] for statement in reranked_results.get('statements', [])] duration=total_duration,
except Exception: details={
statements=[] "search_switch": search_switch,
statements=list(set(statements)) "history_length": len(history),
retrieved_content.append({query:statements}) "intermediate_outputs_count": len(intermediate_outputs),
if retrieved_content==[]: "has_answer": bool(final_answer)
retrieved_content='' }
if '信息不足,无法回答。' != str(final_answer) and str(search_switch).strip() != "2":#and retrieved_content!=[] )
# 使用 upsert 方法 retrieved_content=[]
repo.upsert( repo = ShortTermMemoryRepository(db)
end_user_id=end_user_id, # 确保这个变量在作用域内 if str(search_switch)!="2":
messages=ori_message, for intermediate in intermediate_outputs:
aimessages=final_answer, print(intermediate)
retrieved_content=retrieved_content, intermediate_type=intermediate['type']
search_switch=str(search_switch) if intermediate_type=="search_result":
) query=intermediate['query']
print("写入成功") raw_results=intermediate['raw_results']
reranked_results=raw_results.get('reranked_results',[])
try:
statements=[statement['statement'] for statement in reranked_results.get('statements', [])]
except Exception:
statements=[]
statements=list(set(statements))
retrieved_content.append({query:statements})
if retrieved_content==[]:
retrieved_content=''
if '信息不足,无法回答。' != str(final_answer) and str(search_switch).strip() != "2":#and retrieved_content!=[]
# 使用 upsert 方法
repo.upsert(
end_user_id=end_user_id, # 确保这个变量在作用域内
messages=ori_message,
aimessages=final_answer,
retrieved_content=retrieved_content,
search_switch=str(search_switch)
)
print("写入成功")
return { return {
"answer": final_answer, "answer": final_answer,
"intermediate_outputs": intermediate_outputs "intermediate_outputs": intermediate_outputs
} }
def _create_intermediate_key(self, output: Dict) -> str: def _create_intermediate_key(self, output: Dict) -> str:
""" """
Create a unique key for an intermediate output to detect duplicates. Create a unique key for an intermediate output to detect duplicates.

View File

@@ -267,14 +267,14 @@ class MemoryForgetService:
elif node_type_label == 'memorysummary': elif node_type_label == 'memorysummary':
node_type_label = 'summary' node_type_label = 'summary'
# 将 Neo4j DateTime 对象转换为时间戳 # 将 Neo4j DateTime 对象转换为时间戳(毫秒)
last_access_time = result['last_access_time'] last_access_time = result['last_access_time']
last_access_dt = convert_neo4j_datetime_to_python(last_access_time) last_access_dt = convert_neo4j_datetime_to_python(last_access_time)
# 确保 datetime 带有时区信息(假定为 UTC),避免 naive datetime 导致的时区偏差 # 确保 datetime 带有时区信息(假定为 UTC),避免 naive datetime 导致的时区偏差
if last_access_dt: if last_access_dt:
if last_access_dt.tzinfo is None: if last_access_dt.tzinfo is None:
last_access_dt = last_access_dt.replace(tzinfo=timezone.utc) last_access_dt = last_access_dt.replace(tzinfo=timezone.utc)
last_access_timestamp = int(last_access_dt.timestamp()) last_access_timestamp = int(last_access_dt.timestamp() * 1000)
else: else:
last_access_timestamp = 0 last_access_timestamp = 0
@@ -520,7 +520,7 @@ class MemoryForgetService:
'average_activation_value': result['average_activation'], 'average_activation_value': result['average_activation'],
'low_activation_nodes': result['low_activation_nodes'] or 0, 'low_activation_nodes': result['low_activation_nodes'] or 0,
'forgetting_threshold': forgetting_threshold, 'forgetting_threshold': forgetting_threshold,
'timestamp': int(datetime.now().timestamp()) 'timestamp': int(datetime.now().timestamp() * 1000)
} }
else: else:
activation_metrics = { activation_metrics = {
@@ -530,7 +530,7 @@ class MemoryForgetService:
'average_activation_value': None, 'average_activation_value': None,
'low_activation_nodes': 0, 'low_activation_nodes': 0,
'forgetting_threshold': forgetting_threshold, 'forgetting_threshold': forgetting_threshold,
'timestamp': int(datetime.now().timestamp()) 'timestamp': int(datetime.now().timestamp() * 1000)
} }
# 收集节点类型分布 # 收集节点类型分布
@@ -620,7 +620,7 @@ class MemoryForgetService:
'merged_count': record.merged_count, 'merged_count': record.merged_count,
'average_activation': record.average_activation_value, 'average_activation': record.average_activation_value,
'total_nodes': record.total_nodes, 'total_nodes': record.total_nodes,
'execution_time': int(record.execution_time.timestamp()) 'execution_time': int(record.execution_time.timestamp() * 1000)
}) })
api_logger.info(f"成功获取最近 {len(recent_trends)} 个日期的历史趋势数据") api_logger.info(f"成功获取最近 {len(recent_trends)} 个日期的历史趋势数据")
@@ -661,7 +661,7 @@ class MemoryForgetService:
'node_distribution': node_distribution, 'node_distribution': node_distribution,
'recent_trends': recent_trends, 'recent_trends': recent_trends,
'pending_nodes': pending_nodes, 'pending_nodes': pending_nodes,
'timestamp': int(datetime.now().timestamp()) 'timestamp': int(datetime.now().timestamp() * 1000)
} }
api_logger.info( api_logger.info(

View File

@@ -107,7 +107,7 @@ def multi_agent_config_4_app_release(release: AppRelease) -> MultiAgentConfig:
model_parameters=config_dict.get("model_parameters"), model_parameters=config_dict.get("model_parameters"),
master_agent_id=config_dict.get("master_agent_id"), master_agent_id=config_dict.get("master_agent_id"),
master_agent_name=config_dict.get("master_agent_name"), master_agent_name=config_dict.get("master_agent_name"),
orchestration_mode=config_dict.get("orchestration_mode", "conditional"), orchestration_mode=config_dict.get("orchestration_mode", "supervisor"),
sub_agents=config_dict.get("sub_agents", []), sub_agents=config_dict.get("sub_agents", []),
routing_rules=config_dict.get("routing_rules"), routing_rules=config_dict.get("routing_rules"),
execution_config=config_dict.get("execution_config", {}), execution_config=config_dict.get("execution_config", {}),
@@ -152,7 +152,7 @@ def dict_to_multi_agent_config(config_dict: Dict[str, Any], app_id: Optional[uui
... "app_id": "uuid-here", ... "app_id": "uuid-here",
... "master_agent_id": "master-uuid", ... "master_agent_id": "master-uuid",
... "master_agent_name": "Master Agent", ... "master_agent_name": "Master Agent",
... "orchestration_mode": "conditional", ... "orchestration_mode": "supervisor",
... "sub_agents": [ ... "sub_agents": [
... {"agent_id": "sub1-uuid", "name": "Sub Agent 1", "role": "specialist", "priority": 1}, ... {"agent_id": "sub1-uuid", "name": "Sub Agent 1", "role": "specialist", "priority": 1},
... {"agent_id": "sub2-uuid", "name": "Sub Agent 2", "role": "specialist", "priority": 2} ... {"agent_id": "sub2-uuid", "name": "Sub Agent 2", "role": "specialist", "priority": 2}
@@ -189,7 +189,7 @@ def dict_to_multi_agent_config(config_dict: Dict[str, Any], app_id: Optional[uui
app_id=final_app_id, app_id=final_app_id,
master_agent_id=master_agent_id, master_agent_id=master_agent_id,
master_agent_name=config_dict.get("master_agent_name"), master_agent_name=config_dict.get("master_agent_name"),
orchestration_mode=config_dict.get("orchestration_mode", "conditional"), orchestration_mode=config_dict.get("orchestration_mode", "supervisor"),
sub_agents=config_dict.get("sub_agents", []), sub_agents=config_dict.get("sub_agents", []),
routing_rules=config_dict.get("routing_rules"), routing_rules=config_dict.get("routing_rules"),
execution_config=config_dict.get("execution_config", {}), execution_config=config_dict.get("execution_config", {}),

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>使用帮助备份</title>
<g id="v0.2.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-51, -358)" stroke="#5F6266">
<g id="使用帮助备份" transform="translate(51, 358)">
<g id="编组-35" transform="translate(2, 1.5)">
<path d="M6.13163525,1.97938144 L10.3064533,1.97938144 C11.2417733,1.97938144 12,2.70634106 12,3.6030912 L12,10.3762902 C12,11.2730404 11.2417733,12 10.3064533,12 L1.69354673,12 C0.758226699,12 0,11.2730404 0,10.3762902 L0,3.6030912 C0,2.70634106 0.758226699,1.97938144 1.69354673,1.97938144 L2.02448435,1.97938144 L2.02448435,1.97938144" id="路径"></path>
<path d="M3.52033177,0.78470905 L6.09032258,1.97938144 L6.09032258,1.97938144 L6.09032258,11.8762887 L2.51918436,10.2162282 C2.10022604,10.0214734 1.83225806,9.6014016 1.83225806,9.13938916 L1.83225806,1.86154804 C1.83225806,1.2057099 2.36391992,0.674048044 3.01975806,0.674048044 C3.19268295,0.674048044 3.36352144,0.711815028 3.52033177,0.78470905 Z" id="矩形" stroke-linejoin="round"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>使用帮助</title>
<g id="v0.2.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-24, -358)" stroke="#212332">
<g id="使用帮助" transform="translate(24, 358)">
<g id="编组-35" transform="translate(2, 1.5)">
<path d="M6.13163525,1.97938144 L10.3064533,1.97938144 C11.2417733,1.97938144 12,2.70634106 12,3.6030912 L12,10.3762902 C12,11.2730404 11.2417733,12 10.3064533,12 L1.69354673,12 C0.758226699,12 0,11.2730404 0,10.3762902 L0,3.6030912 C0,2.70634106 0.758226699,1.97938144 1.69354673,1.97938144 L2.02448435,1.97938144 L2.02448435,1.97938144" id="路径"></path>
<path d="M3.52033177,0.78470905 L6.09032258,1.97938144 L6.09032258,1.97938144 L6.09032258,11.8762887 L2.51918436,10.2162282 C2.10022604,10.0214734 1.83225806,9.6014016 1.83225806,9.13938916 L1.83225806,1.86154804 C1.83225806,1.2057099 2.36391992,0.674048044 3.01975806,0.674048044 C3.19268295,0.674048044 3.36352144,0.711815028 3.52033177,0.78470905 Z" id="矩形" stroke-linejoin="round"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -91,6 +91,7 @@ export const en = {
memberManagement: 'Member Management', memberManagement: 'Member Management',
memorySummary: 'Memory Summary', memorySummary: 'Memory Summary',
memoryConversation: 'Memory Validation', memoryConversation: 'Memory Validation',
helpCenter: 'Help Center',
memorySummaryHandlers: 'Memory Summary Handlers', memorySummaryHandlers: 'Memory Summary Handlers',
createMemorySummary: 'Create Memory Summary', createMemorySummary: 'Create Memory Summary',
memoryManagement: 'Memory Management', memoryManagement: 'Memory Management',
@@ -183,14 +184,15 @@ export const en = {
createNewMemorySummary: 'Create New Memory Entry', createNewMemorySummary: 'Create New Memory Entry',
createNewApplication: 'Create New Application', createNewApplication: 'Create New Application',
createNewApplicationDesc: 'Create New Space Application', createNewApplicationDesc: 'Build an app in just 3 minutes with zero-code drag-and-drop.',
createNewKnowledge: 'Create New Knowledge', createNewKnowledge: 'Create New Knowledge',
createNewKnowledgeDesc: 'Create a new memory entry', createNewKnowledgeDesc: 'Transform your data into a fully searchable, dedicated knowledge base in seconds.',
memoryConversation: 'Memory Conversation', memoryConversation: 'Memory Conversation',
memoryConversationDesc: 'Memory Conversation', memoryConversationDesc: 'The more you use it, the better AI understands you.',
helpCenter: 'Help Center',
helpCenterDesc: 'One-stop support to answer your questions and get you started fast.',
memorySummary: 'View Memory Summary', memorySummary: 'View Memory Summary',
memorySummaryDesc: 'View Memory Summary Report', memorySummaryDesc: 'View Memory Summary Report',
@@ -413,6 +415,8 @@ export const en = {
reset: 'Reset', reset: 'Reset',
refresh: 'Refresh', refresh: 'Refresh',
return: 'Return', return: 'Return',
statusEnabled: 'Available',
statusDisabled: 'Unavailable'
}, },
model: { model: {
searchPlaceholder: 'search model…', searchPlaceholder: 'search model…',
@@ -616,6 +620,7 @@ export const en = {
retrieve:'Retrieve', retrieve:'Retrieve',
processing: 'Processing', processing: 'Processing',
processingMode: 'Processing Mode', processingMode: 'Processing Mode',
processMsg: 'Processing Message',
dataSize: 'Data Size', dataSize: 'Data Size',
createUpdateTime: 'Create/Update Time', createUpdateTime: 'Create/Update Time',
operation: 'Operation', operation: 'Operation',
@@ -1221,7 +1226,7 @@ export const en = {
IMPLICIT_MEMORY: 'Implicit Memory', IMPLICIT_MEMORY: 'Implicit Memory',
EMOTIONAL_MEMORY: 'Emotional Memory', EMOTIONAL_MEMORY: 'Emotional Memory',
EPISODIC_MEMORY: 'Episodic Memory', EPISODIC_MEMORY: 'Episodic Memory',
FORGETTING_MANAGEMENT: 'Forgetting Management', FORGET_MEMORY: 'Forget Memory',
endUserProfile: 'Core Profile', endUserProfile: 'Core Profile',
editEndUserProfile: 'Edit', editEndUserProfile: 'Edit',
@@ -1446,6 +1451,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
deduplication_desc: 'Deduplication and disambiguation completed, {{count}} unique entities in total' deduplication_desc: 'Deduplication and disambiguation completed, {{count}} unique entities in total'
}, },
memoryConversation: { memoryConversation: {
chatEmpty:'Is there anything I can help you with',
searchPlaceholder: 'Input user ID...', searchPlaceholder: 'Input user ID...',
userID: 'User ID', userID: 'User ID',
testMemoryConversation: 'Test Memory Conversation', testMemoryConversation: 'Test Memory Conversation',
@@ -1577,6 +1583,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
configured_disabled_desc: 'API is configured but not enabled', configured_disabled_desc: 'API is configured but not enabled',
error_desc: 'API is configured but connection error', error_desc: 'API is configured but connection error',
testConnectionSuccess: 'Test Connection Successful',
serviceEndpoint: 'Service Endpoint URL', serviceEndpoint: 'Service Endpoint URL',
serviceEndpointPlaceholder: 'URL of the service endpoint', serviceEndpointPlaceholder: 'URL of the service endpoint',
serviceEndpointExtra: 'Complete access address of the MCP service', serviceEndpointExtra: 'Complete access address of the MCP service',
@@ -1726,6 +1733,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
method: 'Method', method: 'Method',
path: 'Path', path: 'Path',
viewDetail: 'View Details', viewDetail: 'View Details',
textLink: 'Test Connection',
noResult: 'Processing results will be displayed here' noResult: 'Processing results will be displayed here'
}, },
workflow: { workflow: {

View File

@@ -782,14 +782,15 @@ export const zh = {
createNewMemorySummary: '创建新记忆条目', createNewMemorySummary: '创建新记忆条目',
createNewApplication: '创建新应用', createNewApplication: '创建新应用',
createNewApplicationDesc: '创建新空间应用', createNewApplicationDesc: '零代码拖拽3分钟创应用',
createNewKnowledge: '创建知识', createNewKnowledge: '创建知识',
createNewKnowledgeDesc: '创建新记忆条目', createNewKnowledgeDesc: '秒变可搜索的专属知识库',
memoryConversation: '记忆对话', memoryConversation: '记忆对话',
memoryConversationDesc: '记忆对话', memoryConversationDesc: '让AI越用越懂你',
helpCenter: '帮助中心',
helpCenterDesc: '一站式解决疑问快速上手',
memorySummary: '查看记忆摘要', memorySummary: '查看记忆摘要',
memorySummaryDesc: '查看记忆摘要报告', memorySummaryDesc: '查看记忆摘要报告',
@@ -962,6 +963,8 @@ export const zh = {
reset: '重置', reset: '重置',
refresh: '刷新', refresh: '刷新',
return: '返回', return: '返回',
statusEnabled: '可用',
statusDisabled: '不可用'
}, },
product: { product: {
applicationManagement: '应用管理', applicationManagement: '应用管理',
@@ -1299,8 +1302,8 @@ export const zh = {
IMPLICIT_MEMORY: '隐性记忆', IMPLICIT_MEMORY: '隐性记忆',
EMOTIONAL_MEMORY: '情绪记忆', EMOTIONAL_MEMORY: '情绪记忆',
EPISODIC_MEMORY: '情景记忆', EPISODIC_MEMORY: '情景记忆',
FORGETTING_MANAGEMENT: '遗忘', FORGET_MEMORY: '遗忘记忆',
endUserProfile: '核心档案', endUserProfile: '核心档案',
editEndUserProfile: '编辑', editEndUserProfile: '编辑',
other_name: '姓名', other_name: '姓名',
@@ -1522,6 +1525,7 @@ export const zh = {
deduplication_desc: '去重消歧完成,最终{{count}}个唯一实体' deduplication_desc: '去重消歧完成,最终{{count}}个唯一实体'
}, },
memoryConversation: { memoryConversation: {
chatEmpty:'有什么我可以帮您的吗?',
searchPlaceholder: '输入用户ID...', searchPlaceholder: '输入用户ID...',
userID: '用户ID', userID: '用户ID',
testMemoryConversation: '测试记忆对话', testMemoryConversation: '测试记忆对话',

View File

@@ -45,7 +45,7 @@ export const useUser = create<UserState>((set, get) => ({
const response = res as User; const response = res as User;
set({ user: response }) set({ user: response })
if (flag) { if (flag) {
window.location.href = response.role && response.current_workspace_id ? '/#/' : '/#/space' window.location.href = response.role && response.current_workspace_id ? '/#/' : '/#/index'
} }
localStorage.setItem('user', JSON.stringify(response)) localStorage.setItem('user', JSON.stringify(response))
}) })

View File

@@ -201,7 +201,11 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
...item, ...item,
...filterItem ...filterItem
} }
}) })
setKnowledgeConfig(prev => ({
...prev,
knowledge_bases: [...knowledge_bases]
}))
setData((prev) => { setData((prev) => {
prev = prev as Config prev = prev as Config
const knowledge_retrieval: KnowledgeConfig = { const knowledge_retrieval: KnowledgeConfig = {

View File

@@ -16,7 +16,7 @@ import { maskApiKeys } from '@/utils/apiKeyReplacer'
const Api: FC<{ application: Application | null }> = ({ application }) => { const Api: FC<{ application: Application | null }> = ({ application }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const activeMethods = ['GET']; const activeMethods = ['POST'];
const { message, modal } = App.useApp() const { message, modal } = App.useApp()
const copyContent = window.location.origin + '/v1/chat' const copyContent = window.location.origin + '/v1/chat'
const apiKeyModalRef = useRef<ApiKeyModalRef>(null); const apiKeyModalRef = useRef<ApiKeyModalRef>(null);

View File

@@ -11,6 +11,7 @@ import Empty from '@/components/Empty'
import { formatDateTime } from '@/utils/format'; import { formatDateTime } from '@/utils/format';
import { randomString } from '@/utils/common' import { randomString } from '@/utils/common'
import BgImg from '@/assets/images/conversation/bg.png' import BgImg from '@/assets/images/conversation/bg.png'
import ChatEmpty from '@/assets/images/empty/chatEmpty.png'
import Chat from '@/components/Chat' import Chat from '@/components/Chat'
import type { ChatItem } from '@/components/Chat/types' import type { ChatItem } from '@/components/Chat/types'
import ButtonCheckbox from '@/components/ButtonCheckbox' import ButtonCheckbox from '@/components/ButtonCheckbox'
@@ -261,7 +262,7 @@ const Conversation: FC = () => {
<div className="rb:relative rb:h-screen rb:px-4 rb:flex-[1_1_auto]"> <div className="rb:relative rb:h-screen rb:px-4 rb:flex-[1_1_auto]">
<div className='rb:w-[760px] rb:h-screen rb:mx-auto rb:pt-10'> <div className='rb:w-[760px] rb:h-screen rb:mx-auto rb:pt-10'>
<Chat <Chat
empty={<Empty url={BgImg} className="rb:h-full" size={[320,180]} subTitle={t('memoryConversation.emptyDesc')} />} empty={<Empty url={ChatEmpty} className="rb:h-full" size={[320,180]} title={t('memoryConversation.chatEmpty')} subTitle={t('memoryConversation.emptyDesc')} />}
contentClassName="rb:h-[calc(100%-152px)] " contentClassName="rb:h-[calc(100%-152px)] "
data={chatList} data={chatList}
streamLoading={streamLoading} streamLoading={streamLoading}

View File

@@ -1,3 +1,11 @@
/*
* @Description:
* @Version: 0.0.1
* @Author: yujiangping
* @Date: 2026-01-05 17:22:23
* @LastEditors: yujiangping
* @LastEditTime: 2026-01-15 14:55:51
*/
import { type FC } from 'react' import { type FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@@ -5,33 +13,49 @@ import Card from './Card';
import applicationIcon from '@/assets/images/menu/application_active.svg'; import applicationIcon from '@/assets/images/menu/application_active.svg';
import knowledgeIcon from '@/assets/images/menu/knowledge_active.svg'; import knowledgeIcon from '@/assets/images/menu/knowledge_active.svg';
import memoryConversationIcon from '@/assets/images/menu/memoryConversation_active.svg'; import memoryConversationIcon from '@/assets/images/menu/memoryConversation_active.svg';
import helpCenterIcon from '@/assets/images/menu/helpCenter_active.svg'
import arrowTopRight from '@/assets/images/home/arrow_top_right.svg'; import arrowTopRight from '@/assets/images/home/arrow_top_right.svg';
const quickOperations = [ const quickOperations = [
{ key: 'createNewApplication', url: '/application' }, { key: 'createNewApplication', url: '/application' },
{ key: 'createNewKnowledge', url: '/knowledge-base' }, { key: 'createNewKnowledge', url: '/knowledge-base' },
{ key: 'memoryConversation', url: '/memory-conversation' }, { key: 'memoryConversation', url: '/memory-conversation' },
{ key: 'helpCenter', url: '' },
] ]
const quickOperationIcons: {[key: string]: string | undefined} = { const quickOperationIcons: {[key: string]: string | undefined} = {
createNewApplication: applicationIcon, createNewApplication: applicationIcon,
createNewKnowledge: knowledgeIcon, createNewKnowledge: knowledgeIcon,
memoryConversation: memoryConversationIcon, memoryConversation: memoryConversationIcon,
helpCenter: helpCenterIcon
} }
const QuickOperation:FC = () => { const QuickOperation:FC = () => {
const { t } = useTranslation() const { t, i18n } = useTranslation()
const navigate = useNavigate(); const navigate = useNavigate();
const handleJump = (url: string | null) => { const handleJump = (url: string | null) => {
if (url) { if (url) {
navigate(url) navigate(url)
}else{
const currentLang = i18n.language;
const lang = currentLang === 'zh' ? 'zh' : 'en';
const helpUrl = `https://docs.redbearai.com/s/${lang}-memorybear`;
// 创建隐藏的 a 标签来避免弹窗拦截
const link = document.createElement('a');
link.href = helpUrl;
link.target = '_blank';
link.rel = 'noopener noreferrer';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} }
} }
return ( return (
<Card <Card
title={t('dashboard.quickOperation')} title={t('dashboard.quickOperation')}
> >
<div className="rb:grid rb:grid-cols-3 rb:gap-[16px]"> <div className="rb:grid rb:grid-cols-4 rb:gap-[16px]">
{quickOperations.map(item => ( {quickOperations.map(item => (
<div key={item.key} className="rb:rounded-[8px] rb:p-[20px_16px] rb:border-1 rb:border-[#DFE4ED] rb:cursor-pointer rb:hover:border-[#155EEF]" onClick={() => handleJump(item.url)}> <div key={item.key} className="rb:rounded-[8px] rb:p-[20px_16px] rb:border-1 rb:border-[#DFE4ED] rb:cursor-pointer rb:hover:border-[#155EEF]" onClick={() => handleJump(item.url)}>
<div className="rb:flex rb:justify-between"> <div className="rb:flex rb:justify-between">

View File

@@ -47,7 +47,7 @@ const QuickActions: FC<QuickActionsProps> = ({ onNavigate }) => {
key: 'space-management', key: 'space-management',
icon: spaceIcon, icon: spaceIcon,
title: t('quickActions.spaceManagement'), title: t('quickActions.spaceManagement'),
onClick: () => onNavigate?.('/spce') onClick: () => onNavigate?.('/space')
}, },
// { // {
// key: 'workflow-orchestration', // key: 'workflow-orchestration',

View File

@@ -2,7 +2,7 @@
import { useEffect, useState, useRef, useCallback, type FC } from 'react'; import { useEffect, useState, useRef, useCallback, type FC } from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom'; import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Switch, Button, Dropdown, Space, Modal, message, Radio } from 'antd'; import { Switch, Button, Dropdown, Space, Modal, message, Radio, Tooltip } from 'antd';
import type { MenuProps } from 'antd'; import type { MenuProps } from 'antd';
import SearchInput from '@/components/SearchInput' import SearchInput from '@/components/SearchInput'
import Table, { type TableRef } from '@/components/Table' import Table, { type TableRef } from '@/components/Table'
@@ -564,6 +564,37 @@ const Private: FC = () => {
</span> </span>
); );
} }
},{
title: t('knowledgeBase.processMsg'),
dataIndex: 'progress_msg',
key: 'progress_msg',
width: 320,
render: (value: string) => {
if (!value) return '-';
// 解析日志格式,将 \n 转换为换行
const formattedText = value.replace(/\\n/g, '\n');
return (
<Tooltip title={<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>{formattedText}</pre>} placement="topLeft">
<div
style={{
maxWidth: '320px',
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
lineHeight: '1.5',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}}
>
{formattedText}
</div>
</Tooltip>
);
}
}, },
{ {
title: t('knowledgeBase.processingMode'), title: t('knowledgeBase.processingMode'),

View File

@@ -292,7 +292,7 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
if (params.dataType === 'node') { if (params.dataType === 'node') {
const node = params.data as KnowledgeNode const node = params.data as KnowledgeNode
return ` return `
<div> <div class="rb:max-w-[560px]">
<div><strong>${node.entity_name}</strong></div> <div><strong>${node.entity_name}</strong></div>
<div>类型: ${node.entity_type}</div> <div>类型: ${node.entity_type}</div>
<div>重要度: ${(node.pagerank * 100).toFixed(2)}%</div> <div>重要度: ${(node.pagerank * 100).toFixed(2)}%</div>
@@ -301,10 +301,10 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
} else if (params.dataType === 'edge') { } else if (params.dataType === 'edge') {
const edge = params.data as KnowledgeEdge const edge = params.data as KnowledgeEdge
return ` return `
<div> <div class="rb:max-w-[560px]">
<div><strong>关系</strong></div> <div><strong>关系</strong></div>
<div>权重: ${edge.weight}</div> <div>权重: ${edge.weight}</div>
<div>${edge.description}</div> <div class="rb:break-words rb:whitespace-pre-wrap">${edge.description}</div>
</div> </div>
` `
} }

View File

@@ -10,6 +10,7 @@ import commerce from '@/assets/images/order/commerce.png'
import checkIcon from '@/assets/images/login/checkBg.png' import checkIcon from '@/assets/images/login/checkBg.png'
import alertIcon from '@/assets/images/order/alert.svg'; import alertIcon from '@/assets/images/order/alert.svg';
import { useUser } from '@/store/user' import { useUser } from '@/store/user'
import { useI18n } from '@/store/locale'
interface PriceItem { interface PriceItem {
type: string; type: string;
@@ -116,6 +117,7 @@ const PricingView: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useUser(); const { user } = useUser();
const { language } = useI18n()
const handleChoosePlan = (type: string) => { const handleChoosePlan = (type: string) => {
switch(type) { switch(type) {
@@ -127,6 +129,7 @@ const PricingView: React.FC = () => {
navigate(user.current_workspace_id ? '/' : '/space'); navigate(user.current_workspace_id ? '/' : '/space');
break break
case 'commerce': case 'commerce':
window.open(`https://docs.redbearai.com/s/${language || 'en'}-memorybear`, '_blank')
break break
} }
}; };

View File

@@ -256,7 +256,7 @@ const SelfReflectionEngine: React.FC = () => {
{t('reflectionEngine.exampleText')} {t('reflectionEngine.exampleText')}
</div> </div>
<Button type="primary" block loading={runLoading} onClick={handleRun}>{t('reflectionEngine.run')}</Button> <Button type="primary" block loading={runLoading} disabled={!values?.reflection_enabled} onClick={handleRun}>{t('reflectionEngine.run')}</Button>
</RbCard> </RbCard>
{result && <> {result && <>
<RbCard <RbCard

View File

@@ -32,7 +32,7 @@ const typeList = [
{ key: 'EXPLICIT_MEMORY' } { key: 'EXPLICIT_MEMORY' }
] ]
}, },
{ key: 'FORGETTING_MANAGEMENT', bg: 5 }, { key: 'FORGET_MEMORY', bg: 5 },
] ]
const NodeStatistics: FC = () => { const NodeStatistics: FC = () => {

View File

@@ -38,7 +38,7 @@ const Detail: FC = () => {
}) })
} }
const items = useMemo(() => { const items = useMemo(() => {
return ['PERCEPTUAL_MEMORY', 'WORKING_MEMORY', 'EMOTIONAL_MEMORY', 'SHORT_TERM_MEMORY', 'IMPLICIT_MEMORY', 'EPISODIC_MEMORY', 'EXPLICIT_MEMORY', 'FORGETTING_MANAGEMENT'] return ['PERCEPTUAL_MEMORY', 'WORKING_MEMORY', 'EMOTIONAL_MEMORY', 'SHORT_TERM_MEMORY', 'IMPLICIT_MEMORY', 'EPISODIC_MEMORY', 'EXPLICIT_MEMORY', 'FORGET_MEMORY']
.map(key => ({ key, label: t(`userMemory.${key}`) })) .map(key => ({ key, label: t(`userMemory.${key}`) }))
}, [t]) }, [t])
const onClick = ({ key }: { key: string }) => { const onClick = ({ key }: { key: string }) => {
@@ -67,7 +67,7 @@ const Detail: FC = () => {
</div> </div>
</Dropdown> </Dropdown>
} }
extra={type === 'FORGETTING_MANAGEMENT' && extra={type === 'FORGET_MEMORY' &&
<Button type="primary" ghost className="rb:group rb:h-6! rb:px-2!" onClick={handleRefresh}> <Button type="primary" ghost className="rb:group rb:h-6! rb:px-2!" onClick={handleRefresh}>
<img src={refreshIcon} className="rb:w-4 rb:h-4" /> <img src={refreshIcon} className="rb:w-4 rb:h-4" />
{t('common.refresh')} {t('common.refresh')}
@@ -75,7 +75,7 @@ const Detail: FC = () => {
/> />
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4"> <div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4">
{type === 'EMOTIONAL_MEMORY' && <StatementDetail />} {type === 'EMOTIONAL_MEMORY' && <StatementDetail />}
{type === 'FORGETTING_MANAGEMENT' && <ForgetDetail ref={forgetDetailRef} />} {type === 'FORGET_MEMORY' && <ForgetDetail ref={forgetDetailRef} />}
{type === 'IMPLICIT_MEMORY' && <ImplicitDetail />} {type === 'IMPLICIT_MEMORY' && <ImplicitDetail />}
{type === 'SHORT_TERM_MEMORY' && <ShortTermDetail />} {type === 'SHORT_TERM_MEMORY' && <ShortTermDetail />}
{type === 'PERCEPTUAL_MEMORY' && <PerceptualDetail />} {type === 'PERCEPTUAL_MEMORY' && <PerceptualDetail />}

View File

@@ -1,7 +1,8 @@
import type { FC } from 'react'; import type { FC } from 'react';
import { Select, Button } from 'antd'; import { Select } from 'antd';
import { Node } from '@antv/x6'; // import { Node } from '@antv/x6';
import type { GraphRef } from '../types' import type { GraphRef } from '../types'
import { PlusOutlined, MinusOutlined } from '@ant-design/icons'
interface CanvasToolbarProps { interface CanvasToolbarProps {
miniMapRef: React.RefObject<HTMLDivElement>; miniMapRef: React.RefObject<HTMLDivElement>;
@@ -18,15 +19,16 @@ interface CanvasToolbarProps {
const CanvasToolbar: FC<CanvasToolbarProps> = ({ const CanvasToolbar: FC<CanvasToolbarProps> = ({
miniMapRef, miniMapRef,
graphRef, graphRef,
isHandMode, // isHandMode,
setIsHandMode, // setIsHandMode,
zoomLevel, zoomLevel,
canUndo, // canUndo,
canRedo, // canRedo,
onUndo, // onUndo,
onRedo, // onRedo,
}) => { }) => {
// 整理布局函数 // 整理布局函数
/*
const handleLayout = () => { const handleLayout = () => {
if (!graphRef.current) return; if (!graphRef.current) return;
const nodes = graphRef.current.getNodes(); const nodes = graphRef.current.getNodes();
@@ -144,28 +146,14 @@ const CanvasToolbar: FC<CanvasToolbarProps> = ({
currentY += 300; // 不同树之间的间距 currentY += 300; // 不同树之间的间距
}); });
}; };
*/
return ( return (
<> <>
{/* 小地图 */} {/* 小地图 */}
<div ref={miniMapRef} className="rb:absolute rb:bottom-17 rb:left-5 rb:z-1000"></div> <div ref={miniMapRef} className="rb:absolute rb:bottom-15 rb:right-8 rb:z-1000 rb:rounded-lg rb:overflow-hidden"></div>
{/* 缩放控制按钮 */} {/* 缩放控制按钮 */}
<div className="rb:absolute rb:bottom-5 rb:left-5 rb:flex rb:flex-row rb:gap-2 rb:z-1000"> <div className="rb:h-8.5 rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.15)] rb:px-3 rb:py-2 rb:absolute rb:bottom-5 rb:right-8 rb:flex rb:flex-row rb:gap-4 rb:z-1000">
<Button <MinusOutlined className="rb:text-[16px] rb:cursor-pointer" onClick={() => graphRef.current?.zoom(-0.1)} />
type={isHandMode ? 'primary' : 'default'}
onClick={() => {
const newHandMode = !isHandMode;
setIsHandMode(newHandMode);
if (newHandMode) {
graphRef.current?.enablePanning();
} else {
graphRef.current?.disablePanning();
}
}}
>
{isHandMode ? '✋' : '👆'}
</Button>
<Button onClick={() => graphRef.current?.zoom(0.1)}>+</Button>
<Select <Select
value={Math.round(zoomLevel * 100)} value={Math.round(zoomLevel * 100)}
onChange={(value: number | string) => { onChange={(value: number | string) => {
@@ -179,7 +167,7 @@ const CanvasToolbar: FC<CanvasToolbarProps> = ({
console.log('props', props) console.log('props', props)
return `${props.value}%` return `${props.value}%`
}} }}
className="rb:w-20" className="rb:w-20 rb:h-4!"
options={[ options={[
{ label: '25%', value: 25 }, { label: '25%', value: 25 },
{ label: '50%', value: 50 }, { label: '50%', value: 50 },
@@ -190,11 +178,10 @@ const CanvasToolbar: FC<CanvasToolbarProps> = ({
{ label: '200%', value: 200 }, { label: '200%', value: 200 },
{ label: '自适应', value: 'fit' }, { label: '自适应', value: 'fit' },
]} ]}
variant='borderless'
size="small"
/> />
<Button onClick={() => graphRef.current?.zoom(-0.1)}>-</Button> <PlusOutlined className="rb:text-[16px] rb:cursor-pointer" onClick={() => graphRef.current?.zoom(0.1)} />
<Button disabled={!canUndo} onClick={onUndo}></Button>
<Button disabled={!canRedo} onClick={onRedo}></Button>
<Button onClick={handleLayout}></Button>
</div> </div>
</> </>
); );

View File

@@ -234,9 +234,9 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type)); filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type));
} else { } else {
// Original filtering for non-loop child nodes // Original filtering for non-loop child nodes
filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'break', 'cycle-start'].includes(nodeType.type)); filteredNodes = category.nodes.filter(nodeType => !['start', 'break', 'cycle-start'].includes(nodeType.type));
filteredNodes = category.nodes.filter(nodeType => filteredNodes = category.nodes.filter(nodeType =>
nodeType.type !== 'start' && nodeType.type !== 'end' && nodeType.type !== 'cycle-start' && nodeType.type !== 'break' nodeType.type !== 'start' && nodeType.type !== 'cycle-start' && nodeType.type !== 'break'
); );
} }

View File

@@ -422,7 +422,7 @@ export const useWorkflowGraph = ({
graphRef.current.use( graphRef.current.use(
new MiniMap({ new MiniMap({
container: miniMapRef.current, container: miniMapRef.current,
width: 100, width: 170,
height: 80, height: 80,
padding: 5, padding: 5,
}), }),