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} />