diff --git a/api/app/controllers/app_controller.py b/api/app/controllers/app_controller.py
index f55ea5b5..43f177ef 100644
--- a/api/app/controllers/app_controller.py
+++ b/api/app/controllers/app_controller.py
@@ -11,15 +11,16 @@ from app.core.response_utils import success
from app.db import get_db
from app.dependencies import get_current_user, cur_workspace_access_guard
from app.models import User
-from app.models.app_model import AppType, App
+from app.models.app_model import AppType
from app.repositories import knowledge_repository
+from app.repositories.end_user_repository import EndUserRepository
from app.schemas import app_schema
from app.schemas.response_schema import PageData, PageMeta
+from app.schemas.workflow_schema import WorkflowConfig as WorkflowConfigSchema
from app.schemas.workflow_schema import WorkflowConfigUpdate
from app.services import app_service, workspace_service
from app.services.agent_config_helper import enrich_agent_config
from app.services.app_service import AppService
-from app.schemas.workflow_schema import WorkflowConfig as WorkflowConfigSchema
from app.services.workflow_service import WorkflowService, get_workflow_service
router = APIRouter(prefix="/apps", tags=["Apps"])
@@ -405,6 +406,15 @@ async def draft_run(
# 只读操作,允许访问共享应用
service._validate_app_accessible(app, workspace_id)
+ if payload.user_id is None:
+ end_user_repo = EndUserRepository(db)
+ new_end_user = end_user_repo.get_or_create_end_user(
+ app_id=app_id,
+ other_id=str(current_user.id),
+ original_user_id=str(current_user.id) # Save original user_id to other_id
+ )
+ payload.user_id = str(new_end_user.id)
+
# 处理会话ID(创建或验证)
conversation_id = await draft_service._ensure_conversation(
conversation_id=payload.conversation_id,
diff --git a/api/app/core/tools/mcp/client.py b/api/app/core/tools/mcp/client.py
index e513a147..c082b314 100644
--- a/api/app/core/tools/mcp/client.py
+++ b/api/app/core/tools/mcp/client.py
@@ -96,10 +96,7 @@ class SimpleMCPClient:
"""初始化 SSE MCP 会话 - 参考 Dify 实现"""
try:
# 建立 SSE 连接
- response = await self._session.get(
- self.server_url,
- headers={"Accept": "text/event-stream"}
- )
+ response = await self._session.get(self.server_url)
if response.status != 200:
error_text = await response.text()
diff --git a/api/app/services/conversation_service.py b/api/app/services/conversation_service.py
index 3695a222..275d6413 100644
--- a/api/app/services/conversation_service.py
+++ b/api/app/services/conversation_service.py
@@ -516,8 +516,16 @@ class ConversationService:
conversation_messages = self.get_conversation_history(
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:
system_prompt = f.read()
@@ -536,6 +544,7 @@ class ConversationService:
]
logger.info(f"Invoking LLM for conversation_id={conversation_id}")
model_resp = await llm.ainvoke(messages)
+
try:
if isinstance(model_resp.content, str):
result = json_repair.repair_json(model_resp.content, return_objects=True)
diff --git a/api/app/services/draft_run_service.py b/api/app/services/draft_run_service.py
index 569684d5..50934226 100644
--- a/api/app/services/draft_run_service.py
+++ b/api/app/services/draft_run_service.py
@@ -245,7 +245,8 @@ class DraftRunService:
storage_type: Optional[str] = None,
user_rag_memory_id: Optional[str] = None,
web_search: bool = True,
- memory: bool = True
+ memory: bool = True,
+ sub_agent: bool = False
) -> Dict[str, Any]:
"""执行试运行(使用 LangChain Agent)
@@ -435,7 +436,7 @@ class DraftRunService:
elapsed_time = time.time() - start_time
# 8. 保存会话消息
- if agent_config.memory and agent_config.memory.get("enabled"):
+ if not sub_agent and agent_config.memory and agent_config.memory.get("enabled"):
await self._save_conversation_message(
conversation_id=conversation_id,
user_message=message,
diff --git a/api/app/services/memory_forget_service.py b/api/app/services/memory_forget_service.py
index 8979682d..2db4cdc7 100644
--- a/api/app/services/memory_forget_service.py
+++ b/api/app/services/memory_forget_service.py
@@ -267,14 +267,14 @@ class MemoryForgetService:
elif node_type_label == 'memorysummary':
node_type_label = 'summary'
- # 将 Neo4j DateTime 对象转换为时间戳
+ # 将 Neo4j DateTime 对象转换为时间戳(毫秒)
last_access_time = result['last_access_time']
last_access_dt = convert_neo4j_datetime_to_python(last_access_time)
# 确保 datetime 带有时区信息(假定为 UTC),避免 naive datetime 导致的时区偏差
if last_access_dt:
if last_access_dt.tzinfo is None:
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:
last_access_timestamp = 0
@@ -520,7 +520,7 @@ class MemoryForgetService:
'average_activation_value': result['average_activation'],
'low_activation_nodes': result['low_activation_nodes'] or 0,
'forgetting_threshold': forgetting_threshold,
- 'timestamp': int(datetime.now().timestamp())
+ 'timestamp': int(datetime.now().timestamp() * 1000)
}
else:
activation_metrics = {
@@ -530,7 +530,7 @@ class MemoryForgetService:
'average_activation_value': None,
'low_activation_nodes': 0,
'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,
'average_activation': record.average_activation_value,
'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)} 个日期的历史趋势数据")
@@ -661,7 +661,7 @@ class MemoryForgetService:
'node_distribution': node_distribution,
'recent_trends': recent_trends,
'pending_nodes': pending_nodes,
- 'timestamp': int(datetime.now().timestamp())
+ 'timestamp': int(datetime.now().timestamp() * 1000)
}
api_logger.info(
diff --git a/api/app/services/multi_agent_orchestrator.py b/api/app/services/multi_agent_orchestrator.py
index b0c7a957..1972f344 100644
--- a/api/app/services/multi_agent_orchestrator.py
+++ b/api/app/services/multi_agent_orchestrator.py
@@ -1327,7 +1327,8 @@ class MultiAgentOrchestrator:
web_search=web_search,
memory=memory,
storage_type=storage_type,
- user_rag_memory_id=user_rag_memory_id
+ user_rag_memory_id=user_rag_memory_id,
+ sub_agent=True
)
return result
diff --git a/api/app/services/workflow_service.py b/api/app/services/workflow_service.py
index f9988352..974d5418 100644
--- a/api/app/services/workflow_service.py
+++ b/api/app/services/workflow_service.py
@@ -13,11 +13,10 @@ from sqlalchemy.orm import Session
from app.core.error_codes import BizCode
from app.core.exceptions import BusinessException
from app.core.workflow.validator import validate_workflow_config
-from app.db import get_db, get_db_context
+from app.db import get_db
+from app.models.conversation_model import Message
from app.models.workflow_model import WorkflowConfig, WorkflowExecution
from app.repositories.conversation_repository import MessageRepository
-from app.models.conversation_model import Message
-from app.repositories.end_user_repository import EndUserRepository
from app.repositories.workflow_repository import (
WorkflowConfigRepository,
WorkflowExecutionRepository,
@@ -483,14 +482,6 @@ class WorkflowService:
try:
# 更新状态为运行中
self.update_execution_status(execution.execution_id, "running")
- with get_db_context() as db:
- end_user_repo = EndUserRepository(db)
- new_end_user = end_user_repo.get_or_create_end_user(
- app_id=app_id,
- other_id=payload.user_id,
- original_user_id=payload.user_id # Save original user_id to other_id
- )
- end_user_id = str(new_end_user.id)
executions = self.execution_repo.get_by_conversation_id(conversation_id=conversation_id_uuid)
@@ -511,7 +502,7 @@ class WorkflowService:
input_data=input_data,
execution_id=execution.execution_id,
workspace_id=str(workspace_id),
- user_id=end_user_id
+ user_id=payload.user_id
)
# 更新执行结果
@@ -638,14 +629,6 @@ class WorkflowService:
try:
# 更新状态为运行中
self.update_execution_status(execution.execution_id, "running")
- with get_db_context() as db:
- end_user_repo = EndUserRepository(db)
- new_end_user = end_user_repo.get_or_create_end_user(
- app_id=app_id,
- other_id=payload.user_id,
- original_user_id=payload.user_id # Save original user_id to other_id
- )
- end_user_id = str(new_end_user.id)
executions = self.execution_repo.get_by_conversation_id(conversation_id=conversation_id_uuid)
for exec_res in executions:
@@ -665,7 +648,7 @@ class WorkflowService:
input_data=input_data,
execution_id=execution.execution_id,
workspace_id=str(workspace_id),
- user_id=end_user_id
+ user_id=payload.user_id
):
if event.get("event") == "workflow_end":
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 8e3140d9..1abbc2cc 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -30,15 +30,12 @@ import 'dayjs/plugin/utc'
import { cookieUtils } from './utils/request';
-
-
-
function App() {
const { t } = useTranslation();
const { locale, language, timeZone } = useI18n()
useEffect(() => {
const authToken = cookieUtils.get('authToken')
- if (!authToken && !window.location.hash.includes('#/login')) {
+ if (!authToken && !window.location.hash.includes('#/login') && !window.location.hash.includes('#/conversation/')) {
window.location.href = `/#/login`;
}
}, [])
diff --git a/web/src/assets/images/menu/helpCenter.svg b/web/src/assets/images/menu/helpCenter.svg
new file mode 100644
index 00000000..504e309c
--- /dev/null
+++ b/web/src/assets/images/menu/helpCenter.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/web/src/assets/images/menu/helpCenter_active.svg b/web/src/assets/images/menu/helpCenter_active.svg
new file mode 100644
index 00000000..2840c421
--- /dev/null
+++ b/web/src/assets/images/menu/helpCenter_active.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/web/src/components/Layout/NoAuthLayout.tsx b/web/src/components/Layout/NoAuthLayout.tsx
new file mode 100644
index 00000000..a2e6f274
--- /dev/null
+++ b/web/src/components/Layout/NoAuthLayout.tsx
@@ -0,0 +1,14 @@
+import { Outlet } from 'react-router-dom';
+import { type FC } from 'react';
+
+// 基础布局组件,用于展示内容并保留用户信息获取功能
+const NoAuthLayout: FC = () => {
+
+ return (
+
+
+
+ )
+};
+
+export default NoAuthLayout;
\ No newline at end of file
diff --git a/web/src/components/Markdown/Code.tsx b/web/src/components/Markdown/Code.tsx
index 74c9c89c..de60d0de 100644
--- a/web/src/components/Markdown/Code.tsx
+++ b/web/src/components/Markdown/Code.tsx
@@ -81,7 +81,7 @@ const Code: FC = (props) => {
)
}
- return {children}
+ return {children}
}
export default Code
diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts
index 2d2d96a2..b10f3d5c 100644
--- a/web/src/i18n/en.ts
+++ b/web/src/i18n/en.ts
@@ -91,6 +91,7 @@ export const en = {
memberManagement: 'Member Management',
memorySummary: 'Memory Summary',
memoryConversation: 'Memory Validation',
+ helpCenter: 'Help Center',
memorySummaryHandlers: 'Memory Summary Handlers',
createMemorySummary: 'Create Memory Summary',
memoryManagement: 'Memory Management',
@@ -190,7 +191,8 @@ export const en = {
memoryConversation: 'Memory Conversation',
memoryConversationDesc: 'Memory Conversation',
-
+ helpCenter: 'Help Center',
+ helpCenterDesc: 'Help Center',
memorySummary: 'View Memory Summary',
memorySummaryDesc: 'View Memory Summary Report',
@@ -616,6 +618,7 @@ export const en = {
retrieve:'Retrieve',
processing: 'Processing',
processingMode: 'Processing Mode',
+ processMsg: 'Processing Message',
dataSize: 'Data Size',
createUpdateTime: 'Create/Update Time',
operation: 'Operation',
@@ -1960,6 +1963,9 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
addMessage: 'Add Message',
answerDesc: 'Reply',
addNode: 'Add Node',
+ arrange: 'Arrange',
+ redo: 'Redo',
+ undo: 'Undo',
},
emotionEngine: {
emotionEngineConfig: 'Emotion Engine Configuration',
diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts
index 53e71c84..6c3e3ec1 100644
--- a/web/src/i18n/zh.ts
+++ b/web/src/i18n/zh.ts
@@ -789,7 +789,8 @@ export const zh = {
memoryConversation: '记忆对话',
memoryConversationDesc: '记忆对话',
-
+ helpCenter: '帮助中心',
+ helpCenterDesc: '帮助中心',
memorySummary: '查看记忆摘要',
memorySummaryDesc: '查看记忆摘要报告',
@@ -2057,6 +2058,9 @@ export const zh = {
addMessage: '添加消息',
answerDesc: '回复',
addNode: '添加节点',
+ arrange: '整理',
+ redo: '重做',
+ undo: '撤销',
},
emotionEngine: {
emotionEngineConfig: '情感引擎配置',
diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx
index 5c302565..7189dc9c 100644
--- a/web/src/routes/index.tsx
+++ b/web/src/routes/index.tsx
@@ -34,6 +34,7 @@ const componentMap: Record>> =
AuthSpaceLayout: lazy(() => import('@/components/Layout/AuthSpaceLayout')),
BasicLayout: lazy(() => import('@/components/Layout/BasicLayout')),
LoginLayout: lazy(() => import('@/components/Layout/LoginLayout')),
+ NoAuthLayout: lazy(() => import('@/components/Layout/NoAuthLayout')),
// 视图组件
Index: lazy(() => import('@/views/Index')),
Home: lazy(() => import('@/views/Home')),
diff --git a/web/src/routes/routes.json b/web/src/routes/routes.json
index db0c1b7d..1c317033 100644
--- a/web/src/routes/routes.json
+++ b/web/src/routes/routes.json
@@ -42,12 +42,17 @@
"element": "BasicLayout",
"children": [
{ "path": "/application/config/:id", "element": "ApplicationConfig" },
- { "path": "/conversation/:token", "element": "Conversation" },
{ "path": "/user-memory/neo4j/:id", "element": "Neo4jUserMemoryDetail" },
{ "path": "/statement/:id", "element": "StatementDetail" },
{ "path": "/user-memory/detail/:id/:type", "element": "MemoryNodeDetail" }
]
},
+ {
+ "element": "NoAuthLayout",
+ "children": [
+ { "path": "/conversation/:token", "element": "Conversation" }
+ ]
+ },
{
"element": "LoginLayout",
"children": [
diff --git a/web/src/views/Home/components/QuickOperation.tsx b/web/src/views/Home/components/QuickOperation.tsx
index 892dd8a0..d894417a 100644
--- a/web/src/views/Home/components/QuickOperation.tsx
+++ b/web/src/views/Home/components/QuickOperation.tsx
@@ -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 { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom';
@@ -5,33 +13,49 @@ import Card from './Card';
import applicationIcon from '@/assets/images/menu/application_active.svg';
import knowledgeIcon from '@/assets/images/menu/knowledge_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';
const quickOperations = [
{ key: 'createNewApplication', url: '/application' },
{ key: 'createNewKnowledge', url: '/knowledge-base' },
{ key: 'memoryConversation', url: '/memory-conversation' },
+ { key: 'helpCenter', url: '' },
]
const quickOperationIcons: {[key: string]: string | undefined} = {
createNewApplication: applicationIcon,
createNewKnowledge: knowledgeIcon,
memoryConversation: memoryConversationIcon,
+ helpCenter: helpCenterIcon
}
const QuickOperation:FC = () => {
- const { t } = useTranslation()
+ const { t, i18n } = useTranslation()
const navigate = useNavigate();
const handleJump = (url: string | null) => {
if (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 (
-
+
{quickOperations.map(item => (
handleJump(item.url)}>
diff --git a/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx b/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx
index 8087e596..382deac0 100644
--- a/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx
+++ b/web/src/views/KnowledgeBase/[knowledgeBaseId]/Private.tsx
@@ -2,7 +2,7 @@
import { useEffect, useState, useRef, useCallback, type FC } from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
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 SearchInput from '@/components/SearchInput'
import Table, { type TableRef } from '@/components/Table'
@@ -564,6 +564,37 @@ const Private: FC = () => {
);
}
+ },{
+ 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 (
+
{formattedText}} placement="topLeft">
+
+ {formattedText}
+
+
+ );
+ }
},
{
title: t('knowledgeBase.processingMode'),
diff --git a/web/src/views/UserMemory/index.tsx b/web/src/views/UserMemory/index.tsx
index 064b55be..d8318290 100644
--- a/web/src/views/UserMemory/index.tsx
+++ b/web/src/views/UserMemory/index.tsx
@@ -41,7 +41,9 @@ export default function UserMemory() {
navigate(`/user-memory/${id}`)
}
}
- const handleViewMemoryConfig = () => {
+ const handleViewMemoryConfig = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
navigate(`/memory`)
}
@@ -84,8 +86,9 @@ export default function UserMemory() {
title={name || '-'}
extra={
handleViewDetail(end_user.id)}
>
}
+ className="rb:cursor-pointer"
+ onClick={() => handleViewDetail(end_user.id)}
>
{t('userMemory.capacity')}
@@ -96,7 +99,7 @@ export default function UserMemory() {
{t(`userMemory.${item.type || 'person'}`)}
-
+
{t('userMemory.memory_config_name')}
{
name: displayName,
category: categoryIndex >= 0 ? categoryIndex : 0,
symbolSize: symbolSize, // 根据连接数调整节点大小
- itemStyle: {
- color: colors[categoryIndex % 8]
- }
})
})
+ // 创建节点ID到标签的映射
+ const nodeIdToLabel: Record
= {}
+ nodes.forEach(node => {
+ nodeIdToLabel[node.id] = node.label
+ })
// 处理边数据
edges.forEach(edge => {
curEdges.push({
diff --git a/web/src/views/Workflow/components/Chat/Chat.tsx b/web/src/views/Workflow/components/Chat/Chat.tsx
index 9b1d3254..246c2e4c 100644
--- a/web/src/views/Workflow/components/Chat/Chat.tsx
+++ b/web/src/views/Workflow/components/Chat/Chat.tsx
@@ -40,7 +40,7 @@ const Chat = forwardRef(({ appId
const curVariables = startNodes[0].config.variables?.defaultValue
curVariables.forEach((vo: StartVariableItem) => {
- if (vo.default) {
+ if (typeof vo.default !== 'undefined') {
vo.value = vo.default
}
const lastVo = variables.find(item => item.name === vo.name)
@@ -55,6 +55,7 @@ const Chat = forwardRef(({ appId
setOpen(false)
setChatList([])
setVariables([])
+ setConversationId(null)
}
const handleEditVariables = () => {
variableConfigModalRef.current?.handleOpen(variables)
diff --git a/web/src/views/Workflow/components/Nodes/AddNode.tsx b/web/src/views/Workflow/components/Nodes/AddNode.tsx
index b9e602d4..d2d1d15c 100644
--- a/web/src/views/Workflow/components/Nodes/AddNode.tsx
+++ b/web/src/views/Workflow/components/Nodes/AddNode.tsx
@@ -13,11 +13,12 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
const handleNodeSelect = (selectedNodeType: any) => {
const parentBBox = node.getBBox();
const cycleId = data.cycle;
+ const horizontalSpacing = 20;
const id = `${selectedNodeType.type.replace(/-/g, '_') }_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
const newNode = graph.addNode({
...(graphNodeLibrary[selectedNodeType.type] || graphNodeLibrary.default),
- x: parentBBox.x,
+ x: parentBBox.x + horizontalSpacing,
y: parentBBox.y,
id,
data: {
@@ -47,7 +48,6 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
source: { cell: edge.getSourceCellId(), port: edge.getSourcePortId() },
target: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'left')?.id || 'left' },
attrs: edge.getAttrs(),
- zIndex: 3
});
});
@@ -58,7 +58,6 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
source: { cell: newNode.id, port: newNode.getPorts().find((port: any) => port.group === 'right')?.id || 'right' },
target: { cell: edge.getTargetCellId(), port: targetPortId },
attrs: edge.getAttrs(),
- zIndex: 3
});
});
diff --git a/web/src/views/Workflow/components/Nodes/LoopNode.tsx b/web/src/views/Workflow/components/Nodes/LoopNode.tsx
index 40b4b8ec..26109d58 100644
--- a/web/src/views/Workflow/components/Nodes/LoopNode.tsx
+++ b/web/src/views/Workflow/components/Nodes/LoopNode.tsx
@@ -11,10 +11,14 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
const { t } = useTranslation()
useEffect(() => {
- initNodes()
- // 检查是否需要添加add-node
- checkAndAddAddNode()
- }, [])
+ // 使用setTimeout确保在所有节点都添加完成后再创建连线
+ const timer = setTimeout(() => {
+ initNodes()
+ checkAndAddAddNode()
+ }, 50)
+
+ return () => clearTimeout(timer)
+ }, [graph])
const checkAndAddAddNode = () => {
if (!graph) return;
@@ -29,7 +33,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
const addNode = graph.addNode({
...graphNodeLibrary.addStart,
- x: cycleStartBBox.x + 64,
+ x: cycleStartBBox.x + 84,
y: cycleStartBBox.y,
data: {
type: 'add-node',
@@ -47,7 +51,8 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
const targetPorts = addNode.getPorts();
const sourcePort = sourcePorts.find((port: any) => port.group === 'right')?.id || 'right';
const targetPort = targetPorts.find((port: any) => port.group === 'left')?.id || 'left';
-
+
+ // 然后创建连线
graph.addEdge({
source: { cell: cycleStartNode.id, port: sourcePort },
target: { cell: addNode.id, port: targetPort },
@@ -61,7 +66,6 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
},
},
},
- zIndex: 10
});
}
}
@@ -93,7 +97,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
});
const addNode = graph.addNode({
...graphNodeLibrary.addStart,
- x: centerX + 64,
+ x: centerX + 84,
y: centerY,
data: {
type: 'add-node',
@@ -124,13 +128,11 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
strokeWidth: 1,
targetMarker: {
name: 'block',
- size: 8,
+ size: 2,
},
},
},
- zIndex: 10
}
-
graph.addEdge(edgeConfig)
}
diff --git a/web/src/views/Workflow/components/PortClickHandler.tsx b/web/src/views/Workflow/components/PortClickHandler.tsx
index 9b83215a..050ed35d 100644
--- a/web/src/views/Workflow/components/PortClickHandler.tsx
+++ b/web/src/views/Workflow/components/PortClickHandler.tsx
@@ -36,11 +36,77 @@ const PortClickHandler: React.FC = ({ graph }) => {
if (!sourceNode || !graph) return;
const sourceNodeData = sourceNode.getData();
+ const sourceNodeType = sourceNodeData?.type;
- // 计算新节点位置(在源节点右侧)
+ // 如果是cycle-start节点,需要处理add-node节点
+ let addNodePosition = null;
+ if (sourceNodeType === 'cycle-start' && sourceNodeData.cycle) {
+ const cycleId = sourceNodeData.cycle;
+ const addNodes = graph.getNodes().filter((n: any) =>
+ n.getData()?.type === 'add-node' && n.getData()?.cycle === cycleId
+ );
+
+ if (addNodes.length > 0) {
+ const addNode = addNodes[0];
+ addNodePosition = addNode.getBBox();
+ addNode.remove();
+ }
+ }
+
+ // 计算新节点位置,避免重叠
const sourceBBox = sourceNode.getBBox();
- const newX = sourceBBox.x + sourceBBox.width + 50;
- const newY = sourceBBox.y;
+ const nodeWidth = graphNodeLibrary[selectedNodeType.type]?.width || 120;
+ const nodeHeight = graphNodeLibrary[selectedNodeType.type]?.height || 88;
+ const horizontalSpacing = sourceNodeType === 'cycle-start' ? 40 : 80;
+ const verticalSpacing = 10;
+
+ // 获取源连接桩的group信息
+ const sourcePortInfo = sourceNode.getPorts().find((p: any) => p.id === sourcePort);
+ const sourcePortGroup = sourcePortInfo?.group || sourcePort;
+ console.log('sourcePortGroup', sourcePortGroup, sourcePortInfo)
+
+ // 如果有add-node位置,使用该位置,否则计算新位置
+ let newX, newY;
+ if (addNodePosition) {
+ newX = addNodePosition.x;
+ newY = addNodePosition.y;
+ } else {
+ // 根据连接桩位置决定节点放置方向
+ if (sourcePortGroup === 'left') {
+ // 左侧连接桩,在左侧添加节点
+ newX = sourceBBox.x - nodeWidth*2 - horizontalSpacing;
+ newY = sourceBBox.y;
+ } else {
+ // 右侧连接桩,在右侧添加节点
+ newX = sourceBBox.x + sourceBBox.width + horizontalSpacing;
+ newY = sourceBBox.y;
+ }
+
+ // 检查位置是否与现有节点重叠(只考虑与当前节点相连的节点)
+ const checkOverlap = (x: number, y: number) => {
+ // 获取与源节点相连的节点
+ const connectedNodes = new Set();
+ graph.getConnectedEdges(sourceNode).forEach((edge: any) => {
+ const sourceId = edge.getSourceCellId();
+ const targetId = edge.getTargetCellId();
+ if (sourceId !== sourceNode.id) connectedNodes.add(sourceId);
+ if (targetId !== sourceNode.id) connectedNodes.add(targetId);
+ });
+
+ return graph.getNodes().some((node: any) => {
+ if (node.id === sourceNode.id) return false;
+ if (!connectedNodes.has(node.id)) return false; // 只考虑相连的节点
+ const bbox = node.getBBox();
+ return !(x + nodeWidth < bbox.x || x > bbox.x + bbox.width ||
+ y + nodeHeight < bbox.y || y > bbox.y + bbox.height);
+ });
+ };
+
+ // 如果位置被占用,向下寻找空位
+ while (checkOverlap(newX, newY)) {
+ newY += nodeHeight + verticalSpacing;
+ }
+ }
// 创建新节点
const id = `${selectedNodeType.type.replace(/-/g, '_')}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
@@ -70,7 +136,15 @@ const PortClickHandler: React.FC = ({ graph }) => {
// 创建连线
setTimeout(() => {
const targetPorts = newNode.getPorts();
- const targetPort = targetPorts.find((port: any) => port.group === 'left')?.id || 'left';
+ let targetPort;
+
+ if (sourcePortGroup === 'left') {
+ // 从左侧连接桩连出,连接到新节点的右侧
+ targetPort = targetPorts.find((port: any) => port.group === 'right')?.id || 'right';
+ } else {
+ // 从右侧连接桩连出,连接到新节点的左侧
+ targetPort = targetPorts.find((port: any) => port.group === 'left')?.id || 'left';
+ }
graph.addEdge({
source: { cell: sourceNode.id, port: sourcePort },
@@ -85,7 +159,7 @@ const PortClickHandler: React.FC = ({ graph }) => {
},
},
},
- zIndex: 0
+ // zIndex: sourceNodeData.cycle && sourceNodeType == 'cycle-start' ? 1 : sourceNodeData.cycle ? 2 : 0
});
// 循环节点内子节点通过连接桩添加时,调整循环节点大小
@@ -108,8 +182,9 @@ const PortClickHandler: React.FC = ({ graph }) => {
}, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
const padding = 20;
+ const bottomPadding = 50;
const newWidth = Math.max(240, bounds.maxX - bounds.minX + padding * 2);
- const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding * 2);
+ const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding + bottomPadding);
parentNode.prop('size', { width: newWidth, height: newHeight });
}
diff --git a/web/src/views/Workflow/components/Properties/CaseList/index.tsx b/web/src/views/Workflow/components/Properties/CaseList/index.tsx
index 0126b76d..c475034e 100644
--- a/web/src/views/Workflow/components/Properties/CaseList/index.tsx
+++ b/web/src/views/Workflow/components/Properties/CaseList/index.tsx
@@ -55,6 +55,11 @@ const CaseList: FC = ({
const updateNodePorts = (caseCount: number, removedCaseIndex?: number) => {
if (!selectedNode || !graphRef?.current) return;
+ // 获取当前端口数量来判断是添加还是删除操作
+ const currentPorts = selectedNode.getPorts().filter((port: any) => port.group === 'right');
+ const currentCaseCount = currentPorts.length - 1; // 减去ELSE端口
+ const isAddingCase = removedCaseIndex === undefined && caseCount > currentCaseCount;
+
// 保存现有连线信息(包括左侧端口连线)
const existingEdges = graphRef.current.getEdges().filter((edge: any) =>
edge.getSourceCellId() === selectedNode.id || edge.getTargetCellId() === selectedNode.id
@@ -83,14 +88,10 @@ const CaseList: FC = ({
selectedNode.prop('size', { width: 240, height: newHeight })
- // 计算端口间距
- const dy = totalPorts;
-
// 添加 IF 端口
selectedNode.addPort({
id: 'CASE1',
group: 'right',
- // args: { dy },
attrs: { text: { text: 'IF', fontSize: 12, fill: '#5B6167' }}
});
@@ -99,7 +100,6 @@ const CaseList: FC = ({
selectedNode.addPort({
id: `CASE${i + 1}`,
group: 'right',
- // args: { dy },
attrs: { text: { text: 'ELIF', fontSize: 12, fill: '#5B6167' }}
});
}
@@ -108,22 +108,11 @@ const CaseList: FC = ({
selectedNode.addPort({
id: `CASE${caseCount + 1}`,
group: 'right',
- // args: { dy },
attrs: { text: { text: 'ELSE', fontSize: 12, fill: '#5B6167' }}
});
- // 恢复仍然存在的端口连线
+ // 恢复连线
setTimeout(() => {
- // 计算删除前的总端口数来确定原ELSE端口编号
- const originalCaseCount = removedCaseIndex !== undefined ? caseCount + 1 : caseCount;
- const originalElsePortNumber = originalCaseCount + 1;
-
- // 检查ELSE端口是否有连线
- const elseHasConnection = edgeConnections.some(({ sourcePortId, isIncoming }: any) => {
- const caseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0');
- return !isIncoming && caseNumber === originalElsePortNumber;
- });
-
edgeConnections.forEach(({ edge, sourcePortId, targetCellId, targetPortId, sourceCellId, isIncoming }: any) => {
// 如果是进入连线(左侧端口),直接恢复
if (isIncoming) {
@@ -151,7 +140,7 @@ const CaseList: FC = ({
// 处理右侧端口连线
const originalCaseNumber = parseInt(sourcePortId.match(/CASE(\d+)/)?.[1] || '0');
- // 如果是被删除的端口,只删除该端口的连线
+ // 如果是删除操作且是被删除的端口,删除连线
if (removedCaseIndex !== undefined && originalCaseNumber === removedCaseIndex + 1) {
graphRef.current?.removeCell(edge);
return;
@@ -159,12 +148,22 @@ const CaseList: FC = ({
let newPortId = sourcePortId;
- // 如果是原来的ELSE端口且有连线,重新映射到新的ELSE端口
- if (originalCaseNumber === originalElsePortNumber && elseHasConnection) {
- newPortId = `CASE${caseCount + 1}`; // 新的ELSE端口
- } else if (removedCaseIndex !== undefined && originalCaseNumber > removedCaseIndex + 1) {
- // 如果是被删除端口之后的端口,编号向前移动
- newPortId = `CASE${originalCaseNumber - 1}`;
+ // 如果是删除操作,需要重新映射端口ID
+ if (removedCaseIndex !== undefined) {
+ if (originalCaseNumber > removedCaseIndex + 1) {
+ // 被删除端口之后的端口,编号向前移动
+ newPortId = `CASE${originalCaseNumber - 1}`;
+ }
+ // ELSE端口始终映射到新的ELSE端口位置
+ else if (originalCaseNumber === currentCaseCount + 1) {
+ newPortId = `CASE${caseCount + 1}`;
+ }
+ } else if (isAddingCase) {
+ // 如果是添加操作,ELSE端口需要重新映射
+ if (originalCaseNumber === currentCaseCount + 1) {
+ newPortId = `CASE${caseCount + 1}`; // 新的ELSE端口
+ }
+ // 新添加的端口不恢复任何连线
}
const newPorts = selectedNode.getPorts();
diff --git a/web/src/views/Workflow/components/Properties/VariableEditModal.tsx b/web/src/views/Workflow/components/Properties/VariableEditModal.tsx
index db4a38a3..075cd695 100644
--- a/web/src/views/Workflow/components/Properties/VariableEditModal.tsx
+++ b/web/src/views/Workflow/components/Properties/VariableEditModal.tsx
@@ -110,6 +110,7 @@ const VariableEditModal = forwardRef form.setFieldValue('default', undefined)}
labelRender={(props) => {props.label} {variableType[props.value as keyof typeof variableType]}
}
optionRender={(props) => {props.label} {variableType[props.value as keyof typeof variableType]}
}
/>
diff --git a/web/src/views/Workflow/components/Properties/index.tsx b/web/src/views/Workflow/components/Properties/index.tsx
index 8752121e..f9fbad63 100644
--- a/web/src/views/Workflow/components/Properties/index.tsx
+++ b/web/src/views/Workflow/components/Properties/index.tsx
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import { Graph, Node } from '@antv/x6';
import { Form, Input, Button, Select, InputNumber, Slider, Space, Divider, App, Switch } from 'antd'
-import type { NodeConfig, NodeProperties, StartVariableItem, VariableEditModalRef } from '../../types'
+import type { NodeConfig, NodeProperties, StartVariableItem, VariableEditModalRef, ChatVariable } from '../../types'
import Empty from '@/components/Empty';
import emptyIcon from '@/assets/images/workflow/empty.png'
import CustomSelect from "@/components/CustomSelect";
@@ -34,11 +34,12 @@ interface PropertiesProps {
copyEvent: () => void;
parseEvent: () => void;
config?: any;
+ chatVariables: ChatVariable[];
}
const Properties: FC = ({
selectedNode,
graphRef,
- config: workflowConfig,
+ chatVariables
}) => {
const { t } = useTranslation()
const { modal } = App.useApp()
@@ -47,6 +48,7 @@ const Properties: FC = ({
const values = Form.useWatch([], form);
const variableModalRef = useRef(null)
const [editIndex, setEditIndex] = useState(null)
+ const [graphUpdateTrigger, setGraphUpdateTrigger] = useState(0)
const prevMappingNamesRef = useRef([])
const prevTemplateVarsRef = useRef([])
const syncTimeoutRef = useRef(null)
@@ -242,6 +244,7 @@ const Properties: FC = ({
}, [values, selectedNode, form])
const handleAddVariable = () => {
+ setEditIndex(null)
variableModalRef.current?.handleOpen()
}
const handleEditVariable = (index: number, vo: StartVariableItem) => {
@@ -250,6 +253,7 @@ const Properties: FC = ({
}
const handleRefreshVariable = (value: StartVariableItem) => {
if (!selectedNode) return
+
if (editIndex !== null) {
const defaultValue = selectedNode.data.config.variables.defaultValue ?? []
defaultValue[editIndex] = value
@@ -260,7 +264,7 @@ const Properties: FC = ({
}
selectedNode?.setData({ ...selectedNode.data})
- setConfigs({ ...selectedNode.data.config})
+ setConfigs({ ...selectedNode.data.config })
}
const handleDeleteVariable = (index: number, vo: StartVariableItem) => {
if (!selectedNode) return
@@ -347,11 +351,9 @@ const Properties: FC = ({
const parentPreviousNodeIds = getAllPreviousNodes(parentLoopNode.id);
allRelevantNodeIds.push(...parentPreviousNodeIds);
}
-
-
// Add conversation variables from global config
- const conversationVariables = workflowConfig?.variables || [];
+ const conversationVariables = chatVariables || [];
conversationVariables.forEach((variable: any) => {
const key = `CONVERSATION_${variable.name}`;
@@ -761,7 +763,36 @@ const Properties: FC = ({
}
return variableList;
- }, [selectedNode, graphRef, workflowConfig?.variables]);
+ }, [selectedNode, graphRef, graphUpdateTrigger, chatVariables]);
+
+ // Trigger variableList update when graph edges or nodes change
+ useEffect(() => {
+ if (!graphRef?.current) return;
+
+ const graph = graphRef.current;
+ const handleGraphChange = () => {
+ console.log('handleGraphChange')
+ // Force variableList recalculation by updating trigger
+ setGraphUpdateTrigger(prev => prev + 1);
+ };
+
+ // Listen to graph changes
+ graph.on('edge:added', handleGraphChange);
+ graph.on('edge:removed', handleGraphChange);
+ graph.on('edge:changed', handleGraphChange);
+ graph.on('node:added', handleGraphChange);
+ graph.on('node:removed', handleGraphChange);
+ graph.on('node:change:data', handleGraphChange);
+
+ return () => {
+ graph.off('edge:added', handleGraphChange);
+ graph.off('edge:removed', handleGraphChange);
+ graph.off('edge:changed', handleGraphChange);
+ graph.off('node:added', handleGraphChange);
+ graph.off('node:removed', handleGraphChange);
+ graph.off('node:change:data', handleGraphChange);
+ };
+ }, [graphRef]);
// Filter out boolean type variables for loop and llm nodes
const getFilteredVariableList = (nodeType?: string, key?: string) => {
diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts
index c281a76e..87ee80fc 100644
--- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts
+++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts
@@ -54,7 +54,7 @@ export const useWorkflowGraph = ({
const historyRef = useRef<{ undoStack: string[], redoStack: string[] }>({ undoStack: [], redoStack: [] });
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
- const [isHandMode, setIsHandMode] = useState(false);
+ const [isHandMode, setIsHandMode] = useState(true);
const [config, setConfig] = useState(null);
const [chatVariables, setChatVariables] = useState([])
@@ -275,6 +275,11 @@ export const useWorkflowGraph = ({
}, 100)
}
if (edges.length) {
+ // 计算loop和iteration类型节点的数量
+ const loopIterationCount = nodes.filter(node =>
+ node.type === 'loop' || node.type === 'iteration'
+ ).length;
+
// 去重处理:对于if-else和question-classifier节点,不同连接桩允许连接到相同节点
const uniqueEdges = edges.filter((edge, index, arr) => {
return arr.findIndex(e => {
@@ -349,7 +354,7 @@ export const useWorkflowGraph = ({
},
},
},
- zIndex: targetCell.getData()?.cycle ? 3 : 0
+ // zIndex: loopIterationCount
}
return edgeConfig
@@ -689,10 +694,9 @@ export const useWorkflowGraph = ({
thickness: 1, // 网点大小
}
},
- panning: false,
+ panning: isHandMode,
mousewheel: {
enabled: true,
- modifiers: ['ctrl', 'meta'],
},
connecting: {
// router: 'orth',
@@ -725,7 +729,6 @@ export const useWorkflowGraph = ({
},
},
},
- zIndex: 0,
});
},
validateConnection({ sourceCell, targetCell, targetMagnet }) {
@@ -762,9 +765,8 @@ export const useWorkflowGraph = ({
},
embedding: {
enabled: true,
- validate (this, { parent }) {
- const parentData = parent.getData()
- return parentData.type === 'iteration' || parentData.type === 'loop'
+ validate (this) {
+ return false
}
},
translating: {
diff --git a/web/src/views/Workflow/index.tsx b/web/src/views/Workflow/index.tsx
index ba17a63a..506fd3c4 100644
--- a/web/src/views/Workflow/index.tsx
+++ b/web/src/views/Workflow/index.tsx
@@ -107,6 +107,7 @@ const Workflow = forwardRef((_props, ref) => {
copyEvent={copyEvent}
parseEvent={parseEvent}
config={config}
+ chatVariables={chatVariables}
/>