diff --git a/api/app/controllers/__init__.py b/api/app/controllers/__init__.py index 13e66ea7..2cddfb30 100644 --- a/api/app/controllers/__init__.py +++ b/api/app/controllers/__init__.py @@ -33,6 +33,7 @@ from . import ( emotion_config_controller, prompt_optimizer_controller, tool_controller, + home_page_controller, ) from . import user_memory_controllers @@ -70,5 +71,6 @@ manager_router.include_router(emotion_config_controller.router) manager_router.include_router(prompt_optimizer_controller.router) manager_router.include_router(memory_reflection_controller.router) manager_router.include_router(tool_controller.router) +manager_router.include_router(home_page_controller.router) __all__ = ["manager_router"] diff --git a/api/app/controllers/home_page_controller.py b/api/app/controllers/home_page_controller.py new file mode 100644 index 00000000..6665eec1 --- /dev/null +++ b/api/app/controllers/home_page_controller.py @@ -0,0 +1,29 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.core.response_utils import success +from app.db import get_db +from app.dependencies import get_current_user +from app.models.user_model import User +from app.schemas.response_schema import ApiResponse +from app.services.home_page_service import HomePageService + +router = APIRouter(prefix="/home-page", tags=["Home Page"]) + +@router.get("/statistics", response_model=ApiResponse) +def get_home_statistics( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取首页统计数据""" + statistics = HomePageService.get_home_statistics(db, current_user.tenant_id) + return success(data=statistics, msg="统计数据获取成功") + +@router.get("/workspaces", response_model=ApiResponse) +def get_workspace_list( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取工作空间列表""" + workspace_list = HomePageService.get_workspace_list(db, current_user.tenant_id) + return success(data=workspace_list, msg="工作空间列表获取成功") \ No newline at end of file diff --git a/api/app/controllers/tool_controller.py b/api/app/controllers/tool_controller.py index 79c87205..479686ef 100644 --- a/api/app/controllers/tool_controller.py +++ b/api/app/controllers/tool_controller.py @@ -60,6 +60,22 @@ async def list_tools( raise HTTPException(status_code=500, detail=str(e)) +@router.get("/{tool_id}/methods", response_model=ApiResponse) +async def get_tool_methods( + tool_id: str, + current_user: User = Depends(get_current_user), + service: ToolService = Depends(get_tool_service) +): + """获取工具的所有方法""" + try: + methods = await service.get_tool_methods(tool_id, current_user.tenant_id) + if methods is None: + raise HTTPException(status_code=404, detail="工具不存在") + return success(data=methods, msg="获取工具方法成功") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + @router.get("/{tool_id}", response_model=ApiResponse) async def get_tool( tool_id: str, @@ -159,7 +175,8 @@ async def execute_tool( workspace_id=current_user.current_workspace_id, timeout=request.timeout ) - + if not result.success: + raise HTTPException(status_code=400, detail=result["error"]) return success( data={ "success": result.success, diff --git a/api/app/core/api_key_utils.py b/api/app/core/api_key_utils.py index 877ddd01..fb6b9552 100644 --- a/api/app/core/api_key_utils.py +++ b/api/app/core/api_key_utils.py @@ -3,7 +3,7 @@ import secrets from typing import Optional, Union from datetime import datetime -from app.schemas.api_key_schema import ApiKeyType +from app.models.api_key_model import ApiKeyType from fastapi import Response from fastapi.responses import JSONResponse diff --git a/api/app/core/tools/__init__.py b/api/app/core/tools/__init__.py index 714dc851..9d9407ad 100644 --- a/api/app/core/tools/__init__.py +++ b/api/app/core/tools/__init__.py @@ -1,7 +1,7 @@ """工具管理核心模块""" -from .base import BaseTool, ToolResult, ToolParameter -from .langchain_adapter import LangchainAdapter +from app.core.tools.base import BaseTool, ToolResult, ToolParameter +from app.core.tools.langchain_adapter import LangchainAdapter # 可选导入,避免导入错误 try: diff --git a/api/app/core/tools/base.py b/api/app/core/tools/base.py index c9771ef0..ec15c50f 100644 --- a/api/app/core/tools/base.py +++ b/api/app/core/tools/base.py @@ -193,7 +193,7 @@ class BaseTool(ABC): def to_langchain_tool(self): """转换为Langchain工具格式""" - from .langchain_adapter import LangchainAdapter + from app.core.tools.langchain_adapter import LangchainAdapter return LangchainAdapter.convert_tool(self) def __repr__(self): diff --git a/api/app/core/tools/builtin/__init__.py b/api/app/core/tools/builtin/__init__.py index 3813402c..7d2ea0ef 100644 --- a/api/app/core/tools/builtin/__init__.py +++ b/api/app/core/tools/builtin/__init__.py @@ -1,11 +1,11 @@ """内置工具模块""" -from .base import BuiltinTool -from .datetime_tool import DateTimeTool -from .json_tool import JsonTool -from .baidu_search_tool import BaiduSearchTool -from .mineru_tool import MinerUTool -from .textin_tool import TextInTool +from app.core.tools.builtin.base import BuiltinTool +from app.core.tools.builtin.datetime_tool import DateTimeTool +from app.core.tools.builtin.json_tool import JsonTool +from app.core.tools.builtin.baidu_search_tool import BaiduSearchTool +from app.core.tools.builtin.mineru_tool import MinerUTool +from app.core.tools.builtin.textin_tool import TextInTool __all__ = [ "BuiltinTool", diff --git a/api/app/core/tools/builtin/baidu_search_tool.py b/api/app/core/tools/builtin/baidu_search_tool.py index fddd6eb7..e1f80f34 100644 --- a/api/app/core/tools/builtin/baidu_search_tool.py +++ b/api/app/core/tools/builtin/baidu_search_tool.py @@ -4,7 +4,7 @@ from typing import List, Dict, Any import aiohttp from app.core.tools.base import ToolParameter, ToolResult, ParameterType -from .base import BuiltinTool +from app.core.tools.builtin.base import BuiltinTool class BaiduSearchTool(BuiltinTool): diff --git a/api/app/core/tools/builtin/datetime_tool.py b/api/app/core/tools/builtin/datetime_tool.py index 647914b2..7b6fa8ef 100644 --- a/api/app/core/tools/builtin/datetime_tool.py +++ b/api/app/core/tools/builtin/datetime_tool.py @@ -5,7 +5,7 @@ from typing import List import pytz from app.schemas.tool_schema import ToolParameter, ToolResult, ParameterType -from .base import BuiltinTool +from app.core.tools.builtin.base import BuiltinTool class DateTimeTool(BuiltinTool): @@ -27,7 +27,7 @@ class DateTimeTool(BuiltinTool): type=ParameterType.STRING, description="操作类型", required=True, - enum=["format", "convert_timezone", "timestamp_to_datetime", "datetime_to_timestamp", "calculate", "now"] + enum=["format", "convert_timezone", "timestamp_to_datetime", "now"] ), ToolParameter( name="input_value", diff --git a/api/app/core/tools/builtin/json_tool.py b/api/app/core/tools/builtin/json_tool.py index 62cd98d3..f22e9370 100644 --- a/api/app/core/tools/builtin/json_tool.py +++ b/api/app/core/tools/builtin/json_tool.py @@ -7,7 +7,7 @@ import xml.etree.ElementTree as ET from xml.dom import minidom from app.core.tools.base import ToolParameter, ToolResult, ParameterType -from .base import BuiltinTool +from app.core.tools.builtin.base import BuiltinTool class JsonTool(BuiltinTool): @@ -29,8 +29,7 @@ class JsonTool(BuiltinTool): type=ParameterType.STRING, description="操作类型", required=True, - enum=["format", "minify", "validate", "convert", "to_yaml", "from_yaml", "to_xml", "from_xml", "merge", - "extract", "insert", "replace", "delete", "parse"] + enum=["insert", "replace", "delete", "parse"] ), ToolParameter( name="input_data", diff --git a/api/app/core/tools/builtin/mineru_tool.py b/api/app/core/tools/builtin/mineru_tool.py index b2a544c0..c480d6d8 100644 --- a/api/app/core/tools/builtin/mineru_tool.py +++ b/api/app/core/tools/builtin/mineru_tool.py @@ -4,7 +4,7 @@ from typing import List, Dict, Any import aiohttp from app.core.tools.base import ToolParameter, ToolResult, ParameterType -from .base import BuiltinTool +from app.core.tools.builtin.base import BuiltinTool class MinerUTool(BuiltinTool): diff --git a/api/app/core/tools/builtin/textin_tool.py b/api/app/core/tools/builtin/textin_tool.py index e5218416..4ec32659 100644 --- a/api/app/core/tools/builtin/textin_tool.py +++ b/api/app/core/tools/builtin/textin_tool.py @@ -4,7 +4,7 @@ from typing import List, Dict, Any import aiohttp from app.core.tools.base import ToolParameter, ToolResult, ParameterType -from .base import BuiltinTool +from app.core.tools.builtin.base import BuiltinTool class TextInTool(BuiltinTool): diff --git a/api/app/core/tools/custom/__init__.py b/api/app/core/tools/custom/__init__.py index 87b0488a..d56265e7 100644 --- a/api/app/core/tools/custom/__init__.py +++ b/api/app/core/tools/custom/__init__.py @@ -1,8 +1,8 @@ """自定义工具模块""" -from .base import CustomTool -from .schema_parser import OpenAPISchemaParser -from .auth_manager import AuthManager +from app.core.tools.custom.base import CustomTool +from app.core.tools.custom.schema_parser import OpenAPISchemaParser +from app.core.tools.custom.auth_manager import AuthManager __all__ = [ "CustomTool", diff --git a/api/app/core/tools/mcp/__init__.py b/api/app/core/tools/mcp/__init__.py index faf13ceb..4c9519b3 100644 --- a/api/app/core/tools/mcp/__init__.py +++ b/api/app/core/tools/mcp/__init__.py @@ -1,8 +1,8 @@ """MCP工具模块""" -from .base import MCPTool -from .client import MCPClient, MCPConnectionPool -from .service_manager import MCPServiceManager +from app.core.tools.mcp.base import MCPTool +from app.core.tools.mcp.client import MCPClient, MCPConnectionPool +from app.core.tools.mcp.service_manager import MCPServiceManager __all__ = [ "MCPTool", diff --git a/api/app/core/tools/mcp/base.py b/api/app/core/tools/mcp/base.py index ca77f528..3fa103ab 100644 --- a/api/app/core/tools/mcp/base.py +++ b/api/app/core/tools/mcp/base.py @@ -1,7 +1,6 @@ """MCP工具基类""" import time from typing import Dict, Any, List -import aiohttp from app.models.tool_model import ToolType from app.core.tools.base import BaseTool diff --git a/api/app/core/tools/mcp/client.py b/api/app/core/tools/mcp/client.py index 2e37f2b1..a1d2ecaa 100644 --- a/api/app/core/tools/mcp/client.py +++ b/api/app/core/tools/mcp/client.py @@ -204,7 +204,7 @@ class MCPClient: ) init_response = json.loads(response) - if "error" in init_response: + if init_response.get("error", None) is not None: raise MCPProtocolError(f"初始化失败: {init_response['error']}") return True @@ -325,7 +325,7 @@ class MCPClient: try: response = await self._send_request(request_data, timeout) - if "error" in response: + if response.get("error", None) is not None: error = response["error"] raise MCPProtocolError(f"工具调用失败: {error.get('message', '未知错误')}") diff --git a/api/app/core/tools/mcp/service_manager.py b/api/app/core/tools/mcp/service_manager.py index 51d01535..f7349201 100644 --- a/api/app/core/tools/mcp/service_manager.py +++ b/api/app/core/tools/mcp/service_manager.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import Session from app.models.tool_model import MCPToolConfig, ToolConfig, ToolType, ToolStatus from app.core.logging_config import get_business_logger -from .client import MCPClient, MCPConnectionPool +from app.core.tools.mcp.client import MCPClient, MCPConnectionPool logger = get_business_logger() diff --git a/api/app/core/workflow/executor.py b/api/app/core/workflow/executor.py index 0d0879d7..fe75eace 100644 --- a/api/app/core/workflow/executor.py +++ b/api/app/core/workflow/executor.py @@ -219,17 +219,13 @@ class WorkflowExecutor: # 创建节点实例(现在 start 和 end 也会被创建) node_instance = NodeFactory.create_node(node, self.workflow_config) - if node_type in [NodeType.IF_ELSE, NodeType.HTTP_REQUEST]: - expressions = node_instance.build_conditional_edge_expressions() - - # Number of branches, usually matches the number of conditional expressions - branch_number = len(expressions) + if node_type in [NodeType.IF_ELSE, NodeType.HTTP_REQUEST, NodeType.QUESTION_CLASSIFIER]: # Find all edges whose source is the current node related_edge = [edge for edge in self.edges if edge.get("source") == node_id] # Iterate over each branch - for idx in range(branch_number): + for idx in range(len(related_edge)): # Generate a condition expression for each edge # Used later to determine which branch to take based on the node's output # Assumes node output `node..output` matches the edge's label diff --git a/api/app/core/workflow/nodes/__init__.py b/api/app/core/workflow/nodes/__init__.py index 174fa877..926f86e4 100644 --- a/api/app/core/workflow/nodes/__init__.py +++ b/api/app/core/workflow/nodes/__init__.py @@ -17,6 +17,8 @@ from app.core.workflow.nodes.node_factory import NodeFactory, WorkflowNode from app.core.workflow.nodes.start import StartNode from app.core.workflow.nodes.transform import TransformNode from app.core.workflow.nodes.parameter_extractor import ParameterExtractorNode +from app.core.workflow.nodes.question_classifier import QuestionClassifierNode +from app.core.workflow.nodes.tool import ToolNode __all__ = [ "BaseNode", @@ -33,5 +35,7 @@ __all__ = [ "AssignerNode", "HttpRequestNode", "JinjaRenderNode", - "ParameterExtractorNode" + "ParameterExtractorNode", + "QuestionClassifierNode", + "ToolNode" ] diff --git a/api/app/core/workflow/nodes/configs.py b/api/app/core/workflow/nodes/configs.py index 2ba23d4c..6e9c2c51 100644 --- a/api/app/core/workflow/nodes/configs.py +++ b/api/app/core/workflow/nodes/configs.py @@ -21,6 +21,7 @@ from app.core.workflow.nodes.transform.config import TransformNodeConfig from app.core.workflow.nodes.variable_aggregator.config import VariableAggregatorNodeConfig from app.core.workflow.nodes.parameter_extractor.config import ParameterExtractorNodeConfig from app.core.workflow.nodes.question_classifier.config import QuestionClassifierNodeConfig +from app.core.workflow.nodes.tool.config import ToolNodeConfig from app.core.workflow.nodes.cycle_graph.config import LoopNodeConfig, IterationNodeConfig __all__ = [ @@ -45,4 +46,5 @@ __all__ = [ "LoopNodeConfig", "IterationNodeConfig", "QuestionClassifierNodeConfig" + "ToolNodeConfig" ] diff --git a/api/app/core/workflow/nodes/node_factory.py b/api/app/core/workflow/nodes/node_factory.py index ed26533d..df565efe 100644 --- a/api/app/core/workflow/nodes/node_factory.py +++ b/api/app/core/workflow/nodes/node_factory.py @@ -24,6 +24,7 @@ from app.core.workflow.nodes.transform import TransformNode from app.core.workflow.nodes.variable_aggregator import VariableAggregatorNode from app.core.workflow.nodes.question_classifier import QuestionClassifierNode from app.core.workflow.nodes.breaker import BreakNode +from app.core.workflow.nodes.tool import ToolNode logger = logging.getLogger(__name__) @@ -44,7 +45,8 @@ WorkflowNode = Union[ CycleGraphNode, BreakNode, ParameterExtractorNode, - QuestionClassifierNode + QuestionClassifierNode, + ToolNode ] @@ -72,6 +74,7 @@ class NodeFactory: NodeType.LOOP: CycleGraphNode, NodeType.ITERATION: CycleGraphNode, NodeType.BREAK: BreakNode, + NodeType.TOOL: ToolNode, } @classmethod diff --git a/api/app/core/workflow/nodes/question_classifier/config.py b/api/app/core/workflow/nodes/question_classifier/config.py index f3b2cc20..998e2fb4 100644 --- a/api/app/core/workflow/nodes/question_classifier/config.py +++ b/api/app/core/workflow/nodes/question_classifier/config.py @@ -26,4 +26,3 @@ class QuestionClassifierNodeConfig(BaseNodeConfig): default="问题:{question}\n\n可选分类:{categories}\n\n补充指令:{supplement_prompt}\n\n请选择最合适的分类。", description="用户提示词模板" ) - output_variable: str = Field(default="class_name", description="输出分类结果的变量名") diff --git a/api/app/core/workflow/nodes/question_classifier/node.py b/api/app/core/workflow/nodes/question_classifier/node.py index bd3c8752..67f53801 100644 --- a/api/app/core/workflow/nodes/question_classifier/node.py +++ b/api/app/core/workflow/nodes/question_classifier/node.py @@ -12,6 +12,9 @@ from app.services.model_service import ModelConfigService logger = logging.getLogger(__name__) +DEFAULT_CASE_PREFIX = "CASE" +DEFAULT_EMPTY_QUESTION_CASE = f"{DEFAULT_CASE_PREFIX}1" + class QuestionClassifierNode(BaseNode): """问题分类器节点""" @@ -19,6 +22,7 @@ class QuestionClassifierNode(BaseNode): def __init__(self, node_config: dict[str, Any], workflow_config: dict[str, Any]): super().__init__(node_config, workflow_config) self.typed_config = QuestionClassifierNodeConfig(**self.config) + self.category_to_case_map = self._build_category_case_map() def _get_llm_instance(self) -> RedBearLLM: """获取LLM实例""" @@ -47,48 +51,73 @@ class QuestionClassifierNode(BaseNode): ), type=ModelType(model_type) ) + + def _build_category_case_map(self) -> dict[str, str]: + """ + 预构建 分类名称 -> CASE标识 的映射字典 + 示例:{"产品咨询": "CASE1", "售后问题": "CASE2"} + """ + category_map = {} + categories = self.typed_config.categories or [] + for idx, class_item in enumerate(categories, start=1): + category_name = class_item.class_name.strip() + case_tag = f"{DEFAULT_CASE_PREFIX}{idx}" + category_map[category_name] = case_tag + return category_map - async def execute(self, state: WorkflowState) -> dict[str, Any]: + async def execute(self, state: WorkflowState) -> str: """执行问题分类""" question = self.typed_config.input_variable - - supplement_prompt = "" - if self.typed_config.user_supplement_prompt is not None: - supplement_prompt = self.typed_config.user_supplement_prompt - - category_names = [class_item.class_name for class_item in self.typed_config.categories] + supplement_prompt = self.typed_config.user_supplement_prompt or "" + categories = self.typed_config.categories or [] + category_names = [class_item.class_name.strip() for class_item in categories] + category_count = len(category_names) if not question: - logger.warning(f"节点 {self.node_id} 未获取到输入问题") - return {self.typed_config.output_variable: category_names[0] if category_names else "unknown"} - - llm = self._get_llm_instance() - - # 渲染用户提示词模板,支持工作流变量 - user_prompt = self._render_template( - self.typed_config.user_prompt.format( - question=question, - categories=", ".join(category_names), - supplement_prompt=supplement_prompt - ), - state - ) - - messages = [ - ("system", self.typed_config.system_prompt), - ("user", user_prompt), - ] - - response = await llm.ainvoke(messages) - result = response.content.strip() - - if result in category_names: - category = result - else: - logger.warning(f"LLM返回了未知类别: {result}") - category = category_names[0] if category_names else "unknown" + logger.warning( + f"节点 {self.node_id} 未获取到输入问题,使用默认分支" + f"(默认分支:{DEFAULT_EMPTY_QUESTION_CASE},分类总数:{category_count})" + ) + # 若分类列表为空,返回默认unknown分支,否则返回CASE1 + return DEFAULT_EMPTY_QUESTION_CASE if category_count > 0 else "unknown" - log_supplement = supplement_prompt if supplement_prompt else "无" - logger.info(f"节点 {self.node_id} 分类结果: {category}, 用户补充提示词:{log_supplement}") - - return {self.typed_config.output_variable: category} \ No newline at end of file + try: + llm = self._get_llm_instance() + + # 渲染用户提示词模板,支持工作流变量 + user_prompt = self._render_template( + self.typed_config.user_prompt.format( + question=question, + categories=", ".join(category_names), + supplement_prompt=supplement_prompt + ), + state + ) + + messages = [ + ("system", self.typed_config.system_prompt), + ("user", user_prompt), + ] + + response = await llm.ainvoke(messages) + result = response.content.strip() + + if result in category_names: + category = result + else: + logger.warning(f"LLM返回了未知类别: {result}") + category = category_names[0] if category_names else "unknown" + + log_supplement = supplement_prompt if supplement_prompt else "无" + logger.info(f"节点 {self.node_id} 分类结果: {category}, 用户补充提示词:{log_supplement}") + + return f"CASE{category_names.index(category) + 1}" + except Exception as e: + logger.error( + f"节点 {self.node_id} 分类执行异常:{str(e)}", + exc_info=True # 打印堆栈信息,便于调试 + ) + # 异常时返回默认分支,保证工作流容错性 + if category_count > 0: + return DEFAULT_EMPTY_QUESTION_CASE + return "unknown" diff --git a/api/app/core/workflow/nodes/tool/__init__.py b/api/app/core/workflow/nodes/tool/__init__.py new file mode 100644 index 00000000..8392f05c --- /dev/null +++ b/api/app/core/workflow/nodes/tool/__init__.py @@ -0,0 +1,4 @@ +from app.core.workflow.nodes.tool.config import ToolNodeConfig +from app.core.workflow.nodes.tool.node import ToolNode + +__all__ = ["ToolNode", "ToolNodeConfig"] \ No newline at end of file diff --git a/api/app/core/workflow/nodes/tool/config.py b/api/app/core/workflow/nodes/tool/config.py new file mode 100644 index 00000000..487efae2 --- /dev/null +++ b/api/app/core/workflow/nodes/tool/config.py @@ -0,0 +1,9 @@ +from pydantic import Field +from app.core.workflow.nodes.base_config import BaseNodeConfig + + +class ToolNodeConfig(BaseNodeConfig): + """工具节点配置""" + + tool_id: str = Field(..., description="工具ID") + tool_parameters: dict[str, str] = Field(default_factory=dict, description="工具参数映射,支持工作流变量") diff --git a/api/app/core/workflow/nodes/tool/node.py b/api/app/core/workflow/nodes/tool/node.py new file mode 100644 index 00000000..993a3804 --- /dev/null +++ b/api/app/core/workflow/nodes/tool/node.py @@ -0,0 +1,72 @@ +import logging +import uuid +from typing import Any + +from app.core.workflow.nodes.base_node import BaseNode, WorkflowState +from app.core.workflow.nodes.tool.config import ToolNodeConfig +from app.services.tool_service import ToolService +from app.db import get_db_read + +logger = logging.getLogger(__name__) + + +class ToolNode(BaseNode): + """工具节点""" + + def __init__(self, node_config: dict[str, Any], workflow_config: dict[str, Any]): + super().__init__(node_config, workflow_config) + self.typed_config = ToolNodeConfig(**self.config) + + async def execute(self, state: WorkflowState) -> dict[str, Any]: + """执行工具""" + # 获取租户ID和用户ID + tenant_id = self.get_variable("sys.tenant_id", state) + user_id = self.get_variable("sys.user_id", state) + + # 如果没有租户ID,尝试从工作流ID获取 + if not tenant_id: + workflow_id = self.get_variable("sys.workflow_id", state) + if workflow_id: + from app.repositories.tool_repository import ToolRepository + with get_db_read() as db: + tenant_id = ToolRepository.get_tenant_id_by_workflow_id(db, workflow_id) + + if not tenant_id: + tenant_id = uuid.UUID("6c2c91b0-3f49-4489-9157-2208aa56a097") + # logger.error(f"节点 {self.node_id} 缺少租户ID") + # return {"error": "缺少租户ID"} + + # 渲染工具参数 + rendered_parameters = {} + for param_name, param_template in self.typed_config.tool_parameters.items(): + rendered_value = self._render_template(param_template, state) + rendered_parameters[param_name] = rendered_value + + logger.info(f"节点 {self.node_id} 执行工具 {self.typed_config.tool_id},参数: {rendered_parameters}") + print(self.typed_config.tool_id) + + # 执行工具 + with get_db_read() as db: + tool_service = ToolService(db) + result = await tool_service.execute_tool( + tool_id=self.typed_config.tool_id, + parameters=rendered_parameters, + tenant_id=tenant_id, + user_id=user_id + ) + print(result) + if result.success: + logger.info(f"节点 {self.node_id} 工具执行成功") + return { + "success": True, + "data": result.data, + "execution_time": result.execution_time + } + else: + logger.error(f"节点 {self.node_id} 工具执行失败: {result.error}") + return { + "success": False, + "error": result.error, + "error_code": result.error_code, + "execution_time": result.execution_time + } \ No newline at end of file diff --git a/api/app/repositories/home_page_repository.py b/api/app/repositories/home_page_repository.py new file mode 100644 index 00000000..e37f1f00 --- /dev/null +++ b/api/app/repositories/home_page_repository.py @@ -0,0 +1,137 @@ +from datetime import datetime, timedelta +from sqlalchemy.orm import Session +from sqlalchemy import func +from uuid import UUID +from typing import Dict + +from app.models.end_user_model import EndUser +from app.models.user_model import User +from app.models.workspace_model import Workspace, WorkspaceMember +from app.models.models_model import ModelConfig +from app.models.app_model import App + +class HomePageRepository: + + @staticmethod + def get_model_statistics(db: Session, tenant_id: UUID, month_start: datetime) -> tuple[int, int]: + """获取模型统计数据""" + total_models = db.query(ModelConfig).filter( + ModelConfig.tenant_id == tenant_id, + ModelConfig.is_active == True + ).count() + + new_models_this_month = db.query(ModelConfig).filter( + ModelConfig.tenant_id == tenant_id, + ModelConfig.is_active == True, + ModelConfig.created_at >= month_start + ).count() + + return total_models, new_models_this_month + + @staticmethod + def get_workspace_statistics(db: Session, tenant_id: UUID, month_start: datetime) -> tuple[int, int]: + """获取工作空间统计数据""" + active_workspaces = db.query(Workspace).filter( + Workspace.tenant_id == tenant_id, + Workspace.is_active == True + ).count() + + new_workspaces_this_month = db.query(Workspace).filter( + Workspace.tenant_id == tenant_id, + Workspace.is_active == True, + Workspace.created_at >= month_start + ).count() + + return active_workspaces, new_workspaces_this_month + + @staticmethod + def get_user_statistics(db: Session, tenant_id: UUID, month_start: datetime) -> tuple[int, int]: + """获取用户统计数据""" + workspace_ids = db.query(Workspace.id).filter( + Workspace.tenant_id == tenant_id, + Workspace.is_active == True + ).subquery() + + total_users = db.query(EndUser).join( + App, + EndUser.app_id == App.id + ).filter( + App.workspace_id.in_(workspace_ids), + App.is_active == True, + App.status == "active" + ).count() + + new_users_this_month = db.query(EndUser).join( + App, + EndUser.app_id == App.id + ).filter( + App.workspace_id.in_(workspace_ids), + App.is_active == True, + App.status == "active", + EndUser.created_at >= month_start + ).count() + + return total_users, new_users_this_month + + @staticmethod + def get_app_statistics(db: Session, tenant_id: UUID, week_start: datetime) -> tuple[int, int]: + """获取应用统计数据""" + workspace_ids = db.query(Workspace.id).filter( + Workspace.tenant_id == tenant_id, + Workspace.is_active == True + ).subquery() + + running_apps = db.query(App).filter( + App.workspace_id.in_(workspace_ids), + App.is_active == True, + App.status == "active" + ).count() + + new_apps_this_week = db.query(App).filter( + App.workspace_id.in_(workspace_ids), + App.is_active == True, + App.status == "active", + App.created_at >= week_start + ).count() + + return running_apps, new_apps_this_week + + @staticmethod + def get_workspaces_with_counts(db: Session, tenant_id: UUID) -> tuple[list[Workspace], Dict[UUID, int], Dict[UUID, int]]: + """批量获取工作空间及其统计数据""" + # 获取工作空间列表 + workspaces = db.query(Workspace).filter( + Workspace.tenant_id == tenant_id, + Workspace.is_active == True + ).all() + + workspace_ids = [ws.id for ws in workspaces] + + # 批量获取应用数量 + app_counts = db.query( + App.workspace_id, + func.count(App.id).label('count') + ).filter( + App.workspace_id.in_(workspace_ids), + App.is_active, + App.status == "active" + ).group_by(App.workspace_id).all() + + app_count_dict = {workspace_id: count for workspace_id, count in app_counts} + + # 批量获取用户数量 + user_counts = db.query( + App.workspace_id, + func.count(EndUser.id).label('count') + ).join( + EndUser, + EndUser.app_id == App.id + ).filter( + App.workspace_id.in_(workspace_ids), + App.is_active, + App.status == "active" + ).group_by(App.workspace_id).all() + + user_count_dict = {workspace_id: count for workspace_id, count in user_counts} + + return workspaces, app_count_dict, user_count_dict \ No newline at end of file diff --git a/api/app/repositories/tool_repository.py b/api/app/repositories/tool_repository.py index dc78e761..3aa7b16e 100644 --- a/api/app/repositories/tool_repository.py +++ b/api/app/repositories/tool_repository.py @@ -1,10 +1,9 @@ """工具数据访问层""" import uuid -from typing import List, Optional, Dict, Any +from typing import List, Optional from sqlalchemy.orm import Session -from sqlalchemy import func, or_ +from sqlalchemy import func -from app.repositories.base_repository import BaseRepository from app.models.tool_model import ( ToolConfig, BuiltinToolConfig, CustomToolConfig, MCPToolConfig, ToolExecution, ToolType, ToolStatus @@ -14,6 +13,31 @@ from app.models.tool_model import ( class ToolRepository: """工具仓储类""" + @staticmethod + def get_tenant_id_by_workflow_id(db: Session, workflow_id: uuid.UUID) -> Optional[uuid.UUID]: + """根据工作流ID获取tenant_id + + Args: + db: 数据库会话 + workflow_id: 工作流配置ID + + Returns: + tenant_id或None + """ + from app.models.app_model import App + from app.models.workflow_model import WorkflowConfig + from app.models.workspace_model import Workspace + + result = db.query(Workspace.tenant_id).join( + App, App.workspace_id == Workspace.id + ).join( + WorkflowConfig, WorkflowConfig.app_id == App.id + ).filter( + WorkflowConfig.id == workflow_id + ).first() + + return result[0] if result else None + @staticmethod def find_by_tenant( db: Session, diff --git a/api/app/schemas/home_page_schema.py b/api/app/schemas/home_page_schema.py new file mode 100644 index 00000000..de223e17 --- /dev/null +++ b/api/app/schemas/home_page_schema.py @@ -0,0 +1,32 @@ +from datetime import datetime +from pydantic import BaseModel, field_serializer +from typing import Optional + +from app.core.api_key_utils import datetime_to_timestamp + + +class HomeStatistics(BaseModel): + """首页统计数据""" + total_models: int + new_models_this_month: int + active_workspaces: int + new_workspaces_this_month: int + total_users: int + new_users_this_month: int + running_apps: int + new_apps_this_week: int + +class WorkspaceInfo(BaseModel): + """工作空间信息""" + id: str + name: str + icon: Optional[str] + description: Optional[str] + app_count: int + user_count: int + created_at: datetime + + @field_serializer('created_at') + @classmethod + def serialize_datetime(cls, v: datetime) -> Optional[int]: + return datetime_to_timestamp(v) \ No newline at end of file diff --git a/api/app/services/home_page_service.py b/api/app/services/home_page_service.py new file mode 100644 index 00000000..909da25f --- /dev/null +++ b/api/app/services/home_page_service.py @@ -0,0 +1,67 @@ +from datetime import datetime, timedelta +from sqlalchemy.orm import Session +from uuid import UUID + +from app.repositories.home_page_repository import HomePageRepository +from app.schemas.home_page_schema import HomeStatistics, WorkspaceInfo + +class HomePageService: + + @staticmethod + def get_home_statistics(db: Session, tenant_id: UUID) -> HomeStatistics: + """获取首页统计数据""" + # 计算时间范围 + now = datetime.now() + month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + week_start = now - timedelta(days=now.weekday()) + week_start = week_start.replace(hour=0, minute=0, second=0, microsecond=0) + + # 获取各项统计数据 + total_models, new_models_this_month = HomePageRepository.get_model_statistics( + db, tenant_id, month_start + ) + + active_workspaces, new_workspaces_this_month = HomePageRepository.get_workspace_statistics( + db, tenant_id, month_start + ) + + total_users, new_users_this_month = HomePageRepository.get_user_statistics( + db, tenant_id, month_start + ) + + running_apps, new_apps_this_week = HomePageRepository.get_app_statistics( + db, tenant_id, week_start + ) + + return HomeStatistics( + total_models=total_models, + new_models_this_month=new_models_this_month, + active_workspaces=active_workspaces, + new_workspaces_this_month=new_workspaces_this_month, + total_users=total_users, + new_users_this_month=new_users_this_month, + running_apps=running_apps, + new_apps_this_week=new_apps_this_week + ) + + @staticmethod + def get_workspace_list(db: Session, tenant_id: UUID) -> list[WorkspaceInfo]: + """获取工作空间列表(优化版本)""" + workspaces, app_count_dict, user_count_dict= HomePageRepository.get_workspaces_with_counts( + db, tenant_id + ) + + workspace_list = [] + for workspace in workspaces: + workspace_info = WorkspaceInfo( + id=str(workspace.id), + name=workspace.name, + icon=workspace.icon, + description=workspace.description, + app_count=app_count_dict.get(workspace.id, 0), + user_count=user_count_dict.get(workspace.id, 0), + created_at=workspace.created_at + ) + workspace_list.append(workspace_info) + + return workspace_list \ No newline at end of file diff --git a/api/app/services/tool_service.py b/api/app/services/tool_service.py index 783df81a..50cca957 100644 --- a/api/app/services/tool_service.py +++ b/api/app/services/tool_service.py @@ -297,6 +297,165 @@ class ToolService: self.db.commit() logger.info(f"租户 {tenant_id} 内置工具初始化完成") + async def get_tool_methods(self, tool_id: str, tenant_id: uuid.UUID) -> Optional[List[Dict[str, Any]]]: + """获取工具的所有方法 + + Args: + tool_id: 工具ID + tenant_id: 租户ID + + Returns: + 方法列表或None + """ + config = self._get_tool_config(tool_id, tenant_id) + if not config: + return None + + try: + if config.tool_type == ToolType.BUILTIN.value: + return await self._get_builtin_tool_methods(config) + elif config.tool_type == ToolType.CUSTOM.value: + return await self._get_custom_tool_methods(config) + elif config.tool_type == ToolType.MCP.value: + return await self._get_mcp_tool_methods(config) + else: + return [] + + except Exception as e: + logger.error(f"获取工具方法失败: {tool_id}, {e}") + return [] + + async def _get_builtin_tool_methods(self, config: ToolConfig) -> List[Dict[str, Any]]: + """获取内置工具的方法""" + builtin_config = self.builtin_repo.find_by_tool_id(self.db, config.id) + if not builtin_config or builtin_config.tool_class not in BUILTIN_TOOLS: + return [] + + # 获取工具实例 + tool_instance = self._get_tool_instance(str(config.id), config.tenant_id) + if not tool_instance: + return [] + + # 检查是否有operation参数 + operation_param = None + for param in tool_instance.parameters: + if param.name == "operation" and param.enum: + operation_param = param + break + + if operation_param: + # 有多个操作 + methods = [] + for operation in operation_param.enum: + methods.append({ + "method_id": f"{config.name}_{operation}", + "name": operation, + "description": f"{config.description} - {operation}", + "parameters": [p for p in tool_instance.parameters if p.name != "operation"] + }) + return methods + else: + # 只有一个方法 + return [{ + "method_id": config.name, + "name": config.name, + "description": config.description, + "parameters": [p for p in tool_instance.parameters if p.name != "operation"] + }] + + async def _get_custom_tool_methods(self, config: ToolConfig) -> List[Dict[str, Any]]: + """获取自定义工具的方法""" + custom_config = self.custom_repo.find_by_tool_id(self.db, config.id) + if not custom_config: + return [] + + try: + from app.core.tools.custom.schema_parser import OpenAPISchemaParser + parser = OpenAPISchemaParser() + + # 解析schema + if custom_config.schema_content: + success, schema, error = parser.parse_from_content(custom_config.schema_content, "application/json") + elif custom_config.schema_url: + success, schema, error = await parser.parse_from_url(custom_config.schema_url) + else: + return [] + + if not success: + return [] + + # 提取操作 + tool_info = parser.extract_tool_info(schema) + operations = tool_info.get("operations", {}) + + methods = [] + for operation_id, operation in operations.items(): + # 生成参数列表 + parameters = [] + + # 路径和查询参数 + for param_name, param_info in operation.get("parameters", {}).items(): + parameters.append({ + "name": param_name, + "type": param_info.get("type", "string"), + "description": param_info.get("description", ""), + "required": param_info.get("required", False), + "enum": param_info.get("enum"), + "default": param_info.get("default") + }) + + # 请求体参数 + request_body = operation.get("request_body") + if request_body: + schema_props = request_body.get("schema", {}).get("properties", {}) + required_props = request_body.get("schema", {}).get("required", []) + + for prop_name, prop_schema in schema_props.items(): + parameters.append({ + "name": prop_name, + "type": prop_schema.get("type", "string"), + "description": prop_schema.get("description", ""), + "required": prop_name in required_props, + "enum": prop_schema.get("enum"), + "default": prop_schema.get("default") + }) + + methods.append({ + "method_id": operation_id, + "name": operation.get("summary", operation_id), + "description": operation.get("description", ""), + "method": operation.get("method", "GET"), + "path": operation.get("path", "/"), + "parameters": parameters + }) + + return methods + + except Exception as e: + logger.error(f"解析自定义工具schema失败: {e}") + return [] + + async def _get_mcp_tool_methods(self, config: ToolConfig) -> List[Dict[str, Any]]: + """获取MCP工具的方法""" + mcp_config = self.mcp_repo.find_by_tool_id(self.db, config.id) + if not mcp_config: + return [] + + available_tools = mcp_config.available_tools or [] + if not available_tools: + return [] + + methods = [] + for tool_name in available_tools: + methods.append({ + "method_id": tool_name, + "name": tool_name, + "description": f"MCP工具: {tool_name}", + "parameters": [] # MCP工具参数需要动态获取 + }) + + return methods + def get_tool_statistics(self, tenant_id: uuid.UUID) -> Dict[str, Any]: """获取工具统计信息""" try: diff --git a/api_key_mcp_server.py b/api_key_mcp_server.py new file mode 100644 index 00000000..f611dc59 --- /dev/null +++ b/api_key_mcp_server.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""API Key认证MCP服务器""" + +from fastapi import FastAPI, HTTPException, Depends, Header +from typing import Optional +import uvicorn +from mcp_base import MCPRequest, handle_mcp_request, TOOLS + +app = FastAPI(title="API Key MCP Server", version="1.0.0") + +# API Key配置 +API_KEYS = {"test-api-key", "demo-key-123"} + +def verify_api_key(x_api_key: Optional[str] = Header(None)): + """验证API Key""" + if x_api_key and x_api_key in API_KEYS: + return True + raise HTTPException(status_code=401, detail="Invalid API Key") + +@app.get("/") +async def root(): + return {"name": "API Key MCP Server", "version": "1.0.0", "auth_type": "api_key"} + +@app.get("/health") +async def health(): + return {"status": "healthy", "tools": len(TOOLS), "auth_type": "api_key"} + +@app.post("/mcp") +async def mcp_handler(request: MCPRequest, _: bool = Depends(verify_api_key)): + return await handle_mcp_request(request, "API Key MCP Server") + +if __name__ == "__main__": + print("启动API Key认证MCP服务器...") + print("访问 http://localhost:8004 查看服务状态") + print("MCP端点: http://localhost:8004/mcp") + print("认证方式: API Key (Header: X-API-Key)") + print("测试API Keys: test-api-key, demo-key-123") + uvicorn.run(app, host="0.0.0.0", port=8004) \ No newline at end of file diff --git a/basic_auth_mcp_server.py b/basic_auth_mcp_server.py new file mode 100644 index 00000000..11bb5595 --- /dev/null +++ b/basic_auth_mcp_server.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Basic Auth认证MCP服务器""" + +from fastapi import FastAPI, HTTPException, Depends, Header +from typing import Optional +import uvicorn +import base64 +from mcp_base import MCPRequest, handle_mcp_request, TOOLS + +app = FastAPI(title="Basic Auth MCP Server", version="1.0.0") + +# Basic Auth配置 +BASIC_AUTH_USERS = {"admin": "password", "user": "secret"} + +def verify_basic_auth(authorization: Optional[str] = Header(None)): + """验证Basic Auth""" + if authorization and authorization.startswith("Basic "): + try: + credentials = base64.b64decode(authorization.split(" ")[1]).decode() + username, password = credentials.split(":", 1) + if username in BASIC_AUTH_USERS and BASIC_AUTH_USERS[username] == password: + return True + except: + pass + raise HTTPException(status_code=401, detail="Invalid Basic Auth") + +@app.get("/") +async def root(): + return {"name": "Basic Auth MCP Server", "version": "1.0.0", "auth_type": "basic_auth"} + +@app.get("/health") +async def health(): + return {"status": "healthy", "tools": len(TOOLS), "auth_type": "basic_auth"} + +@app.post("/mcp") +async def mcp_handler(request: MCPRequest, _: bool = Depends(verify_basic_auth)): + return await handle_mcp_request(request, "Basic Auth MCP Server") + +if __name__ == "__main__": + print("启动Basic Auth认证MCP服务器...") + print("访问 http://localhost:8006 查看服务状态") + print("MCP端点: http://localhost:8006/mcp") + print("认证方式: Basic Auth (Header: Authorization: Basic )") + print("测试用户: admin:password, user:secret") + uvicorn.run(app, host="0.0.0.0", port=8006) \ No newline at end of file diff --git a/bearer_token_mcp_server.py b/bearer_token_mcp_server.py new file mode 100644 index 00000000..57d27f2f --- /dev/null +++ b/bearer_token_mcp_server.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Bearer Token认证MCP服务器""" + +from fastapi import FastAPI, HTTPException, Depends, Header +from typing import Optional +import uvicorn +from mcp_base import MCPRequest, handle_mcp_request, TOOLS + +app = FastAPI(title="Bearer Token MCP Server", version="1.0.0") + +# Bearer Token配置 +BEARER_TOKENS = {"bearer-token-123", "demo-bearer-token"} + +def verify_bearer_token(authorization: Optional[str] = Header(None)): + """验证Bearer Token""" + if authorization and authorization.startswith("Bearer "): + token = authorization.split(" ")[1] + if token in BEARER_TOKENS: + return True + raise HTTPException(status_code=401, detail="Invalid Bearer Token") + +@app.get("/") +async def root(): + return {"name": "Bearer Token MCP Server", "version": "1.0.0", "auth_type": "bearer_token"} + +@app.get("/health") +async def health(): + return {"status": "healthy", "tools": len(TOOLS), "auth_type": "bearer_token"} + +@app.post("/mcp") +async def mcp_handler(request: MCPRequest, _: bool = Depends(verify_bearer_token)): + return await handle_mcp_request(request, "Bearer Token MCP Server") + +if __name__ == "__main__": + print("启动Bearer Token认证MCP服务器...") + print("访问 http://localhost:8005 查看服务状态") + print("MCP端点: http://localhost:8005/mcp") + print("认证方式: Bearer Token (Header: Authorization: Bearer )") + print("测试Bearer Tokens: bearer-token-123, demo-bearer-token") + uvicorn.run(app, host="0.0.0.0", port=8005) \ No newline at end of file diff --git a/mcp_base.py b/mcp_base.py new file mode 100644 index 00000000..f571e2fa --- /dev/null +++ b/mcp_base.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""MCP服务器基础模块 - 共享的模型和处理逻辑""" + +from pydantic import BaseModel +from typing import Dict, Any + +class MCPRequest(BaseModel): + jsonrpc: str = "2.0" + id: str + method: str + params: Dict[str, Any] = {} + +class MCPResponse(BaseModel): + jsonrpc: str = "2.0" + id: str + result: Any = None + error: Dict[str, Any] = None + +# 工具定义 +TOOLS = [ + { + "name": "calculator", + "description": "简单计算器", + "inputSchema": { + "type": "object", + "properties": { + "expression": {"type": "string", "description": "数学表达式"} + }, + "required": ["expression"] + } + }, + { + "name": "echo", + "description": "回显工具", + "inputSchema": { + "type": "object", + "properties": { + "message": {"type": "string", "description": "要回显的消息"} + }, + "required": ["message"] + } + } +] + +async def handle_mcp_request(request: MCPRequest, server_name: str = "MCP Server"): + """处理MCP请求""" + try: + if request.method == "initialize": + return MCPResponse( + id=request.id, + result={ + "protocolVersion": "2024-11-05", + "capabilities": {"tools": {"listChanged": True}}, + "serverInfo": {"name": server_name, "version": "1.0.0"} + } + ) + + elif request.method == "tools/list": + return MCPResponse( + id=request.id, + result={"tools": TOOLS} + ) + + elif request.method == "tools/call": + tool_name = request.params.get("name") + arguments = request.params.get("arguments", {}) + + if tool_name == "calculator": + try: + expression = arguments.get("expression", "") + result = eval(expression) + return MCPResponse( + id=request.id, + result={"content": [{"type": "text", "text": f"结果: {result}"}]} + ) + except Exception as e: + return MCPResponse( + id=request.id, + error={"code": -1, "message": f"计算错误: {str(e)}"} + ) + + elif tool_name == "echo": + message = arguments.get("message", "") + return MCPResponse( + id=request.id, + result={"content": [{"type": "text", "text": f"Echo: {message}"}]} + ) + + else: + return MCPResponse( + id=request.id, + error={"code": -1, "message": f"未知工具: {tool_name}"} + ) + + elif request.method == "ping": + return MCPResponse( + id=request.id, + result={"status": "pong"} + ) + + else: + return MCPResponse( + id=request.id, + error={"code": -1, "message": f"未知方法: {request.method}"} + ) + + except Exception as e: + return MCPResponse( + id=request.id, + error={"code": -1, "message": str(e)} + ) \ No newline at end of file diff --git a/simple_mcp_server.py b/simple_mcp_server.py new file mode 100644 index 00000000..fa299e37 --- /dev/null +++ b/simple_mcp_server.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""简化的MCP服务器 - 用于测试MCP工具集成""" + +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from typing import Dict, Any, List +import uvicorn + +app = FastAPI(title="Simple MCP Server", version="1.0.0") + +class MCPRequest(BaseModel): + jsonrpc: str = "2.0" + id: str + method: str + params: Dict[str, Any] = {} + +class MCPResponse(BaseModel): + jsonrpc: str = "2.0" + id: str + result: Any = None + error: Dict[str, Any] = None + +# 可用工具定义 +TOOLS = [ + { + "name": "calculator", + "description": "简单计算器", + "inputSchema": { + "type": "object", + "properties": { + "expression": {"type": "string", "description": "数学表达式"} + }, + "required": ["expression"] + } + }, + { + "name": "echo", + "description": "回显工具", + "inputSchema": { + "type": "object", + "properties": { + "message": {"type": "string", "description": "要回显的消息"} + }, + "required": ["message"] + } + } +] + +@app.get("/") +async def root(): + return {"name": "Simple MCP Server", "version": "1.0.0"} + +@app.get("/health") +async def health(): + return {"status": "healthy", "tools": len(TOOLS)} + +@app.post("/mcp") +async def mcp_handler(request: MCPRequest): + """处理MCP请求""" + try: + if request.method == "initialize": + return MCPResponse( + id=request.id, + result={ + "protocolVersion": "2024-11-05", + "capabilities": {"tools": {"listChanged": True}}, + "serverInfo": {"name": "Simple MCP Server", "version": "1.0.0"} + } + ) + + elif request.method == "tools/list": + return MCPResponse( + id=request.id, + result={"tools": TOOLS} + ) + + elif request.method == "tools/call": + tool_name = request.params.get("name") + arguments = request.params.get("arguments", {}) + + if tool_name == "calculator": + try: + expression = arguments.get("expression", "") + result = eval(expression) # 注意:生产环境不要用eval + return MCPResponse( + id=request.id, + result={"content": [{"type": "text", "text": f"结果: {result}"}]} + ) + except Exception as e: + return MCPResponse( + id=request.id, + error={"code": -1, "message": f"计算错误: {str(e)}"} + ) + + elif tool_name == "echo": + message = arguments.get("message", "") + return MCPResponse( + id=request.id, + result={"content": [{"type": "text", "text": f"Echo: {message}"}]} + ) + + else: + return MCPResponse( + id=request.id, + error={"code": -1, "message": f"未知工具: {tool_name}"} + ) + + elif request.method == "ping": + return MCPResponse( + id=request.id, + result={"status": "pong"} + ) + + else: + return MCPResponse( + id=request.id, + error={"code": -1, "message": f"未知方法: {request.method}"} + ) + + except Exception as e: + return MCPResponse( + id=request.id, + error={"code": -1, "message": str(e)} + ) + +if __name__ == "__main__": + print("启动简化MCP服务器...") + print("访问 http://localhost:8002 查看服务状态") + print("MCP端点: http://localhost:8002/mcp") + uvicorn.run(app, host="0.0.0.0", port=8002) \ No newline at end of file diff --git a/web/.gitignore b/web/.gitignore index b398d222..9608e0b9 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -22,5 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? - +vite.config.js package-lock.json diff --git a/web/package.json b/web/package.json index d2c254ec..9d157982 100644 --- a/web/package.json +++ b/web/package.json @@ -1,7 +1,7 @@ { "name": "memory-bear-font-end", "private": true, - "version": "0.1.0", + "version": "0.2.0", "type": "module", "scripts": { "dev": "vite", @@ -10,10 +10,18 @@ "preview": "vite preview" }, "dependencies": { + "@antv/layout": "^1.2.14-beta.8", + "@antv/x6": "^3.0.1", + "@antv/x6-react-shape": "^3.0.1", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@lexical/code": "^0.39.0", + "@lexical/link": "^0.39.0", + "@lexical/list": "^0.39.0", + "@lexical/react": "^0.39.0", + "@lexical/rich-text": "^0.39.0", "antd": "^5.27.4", "axios": "^1.12.2", "clsx": "^2.1.1", @@ -23,6 +31,8 @@ "echarts": "^5.6.0", "echarts-for-react": "^3.0.2", "i18next": "^25.6.0", + "js-yaml": "^4.1.1", + "lexical": "^0.39.0", "mermaid": "^11.12.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -31,7 +41,6 @@ "react-markdown": "^10.1.0", "react-router-dom": "^6.22.0", "react-syntax-highlighter": "^16.1.0", - "reactflow": "^11.11.4", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", @@ -46,6 +55,7 @@ "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.14", "@types/crypto-js": "^4.2.2", + "@types/js-yaml": "^4.0.9", "@types/node": "^24.6.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", diff --git a/web/public/auto-imports.d.ts b/web/public/auto-imports.d.ts index 62f88140..61cb2733 100644 --- a/web/public/auto-imports.d.ts +++ b/web/public/auto-imports.d.ts @@ -6,22 +6,31 @@ // biome-ignore lint: disable export {} declare global { + const Activity: typeof import('react').Activity + const Fragment: typeof import('react').Fragment const Link: typeof import('react-router-dom').Link const NavLink: typeof import('react-router-dom').NavLink const Navigate: typeof import('react-router-dom').Navigate const Outlet: typeof import('react-router-dom').Outlet const Route: typeof import('react-router-dom').Route const Routes: typeof import('react-router-dom').Routes + const Suspense: typeof import('react').Suspense + const cache: typeof import('react').cache + const cacheSignal: typeof import('react').cacheSignal + const createContext: typeof import('react').createContext const createRef: typeof import('react').createRef const forwardRef: typeof import('react').forwardRef const lazy: typeof import('react').lazy const memo: typeof import('react').memo const startTransition: typeof import('react').startTransition + const use: typeof import('react').use + const useActionState: typeof import('react').useActionState const useCallback: typeof import('react').useCallback const useContext: typeof import('react').useContext const useDebugValue: typeof import('react').useDebugValue const useDeferredValue: typeof import('react').useDeferredValue const useEffect: typeof import('react').useEffect + const useEffectEvent: typeof import('react').useEffectEvent const useHref: typeof import('react-router-dom').useHref const useId: typeof import('react').useId const useImperativeHandle: typeof import('react').useImperativeHandle @@ -33,6 +42,7 @@ declare global { const useMemo: typeof import('react').useMemo const useNavigate: typeof import('react-router-dom').useNavigate const useNavigationType: typeof import('react-router-dom').useNavigationType + const useOptimistic: typeof import('react').useOptimistic const useOutlet: typeof import('react-router-dom').useOutlet const useOutletContext: typeof import('react-router-dom').useOutletContext const useParams: typeof import('react-router-dom').useParams diff --git a/web/src/App.tsx b/web/src/App.tsx index c255f522..8e3140d9 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -27,12 +27,21 @@ import 'dayjs/locale/en' import 'dayjs/locale/zh-cn' import 'dayjs/plugin/timezone' 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')) { + window.location.href = `/#/login`; + } + }, []) useEffect(() => { document.title = t('memoryBear') diff --git a/web/src/api/apiKey.ts b/web/src/api/apiKey.ts new file mode 100644 index 00000000..56ad79c4 --- /dev/null +++ b/web/src/api/apiKey.ts @@ -0,0 +1,33 @@ +import { request } from '@/utils/request' +import type { ApiKey } from '@/views/ApiKeyManagement/types' + +// API Key列表 +export const getApiKeyListUrl = '/apikeys' +export const getApiKeyList = (data: Record) => { + return request.get(getApiKeyListUrl, data) +} + +// API Key详情 +export const getApiKey = (id: string) => { + return request.get(`/apikeys/${id}`) +} + +// 创建API Key +export const createApiKey = (values: ApiKey) => { + return request.post('/apikeys', values) +} + +// 更新API Key +export const updateApiKey = (id: string, values: ApiKey) => { + return request.put(`/apikeys/${id}`, values) +} + +// 删除 API Key +export const deleteApiKey = (id: string) => { + return request.delete(`/apikeys/${id}`) +} + +// 使用统计 +export const getApiKeyStats = (app_key_id: string) => { + return request.get(`/apikeys/${app_key_id}/stats`) +} \ No newline at end of file diff --git a/web/src/api/application.ts b/web/src/api/application.ts index 583ff8b9..69d27d44 100644 --- a/web/src/api/application.ts +++ b/web/src/api/application.ts @@ -1,7 +1,9 @@ import { request } from '@/utils/request' -import type { Application } from '@/views/ApplicationManagement/types' +import type { ApplicationModalData } from '@/views/ApplicationManagement/types' import type { Config } from '@/views/ApplicationConfig/types' -import { handleSSE } from '@/utils/stream' +import { handleSSE, type SSEMessage } from '@/utils/stream' +import type { QueryParams } from '@/views/Conversation/types' +import type { WorkflowConfig } from '@/views/Workflow/types' // 应用列表 export const getApplicationListUrl = '/apps' @@ -12,20 +14,24 @@ export const getApplicationList = (data: Record) => { export const getApplicationConfig = (id: string) => { return request.get(`/apps/${id}/config`) } -// 获取集群应配置 +// 获取集群应用配置 export const getMultiAgentConfig = (id: string) => { return request.get(`/apps/${id}/multi-agent`) } +// 获取 workflow应用配置 +export const getWorkflowConfig = (id: string) => { + return request.get(`/apps/${id}/workflow`) +} // 应用详情 export const getApplication = (id: string) => { return request.get(`/apps/${id}`) } // 更新应用 -export const updateApplication = (id: string, values: Application) => { +export const updateApplication = (id: string, values: ApplicationModalData) => { return request.put(`/apps/${id}`, values) } // 创建应用 -export const addApplication = (values: Application) => { +export const addApplication = (values: ApplicationModalData) => { return request.post('/apps', values) } // 保存Agent配置 @@ -36,11 +42,15 @@ export const saveAgentConfig = (app_id: string, values: Config) => { export const saveMultiAgentConfig = (app_id: string, values: Config) => { return request.put(`/apps/${app_id}/multi-agent`, values) } +// 保存workflow配置 +export const saveWorkflowConfig = (app_id: string, values: WorkflowConfig) => { + return request.put(`/apps/${app_id}/workflow`, values) +} // 模型比对试运行 -export const runCompare = (app_id: string, values: Record, onMessage?: (data: string) => void) => { +export const runCompare = (app_id: string, values: Record, onMessage?: (data: SSEMessage[]) => void) => { return handleSSE(`/apps/${app_id}/draft/run/compare`, values, onMessage) } -export const draftRun = (app_id: string, values: Record, onMessage?: (data: string) => void) => { +export const draftRun = (app_id: string, values: Record, onMessage?: (data: SSEMessage[]) => void) => { return handleSSE(`/apps/${app_id}/draft/run`, values, onMessage) } // 删除应用 @@ -76,18 +86,7 @@ export const getConversationHistory = (share_token: string, data: { page: number }) } // 发送体验对话 -export const sendConversation = (share_token: string, values: { - message: string; - web_search: boolean; - memory: boolean; - stream: boolean; - conversation_id: string | null; -}, onMessage, shareToken: string) => { - // return request.post(`/public/share/chat`, values, { - // headers: { - // 'Authorization': `Bearer ${localStorage.getItem(`shareToken_${share_token}`)}` - // } - // }) +export const sendConversation = (values: QueryParams, onMessage: (data: SSEMessage[]) => void, shareToken: string) => { return handleSSE(`/public/share/chat`, values, onMessage, { headers: { 'Authorization': `Bearer ${shareToken}` diff --git a/web/src/api/knowledgeBase.ts b/web/src/api/knowledgeBase.ts index 9791ee8d..c8e388a0 100644 --- a/web/src/api/knowledgeBase.ts +++ b/web/src/api/knowledgeBase.ts @@ -64,8 +64,8 @@ export const getModelTypeList = async () => { return response as any[]; }; // 获取模型列表 -export const getModelList = async (type: string | string[], pageInfo: PageRequest) => { - const response = await request.get(`${apiPrefix}/models`, { type, ...pageInfo }); +export const getModelList = async (pageInfo: PageRequest) => { + const response = await request.get(`${apiPrefix}/models`, pageInfo); return response as any; }; //获取模型提供者 @@ -135,16 +135,18 @@ interface UploadFileOptions { kb_id?: string; parent_id?: string; onUploadProgress?: (event: AxiosProgressEvent) => void; + signal?: AbortSignal; } // 上传文件 export const uploadFile = async (data: FormData, options?: UploadFileOptions) => { - const { kb_id, parent_id, onUploadProgress } = options || {}; + const { kb_id, parent_id, onUploadProgress, signal } = options || {}; const params: Record = {}; if (kb_id) params.kb_id = kb_id; if (parent_id) params.parent_id = parent_id; const response = await request.uploadFile(`${apiPrefix}/files/file`, data, { params, onUploadProgress, + signal, }); return response as UploadFileResponse; }; @@ -199,8 +201,8 @@ export const deleteFile = async (id: string) => { }; // 获取文档列表 -export const getDocumentList = async (query: PathQuery) => { - const response = await request.get(`${apiPrefix}/documents/${query.kb_id}/${query.parent_id}/documents`, query); +export const getDocumentList = async (kb_id:string, query: PathQuery) => { + const response = await request.get(`${apiPrefix}/documents/${kb_id}/documents`, query); return response as KnowledgeBaseDocumentData[]; }; // 文档详情 @@ -213,6 +215,11 @@ export const createDocument = async (data: KnowledgeBaseDocumentData) => { const response = await request.post(`${apiPrefix}/documents/document`, data); return response as KnowledgeBaseDocumentData; }; +// 自定义文档上传并创建 +export const createDocumentAndUpload = async ( data: any, params: PathQuery) => { + const response = await request.post(`${apiPrefix}/files/customtext`, data, { params } ); + return response as any; +}; // 更新文档 export const updateDocument = async (id: string, data: KnowledgeBaseDocumentData) => { const response = await request.put(`${apiPrefix}/documents/${id}`, data); @@ -223,9 +230,9 @@ export const deleteDocument = async (id: string) => { const response = await request.delete(`${apiPrefix}/documents/${id}`); return response; }; -// 文档解析 -export const parseDocument = async (id: string) => { - const response = await request.post(`${apiPrefix}/documents/${id}/chunks`); +// 文档解析 / 分块 +export const parseDocument = async (id: string, data: any) => { + const response = await request.post(`${apiPrefix}/documents/${id}/chunks`, data); return response as any; }; // 文档分块预览 diff --git a/web/src/api/memory.ts b/web/src/api/memory.ts index 7c7ae4c1..750f559c 100644 --- a/web/src/api/memory.ts +++ b/web/src/api/memory.ts @@ -8,7 +8,15 @@ import type { import type { ConfigForm as ExtractionConfigForm } from '@/views/MemoryExtractionEngine/types' +import type { + ConfigForm as EmotionConfig +} from '@/views/EmotionEngine/types' +import type { + ConfigForm as SelfReflectionEngineConfig +} from '@/views/SelfReflectionEngine/types' import type { TestParams } from '@/views/MemoryConversation' +import type { EndUser } from '@/views/UserMemoryDetail/types' +import { handleSSE, type SSEMessage } from '@/utils/stream' // 记忆对话 export const readService = (query: TestParams) => { @@ -59,6 +67,7 @@ export const getTotalEndUsers = () => { export const getUserProfile = (end_user_id: string) => { return request.get(`/memory/analytics/user_profile`, { end_user_id }) } + // 用户记忆-记忆洞察 export const getMemoryInsightReport = (end_user_id: string) => { return request.get(`/memory-storage/analytics/memory_insight/report`, { end_user_id }) @@ -67,9 +76,20 @@ export const getMemoryInsightReport = (end_user_id: string) => { export const getUserSummary = (end_user_id: string) => { return request.get(`/memory-storage/analytics/user_summary`, { end_user_id }) } +// 记忆分类 +export const getNodeStatistics = (end_user_id: string) => { + return request.get(`/memory-storage/analytics/node_statistics`, { end_user_id }) +} +// 基本信息 +export const getEndUserProfile = (end_user_id: string) => { + return request.get(`/memory-storage/read_end_user/profile`, { end_user_id }) +} +export const updatedEndUserProfile = (values: EndUser) => { + return request.post(`/memory-storage/updated_end_user/profile`, values) +} // 用户记忆-关系网络 export const getMemorySearchEdges = (end_user_id: string) => { - return request.get(`/memory-storage/search/entity_graph`, { end_user_id }) + return request.get(`/memory-storage/analytics/graph_data`, { end_user_id }) } // 用户记忆-用户兴趣分布 export const getHotMemoryTagsByUser = (end_user_id: string) => { @@ -95,6 +115,26 @@ export const getChunkInsight = (end_user_id: string) => { export const getRagContent = (end_user_id: string) => { return request.get(`/dashboard/rag_content`, { end_user_id, limit: 20 }) } +// 情感分布分析 +export const getWordCloud = (group_id: string) => { + return request.post(`/memory/emotion/wordcloud`, { group_id, limit: 20 }) +} +// 高频情绪关键词 +export const getEmotionTags = (group_id: string) => { + return request.post(`/memory/emotion/tags`, { group_id, limit: 20 }) +} +// 情绪健康指数 +export const getEmotionHealth = (group_id: string) => { + return request.post(`/memory/emotion/health`, { group_id, limit: 20 }) +} +// 个性化建议 +export const getEmotionSuggestions = (group_id: string) => { + return request.post(`/memory/emotion/suggestions`, { group_id, limit: 20 }) +} +export const analyticsRefresh = (end_user_id: string) => { + return request.post('/memory-storage/analytics/generate_cache', { end_user_id }) +} + /*************** end 用户记忆 相关接口 ******************************/ /****************** 记忆管理 相关接口 *******************************/ @@ -117,11 +157,11 @@ export const deleteMemoryConfig = (config_id: number) => { } // 遗忘引擎-获取配置 export const getMemoryForgetConfig = (config_id: number | string) => { - return request.get('/memory-storage/read_config_forget', { config_id }) + return request.get('/memory/forget/read_config', { config_id }) } // 遗忘引擎-更新配置 export const updateMemoryForgetConfig = (values: ForgetConfigForm) => { - return request.post('/memory-storage/update_config_forget', values) + return request.post('/memory/forget/update_config', values) } // 记忆萃取引擎-获取配置 export const getMemoryExtractionConfig = (config_id: number | string) => { @@ -132,9 +172,30 @@ export const updateMemoryExtractionConfig = (values: ExtractionConfigForm) => { return request.post('/memory-storage/update_config_extracted', values) } // 记忆萃取引擎-试运行 -export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string }) => { - return request.post('/memory-storage/pilot_run', values) +export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string; }, onMessage?: (data: SSEMessage[]) => void) => { + return handleSSE('/memory-storage/pilot_run', values, onMessage) } +// 情绪引擎-获取配置 +export const getMemoryEmotionConfig = (config_id: number | string) => { + return request.get('/memory/emotion/read_config', { config_id: config_id }) +} +// 情绪引擎-更新配置 +export const updateMemoryEmotionConfig = (values: EmotionConfig) => { + return request.post('/memory/emotion/updated_config', values) +} +// 反思引擎-获取配置 +export const getMemoryReflectionConfig = (config_id: number | string) => { + return request.get('/memory/reflection/configs', { config_id: config_id }) +} +// 反思引擎-更新配置 +export const updateMemoryReflectionConfig = (values: SelfReflectionEngineConfig) => { + return request.post('/memory/reflection/save', values) +} +// 反思引擎-试运行 +export const pilotRunMemoryReflectionConfig = (values: { config_id: number | string; language_type: string; }) => { + return request.get('/memory/reflection/run', values) +} + /*************** end 记忆管理 相关接口 ******************************/ diff --git a/web/src/api/order.ts b/web/src/api/order.ts new file mode 100644 index 00000000..e5d9d916 --- /dev/null +++ b/web/src/api/order.ts @@ -0,0 +1,17 @@ +import { request } from '@/utils/request' +import type { VoucherForm } from '@/views/OrderPayment/types' + +export const getOrderListUrl = '/v1/orders/customer' + +// 提交支付凭证API +export const submitPaymentVoucherAPI = (voucherData: VoucherForm) => { + return request.post('/v1/orders/', voucherData) +} +// 订单详情 +export const getOrderDetail = (order_no: string) => { + return request.get(`/v1/orders/customer/${order_no}`) +} +export const orderStatusUrl = '/v1/order-status/' +export const getOrderStatus = () => { + return request.get(orderStatusUrl) +} \ No newline at end of file diff --git a/web/src/api/prompt.ts b/web/src/api/prompt.ts new file mode 100644 index 00000000..77ea1271 --- /dev/null +++ b/web/src/api/prompt.ts @@ -0,0 +1,12 @@ +import { request } from '@/utils/request' +import type { AiPromptForm } from '@/views/ApplicationConfig/types' + +export const createPromptSessions = () => { + return request.post(`/prompt/sessions`) +} +export const getPrompt = (session_id: string) => { + return request.get(`/prompt/sessions/${session_id}`) +} +export const updatePromptMessages = (session_id: string, data: AiPromptForm) => { + return request.post(`/prompt/sessions/${session_id}/messages`, data) +} \ No newline at end of file diff --git a/web/src/api/tools.ts b/web/src/api/tools.ts new file mode 100644 index 00000000..142d4c41 --- /dev/null +++ b/web/src/api/tools.ts @@ -0,0 +1,31 @@ +import { request } from '@/utils/request' +import type { Query, CustomToolItem, ExecuteData, MCPToolItem, InnerToolItem } from '@/views/ToolManagement/types' + +// 工具列表 +export const getTools = (data: Query) => { + return request.get('/tools', data) +} +// 创建MCP工具 +export const addTool = (values: MCPToolItem | CustomToolItem) => { + return request.post('/tools', values) +} +// 更新工具 +export const updateTool = (tool_id: string, data: MCPToolItem | InnerToolItem | CustomToolItem) => { + return request.put(`/tools/${tool_id}`, data) +} +// 删除工具 +export const deleteTool = (tool_id: string) => { + return request.delete(`/tools/${tool_id}`) +} +// MCP 测试连接 +export const testConnection = (tool_id: string) => { + return request.post(`/tools/${tool_id}/test`) +} +// 工具测试 +export const execute = (data: ExecuteData) => { + return request.post(`/tools/execution/execute`, data) +} +export const parseSchema = (data: Record) => { + return request.post(`/tools/parse_schema`, data) + +} \ No newline at end of file diff --git a/web/src/api/workspaces.ts b/web/src/api/workspaces.ts index 428b1280..4e78194b 100644 --- a/web/src/api/workspaces.ts +++ b/web/src/api/workspaces.ts @@ -1,5 +1,6 @@ import { request } from '@/utils/request' import type { SpaceModalData } from '@/views/SpaceManagement/types' +import type { ConfigModalData } from '@/views/UserMemory/types' // 空间列表 export const getWorkspaces = () => { @@ -22,6 +23,6 @@ export const getWorkspaceModels = () => { return request.get(`/workspaces/workspace_models`) } // 更新空间模型配置 -export const updateWorkspaceModels = () => { - return request.post(`/workspaces/workspace_models`) +export const updateWorkspaceModels = (data: ConfigModalData) => { + return request.put(`/workspaces/workspace_models`, data) } diff --git a/web/src/assets/images/fullScreen.svg b/web/src/assets/images/fullScreen.svg new file mode 100644 index 00000000..a2fae058 --- /dev/null +++ b/web/src/assets/images/fullScreen.svg @@ -0,0 +1,24 @@ + + + 全屏 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/fullScreen_hover.svg b/web/src/assets/images/fullScreen_hover.svg new file mode 100644 index 00000000..fe3b8361 --- /dev/null +++ b/web/src/assets/images/fullScreen_hover.svg @@ -0,0 +1,24 @@ + + + 全屏 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/home/arrow_top_right_hover.svg b/web/src/assets/images/home/arrow_top_right_hover.svg new file mode 100644 index 00000000..903f9618 --- /dev/null +++ b/web/src/assets/images/home/arrow_top_right_hover.svg @@ -0,0 +1,16 @@ + + + 编组 16 + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/menu/apiKey.png b/web/src/assets/images/menu/apiKey.png new file mode 100644 index 00000000..53d19428 Binary files /dev/null and b/web/src/assets/images/menu/apiKey.png differ diff --git a/web/src/assets/images/menu/apiKey_active.png b/web/src/assets/images/menu/apiKey_active.png new file mode 100644 index 00000000..4f8d1cfa Binary files /dev/null and b/web/src/assets/images/menu/apiKey_active.png differ diff --git a/web/src/assets/images/menu/pricing.svg b/web/src/assets/images/menu/pricing.svg new file mode 100644 index 00000000..5510ba23 --- /dev/null +++ b/web/src/assets/images/menu/pricing.svg @@ -0,0 +1,22 @@ + + + 菜单-收费管理 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/menu/pricing_active.svg b/web/src/assets/images/menu/pricing_active.svg new file mode 100644 index 00000000..f708877d --- /dev/null +++ b/web/src/assets/images/menu/pricing_active.svg @@ -0,0 +1,22 @@ + + + 菜单-收费管理 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/menu/space_acitve.svg b/web/src/assets/images/menu/space_active.svg similarity index 100% rename from web/src/assets/images/menu/space_acitve.svg rename to web/src/assets/images/menu/space_active.svg diff --git a/web/src/assets/images/menu/tool.png b/web/src/assets/images/menu/tool.png new file mode 100644 index 00000000..669238e8 Binary files /dev/null and b/web/src/assets/images/menu/tool.png differ diff --git a/web/src/assets/images/menu/tool_active.png b/web/src/assets/images/menu/tool_active.png new file mode 100644 index 00000000..252cd702 Binary files /dev/null and b/web/src/assets/images/menu/tool_active.png differ diff --git a/web/src/assets/images/menu/userMemory_acitve.svg b/web/src/assets/images/menu/userMemory_acitve.svg deleted file mode 100644 index 554dc0bc..00000000 --- a/web/src/assets/images/menu/userMemory_acitve.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - 编组 29 - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/src/assets/images/order/alert.svg b/web/src/assets/images/order/alert.svg new file mode 100644 index 00000000..18e9fd5c --- /dev/null +++ b/web/src/assets/images/order/alert.svg @@ -0,0 +1,13 @@ + + + 注意 + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/order/bg.png b/web/src/assets/images/order/bg.png new file mode 100644 index 00000000..da821814 Binary files /dev/null and b/web/src/assets/images/order/bg.png differ diff --git a/web/src/assets/images/order/biz.png b/web/src/assets/images/order/biz.png new file mode 100644 index 00000000..bacdb54e Binary files /dev/null and b/web/src/assets/images/order/biz.png differ diff --git a/web/src/assets/images/order/check.svg b/web/src/assets/images/order/check.svg new file mode 100644 index 00000000..7221eb31 --- /dev/null +++ b/web/src/assets/images/order/check.svg @@ -0,0 +1,15 @@ + + + 对号备份 + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/order/commerce.png b/web/src/assets/images/order/commerce.png new file mode 100644 index 00000000..14748ecb Binary files /dev/null and b/web/src/assets/images/order/commerce.png differ diff --git a/web/src/assets/images/order/corporate.svg b/web/src/assets/images/order/corporate.svg new file mode 100644 index 00000000..07b92a45 --- /dev/null +++ b/web/src/assets/images/order/corporate.svg @@ -0,0 +1,15 @@ + + + 企业_画板 39@2x + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/order/order.svg b/web/src/assets/images/order/order.svg new file mode 100644 index 00000000..5ddb6680 --- /dev/null +++ b/web/src/assets/images/order/order.svg @@ -0,0 +1,16 @@ + + + 订单 + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/order/order_hover.svg b/web/src/assets/images/order/order_hover.svg new file mode 100644 index 00000000..7e9c7b46 --- /dev/null +++ b/web/src/assets/images/order/order_hover.svg @@ -0,0 +1,16 @@ + + + 订单 + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/order/personal.png b/web/src/assets/images/order/personal.png new file mode 100644 index 00000000..1e64efd6 Binary files /dev/null and b/web/src/assets/images/order/personal.png differ diff --git a/web/src/assets/images/order/team.png b/web/src/assets/images/order/team.png new file mode 100644 index 00000000..2a059d73 Binary files /dev/null and b/web/src/assets/images/order/team.png differ diff --git a/web/src/assets/images/refresh.svg b/web/src/assets/images/refresh.svg index 7fb0dc34..c592feff 100644 --- a/web/src/assets/images/refresh.svg +++ b/web/src/assets/images/refresh.svg @@ -2,7 +2,7 @@ 刷新 - + diff --git a/web/src/assets/images/refresh_hover.svg b/web/src/assets/images/refresh_hover.svg new file mode 100644 index 00000000..1d4dcf7c --- /dev/null +++ b/web/src/assets/images/refresh_hover.svg @@ -0,0 +1,15 @@ + + + 刷新 + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/userMemory/detail_empty.png b/web/src/assets/images/userMemory/detail_empty.png new file mode 100644 index 00000000..67666235 Binary files /dev/null and b/web/src/assets/images/userMemory/detail_empty.png differ diff --git a/web/src/assets/images/workflow/agent_arbitration.png b/web/src/assets/images/workflow/agent_arbitration.png new file mode 100644 index 00000000..d555e3e2 Binary files /dev/null and b/web/src/assets/images/workflow/agent_arbitration.png differ diff --git a/web/src/assets/images/workflow/agent_collaboration.png b/web/src/assets/images/workflow/agent_collaboration.png new file mode 100644 index 00000000..7a92aecf Binary files /dev/null and b/web/src/assets/images/workflow/agent_collaboration.png differ diff --git a/web/src/assets/images/workflow/agent_scheduling.png b/web/src/assets/images/workflow/agent_scheduling.png new file mode 100644 index 00000000..97028422 Binary files /dev/null and b/web/src/assets/images/workflow/agent_scheduling.png differ diff --git a/web/src/assets/images/workflow/aggregator.png b/web/src/assets/images/workflow/aggregator.png new file mode 100644 index 00000000..6253733a Binary files /dev/null and b/web/src/assets/images/workflow/aggregator.png differ diff --git a/web/src/assets/images/workflow/answer.png b/web/src/assets/images/workflow/answer.png new file mode 100644 index 00000000..57f9c94d Binary files /dev/null and b/web/src/assets/images/workflow/answer.png differ diff --git a/web/src/assets/images/workflow/arrow.png b/web/src/assets/images/workflow/arrow.png new file mode 100644 index 00000000..67ce7b48 Binary files /dev/null and b/web/src/assets/images/workflow/arrow.png differ diff --git a/web/src/assets/images/workflow/classification.png b/web/src/assets/images/workflow/classification.png new file mode 100644 index 00000000..87d34bb8 Binary files /dev/null and b/web/src/assets/images/workflow/classification.png differ diff --git a/web/src/assets/images/workflow/code_execution.png b/web/src/assets/images/workflow/code_execution.png new file mode 100644 index 00000000..7f802b3c Binary files /dev/null and b/web/src/assets/images/workflow/code_execution.png differ diff --git a/web/src/assets/images/workflow/condition.png b/web/src/assets/images/workflow/condition.png new file mode 100644 index 00000000..a0bf9160 Binary files /dev/null and b/web/src/assets/images/workflow/condition.png differ diff --git a/web/src/assets/images/workflow/empty.png b/web/src/assets/images/workflow/empty.png new file mode 100644 index 00000000..58dd77b2 Binary files /dev/null and b/web/src/assets/images/workflow/empty.png differ diff --git a/web/src/assets/images/workflow/end.png b/web/src/assets/images/workflow/end.png new file mode 100644 index 00000000..7f4628c6 Binary files /dev/null and b/web/src/assets/images/workflow/end.png differ diff --git a/web/src/assets/images/workflow/http_request.png b/web/src/assets/images/workflow/http_request.png new file mode 100644 index 00000000..64e55d36 Binary files /dev/null and b/web/src/assets/images/workflow/http_request.png differ diff --git a/web/src/assets/images/workflow/iteration.png b/web/src/assets/images/workflow/iteration.png new file mode 100644 index 00000000..dd73767b Binary files /dev/null and b/web/src/assets/images/workflow/iteration.png differ diff --git a/web/src/assets/images/workflow/llm.png b/web/src/assets/images/workflow/llm.png new file mode 100644 index 00000000..5d9e7465 Binary files /dev/null and b/web/src/assets/images/workflow/llm.png differ diff --git a/web/src/assets/images/workflow/loop.png b/web/src/assets/images/workflow/loop.png new file mode 100644 index 00000000..a4313229 Binary files /dev/null and b/web/src/assets/images/workflow/loop.png differ diff --git a/web/src/assets/images/workflow/memory_enhancement.png b/web/src/assets/images/workflow/memory_enhancement.png new file mode 100644 index 00000000..998c02fe Binary files /dev/null and b/web/src/assets/images/workflow/memory_enhancement.png differ diff --git a/web/src/assets/images/workflow/model_selection.png b/web/src/assets/images/workflow/model_selection.png new file mode 100644 index 00000000..e3e93962 Binary files /dev/null and b/web/src/assets/images/workflow/model_selection.png differ diff --git a/web/src/assets/images/workflow/model_voting.png b/web/src/assets/images/workflow/model_voting.png new file mode 100644 index 00000000..8324541e Binary files /dev/null and b/web/src/assets/images/workflow/model_voting.png differ diff --git a/web/src/assets/images/workflow/output_audit.png b/web/src/assets/images/workflow/output_audit.png new file mode 100644 index 00000000..50128f82 Binary files /dev/null and b/web/src/assets/images/workflow/output_audit.png differ diff --git a/web/src/assets/images/workflow/parallel.png b/web/src/assets/images/workflow/parallel.png new file mode 100644 index 00000000..e77d79d8 Binary files /dev/null and b/web/src/assets/images/workflow/parallel.png differ diff --git a/web/src/assets/images/workflow/parameter_extraction.png b/web/src/assets/images/workflow/parameter_extraction.png new file mode 100644 index 00000000..d4b50ee0 Binary files /dev/null and b/web/src/assets/images/workflow/parameter_extraction.png differ diff --git a/web/src/assets/images/workflow/process_evolution.png b/web/src/assets/images/workflow/process_evolution.png new file mode 100644 index 00000000..8262c00d Binary files /dev/null and b/web/src/assets/images/workflow/process_evolution.png differ diff --git a/web/src/assets/images/workflow/rag.png b/web/src/assets/images/workflow/rag.png new file mode 100644 index 00000000..3749dbfa Binary files /dev/null and b/web/src/assets/images/workflow/rag.png differ diff --git a/web/src/assets/images/workflow/reasoning_control.png b/web/src/assets/images/workflow/reasoning_control.png new file mode 100644 index 00000000..649e165c Binary files /dev/null and b/web/src/assets/images/workflow/reasoning_control.png differ diff --git a/web/src/assets/images/workflow/robot-2-line@2x.png b/web/src/assets/images/workflow/robot-2-line@2x.png new file mode 100644 index 00000000..f1dc247e Binary files /dev/null and b/web/src/assets/images/workflow/robot-2-line@2x.png differ diff --git a/web/src/assets/images/workflow/self_optimization.png b/web/src/assets/images/workflow/self_optimization.png new file mode 100644 index 00000000..08ed8598 Binary files /dev/null and b/web/src/assets/images/workflow/self_optimization.png differ diff --git a/web/src/assets/images/workflow/self_reflection.png b/web/src/assets/images/workflow/self_reflection.png new file mode 100644 index 00000000..099aac60 Binary files /dev/null and b/web/src/assets/images/workflow/self_reflection.png differ diff --git a/web/src/assets/images/workflow/sensitive_detection.png b/web/src/assets/images/workflow/sensitive_detection.png new file mode 100644 index 00000000..637a4f13 Binary files /dev/null and b/web/src/assets/images/workflow/sensitive_detection.png differ diff --git a/web/src/assets/images/workflow/start.png b/web/src/assets/images/workflow/start.png new file mode 100644 index 00000000..f6828988 Binary files /dev/null and b/web/src/assets/images/workflow/start.png differ diff --git a/web/src/assets/images/workflow/task_planning.png b/web/src/assets/images/workflow/task_planning.png new file mode 100644 index 00000000..33f322fd Binary files /dev/null and b/web/src/assets/images/workflow/task_planning.png differ diff --git a/web/src/assets/images/workflow/template_rendering.png b/web/src/assets/images/workflow/template_rendering.png new file mode 100644 index 00000000..064caeb6 Binary files /dev/null and b/web/src/assets/images/workflow/template_rendering.png differ diff --git a/web/src/assets/images/workflow/tools.png b/web/src/assets/images/workflow/tools.png new file mode 100644 index 00000000..49ff2fa4 Binary files /dev/null and b/web/src/assets/images/workflow/tools.png differ diff --git a/web/src/components/ButtonCheckbox/index.tsx b/web/src/components/ButtonCheckbox/index.tsx index bff09c88..65813809 100644 --- a/web/src/components/ButtonCheckbox/index.tsx +++ b/web/src/components/ButtonCheckbox/index.tsx @@ -34,12 +34,12 @@ const ButtonCheckbox: FC = ({ } return ( -
- {icon && !checked && } - {checkedIcon && checked && } + {icon && !checked && } + {checkedIcon && checked && } {children}
); diff --git a/web/src/components/Chat/ChatContent.tsx b/web/src/components/Chat/ChatContent.tsx new file mode 100644 index 00000000..2067f57e --- /dev/null +++ b/web/src/components/Chat/ChatContent.tsx @@ -0,0 +1,84 @@ +/* + * @Author: ZhaoYing + * @Date: 2025-12-10 16:46:17 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2025-12-11 13:40:18 + */ +import { type FC, useRef, useEffect } from 'react' +import clsx from 'clsx' +import Markdown from '@/components/Markdown' +import type { ChatContentProps } from './types' + +/** + * 聊天内容显示组件 + * 负责渲染聊天消息列表,支持不同角色的消息样式和自动滚动 + */ +const ChatContent: FC = ({ + classNames, + contentClassNames, + data = [], + streamLoading = false, + empty, + labelPosition = 'bottom', + labelFormat, + errorDesc +}) => { + // 滚动容器引用,用于控制自动滚动到底部 + const scrollContainerRef = useRef<(HTMLDivElement | null)>(null) + + // 当数据变化时,自动滚动到底部显示最新消息 + useEffect(() => { + setTimeout(() => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight; + } + }, 0); + }, [data]) + return ( +
+ {data.length === 0 + ? empty // 显示空状态 + : data.map((item, index) => ( +
+ {/* 流式加载时且内容为空则不显示 */} + {streamLoading && item.content === '' + ? null + : <> + {/* 顶部标签(如时间戳、用户名等) */} + {labelPosition === 'top' && +
+ {labelFormat(item)} +
+ } + {/* 消息气泡框 */} +
+ {/* 使用Markdown组件渲染消息内容 */} + +
+ {/* 底部标签(如时间戳、用户名等) */} + {labelPosition === 'bottom' && +
+ {labelFormat(item)} +
+ } + + } +
+ )) + } +
+ ) +} + +export default ChatContent diff --git a/web/src/components/Chat/ChatInput.tsx b/web/src/components/Chat/ChatInput.tsx new file mode 100644 index 00000000..be9fc48d --- /dev/null +++ b/web/src/components/Chat/ChatInput.tsx @@ -0,0 +1,80 @@ +/* + * @Author: ZhaoYing + * @Date: 2025-12-10 16:46:14 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2025-12-20 15:38:40 + */ +import { useEffect } from 'react' +import { Flex, Input, Form } from 'antd' +import SendIcon from '@/assets/images/conversation/send.svg' +import SendDisabledIcon from '@/assets/images/conversation/sendDisabled.svg' +import LoadingIcon from '@/assets/images/conversation/loading.svg' +import type { ChatInputProps } from './types' + +/** + * 聊天输入框组件 + * 提供消息输入、发送功能,支持键盘快捷键和加载状态显示 + */ +const ChatInput = ({ message, onChange, onSend, loading, children }: ChatInputProps) => { + const [form] = Form.useForm() + // 监听表单值变化,用于控制发送按钮状态 + const values = Form.useWatch([], form); + + // 当外部message为空时,清空表单 + useEffect(() => { + if (!message) { + form.setFieldsValue({ + message: undefined, + }) + } + }, [form, message]) + + // 当加载状态时,清空输入框 + useEffect(() => { + if (loading) { + form.setFieldsValue({ + message: undefined, + }) + } + }, [loading]) + + return ( +
+ + {/* 消息输入表单 */} +
+ + onChange(e.target.value)} + onKeyDown={(e) => { + // Enter键发送,Shift+Enter换行 + if (e.key === 'Enter' && !e.shiftKey && (e.target as HTMLTextAreaElement).value?.trim() !== '' && !loading) { + e.preventDefault(); + onSend(); + } + }} + /> + +
+ + {/* 底部操作区域 */} + + {/* 子组件内容(如按钮等) */} + {children} + {/* 发送按钮 - 根据状态显示不同图标 */} + {loading + ? + : !values || !values?.message || values?.message?.trim() === '' + ? + : + } + +
+
+ ) +} + +export default ChatInput diff --git a/web/src/components/Chat/index.tsx b/web/src/components/Chat/index.tsx new file mode 100644 index 00000000..7db29bfc --- /dev/null +++ b/web/src/components/Chat/index.tsx @@ -0,0 +1,47 @@ +/* + * @Author: ZhaoYing + * @Date: 2025-12-10 16:46:09 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2025-12-11 13:43:51 + */ +import { type FC } from 'react' +import ChatInput from './ChatInput' +import type { ChatProps } from './types' +import ChatContent from './ChatContent' + +/** + * 聊天组件 - 主要组件,由内容区域和输入框组成 + * 提供完整的聊天界面功能,包括消息显示和输入交互 + */ +const Chat: FC = ({ + empty, + data, + onChange, + onSend, + streamLoading = false, + loading, + contentClassName = '', + children, + labelFormat, + errorDesc +}) => { + return ( +
+ {/* 聊天内容显示区域 */} + + + {/* 聊天输入框区域 */} + + {children} + +
+ ) +} +export default Chat diff --git a/web/src/components/Chat/types.ts b/web/src/components/Chat/types.ts new file mode 100644 index 00000000..851a8ccc --- /dev/null +++ b/web/src/components/Chat/types.ts @@ -0,0 +1,84 @@ +/* + * @Author: ZhaoYing + * @Date: 2025-12-10 16:45:54 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2025-12-11 13:43:52 + */ +import { type ReactNode } from 'react' + +/** + * 聊天消息项接口 + */ +export interface ChatItem { + /** 消息唯一标识 */ + id?: string; + /** 会话ID */ + conversation_id?: string | null; + /** 消息角色:用户或助手 */ + role?: 'user' | 'assistant'; + /** 消息内容 */ + content?: string | null; + /** 创建时间 */ + created_at?: number | string +} + +/** + * 聊天组件主要属性接口 + */ +export interface ChatProps { + /** 空状态显示内容 */ + empty?: ReactNode; + /** 聊天数据列表 */ + data: ChatItem[]; + /** 输入内容变化回调 */ + onChange: (message: string) => void; + /** 发送消息回调 */ + onSend: () => void; + /** 流式加载状态 */ + streamLoading?: boolean; + /** 加载状态 */ + loading: boolean; + /** 内容区域自定义样式类名 */ + contentClassName?: string; + /** 子组件内容 */ + children?: ReactNode; + /** 标签格式化函数 */ + labelFormat: (item: ChatItem) => any; + errorDesc?: string; +} + +/** + * 聊天输入框组件属性接口 + */ +export interface ChatInputProps { + /** 当前输入消息 */ + message?: string; + /** 输入内容变化回调 */ + onChange: (message: string) => void; + /** 发送消息回调 */ + onSend: () => void; + /** 加载状态 */ + loading: boolean; + /** 子组件内容 */ + children?: ReactNode; +} + +/** + * 聊天内容区域组件属性接口 + */ +export interface ChatContentProps { + /** 自定义样式类名 */ + classNames?: string | Record; + contentClassNames?: string | Record; + /** 聊天数据列表 */ + data: ChatItem[]; + /** 流式加载状态 */ + streamLoading: boolean; + /** 空状态显示内容 */ + empty?: ReactNode; + /** 标签位置:顶部或底部 */ + labelPosition?: 'top' | 'bottom'; + /** 标签格式化函数 */ + labelFormat: (item: ChatItem) => any; + errorDesc?: string; +} \ No newline at end of file diff --git a/web/src/components/CustomSelect/index.tsx b/web/src/components/CustomSelect/index.tsx index 411e36e0..97ca4e4b 100644 --- a/web/src/components/CustomSelect/index.tsx +++ b/web/src/components/CustomSelect/index.tsx @@ -9,7 +9,7 @@ interface ApiResponse { items?: T[]; } -interface CustomSelectProps { +interface CustomSelectProps extends Omit { url: string; params?: Record; valueKey?: string; diff --git a/web/src/components/Empty/index.tsx b/web/src/components/Empty/index.tsx index 53f52941..6cc8cda0 100644 --- a/web/src/components/Empty/index.tsx +++ b/web/src/components/Empty/index.tsx @@ -27,7 +27,7 @@ const Empty: FC = ({
404 {title &&
{title}
} - {curSubTitle &&
{subTitle}
} + {curSubTitle &&
{curSubTitle}
}
); } diff --git a/web/src/components/Header/index.tsx b/web/src/components/Header/index.tsx index 6ce5484e..9aeeab6b 100644 --- a/web/src/components/Header/index.tsx +++ b/web/src/components/Header/index.tsx @@ -3,6 +3,7 @@ import { Layout, Dropdown, Space, Breadcrumb } from 'antd'; import type { MenuProps, BreadcrumbProps } from 'antd'; import { UserOutlined, LogoutOutlined, SettingOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; +import { useLocation } from 'react-router-dom'; import { useUser } from '@/store/user'; import { useMenu } from '@/store/menu'; import styles from './index.module.css' @@ -12,12 +13,35 @@ const { Header } = Layout; const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => { const { t } = useTranslation(); + const location = useLocation(); const settingModalRef = useRef(null) const userInfoModalRef = useRef(null) const { user, logout } = useUser(); const { allBreadcrumbs } = useMenu(); - const breadcrumbs = allBreadcrumbs[source] || []; + + // 根据当前路由动态选择面包屑源 + const getBreadcrumbSource = () => { + const pathname = location.pathname; + + // 知识库列表页面使用默认的 space 面包屑 + if (pathname === '/knowledge-base') { + return 'space'; + } + + // 知识库详情相关页面使用独立的面包屑 + if (pathname.includes('/knowledge-base/') && pathname !== '/knowledge-base') { + return 'space-detail'; + } + + // 其他页面使用传入的 source + return source; + }; + + const breadcrumbSource = getBreadcrumbSource(); + const breadcrumbs = allBreadcrumbs[breadcrumbSource] || []; + + // 处理退出登录 const handleLogout = () => { diff --git a/web/src/components/Layout/AuthLayout.tsx b/web/src/components/Layout/AuthLayout.tsx index a969298d..94d28e11 100644 --- a/web/src/components/Layout/AuthLayout.tsx +++ b/web/src/components/Layout/AuthLayout.tsx @@ -13,7 +13,7 @@ const { Content } = Layout; // 认证布局组件,使用useRouteGuard hook进行路由鉴权 const AuthLayout: FC = () => { - const { getUserInfo } = useUser(); + const { getUserInfo, getStorageType } = useUser(); // 使用路由守卫hook处理认证和权限检查 useRouteGuard('manage'); // 自动更新面包屑导航 @@ -24,6 +24,7 @@ const AuthLayout: FC = () => { window.location.href = `/#/login`; } else { getUserInfo() + getStorageType() } }, []); diff --git a/web/src/components/Layout/BasicLayout.tsx b/web/src/components/Layout/BasicLayout.tsx index 76ab1690..6b3d2904 100644 --- a/web/src/components/Layout/BasicLayout.tsx +++ b/web/src/components/Layout/BasicLayout.tsx @@ -4,12 +4,13 @@ import { useUser } from '@/store/user'; // 基础布局组件,用于展示内容并保留用户信息获取功能 const BasicLayout: FC = () => { - const { getUserInfo } = useUser(); + const { getUserInfo, getStorageType } = useUser(); // 获取用户信息 useEffect(() => { getUserInfo(); - }, [getUserInfo]); + getStorageType() + }, [getUserInfo, getStorageType]); return (
diff --git a/web/src/components/Markdown/index.tsx b/web/src/components/Markdown/index.tsx index 12e4235d..fdaec143 100644 --- a/web/src/components/Markdown/index.tsx +++ b/web/src/components/Markdown/index.tsx @@ -1,4 +1,5 @@ -import { Image, Input, Select, Form, Checkbox, Radio, ColorPicker, DatePicker, TimePicker, InputNumber, Slider } from 'antd' +import { Image, Input, Select, Form, Checkbox, Radio, ColorPicker, DatePicker, TimePicker, InputNumber, Slider, Button } from 'antd' +import { EditOutlined, SaveOutlined, CloseOutlined } from '@ant-design/icons' import ReactMarkdown from 'react-markdown' import RemarkGfm from 'remark-gfm' import RemarkMath from 'remark-math' @@ -6,6 +7,7 @@ import RemarkBreaks from 'remark-breaks' import RehypeKatex from 'rehype-katex' import RehypeRaw from 'rehype-raw' import type { FC } from 'react' +import { useState, useRef, useEffect } from 'react' import Code from './Code' import VideoBlock from './VideoBlock' @@ -16,42 +18,45 @@ import RbButton from './RbButton' interface RbMarkdownProps { content: string; showHtmlComments?: boolean; // 是否显示 HTML 注释,默认为 false(隐藏) + editable?: boolean; // 是否可编辑,默认为 false + onContentChange?: (content: string) => void; // 内容变化回调 + onSave?: (content: string) => void; // 保存回调 } const components = { - h1: ({ children }: { children: string }) =>

{children}

, - h2: ({ children }: { children: string }) =>

{children}

, - h3: ({ children }: { children: string }) =>

{children}

, - h4: ({ children }: { children: string }) =>

{children}

, - h5: ({ children }: { children: string }) =>
{children}
, - h6: ({ children }: { children: string }) =>
{children}
, - ul: ({ children }: { children: string }) =>
    {children}
, - ol: ({ children }: { children: string }) =>
    {children}
, - li: ({ children }: { children: string }) =>
  • {children}
  • , - blockquote: ({ children }: { children: string }) =>
    {children}
    , - p: ({ children }: { children: string }) =>

    {children}

    , - strong: ({ children }: { children: string }) => {children}, - em: ({ children }: { children: string }) => {children}, - del: ({ children }: { children: string }) => {children}, - span: ({ children, ...props }: any) => { + h1: ({ children, ...props }: any) =>

    {children}

    , + h2: ({ children, ...props }: any) =>

    {children}

    , + h3: ({ children, ...props }: any) =>

    {children}

    , + h4: ({ children, ...props }: any) =>

    {children}

    , + h5: ({ children, ...props }: any) =>
    {children}
    , + h6: ({ children, ...props }: any) =>
    {children}
    , + ul: ({ children, ...props }: any) =>
      {children}
    , + ol: ({ children, ...props }: any) =>
      {children}
    , + li: ({ children, ...props }: any) =>
  • {children}
  • , + blockquote: ({ children, ...props }: any) =>
    {children}
    , + p: ({ children, ...props }: any) =>

    {children}

    , + strong: ({ children, ...props }: any) => {children}, + em: ({ children, ...props }: any) => {children}, + del: ({ children, ...props }: any) => {children}, + span: ({ children, style, ...restProps }: any) => { // 如果是 HTML 注释的 span,应用特殊样式 - if (props.style?.color === '#999') { + if (style?.color === '#999') { return {children} } - return {children} + return {children} }, - code: Code, - img: Image, - video: VideoBlock, - audio: AudioBlock, - a: Link, - button: RbButton, - table: ({ children }: { children: string }) => {children}
    , - tr: ({ children }: { children: string }) => {children}, - th: ({ children }: { children: string }) => {children}, - td: ({ children }: { children: string }) => {children}, - input: ({ children, ...props }: { children: string }) => { + code: ({ children, className, ...props }: any) => , + img: ({ src, alt, ...props }: any) => {alt}, + video: ({ src, ...props }: any) => , + audio: ({ src, ...props }: any) => , + a: ({ href, children, ...props }: any) => {children}, + button: ({ children, ...props }: any) => {[children]}, + table: ({ children, ...props }: any) => {children}
    , + tr: ({ children, ...props }: any) => {children}, + th: ({ children, ...props }: any) => {children}, + td: ({ children, ...props }: any) => {children}, + input: ({ children, ...props }: any) => { switch (props.type) { case 'color': return @@ -74,7 +79,7 @@ const components = { return case 'submit': case 'button': - return {props.value} + return {[props.value || children]} case 'checkbox': return {children} case 'password': @@ -85,37 +90,158 @@ const components = { return } }, - select: ({ children, ...props }: { children: string }) => , - textarea: ({ children, ...props }: { children: string }) => {children}, - form: ({ children }: { children: string }) =>
    {children}
    , + select: ({ children, ...props }: any) => , + textarea: ({ children, ...props }: any) => {children}, + form: ({ children, ...props }: any) =>
    {children}
    , } const RbMarkdown: FC = ({ content, showHtmlComments = false, + editable = false, + onContentChange, + onSave, }) => { + const [isEditing, setIsEditing] = useState(editable) // 如果可编辑,默认进入编辑模式 + const [editContent, setEditContent] = useState(content) + const textareaRef = useRef(null) + + // 当外部 content 变化时,同步更新编辑内容 + useEffect(() => { + setEditContent(content) + }, [content]) + + // 当editable变化时,自动切换编辑状态 + useEffect(() => { + if (editable) { + setIsEditing(true) + // 延迟聚焦,确保 textarea 已渲染 + setTimeout(() => { + textareaRef.current?.focus() + }, 100) + } + }, [editable]) + + // 进入编辑模式 + const handleEdit = () => { + setIsEditing(true) + setEditContent(content) + // 延迟聚焦,确保 textarea 已渲染 + setTimeout(() => { + textareaRef.current?.focus() + }, 100) + } + + // 保存编辑 + const handleSave = () => { + onContentChange?.(editContent) + onSave?.(editContent) + if (!editable) { + setIsEditing(false) // 只有在非强制编辑模式下才退出编辑 + } + } + + // 取消编辑 + const handleCancel = () => { + setEditContent(content) // 恢复原内容 + if (!editable) { + setIsEditing(false) // 只有在非强制编辑模式下才退出编辑 + } + } + + // 处理 textarea 内容变化 + const handleTextareaChange = (e: React.ChangeEvent) => { + const newContent = e.target.value + setEditContent(newContent) + // 实时回调内容变化 + onContentChange?.(newContent) + } + // 根据参数决定是否将 HTML 注释转换为可见文本 // 使用特殊的 markdown 语法来显示注释,避免被 rehype-raw 过滤 const processedContent = showHtmlComments - ? content.replace(//g, (_match, commentContent) => { + ? (isEditing ? editContent : content).replace(//g, (_match, commentContent) => { // 转换为带样式的文本,使用 标记 const escaped = commentContent.trim().replace(//g, '>') return `<!-- ${escaped} -->` }) - : content + : (isEditing ? editContent : content) + // 如果是编辑模式,显示 textarea + if (isEditing) { + return ( +
    + + + {/* 编辑工具栏 - 只在非强制编辑模式下显示 */} + {!editable && ( +
    + + +
    + )} + + {/* 编辑区域 */} + +
    + ) + } + + // 预览模式 return ( -
    +
    + + {/* 编辑按钮 - 只在非强制编辑模式且鼠标悬停时显示 */} + {!editable && ( +
    + +
    + )} + (({ renderItem, - query = {}, + query, url, column = 4, className = '', @@ -51,11 +51,11 @@ const PageScrollList = forwardRef(({ request.get(url, { page: page, pagesize: PAGE_SIZE, - ...query, + ...(query||{}), }) .then((res) => { const response = res as ApiResponse; - const results = Array.isArray(response.items) ? response.items : Array.isArray(response.hosts) ? response.hosts : Array.isArray(response) ? response : []; + const results = Array.isArray(response.items) ? response.items : Array.isArray(response) ? response : []; if (flag) { setData(results); } else { diff --git a/web/src/components/RadioGroupCard/index.tsx b/web/src/components/RadioGroupCard/index.tsx index ac0c10ac..aa68852c 100644 --- a/web/src/components/RadioGroupCard/index.tsx +++ b/web/src/components/RadioGroupCard/index.tsx @@ -15,6 +15,7 @@ interface RadioCardProps extends Omit { onValueChange?: (value: string | null | undefined, option?: RadioCardOption) => void; onChange?: (value: string | null | undefined, option?: RadioCardOption) => void; itemRender?: (option: RadioCardOption) => ReactNode; + allowClear?: boolean; } const RadioGroupCard: FC = ({ @@ -22,7 +23,8 @@ const RadioGroupCard: FC = ({ value, onValueChange, onChange, - itemRender + itemRender, + allowClear = true }) => { // 监听value变化 useEffect(() => { @@ -34,23 +36,27 @@ const RadioGroupCard: FC = ({ const handleChange = (option: RadioCardOption) => { if (option.disabled) return if (onChange) { - onChange(value === option.value ? null : String(option.value), value === option.value ? undefined : option); + if (allowClear && value === option.value) { + onChange(null, undefined); + } else { + onChange(String(option.value), option); + } } } return ( -
    +
    {options.map(option => ( -
    handleChange(option)}> {itemRender ? itemRender(option) : ( <> - {option.icon && } + {option.icon && }
    {option.label}
    -
    {option.labelDesc}
    +
    {option.labelDesc}
    )}
    diff --git a/web/src/components/RbAlert/index.tsx b/web/src/components/RbAlert/index.tsx index a92c34b5..68a88cd0 100644 --- a/web/src/components/RbAlert/index.tsx +++ b/web/src/components/RbAlert/index.tsx @@ -3,7 +3,7 @@ import { type FC, type ReactNode } from 'react' interface RbAlertProps { color?: 'blue' | 'green' | 'orange' | 'purple', children: ReactNode | string; - icon: ReactNode; + icon?: ReactNode; className?: string; } @@ -16,8 +16,8 @@ const colors = { const RbAlert: FC = ({ color = 'blue', icon, className, children }) => { return ( -
    - {icon && {icon}} +
    + {icon && {icon}} {children}
    ) diff --git a/web/src/components/RbCard/Card.tsx b/web/src/components/RbCard/Card.tsx index beb237cd..f86b1c60 100644 --- a/web/src/components/RbCard/Card.tsx +++ b/web/src/components/RbCard/Card.tsx @@ -52,7 +52,7 @@ const RbCard: FC = ({ title={typeof title === 'function' ? title() : title ?
    {avatarUrl - ? + ? : avatar ? avatar : null }
    = ({ onOk, onCancel, children, + className, ...props }) => { const { t } = useTranslation() @@ -16,9 +24,11 @@ const RbModal: FC = ({ cancelText={t('common.cancel')} onOk={onOk} destroyOnHidden={true} + className={`rb-modal ${className || ''}`} + maskClosable={false} {...props} > -
    +
    {children}
    diff --git a/web/src/components/SiderMenu/index.tsx b/web/src/components/SiderMenu/index.tsx index d148a346..a39a0b4e 100644 --- a/web/src/components/SiderMenu/index.tsx +++ b/web/src/components/SiderMenu/index.tsx @@ -21,11 +21,11 @@ import modelActiveIcon from '@/assets/images/menu/model_active.svg'; import memoryIcon from '@/assets/images/menu/memory.svg'; import memoryActiveIcon from '@/assets/images/menu/memory_active.svg'; import spaceIcon from '@/assets/images/menu/space.svg'; -import spaceActiveIcon from '@/assets/images/menu/space_acitve.svg'; +import spaceActiveIcon from '@/assets/images/menu/space_active.svg'; import userIcon from '@/assets/images/menu/user.svg'; import userActiveIcon from '@/assets/images/menu/user_active.svg'; import userMemoryIcon from '@/assets/images/menu/userMemory.svg'; -import userMemoryActiveIcon from '@/assets/images/menu/userMemory_acitve.svg'; +import userMemoryActiveIcon from '@/assets/images/menu/userMemory_active.svg'; import applicationIcon from '@/assets/images/menu/application.svg'; import applicationActiveIcon from '@/assets/images/menu/application_active.svg'; import knowledgeIcon from '@/assets/images/menu/knowledge.svg'; @@ -34,6 +34,12 @@ import memoryConversationIcon from '@/assets/images/menu/memoryConversation.svg' import memoryConversationActiveIcon from '@/assets/images/menu/memoryConversation_active.svg'; import memberIcon from '@/assets/images/menu/member.svg'; import memberActiveIcon from '@/assets/images/menu/member_active.svg'; +import toolIcon from '@/assets/images/menu/tool.png'; +import toolActiveIcon from '@/assets/images/menu/tool_active.png'; +import apiKeyIcon from '@/assets/images/menu/apiKey.png'; +import apiKeyActiveIcon from '@/assets/images/menu/apiKey_active.png'; +import pricingIcon from '@/assets/images/menu/pricing.svg' +import pricingActiveIcon from '@/assets/images/menu/pricing_active.svg' // 图标路径映射表 const iconPathMap: Record = { @@ -57,6 +63,12 @@ const iconPathMap: Record = { 'memoryConversationActive': memoryConversationActiveIcon, 'member': memberIcon, 'memberActive': memberActiveIcon, + 'tool': toolIcon, + 'toolActive': toolActiveIcon, + 'apiKey': apiKeyIcon, + 'apiKeyActive': apiKeyActiveIcon, + 'pricing': pricingIcon, + 'pricingActive': pricingActiveIcon }; const { Sider } = Layout; diff --git a/web/src/components/Table/index.tsx b/web/src/components/Table/index.tsx index 6524d1a8..62c68dc3 100644 --- a/web/src/components/Table/index.tsx +++ b/web/src/components/Table/index.tsx @@ -19,6 +19,7 @@ interface TableComponentProps extends Omit { isScroll?: boolean; scrollX?: number | string | true; // 支持自定义横向滚动宽度 scrollY?: number | string; // 支持自定义纵向滚动高度 + currentPageKey?: string; } export interface TableRef { loadData: () => void; @@ -48,6 +49,7 @@ const TableComponent = forwardRef(({ isScroll = false, scrollX, scrollY, + currentPageKey = 'page', ...props }, ref) => { const { t } = useTranslation(); @@ -86,7 +88,7 @@ const TableComponent = forwardRef(({ ...currentPagination, ...pageData, }) - params = {...params, ...pageData} + params = { ...params, ...pageData, [currentPageKey]: pageData.page} } setLoading(true) // 构建查询参数并调用API @@ -95,7 +97,7 @@ const TableComponent = forwardRef(({ // 支持两种响应格式:直接返回 total 或在 page 对象中返回 const totalCount = res.page?.total ?? res.total ?? 0; setTotal(totalCount) - setData(Array.isArray(res.items) ? res.items : Array.isArray(res.hosts) ? res.hosts : res || []) + setData(Array.isArray(res.items) ? res.items : Array.isArray(res.hosts) ? res.hosts : Array.isArray(res.list) ? res.list : res || []) setLoading(false) }) .catch(err => { diff --git a/web/src/components/Tag/index.tsx b/web/src/components/Tag/index.tsx index dd929dca..5113c829 100644 --- a/web/src/components/Tag/index.tsx +++ b/web/src/components/Tag/index.tsx @@ -1,6 +1,6 @@ import { type FC, type ReactNode } from 'react' -interface TagProps { +export interface TagProps { color?: 'processing' | 'error' | 'success' | 'warning' | 'default', children: ReactNode; className?: string; @@ -16,7 +16,7 @@ const colors = { const Tag: FC = ({ color = 'processing', children, className }) => { return ( - + {children} ) diff --git a/web/src/components/Upload/UploadFiles.tsx b/web/src/components/Upload/UploadFiles.tsx index aedd0e17..a725278c 100644 --- a/web/src/components/Upload/UploadFiles.tsx +++ b/web/src/components/Upload/UploadFiles.tsx @@ -38,6 +38,8 @@ interface UploadFilesProps extends Omit { maxCount?: number; /** 是否支持拖拽上传,默认为false */ isCanDrag?: boolean; + /** 自定义移除文件回调 */ + onRemove?: (file: UploadFile) => boolean | void | Promise; } const ALL_FILE_TYPE: { [key: string]: string; @@ -77,6 +79,7 @@ const UploadFiles = forwardRef(({ isAutoUpload = true, maxCount = 1, isCanDrag = false, + onRemove: customOnRemove, ...props }, ref) => { const { t } = useTranslation(); @@ -86,11 +89,20 @@ const UploadFiles = forwardRef(({ // 处理文件移除 const handleRemove = (file: UploadFile) => { + // 如果有自定义的 onRemove 回调,先执行它 + if (customOnRemove) { + const result = customOnRemove(file); + // 如果返回 false,阻止移除 + if (result === false) { + return false; + } + } + confirm({ - title: '确定要删除此文件吗?', - okText: '确定', + title: `${t('common.confirmRemoveFile')}`, + okText: `${t('common.confirm')}`, okType: 'danger', - cancelText: '取消', + cancelText: `${t('common.cancel')}`, onOk: () => { const newFileList = fileList.filter((item) => item.uid !== file.uid); setFileList(newFileList); @@ -236,7 +248,7 @@ const UploadFiles = forwardRef(({
    {file.name} - actions?.remove()}>Cancel + actions?.remove()}>Cancel
    diff --git a/web/src/hooks/useBreadcrumbManager.ts b/web/src/hooks/useBreadcrumbManager.ts new file mode 100644 index 00000000..54095925 --- /dev/null +++ b/web/src/hooks/useBreadcrumbManager.ts @@ -0,0 +1,319 @@ +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useMenu } from '@/store/menu'; +import type { MenuItem } from '@/store/menu'; + +export interface BreadcrumbItem { + id: string; + name: string; + type?: 'knowledgeBase' | 'folder' | 'document'; +} + +export interface BreadcrumbPath { + knowledgeBaseFolderPath: BreadcrumbItem[]; // 知识库文件夹路径 + knowledgeBase?: BreadcrumbItem; // 知识库信息 + documentFolderPath: BreadcrumbItem[]; // 文档文件夹路径 + document?: BreadcrumbItem; // 文档信息 +} + +export interface BreadcrumbOptions { + onKnowledgeBaseMenuClick?: () => void; + onKnowledgeBaseFolderClick?: (folderId: string, folderPath: BreadcrumbItem[]) => void; + // 新增:区分面包屑类型 + breadcrumbType?: 'list' | 'detail'; +} + +export const useBreadcrumbManager = (options?: BreadcrumbOptions) => { + const { allBreadcrumbs, setCustomBreadcrumbs } = useMenu(); + const navigate = useNavigate(); + + const updateBreadcrumbs = useCallback((breadcrumbPath: BreadcrumbPath) => { + const breadcrumbType = options?.breadcrumbType || 'list'; + + // 对于详情页面,直接使用固定的知识库管理面包屑,不依赖可能被污染的 allBreadcrumbs + let baseBreadcrumbs: MenuItem[] = []; + + if (breadcrumbType === 'detail') { + // 详情页面:始终使用固定的知识库管理面包屑 + baseBreadcrumbs = [ + { + id: 6, + parent: 0, + code: 'knowledge', + label: '知识库', + i18nKey: 'menu.knowledgeManagement', + path: '/knowledge-base', + enable: true, + display: true, + level: 1, + sort: 0, + icon: null, + iconActive: null, + menuDesc: null, + deleted: null, + updateTime: 0, + new_: null, + keepAlive: false, + master: null, + disposable: false, + appSystem: null, + subs: [], + } + ]; + } else { + // 列表页面:从 space 获取基础面包屑,但确保包含知识库管理 + const spaceBreadcrumbs = allBreadcrumbs['space'] || []; + const knowledgeBaseMenuIndex = spaceBreadcrumbs.findIndex(item => item.path === '/knowledge-base'); + + if (knowledgeBaseMenuIndex >= 0) { + baseBreadcrumbs = spaceBreadcrumbs.slice(0, knowledgeBaseMenuIndex + 1); + } else { + // 如果没有找到知识库菜单,使用默认的知识库管理面包屑 + baseBreadcrumbs = [ + { + id: 6, + parent: 0, + code: 'knowledge', + label: '知识库', + i18nKey: 'menu.knowledgeManagement', + path: '/knowledge-base', + enable: true, + display: true, + level: 1, + sort: 0, + icon: null, + iconActive: null, + menuDesc: null, + deleted: null, + updateTime: 0, + new_: null, + keepAlive: false, + master: null, + disposable: false, + appSystem: null, + subs: [], + } + ]; + } + } + + const filteredBaseBreadcrumbs = baseBreadcrumbs; + + // 给"知识库管理"添加点击事件 + const breadcrumbsWithClick = filteredBaseBreadcrumbs.map((item) => { + if (item.path === '/knowledge-base') { + return { + ...item, + onClick: (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + + if (options?.onKnowledgeBaseMenuClick) { + // 如果提供了回调函数,执行回调 + options.onKnowledgeBaseMenuClick(); + } else if (breadcrumbType === 'detail') { + // 知识库详情页面:没有回调函数时,返回到知识库列表页面 + navigate('/knowledge-base', { + state: { + resetToRoot: true, + } + }); + } + return false; + }, + }; + } + return item; + }); + + let customBreadcrumbs: MenuItem[] = [...breadcrumbsWithClick]; + + if (breadcrumbType === 'list') { + // 知识库列表页面:只显示知识库文件夹路径 + customBreadcrumbs = [ + ...breadcrumbsWithClick, + ...breadcrumbPath.knowledgeBaseFolderPath.map((folder, index) => ({ + id: 0, + parent: 0, + code: null, + label: folder.name, + i18nKey: null, + path: null, + enable: true, + display: true, + level: 0, + sort: 0, + icon: null, + iconActive: null, + menuDesc: null, + deleted: null, + updateTime: 0, + new_: null, + keepAlive: false, + master: null, + disposable: false, + appSystem: null, + subs: [], + onClick: (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + + // 如果有回调函数,直接调用回调函数来更新状态 + if (options?.onKnowledgeBaseFolderClick) { + options.onKnowledgeBaseFolderClick(folder.id, breadcrumbPath.knowledgeBaseFolderPath.slice(0, index + 1)); + } else { + // 否则使用导航(兜底逻辑) + navigate('/knowledge-base', { + state: { + navigateToFolder: folder.id, + folderPath: breadcrumbPath.knowledgeBaseFolderPath.slice(0, index + 1) + } + }); + } + return false; + }, + })), + ]; + } else { + // 知识库详情页面:显示知识库名称 + 文档文件夹路径 + 文档名称 + customBreadcrumbs = [ + ...breadcrumbsWithClick, + + // 添加知识库名称 + ...(breadcrumbPath.knowledgeBase ? [{ + id: 0, + parent: 0, + code: null, + label: breadcrumbPath.knowledgeBase.name, + i18nKey: null, + path: null, + enable: true, + display: true, + level: 0, + sort: 0, + icon: null, + iconActive: null, + menuDesc: null, + deleted: null, + updateTime: 0, + new_: null, + keepAlive: false, + master: null, + disposable: false, + appSystem: null, + subs: [], + onClick: (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + // 返回到知识库详情页的根目录 + const navigationState = { + fromKnowledgeBaseList: true, + knowledgeBaseFolderPath: breadcrumbPath.knowledgeBaseFolderPath, + resetToRoot: true, // 添加重置到根目录的标志 + refresh: true, // 添加刷新标志 + timestamp: Date.now(), // 添加时间戳确保状态变化 + }; + + // 使用当前页面路径进行导航,避免不必要的路由变化 + const currentPath = window.location.pathname; + const targetPath = `/knowledge-base/${breadcrumbPath.knowledgeBase!.id}/private`; + + if (currentPath === targetPath) { + // 如果已经在目标页面,直接更新状态而不导航 + navigate(targetPath, { + state: navigationState, + replace: true // 使用 replace 避免历史记录堆积 + }); + } else { + // 如果不在目标页面,正常导航 + navigate(targetPath, { + state: navigationState + }); + } + return false; + }, + }] : []), + + // 添加文档文件夹路径 + ...breadcrumbPath.documentFolderPath.map((folder, index) => ({ + id: 0, + parent: 0, + code: null, + label: folder.name, + i18nKey: null, + path: null, + enable: true, + display: true, + level: 0, + sort: 0, + icon: null, + iconActive: null, + menuDesc: null, + deleted: null, + updateTime: 0, + new_: null, + keepAlive: false, + master: null, + disposable: false, + appSystem: null, + subs: [], + onClick: (e?: React.MouseEvent) => { + e?.preventDefault(); + e?.stopPropagation(); + // 返回到知识库详情页的对应文件夹 + const navigationState = { + fromKnowledgeBaseList: true, + knowledgeBaseFolderPath: breadcrumbPath.knowledgeBaseFolderPath, + navigateToDocumentFolder: folder.id, + documentFolderPath: breadcrumbPath.documentFolderPath.slice(0, index + 1), + refresh: true, // 添加刷新标志 + timestamp: Date.now(), // 添加时间戳确保状态变化 + }; + navigate(`/knowledge-base/${breadcrumbPath.knowledgeBase!.id}/private`, { + state: navigationState, + replace: true // 使用 replace 避免历史记录堆积 + }); + return false; + }, + })), + + // 添加文档名称(如果存在) + ...(breadcrumbPath.document ? [{ + id: 0, + parent: 0, + code: null, + label: breadcrumbPath.document.name, + i18nKey: null, + path: null, + enable: true, + display: true, + level: 0, + sort: 0, + icon: null, + iconActive: null, + menuDesc: null, + deleted: null, + updateTime: 0, + new_: null, + keepAlive: false, + master: null, + disposable: false, + appSystem: null, + subs: [], + // 文档名称不可点击 + }] : []), + ]; + } + + // 根据面包屑类型使用不同的键,实现独立的面包屑路径 + const breadcrumbKey = breadcrumbType === 'list' ? 'space' : 'space-detail'; + + + + setCustomBreadcrumbs(customBreadcrumbs, breadcrumbKey); + }, [setCustomBreadcrumbs, navigate, options?.breadcrumbType, options?.onKnowledgeBaseMenuClick, options?.onKnowledgeBaseFolderClick]); + + return { + updateBreadcrumbs, + }; +}; \ No newline at end of file diff --git a/web/src/hooks/useRouteGuard.ts b/web/src/hooks/useRouteGuard.ts index 23c31ec5..0bf6d485 100644 --- a/web/src/hooks/useRouteGuard.ts +++ b/web/src/hooks/useRouteGuard.ts @@ -11,8 +11,10 @@ export const checkAuthStatus = (): boolean => { // 递归检查路由是否存在于菜单数据中 export const checkRoutePermission = (menus: MenuItem[], currentPath: string): boolean => { - // 首页默认有权限 - if (currentPath === '/' || currentPath.includes('knowledge-detail')) return true; + // 首页和知识库相关页面默认有权限 + if (currentPath === '/' || currentPath.includes('knowledge-detail') || currentPath.includes('knowledge-base')) { + return true; + } for (const menu of menus) { // 检查当前菜单的path是否匹配 @@ -26,6 +28,7 @@ export const checkRoutePermission = (menus: MenuItem[], currentPath: string): bo } } } + return false; }; @@ -52,7 +55,7 @@ export const useRouteGuard = (source: 'space' | 'manage') => { const hasPermission = checkRoutePermission(menus, location.pathname); if (!hasPermission) { // 无权限访问该路由,重定向到无权限页面 - // navigate('/not-found', { replace: true }); + // navigate('/no-permission', { replace: true }); } } }, [navigate, location.pathname, location.search, location.hash, menus]); diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 41347c66..ee6400f0 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -21,7 +21,7 @@ export const en = { userMemory: 'User Memory', memberManagement: 'Member Management', memorySummary: 'Memory Summary', - memoryConversation: 'Memory Verification', + memoryConversation: 'Memory Validation', memorySummaryHandlers: 'Memory Summary Handlers', createMemorySummary: 'Create Memory Summary', memoryManagement: 'Memory Management', @@ -33,6 +33,14 @@ export const en = { knowledgeCreateDataset: 'Create Dataset', knowledgeDocumentDetails: 'Document Details', userMemoryDetail: 'UserMemory Detail', + apiKeyManagement: 'API KEY Management', + toolManagement: 'Tool Management', + emotionEngine: 'Emotion Engine', + statementDetail: 'Emotion Memory', + selfReflectionEngine: 'Self Reflection Engine', + pricing: 'Pricing Management', + orderPayment: 'Order Payment', + orderHistory: 'Order History', }, dashboard: { totalMemoryCapacity: 'Total Memory Capacity', @@ -57,13 +65,13 @@ export const en = { forgettingExecutionRate: 'Forgetting Execution Rate', memoryClassificationDistribution: 'Memory classification distribution', - knowledgeBaseTypeDistribution: 'Distribution of knowledge base types', - memoryGrowthTrend: 'Memory growth trend', + knowledgeBaseTypeDistribution: 'Distribution of Knowledge Base Types', + memoryGrowthTrend: 'Memory Growth Trend', corporateMemory: 'Corporate Memory', - recentMemoryActivities: 'Recent memory activities', + recentMemoryActivities: 'Recent Memory Activities', apiCallTrend: 'API call trend', quickOperation: 'Quick Operation', - popularMemoryTags: 'Popular memory tags', + popularMemoryTags: 'Popular Memory Tags', title: 'Real-time Monitoring of Your AI Memory Core and Agent Status', loading: 'Loading...', @@ -115,9 +123,9 @@ export const en = { statements_count_desc: 'Manage {{count}} knowledge statements', triplet_count: 'Entity Relation Extraction', triplet_count_desc: 'Build {{entities_count}} entity nodes and {{relations_count}} relation connections', - temporal_count: 'Time extraction', + temporal_count: 'Time Extraction', temporal_count_desc: 'Record {{count}} time series information', - + dialogue: 'Dialogue', chunk: 'Chunk', statement: 'Statement', @@ -262,6 +270,7 @@ export const en = { exportList: 'Export List', selectPlaceholder: 'Please select {{title}}', inputPlaceholder: 'Please enter {{title}}', + enterPlaceholder: 'Enter {{title}}', saveSuccess: 'Save Success', saveFailure: 'Save Failure', pleaseSelect: 'Please select', @@ -287,9 +296,11 @@ export const en = { add: 'Add', addOption: 'Add Option', viewDetail: 'View Detail', + confirmRemoveFile: 'Are you sure you want to remove this file?', deleteSuccess: 'Delete successfully', - foldUp: 'Fold Up', - expanded: 'Expanded', + deleteFailed: 'Delete Failure', + foldUp: 'Collapse', + expanded: 'Expand', clickUploadIcon: 'click on the upload icon', export: 'Export', active: 'Active', @@ -320,8 +331,8 @@ export const en = { publicApiCannotRefreshToken: 'Public API cannot refresh token', refreshTokenNotExist: 'Refresh token does not exist', reset: 'Reset', - statusEnabled: 'Enabled', - statusDisabled: 'Disabled', + refresh: 'Refresh', + return: 'Return', }, model: { searchPlaceholder: 'search model…', @@ -329,7 +340,7 @@ export const en = { provider: 'Provider', status: 'Status', created: 'Created', - configureBtn: 'Click to Configure', + configureBtn: 'Run Configuration', name: 'Name', displayName: 'Display Name', nameRequired: 'Please enter model name', @@ -401,11 +412,19 @@ export const en = { saveConfig: 'Save Config', apiKeyName: 'API Key Name', + + llm: 'LLM', + chat: 'Chat', + embedding: 'Embedding', + rerank: 'Rerank', + openai: "Openai", + dashscope: "Dashscope", + ollama: "Ollama", + xinference: "Xinference", + gpustack: "Gpustack", + bedrock: "Bedrock" }, knowledgeBase: { - home: 'Home', - selectSpace: 'Please select a workspace.', - preview:'Preview', pleaseUploadFileFirst: 'Please upload file first', shareSuccess: 'Share successfully', shareFailed: 'Share failed', @@ -439,7 +458,7 @@ export const en = { recallTestDescription:'Input test questions to evaluate the recall effectiveness and relevance of the knowledge base', similarityThreshold: 'Similarity Threshold', startTesting: 'Start Testing', - semanticSimilarity: 'Semantic similarity', + semanticSimilarity: 'Semantic Similarity', recallResult: 'Result', setting: 'Setting', similarity: 'Similarity', @@ -456,7 +475,7 @@ export const en = { knowledgeBase: 'Knowledge Base', selectDataSource: 'Select Source', localFile: 'Local File', - uploadFileTypes: 'Upload PDF, TXT, DOCX and other format files', + uploadFileTypes: 'Upload PDF, TXT, DOCX, IMAGE, MEDIA and other format files', webLink: 'Web Link', webLinkPlaceholder:'Please enter', webLinkDesc: 'Only static links are supported. If the uploaded data shows as empty, the link may not be readable. One per line, with a maximum of {{count}} links at a time', @@ -464,6 +483,7 @@ export const en = { readStaticWebPage: 'Read static web page content as dataset', customText: 'Custom Text', customContent: 'Custom Content', + createContentError: 'Failed to create custom content', manuallyInputText: 'Manually input a text as dataset', importTemplate: 'Template Import', importBackup: 'Backup Import', @@ -479,11 +499,19 @@ export const en = { noDataSets: 'No datasets yet, click the button below or drag files to create.', createEmptyDataSet: '+ Empty Dataset', createImageDataSet: '+ Image Dataset', + createContent: 'Create Content', + title: 'Title', + content: 'Content', + pleaseEnterTitle: 'Please enter title', + pleaseEnterContent: 'Please enter content', + // createImageDataSet: '+ Image Dataset', dragFilesHere: 'Drag files here to upload', createImport: 'Create/Import', textDataSet: 'Text Dataset', imageDataSet: 'Image Dataset', + mediaDataSet: 'Media Dataset', blankDataset: 'Blank Dataset', + customTextDataset: 'Custom Text Dataset', text: 'Text', search: 'Search', image: 'Image', @@ -494,7 +522,7 @@ export const en = { delete: 'Delete', rechunking: 'Rechunking', download: 'Download', - selectSource:'Please select the source', + selectSource:'Please select a source', confirmDelete: 'Are you sure you want to delete this document?', knowledgeBaseSettings: 'Knowledge Base Settings', modelConfiguration: 'Model Configuration', @@ -558,6 +586,7 @@ export const en = { datasetName: 'Dataset Name', pleaseEnterDatasetName: 'Please enter dataset name', uploadImages: 'Upload Images', + uploadMedia: 'Upload Media files', pleaseUploadImages: 'Please upload images', embedding_id: 'Embedding', llm_id: 'LLM', @@ -578,7 +607,6 @@ export const en = { insertContent: 'Insert Content', editContent:'Edit Content', insertContentPlaceholder: 'Please enter the content', - pleaseEnterContent: 'Please enter content', documentIdRequired: 'Document ID is required', editContentDesc:'Edit content', insertContentDesc:'Insert content', @@ -591,6 +619,17 @@ export const en = { semantic:'Semantic', hybrid:'Hybrid', updateEmbeddingContent:'Are you sure about updating the embedding model? After the update, will the block vector data need to be reconstructed?', + question: 'Question', + answer: 'Answer', + normalMode: 'Normal Mode', + qaMode: 'QA Mode', + fileParsingSettings: 'File Parsing Settings', + pdfEnhancementAnalysis: 'PDF Enhancement Analysis', + fileSizeExceeds: 'File size exceeds the limit', + sizeLimitError: 'The file size exceeds the limit. The maximum supported size is 256MB. The current file size is', + fileDurationExceeds: 'File duration exceeds the limit', + fileDurationLimitError: 'The duration of the media file exceeds the limit. The maximum supported duration is 150 seconds. Current duration', + unableReadFile:'Unable to read the information of the media file. Please check the file format.', createForm:{ name: 'Name', embedding_id: 'Embedding', @@ -642,6 +681,8 @@ export const en = { active: 'Active', inactive: 'Inactive', configurationName: 'Configuration Name', + emotionEngine: 'Emotion Engine', + reflectionEngine: 'Self-Reflection Engine' }, member: { username: 'Username', @@ -649,16 +690,14 @@ export const en = { role: 'Role', lastLoginTime: 'Last Login Time', editMember: 'Edit Member', - createMember: 'Create Member', + createMember: 'Add Member', email: 'Email', - inviteToMember: 'Invite to Member', + inviteToMember: 'Member Role', member: 'Member', memberDesc: 'Can only use the application, cannot create the application', - admin: 'Admin', - adminDesc: 'Can create applications and manage team settings', sendInvitation: 'Send Invitation', manager: 'Admin', - managerDesc: 'Can create applications and manage team settings', + managerDesc: 'Can access applications, but cannot create or manage them', inviteLinkDesc: 'Invite link 【{{inviteLink}}】, please copy and send to the member', inviteLinkTip: 'Please copy the invite link and send it to the user to complete the invitation', }, @@ -713,6 +752,14 @@ export const en = { quicklyForget: 'Quickly forget', slowForgetting: 'Slow forgetting', currentConfig: 'Current Config', + + decay_constant: 'Decay Constant', + max_history_length: 'Max Access History Length', + forgetting_threshold: 'Forgetting Threshold', + min_days_since_access: 'Minimum Days Since Access', + enable_llm_summary: 'Enable LLM Summary Generation', + max_merge_batch_size: 'Max Merge Batch Size', + forgetting_interval_hours: 'Forgetting Interval Hours' }, application: { searchPlaceholder: 'Search for applications or clusters', @@ -735,10 +782,10 @@ export const en = { workflowDesc: 'To be opened, please stay tuned', editApplication: 'Edit Application Info', - + currentModel: 'Current Model', modelConfig: 'Model Config', - parameterConfig: 'Parameter Config', + parameterConfig: 'Parameter Configuration', apply: 'Apply', resetDefault: 'Reset Default', @@ -782,7 +829,7 @@ export const en = { promptConfiguration: 'Prompt Configuration', configurationDesc: 'Define the role, capabilities, and behavioral guidelines of the Agent', aiPrompt: 'AI Prompt', - promptPlaceholder: 'You are a professional AI assistant, and your responsibilities are ..', + promptPlaceholder: 'You are a professional AI assistant, and your responsibility is to help users solve problems.', knowledgeBaseAssociation: 'Knowledge base association', associatedKnowledgeBase: 'Associated Knowledge Base', addKnowledgeBase: 'Add Knowledge Base', @@ -866,8 +913,6 @@ export const en = { toolCalling: 'Tool Calling', toolCallingDesc: 'The main control agent calls sub agents as tools', toolCallingFeature: 'Centralized control, suitable for structured workflow', - handoffs: 'Handoffs', - handoffsDesc: 'Agents between dynamic transfer of control rights', handoffsFeature: 'Decentralized control, suitable for complex conversation scenarios', recommend: 'Recommend', advanced: 'Advanced', @@ -895,7 +940,7 @@ export const en = { frequency_penalty_desc: 'Frequency penalty', presence_penalty: 'Presence Penalty', presence_penalty_desc: 'Presence Penalty', - n: 'Number of replies generated (n)', + n: 'Number of Replies Generated (n)', n_desc: 'Number of replies generated', contains: 'Contains {{include_count}} documents', @@ -908,7 +953,7 @@ export const en = { versionNumber: 'Version Number', versionNumberTip: 'Version number format: v[major version number].[next version number].[revision number] (e.g. v1.3.0)', versionDescription: 'Version Description', - versionDescriptionTip: 'Suggest explaining the feature updates, bug fixes, and optimization items for this release', + versionDescriptionTip: 'Please describe the feature updates, bug fixes, and optimizations included in this release.', releasePreview: 'Release Preview', globalConfig: 'Global Config', globalConfigDesc: 'The global configuration will be applied to all associated knowledge bases as the default configuration. The configuration of a single knowledge base will override the global configuration.', @@ -929,7 +974,7 @@ export const en = { similarity_threshold: 'Semantic similarity threshold', similarity_threshold_desc: 'Only return results with semantic similarity higher than this threshold', similarity_threshold_desc1: 'The minimum similarity threshold for semantic retrieval', - + vector_similarity_weight: 'Vector Similarity Weight', vector_similarity_weight_desc: 'Only return results with BM25 scores above this threshold', vector_similarity_weight_desc1: 'The minimum BM25 score threshold for word segmentation retrieval', @@ -947,7 +992,7 @@ export const en = { versionNameTip: 'Version number format: v[major version number].[next version number].[revision number] (e.g. v1.3.0)', agentName: 'Agent Name', roleType: 'Role Type', - + coordinator: 'Coordinator', analyzer: 'Analyzer', executor: 'Executor', @@ -957,8 +1002,52 @@ export const en = { capabilities: 'Capabilities', subAgent: 'Sub Agent', maxChatCount: 'Add up to 4 models', - addApiKey: 'Add API Key', - ReplyException: 'Reply exception' + ReplyException: 'Reply exception', + + endpointConfigurationSubTitle: 'Configure API access address and supported HTTP methods', + apiKeys: 'API Keys Management', + apiKeySubTitle: 'Manage API keys, view usage and traffic statistics for each key', + addApiKey: 'Add New API Key', + apiKeyName: 'Key Name', + apiKeyNamePlaceholder: 'e.g.: Production, Testing, Development', + apiKeyDescPlaceholder: 'Describe the purpose of this Key', + apiKeyTotal: 'Total Keys', + apiKeyRequestTotal: 'Total Requests', + qps: 'Average QPS', + qpsLimit: 'QPS Limit', + qpsLimitTip: '(Requests per second)', + apiLimitConfig: 'Rate Limiting Configuration', + qpsLimitDesc: 'Limit the maximum number of requests this Key can make per second', + dailyUsageLimit: 'Daily Usage Limit', + dailyUsageLimitDesc: 'Limit the maximum total number of requests this Key can make per day', + dailyUsageLimitUnit: 'times/day', + apiKeyDeleteContent: 'Once deleted, it cannot be recovered, and applications using this Key will not be able to access the API', + currentValue: 'Current Value', + qpsLimitUnit: 'times/second', + addVariable: 'Insert Variable', + defineVariableName: 'Custom Variable Name', + defineVariableNamePlaceholder: 'Enter variable name, e.g.: user_name', + defineVariableNameExtra: 'Format: Variable name will be automatically wrapped with {{}}', + you: 'You', + ai: 'AI Assistant', + promptChatPlaceholder: 'Describe the prompt you need, e.g.: I need a customer service assistant', + promptChatEmpty: 'No conversation content available', + promptEmpty: 'Describe your use case on the left, and the orchestration preview will be displayed here.', + + master: 'Supervisor Mode', + master_agent: 'Supervisor Mode', + master_agentDesc: 'Unified scheduling and management by the main Agent, with sub-Agents executing tasks assigned by the supervisor, suitable for scenarios requiring centralized control.', + handoffs: 'Collaboration Mode', + handoffsDesc: 'Multiple Agents collaborate equally, autonomously coordinating according to task requirements, suitable for complex scenarios requiring flexible interaction.', + masterConfig: 'Supervisor Configuration', + orchestrationMode: 'Task Assignment Strategy', + conditional: 'Intelligent Assignment', + sequential: 'Sequential Assignment', + parallel: 'Parallel Assignment', + aggregationStrategy: 'Result Aggregation Method', + merge: 'Complete Aggregation', + vote: 'Key Information Extraction', + priority: 'Structured Integration', }, userMemory: { userMemory: 'User Memory', @@ -974,7 +1063,7 @@ export const en = { memoryInsight: 'Memory Insight', relationshipNetwork: 'Relationship Network', aboutMe: 'About Me', - foldUp: 'Fold Up', + foldUp: 'Collapse', interestDistribution: 'Interest Distribution', memoryDetails: 'Memory Details', importantMomentsInLife: 'Important Moments in Life', @@ -983,9 +1072,9 @@ export const en = { drag: 'Drag and drop to move nodes', zoom: 'Scroll zoom view', memoryDetailEmpty: 'Please select a memory node', - memoryDetailEmptyDesc: 'Click on any node in the above view to view detailed information', + memoryDetailEmptyDesc: 'Click on the node in the left graph to view the details of entity memory', - totalNumOfMemories: 'Total number of memories', + totalNumOfMemories: 'Total Number of Memories', footprintCity: 'Footprint City', totalNumOfPhotos: 'Total number of photos', importantRelationships: 'Important Relationships', @@ -995,13 +1084,40 @@ export const en = { emotions: 'Emotions', occupation: 'Occupation', memories: 'memories', - expanded: 'Expanded', + expanded: 'Expand', description: 'Description', - entityType: 'Entity Type', conversationMemory: 'Conversation Storage Content', - sortByTimeDesc: 'Sort by time in descending order', editConfig: 'Edit Config', - chooseModel: 'Choose Model' + chooseModel: 'Choose Model', + + nodeStatistics: 'Memory Classification', + total: 'Total', + Chunk: 'Long-term Memory', + MemorySummary: 'Episodic Memory', + Statement: 'Emotional Memory', + ExtractedEntity: 'Short-term Memory', + + PERCEPTUAL_MEMORY: 'Perceptual Memory', + WORKING_MEMORY: 'Working Memory', + SHORT_TERM_MEMORY: 'Shot Term Memory', + LONG_TERM_MEMORY: 'Long Term Memory', + EXPLICIT_MEMORY: 'Explicit Memory', + IMPLICIT_MEMORY: 'Implicit Memory', + EMOTIONAL_MEMORY: 'Emotional Memory', + EPISODIC_MEMORY: 'Episodic Memory', + + endUserProfile: 'Core Profile', + editEndUserProfile: 'Edit', + other_name: 'Name', + position: 'Position', + department: 'Department', + contact: 'Contact', + phone: 'Phone', + hire_date: 'Hire Date', + memoryContent: 'Memory Content', + created_at: 'Created At', + + memoryWindow: "{{name}}'s Window of Memory" }, space: { createSpace: 'Create Space', @@ -1011,32 +1127,32 @@ export const en = { associated: 'Associated', notAssociated: 'Not Associated', storageType: 'Storage Type', - rag: 'RAG storage', + rag: 'RAG Storage', ragDesc: 'Based on vector retrieval, suitable for document Q&A and semantic search', - neo4j: 'Graph storage', + neo4j: 'Graph Storage', neo4jDesc: 'Based on knowledge graph, suitable for relational reasoning and path query', llmModel: 'LLM Model', embeddingModel: 'Embedding Model', rerankModel: 'Rerank Model' }, memoryExtractionEngine: { - title: 'Memory Engine module configuration center', + title: 'Memory Engine Module Configuration Center', subTitle: 'Configure the parameters of six core modules, and view in real-time the impact on the memory processing conclusions of the "sample memory text (insights from the technology conference)". Any parameter changes will be instantly reflected in the results area on the right.', - example: 'Example memory text', - storageLayerModule: 'Storage layer module', + example: 'Example Memory Text', + storageLayerModule: 'Storage Layer Module', - enableLlmDedupBlockwise: 'Entity de-duplication (LLM decision-making)', - enableLlmDisambiguation: 'Memory disambiguation function (LLM decision)', - tNameStrict: 'Name matching threshold', - tTypeStrict: 'Type matching threshold', - tOverall: 'Comprehensive matching threshold', + enableLlmDedupBlockwise: 'Entity De-duplication (LLM decision-making)', + enableLlmDisambiguation: 'Memory Disambiguation Function (LLM decision)', + tNameStrict: 'Name Matching Threshold', + tTypeStrict: 'Type Matching Threshold', + tOverall: 'Comprehensive Matching Threshold', - arrangementLayerModule: 'Arrangement layer module', - queryMode: 'Query mode', + arrangementLayerModule: 'Arrangement Layer Module', + queryMode: 'Query Mode', queryModeSubTitle: 'Control whether to activate deeper search functions', deepRetrieval: 'Deep Retrieval', deepRetrievalMeaning: 'Control whether to initiate deep memory retrieval (true/false).', - dataPreprocessing: 'Data preprocessing', + dataPreprocessing: 'Data Preprocessing', dataPreprocessingSubTitle: 'Through reflection and refinement, transform episodic memory into deeper semantic memory.', entityDeduplicationModuleThreshold: 'Entity de-duplication - name matching threshold', @@ -1045,20 +1161,19 @@ export const en = { control: 'Control', button: 'button', inputNumber: 'progress value', - slider: 'progress value', + slider: 'Slider', select: 'select', location: 'Location', CurrentValue: 'Current Value', type: 'Type', Meaning: 'Meaning', - exampleMemoryExtractionResults: 'Example memory extraction results', + exampleMemoryExtractionResults: 'Example Memory Extraction Results', exampleMemoryExtractionResultsSubTitle: '(from a technology conference)', - warning: 'When you modify the configuration items on the left, the extraction conclusion will be updated in real-time here', extractTheNumberOfEntities: 'Extract the number of entities', extractTheNumberOfEntitiesDesc: 'Merge after deduplication: {{num}} (exact: {{exact}}, fuzzy: {{fuzzy}}, LLM: {{llm}})', - + numberOfEntityDisambiguation: 'Number of entity disambiguation', numberOfEntityDisambiguationDesc: 'Total {{num}} times (blocking: {{block_count}})', @@ -1083,26 +1198,26 @@ export const en = { lateChunker: 'Late Chunker', debug: 'Debug', model: 'Model', - chunkerStrategy: 'Chunker strategy', + chunkerStrategy: 'Chunker Strategy', chunkerStrategyDesc: 'Choose a partitioning strategy.', - intelligentSemanticPruning: 'Intelligent semantic pruning', + intelligentSemanticPruning: 'Intelligent Semantic Pruning', intelligentSemanticPruningSubTitle: 'Whether to activate the intelligent semantic pruning function, select pruning scenarios, and set thresholds.', - intelligentSemanticPruningFunction: 'Intelligent semantic pruning function', + intelligentSemanticPruningFunction: 'Intelligent Semantic Pruning Function', intelligentSemanticPruningFunctionDesc: 'Whether to activate intelligent semantic pruning (true/false).', - intelligentSemanticPruningScene: 'Intelligent semantic pruning scene', + intelligentSemanticPruningScene: 'Intelligent Semantic Pruning Scene', intelligentSemanticPruningSceneDesc: 'Select intelligent semantic pruning scene (education, online_service, outbound).', - intelligentSemanticPruningThreshold: 'Intelligent semantic pruning threshold', + intelligentSemanticPruningThreshold: 'Intelligent Semantic Pruning Threshold', intelligentSemanticPruningThresholdDesc: 'Set intelligent semantic pruning threshold (0-0.9).', - selfReflexionEngine: 'Self-reflexion engine', + reflectionEngine: 'Self-Reflexion Engine', selfReflexionEngineSubTitle: 'Through reflection and refinement, transform episodic memory into deeper semantic memory.', enableSelfReflexion: 'Enable self-reflexion', - iterationPeriod: 'Iteration period', + iterationPeriod: 'Iteration Period', iterationPeriodDesc: 'Set the iteration period for self-reflexion (hourly, 3_hours, 6_hours, 12_hours, daily).', - reflexionRange: 'Reflexion range', + reflexionRange: 'Reflexion Range', reflexionRangeDesc: "When selecting 'Database', the iteration cycle is non configurable and fixed at daily", retrieval: 'Retrieval', database: 'Database', - reflectOnTheBaseline: 'Reflect on the baseline', + reflectOnTheBaseline: 'Reflect on the Baseline', basedOnTime: 'Based on time', basedOnFacts: 'Based on facts', basedOnFactsAndTime: 'Based on facts and time', @@ -1114,15 +1229,15 @@ export const en = { education: 'Education', online_service: 'Online service', outbound: 'Outbound', - entityDeduplicationDisambiguation: 'Entity de-duplication disambiguation', + entityDeduplicationDisambiguation: 'Entity De-duplication Disambiguation', entityDeduplicationDisambiguationSubTitle: 'Control the LLM decision-making function for memory deduplication and disambiguation, set various matching thresholds, and affect the accuracy of memory deduplication.', - semanticAnchorAnnotationModule: 'Semantic anchor annotation module', + semanticAnchorAnnotationModule: 'Semantic Anchor Annotation Module', semanticAnchorAnnotationModuleSubTitle: 'Control the granularity of statement extraction and whether to include dialog context.', - statementGranularity: 'Statement granularity', + statementGranularity: 'Statement Granularity', statementGranularityDesc: 'Statement extraction granularity (1-3): 1 represents breaking down sentences into different statements, 2 represents sentence level, and 3 represents merging sentences into paragraphs.', - includeDialogueContext: 'Include dialogue context', + includeDialogueContext: 'Include Dialogue Context', includeDialogueContextDesc: 'Control whether the complete dialogue context is included in the extraction process (true/false).', - maxDialogueContextChars: 'Max dialogue context chars', + maxDialogueContextChars: 'Max Dialogue Context Chars', maxDialogueContextCharsDesc: 'The maximum number of characters included in the dialogue context (to avoid character limit issues) (greater than 100).', coreEntitiesAfterDedup: 'Core entities after deduplication', extractRelationalTriples: 'Extracted relational triples (partial)', @@ -1145,7 +1260,28 @@ Memory Bear: Qin succeeded for several reasons: Shang Yang’s reforms were thor Student: Then switching to Tang history: after the An Lushan Rebellion, the central government began reforms, so why did regional warlordism (the fanzhen problem) actually get worse? Memory Bear: After the rebellion, regional warlordism intensified for several reasons: military governors (jiedushi) held the power to recruit troops, control local finances, and command military forces, effectively becoming regional warlords; the central government’s finances declined due to the breakdown of the equal-field system and the collapse of the tax-labor system, making it increasingly unable to support the army, which pushed military forces to rely on the jiedushi; the recruitment-based military system made soldiers loyal to individual commanders rather than the state; eunuchs controlled the imperial guards, the civil bureaucracy lost influence, and the central government’s ability to balance regional power weakened. -` +`, + + warning: 'When you modify the configuration items on the left, click [Debug], and the extraction conclusions will be updated in real time here', + processing: 'Configuration updated, re-extracting sample memory...', + success: 'Memory extraction completed!', + overallProgress: 'Overall Progress', + text_preprocessing: 'Text Preprocessing', + fragment: 'Fragment', + knowledge_extraction: 'Knowledge Extraction', + creating_nodes_edges: 'Creating Entity Relationships', + deduplication: 'Deduplication and Disambiguation', + status: { + pending: 'Pending', + processing: 'Processing', + completed: 'Completed', + failed: 'Failed' + }, + time: 'Time: ', + text_preprocessing_desc: 'Text split into {{count}} semantic fragments', + knowledge_extraction_desc: 'Knowledge extraction completed, identified {{entities}} entities, {{statements}} statements, {{temporal_ranges_count}} temporal extractions, {{triplets}} triplets', + creating_nodes_edges_desc: 'Entity relationship creation completed, {{num}} relationships in total', + deduplication_desc: 'Deduplication and disambiguation completed, {{count}} unique entities in total' }, memoryConversation: { searchPlaceholder: 'Input user ID...', @@ -1172,8 +1308,6 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re quickReply: 'Quick Reply', web_search: 'Online search', memory: 'Memory', - memoryConversationAnalysisEmpty: 'There is currently no dialogue analysis content available', - memoryConversationAnalysisEmptySubTitle: 'After entering your user ID, click on "Send" to view the conversation memory' }, login: { title: 'Red Bear Memory Science', @@ -1229,6 +1363,595 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re tableEmpty: 'There are currently no data', loadingEmpty: 'The content is loading…', loadingEmptyDesc: 'Your content is on its way by rocket! It will soon land on your screen' - } + }, + apiKey: { + name: 'Project Name', + createApiKey: 'Create API Key', + updateApiKey: 'Edit API Key', + id: 'ID', + created_at: 'Created At', + description: 'Description', + memoryEngine: 'Memory Engine', + knowledgeBase: 'Knowledge Base', + advancedSettings: 'Advanced Settings', + expires_at: 'Expiration At', + apiKey: 'API Key', + status: 'Status', + createdAt: 'Created At', + expiresAt: 'Expires At', + requestsPerMinute: 'Requests/Minute', + viewDetail: 'View Details', + disable: 'Disable', + enable: 'Enable', + baseInfo: 'Basic Information', + permissionInfo: 'Permission Information', + is_expired: 'Status', + active: 'Active', + inactive: 'Expired' + }, + tool: { + mcp: 'MCP Service', + inner: 'Built-in Tools', + custom: 'Custom Tools', + mcpSearchPlaceholder: 'Search MCP services...', + innerSearchPlaceholder: 'Search tools...', + customSearchPlaceholder: 'Search custom tools...', + addService: 'Add MCP Service', + editService: 'Edit MCP Service', + addServiceSuccess: 'Service added successfully', + server_url: 'Service URL', + last_health_check: 'Last Connection', + responseTime: 'Response Time', + status: { + available: 'Available', + unconfigured: 'Unconfigured', + configured_disabled: 'Configured but Disabled', + error: 'Connection Error' + }, + available_desc: 'API is configured and enabled', + unconfigured_desc: 'Need to configure API Key', + configured_disabled_desc: 'API is configured but not enabled', + error_desc: 'API is configured but connection error', + + serviceEndpoint: 'Service Endpoint URL', + serviceEndpointPlaceholder: 'URL of the service endpoint', + serviceEndpointExtra: 'Complete access address of the MCP service', + nameAndIcon: 'Name and Icon', + namePlaceholder: 'Name your MCP service', + serverIdentifier: 'Server Identifier', + serverIdentifierPlaceholder: 'Unique server identifier, e.g. my-mcp-server', + serverIdentifierLength: 'Maximum 24 characters', + serverIdentifierPattern: 'Supports lowercase letters, numbers, underscores and hyphens', + description: 'Description', + auth: 'Authentication', + requestHeader: 'Request Headers', + config: 'Configuration', + auth_type: 'Authentication Type', + none: 'No Authentication', + api_key: 'API Key', + basic_auth: 'Basic Auth', + bearer_token: 'Bearer Token', + username: 'Username', + password: 'Password', + key_name: 'Key Name', + requestHeaderDesc: 'Additional HTTP request headers sent to MCP server', + addRequestHeader: 'Add Request Header', + editRequestHeader: 'Edit Request Header', + requestHeaderName: 'Request Header Name', + requestHeaderValue: 'Request Header Value', + timeout: 'Timeout (seconds)', + sseReadTimeout: 'SSE Read Timeout (seconds)', + saveAndTest: 'Save and Test', + + timeFormat: 'Time Formatting', + timeZoneConversion: 'Time Zone Conversion', + timestampConversion: 'Timestamp Conversion', + timeCalculation: 'Time Calculation', + time_desc: 'Date and Time Processing', + DateTimeTool_features: 'Provides time format conversion, time zone conversion, timestamp calculation and other functions', + currentTime: 'Current Time', + timestamp: 'Timestamp', + localTime: 'Local Time', + utcTime: 'UTC Time', + secondsTimestamp: 'Timestamp (seconds)', + millisecondsTimestamp: 'Timestamp (milliseconds)', + enterTimestamp: 'Enter Timestamp', + conversion: 'Conversion', + conversionResult: 'Conversion Result', + chooseFormatType: 'Choose Format', + + JsonTool_desc: 'Data Format Conversion', + JsonTool_features: 'JSON formatting, compression, validation and conversion functions', + jsonFormat: 'JSON Formatting', + jsonGzip: 'JSON Compression', + jsonCheck: 'JSON Validation', + jsonConversion: 'Format Conversion', + jsonEg: 'Example JSON', + enterJson: 'Enter JSON', + jsonPlaceholder: 'Enter JSON data, e.g.: {"name": "test", "value": 123}', + clear: 'Clear', + parse: 'Paste', + format: 'Format', + minify: 'Minify', + validate: 'Validate', + convert: 'Escape', + outputResult: 'Output Result', + validJosn: 'JSON format is correct, validation passed!', + + BaiduSearchTool_desc: 'Search Engine Service', + BaiduSearchTool_features: 'Integrated Baidu Search API, providing web search, news search and other functions', + webSearch: 'Web Search', + newsSearch: 'News Search', + imageSearch: 'Image Search', + realTimeResults: 'Real-time Results', + configStatus: 'Configuration Status', + hasApiKey: 'API configured and enabled', + needApiKey: 'Need to configure API Key', + + MinerUTool_desc: 'PDF Parsing Tool', + MinerUTool_features: 'High-precision PDF document parsing tool, supports text, table, and image extraction', + pdfParser: 'PDF Parser', + tableExtraction: 'Table Extraction', + imageRecognition: 'Image Recognition', + textExtraction: 'Text Extraction', + + TextInTool_desc: 'OCR Text Recognition', + TextInTool_features: 'Intelligent OCR text recognition service, supports multi-language and handwriting recognition', + universalOCR: 'Universal OCR', + handwritingRecognition: 'Handwriting Recognition', + multilingualSupport: 'Multi-language Support', + highPrecisionRecognition: 'High Precision Recognition', + + configDesc: 'Configuration Description', + BaiduSearchTool_config_desc: 'To use Baidu Search API, you need to apply for API Key and Secret Key on Baidu Open Platform first.', + MinerUTool_config_desc: 'MinerU is a high-precision PDF document parsing tool that requires an API Key to use.', + TextInTool_config_desc: 'TextIn provides intelligent OCR text recognition service with multi-language support.', + link: 'Application URL', + BaiduSearchTool_api_key_desc: 'API Key obtained from Baidu Open Platform', + MinerUTool_api_key_desc: 'API Key obtained from MinerU platform', + secret_key: 'Secret Key', + BaiduSearchTool_secret_key_desc: 'Secret Key obtained from Baidu Open Platform', + TextInTool_secret_key_desc: 'Secret Key obtained from TextIn platform', + type: 'Search Type', + pagesize: 'Results Per Page', + pagesize_desc: 'Number of results returned per search ({{count1}}-{{count2}})', + BaiduSearchTool_enable: 'Enable Baidu Search', + BaiduSearchTool_safe_enable: 'Enable Safe Search', + BaiduSearchTool_safe_enable_desc: 'Filter inappropriate content', + api_address: 'API Address', + MinerUTool_api_address_desc: 'Uses official API address by default, can be modified if privately deployed', + TextInTool_api_address_desc: 'Uses official API address by default', + parsing_mode: 'Parsing Mode', + auto_recognition: 'Auto Recognition', + pure_text_mode: 'Pure Text Mode', + table_priority: 'Table Priority', + image_priority: 'Image Priority', + MinerUTool_timeout_desc: 'PDF parsing timeout (10-300 seconds)', + MinerUTool_enable: 'Enable MinerU', + MinerUTool_extract_images_enable: 'Extract Images', + MinerUTool_extract_images_enable_desc: 'Whether to extract image content from PDF', + app_id: 'APP ID', + TextInTool_app_id_desc: 'App ID obtained from TextIn platform', + language_identification: 'Recognition Language', + automatic_detection: 'Automatic Detection', + simplified_chinese: 'Simplified Chinese', + traditional_chinese: 'Traditional Chinese', + english: 'English', + japanese: 'Japanese', + korean_language: 'Korean', + pattern_recognition: 'Recognition Mode', + universal_identification: 'Universal Recognition', + high_precision_identification: 'High Precision Recognition', + handwriting_recognition: 'Handwriting Recognition', + formula_recognition: 'Formula Recognition', + TextInTool_enable: 'Enable TextIn', + return_text_position_enable: 'Return Text Position Info', + return_text_position_enable_desc: 'Whether to return coordinate positions of recognized text', + + addCustom: 'Add Custom Tool', + editCustom: 'Edit Custom Tool', + schema: 'Schema', + schemaPlaceholder: 'Enter your OpenAPI schema here', + authentication: 'Authentication Method', + tags: 'Tag', + created_at: 'Created At', + headerName: 'Header Name', + null: 'None', + tagDesc: 'Multiple tags separated by commas', + availableTools: 'Available Tools', + name: 'Name', + desc: 'Description', + method: 'Method', + path: 'Path', + viewDetail: 'View Details', + noResult: 'Processing results will be displayed here' + }, + workflow: { + coreNode: 'Core Nodes', + start: 'Start', + end: 'End', + answer: 'Answer', + aiAndCognitiveProcessing: 'AI & Cognitive Processing', + llm: 'Large Language Model (LLM)', + model_selection: 'Multi-Model Selection', + model_voting: 'Multi-Model Voting', + 'knowledge-retrieval': 'Knowledge Retrieval (RAG)', + classification: 'Intelligent Classification', + 'parameter-extractor': 'Parameter Extraction', + flowControl: 'Flow Control', + 'if-else': 'Conditional Branch', + iteration: 'Iteration', + loop: 'Loop', + parallel: 'Parallel Execution', + 'var-aggregator': 'Variable Aggregator', + externalInteraction: 'External Interaction', + "http-request": 'HTTP Request', + tools: 'Tools', + code_execution: 'Code Execution', + "jinja-render": 'Template Rendering', + cognitiveUpgrading: 'Cognitive Upgrading (Innovation)', + task_planning: 'Task Planning', + reasoning_control: 'Reasoning Control', + self_reflection: 'Self Reflection', + memory_enhancement: 'Memory Enhancement', + agentCollaborationNode: 'Agent Collaboration Nodes', + agent_scheduling: 'Agent Scheduling', + agent_collaboration: 'Agent Collaboration', + agent_arbitration: 'Agent Arbitration', + safetyAndCompliance: 'Safety & Compliance', + sensitive_detection: 'Sensitive Detection', + output_audit: 'Output Audit', + evolutionAndGovernance: 'Evolution & Governance', + self_optimization: 'Self Optimization', + process_evolution: 'Process Evolution', + + clickToConfigure: 'Click to configure node parameters', + nodeProperties: 'Node Properties', + empty: "Emmm... The box is empty, nothing here~", + nodeName: 'Node Name', + + + config: { + llm: { + model_id: 'Model', + temperature: 'Temperature', + max_tokens: 'Max Tokens', + context: 'Context', + }, + start: { + variables: 'Input Fields', + + string: 'Text', + number: 'Number', + boolean: 'Checkbox', + array: 'Dropdown Options', + object: 'Object', + + addVariable: 'Add Variable', + editVariable: 'Edit Variable', + variableType: 'Variable Type', + variableName: 'Variable Name', + invalidVariableName: 'Variable name must start with a letter and contain only letters, numbers, and underscores', + description: 'Display Name', + default: 'Default Value', + required: 'Required', + max_length: 'Max Length', + defaultChecked: 'Checked', + notDefaultChecked: 'Unchecked', + options: 'Options', + }, + end: { + output: 'Reply' + }, + 'knowledge-retrieval': { + query: 'Query Variable', + knowledge_retrieval: 'Knowledge Base', + recallConfig: 'Recall Test', + }, + 'parameter-extractor': { + model_id: 'Model', + text: 'Input Variable', + params: 'Extract Parameters', + prompt: 'Instruction', + + addParam: 'Add Extract Parameter', + editParam: 'Edit Extract Parameter', + + name: 'Name', + invalidParamName: 'Parameter name must start with a letter and contain only letters, numbers, and underscores', + type: 'Type', + desc: 'Description', + required: 'Required', + + 'string': 'String', + 'number': 'Number', + 'boolean': 'Boolean', + 'array[string]': 'Array[String]', + 'array[number]': 'Array[Number]', + 'array[boolean]': 'Array[Boolean]', + 'array[object]': 'Array[Object]', + }, + 'var-aggregator': { + group: 'Aggregation Group', + invalidVariableName: 'Variable name must start with a letter and contain only letters, numbers, and underscores', + addGroup: 'Add Group', + variable: 'Variable Assignment' + }, + 'if-else': { + "empty": 'Is Empty', + "not_empty": 'Is Not Empty', + "contains": 'Contains', + "not_contains": 'Does Not Contain', + "startwith": 'Starts With', + "endwith": 'Ends With', + "eq": '==', + "ne": '!=', + "lt": '<', + "le": '<=', + "gt": '>', + "ge": '>=', + else_desc: 'Used to define the logic that should be executed when the if condition is not met.' + } + }, + + clear: 'Clear', + run: 'Run', + save: 'Save', + export: 'Export', + variableConfig: 'Variable Configuration', + variableRequired: 'Required', + addMessage: 'Add Message', + answerDesc: 'Reply' + }, + emotionEngine: { + emotionEngineConfig: 'Emotion Engine Configuration', + + emotion_enabled: 'Enable Emotion Engine', + emotion_enabled_desc: 'Automatically analyze emotional tendencies in conversations', + + emotion_model_id: 'Emotion Analysis Model', + emotion_model_id_desc: 'Different models vary in accuracy and speed', + + emotion_extract_keywords: 'Emotion Keyword Extraction', + emotion_extract_keywords_subTitle: 'Automatically extract emotion-related keywords from conversations', + emotion_extract_keywords_desc: 'Extract emotional keywords like "happy", "disappointed", "excited" to better understand user emotions', + emotion_min_intensity: 'Confidence Threshold', + emotion_min_intensity_desc: 'Higher confidence leads to more accurate recognition, but may miss some information', + + emotion_enable_subject: 'Emotion Subject Classification', + emotion_enable_subject_subTitle: 'Identify emotion attribution (self/other/object)', + emotion_enable_subject_desc: 'Distinguish emotion subjects: self (I feel happy), other (he is angry), object (this product is great)', + + currentValue: 'Current Value', + emotion_min_intensity_description: 'Confidence Threshold Description', + question: 'What is Confidence Threshold?', + answer: 'Confidence threshold is the "certainty level" standard for emotion engine to judge emotions. When the emotional confidence analyzed by AI is lower than the set threshold, the emotion will not be recorded.', + differentTitle: 'Impact of Different Thresholds', + advantage: 'Advantages', + shortcoming: 'Disadvantages', + scene: 'Applicable Scenarios', + low_title: 'Low Threshold (0.0 - 0.4)', + low_tag: 'Sensitive', + low_advantage: 'Can capture more subtle emotional changes without missing potential emotional signals', + low_shortcoming: 'May cause misjudgments, identifying neutral or unclear expressions as specific emotions', + low_scene: 'Scenarios requiring comprehensive understanding of user emotional fluctuations with low accuracy requirements', + middle_title: 'Medium Threshold (0.5 - 0.7)', + middle_tag: 'Recommended', + middle_advantage: 'Balances accuracy and coverage, can identify obvious emotions without being overly sensitive', + middle_shortcoming: 'May miss some less obvious emotional expressions', + middle_scene: 'Most daily conversation scenarios, suitable for general emotional analysis needs', + high_title: 'High Threshold (0.8 - 1.0)', + high_tag: 'Precise', + high_advantage: 'Only records very clear emotional expressions, extremely high accuracy with low misjudgment rate', + high_shortcoming: 'Will miss a large amount of less obvious emotional information, low data coverage', + high_scene: 'Scenarios requiring extremely high accuracy, such as emotional crisis warnings and important decision references', + + configSuggest: 'Configuration Suggestions', + first: 'First Time Use', + first_desc: 'Recommend starting with medium threshold (0.6-0.7), observe for a period and adjust based on actual results', + customer_service: 'Customer Service Scenarios', + customer_service_desc: 'Recommend using lower threshold (0.4-0.6) to timely capture user dissatisfaction', + data_analysis: 'Data Analysis', + data_analysis_desc: 'Recommend using medium threshold (0.6-0.7) to ensure data quality while having sufficient sample size', + risk_warning: 'Risk Warning', + risk_warning_desc: 'Recommend using higher threshold (0.7-0.8) to ensure warning accuracy', + + actual_case: 'Actual Case', + user_input: 'User Input', + user_input_message: '"This feature is okay, but there are some minor issues"', + neutral_emotion: 'Neutral Emotion', + neutral_emotion_tag: 'All thresholds will record', + minor_dissatisfaction: 'Minor Dissatisfaction', + minor_dissatisfaction_tag: 'Only low/medium thresholds will record', + expect_improvement: 'Expect Improvement', + expect_improvement_tag: 'Only low threshold will record', + confidence: 'Confidence' + }, + statementDetail: { + wordCloud: 'Emotion Distribution Analysis', + pieces: 'items', + emotionTags: 'High-Frequency Emotion Keywords', + joy: 'Joy', + anger: 'Anger', + sadness: 'Sadness', + fear: 'Fear', + neutral: 'Neutral', + surprise: 'Surprise', + + health: 'Emotional Health Index', + positivity_rate: 'Positivity Rate', + stability: 'Stability', + resilience: 'Resilience', + suggestions: 'Personalized Suggestions', + }, + reflectionEngine: { + reflectionEngineConfig: 'Reflection Engine Configuration', + reflection_enabled: 'Enable Reflection Engine', + reflection_enabled_desc: 'Transform episodic memory into semantic memory, forming long-term cognition', + reflection_model_id: 'Reflection Model', + reflection_model_id_desc: 'Different models vary in accuracy and speed', + reflection_period_in_hours: 'Iteration Period', + reflection_period_in_hours_desc: 'Determines how often the system performs memory reflection and refinement', + reflexion_range: 'Reflection Range', + partial: 'Partial Reflection (New memories only)', + all: 'Full Reflection (All historical memories)', + reflexion_range_desc: '', + baseline: 'Reflection Baseline', + baseline_desc: '', + TIME: 'Time-based (Temporal relationships)', + FACT: 'Fact-based (Knowledge points)', + HYBRID: 'Fact + Time (Comprehensive dimension)', + quality_assessment: 'Enable Quality Assessment', + quality_assessment_desc: 'Automatically evaluate memory accuracy, completeness and timeliness', + memory_verify: 'Enable Memory Verification', + memory_verify_desc: 'Detect sensitive information and filter inappropriate content', + oneHour: 'Every 1 hour', + threeHours: 'Every 3 hours', + sixHours: 'Every 6 hours', + twelveHours: 'Every 12 hours', + daily: 'Daily', + run: 'Run Debug', + example: 'Raw Data', + exampleText: 'I went to Beijing for work in the spring of 2023, and have basically been working in Beijing ever since, without changing cities much. However, due to company restructuring, I was transferred to Shanghai for about half a year in the first half of 2024, during which time I checked in at the Shanghai office every day. At that time, my employment records still used my previous identity information, with ID number 11010119950308123X and bank card 6222023847595898, which have never changed. By the way, I have actually been living in Beijing since 2023 and have never left Beijing for long periods. The Shanghai period was more like remote collaboration.', + runTitle: 'Reflection Test Run', + status: 'Status', + message: 'Message', + + conflictDetection: 'Conflict Detection', + reason: 'Conflict Reason', + solution: 'Solution', + + qualityAssessment: 'Quality Assessment', + qualityAssessmentObj: { + score: 'Quality Score', + summary: 'Assessment Summary', + }, + + privacyAudit: 'Privacy Audit', + privacyAuditObj: { + true: 'Yes', + false: 'No', + has_privacy: 'Contains Privacy Information', + privacy_types: 'Privacy Types', + summary: 'Audit Summary', + } + }, + pricing: { + title: 'Flexible Pricing for Teams of All Sizes', + desc: 'Transparent pricing that helps you easily find a plan that fits your budget.', + solution: 'Product Positioning', + targetAudience: 'Target Audience', + orderPayment: 'Order Payment', + creationTime: 'Creation Time', + orderPaymentDesc: 'Please confirm the order information and complete the payment.', + orderInformation: 'Order Information', + paymentMethod: 'Payment Method', + paymentVoucher: 'Payment Voucher', + corporateTransfer: 'Corporate transfer', + corporateTransferDesc: 'Pay via corporate bank transfer.', + payeeInformation: 'Payee Account Information', + receivingEntity: 'Receiving Entity', + bankName: 'Bank Name', + bankAccountNumber: 'Bank Account Number', + pay_txn_id: 'Payment Transaction ID', + pay_txn_idPlaceholder: 'Please enter the bank transfer transaction ID.', + pay_txn_idDesc: 'Please fill in the serial number on the bank transfer voucher for verification of payment', + payer: 'Payer(Company/Individual)', + payerPlaceholder: 'Please enter the bank transfer transaction ID.', + redirectCountdown: 'seconds until redirecting to Memory Bear...', + confirmRedirect: 'Payment voucher submitted successfully!', + confirmRedirectContent: 'We will verify your payment information within 1-3 business days. Would you like to go to Memory Bear now to start your experience?', + stayCurrentPage: 'Stay on current page', + goBack: 'Go to backend', + transferDate: 'Transfer Date', + payerAccount: 'Payer Account', + remark: 'Remark', + remarkPlaceholder: 'Please provide any additional remarks here (optional).', + confirm: 'SUBMIT PAYMENT', + submitting: 'Submitting...', + payInfo: 'After submission, we will verify your payment information within 1–3 business days.', + paySuccess: 'Once verified, your subscription service will be automatically activated by the system.', + comboName: 'Combo Name', + spAndTa: 'Solution positioning and target audience', + versionInformation: 'Version information', + orderCycle: 'Order Cycle', + orderAmount: 'Order Amount', + personal: { + type: 'Personal', + label: 'Current Package', + typeDesc: 'For individuals', + solution: "A person's second brain, capable of storing up to 2000 memories.", + targetAudience: 'individual users, students, and first-time users', + priceDesc: '/Forever free', + supportServices: 'Community Forum + Email Support', + }, + team: { + type: 'Team', + label: 'Small Team', + typeDesc: 'Small Team Version', + solution: "Enable every team to build a shared second brain in seconds.", + targetAudience: 'Small teams, early-stage startups, and small projects.', + priceDesc: '/Month', + supportServices: 'Standard customer service support', + }, + biz: { + type: 'Biz+', + label: 'Most Popular', + typeDesc: 'Enterprise Growth Edition', + solution: "Scale your organization with a powerful, enterprise-ready second-brain system.", + targetAudience: 'Growing teams, startups, and SMBs requiring advanced memory capabilities.', + priceDesc: '/Month/workspace', + supportServices: 'Priority customer service support', + }, + commerce: { + type: 'Commerce', + label: 'Commercial OEM', + typeDesc: 'Commercial OEM version', + solution: "Seamlessly integrate advanced memory capabilities into your SaaS or enterprise product.", + targetAudience: 'Large enterprises, SaaS vendors, and system integrators requiring fully customizable and secure deployment.', + priceDesc: 'On-premises deployment', + supportServices: 'Standard customer service support', + flexibleDeployment: 'Supports localized deployment in data centers', + reliableGuarantee: '99.9% SLA guarantee' + }, + mostPopular: 'Most Popular', + startedBtn: 'GET STARTED', + choosePlanBtn: 'CHOOSE PLAN', + contactBtn: 'CONTACT US', + memoryCapacity: 'Memory capacity:', + entries: 'entries', + intelligentSearchFrequency: 'Intelligent search frequency:', + timesMonth: 'times/month', + supportServices: 'Support Services:', + flexibleDeployment: 'Flexible deployment:', + reliableGuarantee: 'Reliable guarantee:', + alertTitle: 'Intellectual Property Authorization Reminder', + alertContent: 'Please note: Using certain AI models (such as GPT-4, Claude, etc.) may involve third-party API call fees, which are not included in the Memory Bear platform subscription fee. You need to pay the relevant fees separately to the model provider. Memory Bear only charges platform management and service fees and does not bear the usage fees of third-party APIs.', + currentAccountType: 'Current Account Type', + validUntil: 'Valid Until', + orderHistory: 'Order History', + order_no: 'Order Number', + product_type: 'Package Name', + payable_amount: 'Order Amount', + status: 'Order Status', + pay_time: 'Payment Time', + viewDetail: 'View Details', + PENDING: 'Pending Review', + APPROVED: 'Approved', + REJECTED: 'Rejected', + allStatus: 'All Status', + allTime: 'All Time', + today: 'Today', + week: 'Last Week', + month: 'Last Month', + threeMonth: 'Last Three Months', + year: 'Last Year', + searchPlaceholder: 'Search order number', + allType: 'All Packages', + orderDetail: 'Order Details', + orderInfo: 'Order Information', + orderPayInfo: 'Payment Information', + create_time: 'Creation Time', + }, }, }; diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 7aeaed03..38fc04d6 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -28,16 +28,24 @@ export const zh = { spaceManagement: '空间管理', memoryExtractionEngine: '记忆提取引擎', forgettingEngine: '遗忘引擎', + apiKeyManagement: 'API KEY管理', knowledgePrivate: '详情', knowledgeShare: '详情', knowledgeCreateDataset: '新建数据集', knowledgeDocumentDetails: '详情', userMemoryDetail: '用户记忆详情', + toolManagement: '工具管理', + emotionEngine: '情感引擎', + statementDetail: '情绪记忆', + selfReflectionEngine: '反思引擎', + pricing: '收费管理', + orderPayment: '订单支付', + orderHistory: '订单记录', }, knowledgeBase: { home: '首页', selectSpace: '请选择空间', - preview:'预览', + preview: '预览', pleaseUploadFileFirst: '请先上传文件', shareSuccess: '分享成功', shareFailed: '分享失败', @@ -86,7 +94,7 @@ export const zh = { operation: '操作', selectDataSource: '选择来源', localFile: '本地文件', - uploadFileTypes: '上传 PDF、TXT、DOCX 等格式的文件', + uploadFileTypes: '上传 PDF、 TXT、 DOCX、 IMAGE、 MEDIA 等格式的文件', webLink: '网页链接', webLinkPlaceholder: '请输入', webLinkDesc: '仅支持静态链接。如果上传的数据显示为空,则该链接可能无法读取。每行一个,一次最多{{count}}个链接', @@ -94,6 +102,7 @@ export const zh = { readStaticWebPage: '读取静态网页内容作为数据集', customText: '自定义文本', customContent: '自定义内容', + createContentError: '创建自定义文件失败', manuallyInputText: '手动输入一段文本作为数据集', openKnowledgeBase: '打开知识库', searchPlaceholder: '搜索', @@ -110,13 +119,20 @@ export const zh = { noDataSets: '暂无数据集,点击下方按钮或拖拽文件创建。', createEmptyDataSet: '+ 空白数据集', createImageDataSet: '+ 图片数据集', + createContent: '创建内容', + title: '标题', + content: '内容', + pleaseEnterTitle: '请输入标题', + pleaseEnterContent: '请输入内容', dragFilesHere: '拖拽文件到此处上传', downloadOriginal: '下载原始内容', createImport: '新建/导入', textDataSet: '文本数据集', imageDataSet: '图片数据集', + mediaDataSet: '媒体数据集', blankDataset: '空白数据集', emptyDataSet: '空白数据集', + customTextDataset: '自定义文本数据集', text: '文本', search: '搜索', image: '图片', @@ -190,6 +206,7 @@ export const zh = { datasetName: '数据集名称', pleaseEnterDatasetName: '请输入数据集名称', uploadImages: '上传图片', + uploadMedia: '上传媒体文件', pleaseUploadImages: '请上传图片', embedding_id: '嵌入模型', llm_id: '大语言模型', @@ -210,7 +227,6 @@ export const zh = { insertContent: '插入内容', editContent: '编辑内容', insertContentPlaceholder: '请输入内容', - pleaseEnterContent: '请输入内容', documentIdRequired: '文档ID是必需的', editContentDesc: '编辑内容', insertContentDesc: '插入内容', @@ -223,6 +239,17 @@ export const zh = { semantic: '语义', hybrid: '混合', updateEmbeddingContent: '确定要更新嵌入模型吗?更新后,分块向量数据需要重新构建?', + question: '问题', + answer: '答案', + normalMode: '常规模式', + qaMode: '问答模式', + fileParsingSettings: '文件解析设置', + pdfEnhancementAnalysis: 'PDF增强解析', + fileSizeExceeds: '文件大小超过限制', + sizeLimitError: '文件大小超过限制,最大支持256MB,当前文件大小', + fileDurationExceeds:'文件时长超过限制', + fileDurationLimitError: '媒体文件时长超过限制,最大支持150秒,当前时长', + unableReadFile:'无法读取媒体文件信息,请检查文件格式', createForm: { name: '名称', embedding_id: '嵌入模型', @@ -288,7 +315,7 @@ export const zh = { number: '数字', checkbox: '复选框', apiVariable: 'API变量', - + displayName: '显示名称', maxLength: '最大长度', required: '必填', @@ -308,7 +335,7 @@ export const zh = { promptConfiguration: '提示词配置', configurationDesc: '定义Agent的角色、能力和行为准则', aiPrompt: 'AI提示词', - promptPlaceholder: '你是一个专业的AI助手,你的职责是..', + promptPlaceholder: '你是一个专业的AI助手,你的职责是帮助用户解决问题。', knowledgeBaseAssociation: '知识库关联', associatedKnowledgeBase: '关联知识库', addKnowledgeBase: '添加知识库', @@ -390,12 +417,6 @@ export const zh = { clusterName: '集群名称', clusterDescription: '集群描述', clusterDescriptionPlaceholder: '这是一个专门处理核心业务的Agent集群,能够协作完成复杂的业务处理任务。', - toolCalling: '工具调用', - toolCallingDesc: '主控代理将子代理作为工具调用', - toolCallingFeature: '集中控制,适合结构化工作流', - handoffs: '交接', - handoffsDesc: '代理之间动态转移控制权', - handoffsFeature: '去中心化控制,适合复杂对话场景', recommend: '推荐', advanced: '高级', multiAgentArchitecture: '多代理架构模式', @@ -422,7 +443,7 @@ export const zh = { stateSharingStrategy: '状态共享策略', intermediateResultProcessing: '中间结果处理', metadataTransfer: '元数据传输', - + knowledgeConfig: '知识库配置', temperature: '温度', temperature_desc: '温度参数,控制输出的随机性', max_tokens: '最大令牌数', @@ -450,9 +471,9 @@ export const zh = { releasePreview: '发布预览', globalConfig: '全局配置', globalConfigDesc: '全局配置将应用于所有关联的知识库作为默认配置。单个知识库的配置将覆盖全局配置。', - rerankModel: '重排序模型', + rerankModel: 'Reranker 模型', rerankModelDesc: '激活后,搜索结果将被重新排序以提高相关性', - rearrangementModel: '重排序模型', + rearrangementModel: 'Reranker 模型', rearrangementModelDesc: '选择用于重新排序的模型', reranker_top_k: 'Top K', reranker_top_k_desc: '返回的文档片段数量,范围从1到20', @@ -467,7 +488,7 @@ export const zh = { similarity_threshold: '语义相似度阈值', similarity_threshold_desc: '仅返回语义相似度高于此阈值的结果', similarity_threshold_desc1: '语义检索的最小相似度阈值', - + vector_similarity_weight: '向量相似度权重', vector_similarity_weight_desc: '仅返回BM25分数高于此阈值的结果', vector_similarity_weight_desc1: '分词检索的最小BM25分数阈值', @@ -481,6 +502,51 @@ export const zh = { chooseKnowledge: '选择知识库', active: '活跃', inactive: '不活跃', + + endpointConfigurationSubTitle: '配置 API 访问地址和支持的 HTTP 方法', + apiKeys: 'API Keys 管理', + apiKeySubTitle: '管理 API 密钥,查看每个密钥的使用情况和流量统计', + addApiKey: '添加新 API Key', + apiKeyName: 'Key 名称', + apiKeyNamePlaceholder: '例如:生产环境、测试环境、开发环境', + apiKeyDescPlaceholder: '描述这个 Key 的用途', + apiKeyTotal: '总 Keys', + apiKeyRequestTotal: '总请求数', + qps: '平均 QPS', + qpsLimit: 'QPS 限制', + qpsLimitTip: '(每秒请求数)', + apiLimitConfig: '限流配置', + qpsLimitDesc: '限制此 Key 每秒最多可以发起的请求数', + dailyUsageLimit: '日调用量限制', + dailyUsageLimitDesc: '限制此 Key 每天最多可以发起的请求总数', + dailyUsageLimitUnit: '次/天', + apiKeyDeleteContent: '删除后将无法恢复,使用此Key的应用将无法访问 API', + currentValue: '当前值', + qpsLimitUnit: '次/秒', + addVariable: '插入变量', + defineVariableName: '自定义变量名', + defineVariableNamePlaceholder: '输入变量名,例如:user_name', + defineVariableNameExtra: '格式:变量名会自动添加{{}}包裹', + you: '你', + ai: 'AI 助手', + promptChatPlaceholder: '描述你需要的提示词,例如:我需要一个客服助手', + promptChatEmpty: '目前没有对话内容', + promptEmpty: '在左侧描述您的用例,编排预览将在此处显示。', + + master: '主管模式', + master_agent: '主管模式', + master_agentDesc: '由主 Agent 统一调度和管理,子 Agent 按照主管分配的任务执行,适合需要集中控制的场景。', + handoffs: '协作模式', + handoffsDesc: '多个 Agent 平等协作,根据任务需求自主协调配合,适合需要灵活互动的复杂场景。', + masterConfig: '主管配置', + orchestrationMode: '任务分配策略', + conditional: '智能分配', + sequential: '顺序分配', + parallel: '并行分配', + aggregationStrategy: '结果汇总方式', + merge: '完整汇总', + vote: '关键信息提取', + priority: '结构化整合', }, // 角色管理相关翻译 role: { @@ -618,7 +684,7 @@ export const zh = { triplet_count_desc: '构建{{entities_count}}个实体节点和{{relations_count}}个关系连接', temporal_count: '时间提取', temporal_count_desc: '记录{{count}}条时间序列信息', - + dialogue: '对话', chunk: '分块', statement: '语句', @@ -730,8 +796,8 @@ export const zh = { copy: '复制', copySuccess: '复制成功', viewDetails: '查看详情', - enabled: '启用', - disabled: '停用', + enabled: '已启用', + disabled: '已停用', updateWarning: '更新警告', deleteWarning: '删除警告', deleteWarningContent: '确定要删除此{{content}}吗?', @@ -740,7 +806,9 @@ export const zh = { add: '添加', addOption: '添加选项', viewDetail: '查看详情', + confirmRemoveFile: '确定要移除此文件吗?', deleteSuccess: '删除成功', + deleteFailed: '删除失败', foldUp: '收起', expanded: '展开', clickUploadIcon: '点击上传图标', @@ -772,8 +840,8 @@ export const zh = { publicApiCannotRefreshToken: '公共接口不能刷新token', refreshTokenNotExist: '刷新token不存在', reset: '重置', - statusEnabled: '已启用', - statusDisabled: '已禁用', + refresh: '刷新', + return: '返回', }, product: { applicationManagement: '应用管理', @@ -871,6 +939,17 @@ export const zh = { saveConfig: '保存配置', apiKeyName: 'API密钥名称', + + llm: 'LLM', + chat: 'Chat', + embedding: 'Embedding', + rerank: 'Rerank', + openai: "Openai", + dashscope: "Dashscope", + ollama: "Ollama", + xinference: "Xinference", + gpustack: "Gpustack", + bedrock: "Bedrock" }, timezones: { 'Asia/Shanghai': '中国标准时间 (UTC+8)', @@ -967,6 +1046,8 @@ export const zh = { active: '活跃', inactive: '不活跃', configurationName: '配置名称', + emotionEngine: '情感引擎', + reflectionEngine: '反思引擎' }, member: { username: '用户名', @@ -979,8 +1060,6 @@ export const zh = { inviteToMember: '邀请成员', member: '成员', memberDesc: '只能使用应用,不能创建应用', - admin: '管理员', - adminDesc: '可以创建应用和管理团队设置', sendInvitation: '发送邀请', manager: '管理员', managerDesc: '可以创建应用和管理团队设置', @@ -1027,10 +1106,10 @@ export const zh = { minimumRetention: '时间遗忘率 (λ_time)', minimumRetentionDesc: '控制记忆随时间的遗忘速度,值越高时间越短', - forgettingRate: '记忆遗忘率 (λ_mem)', + forgettingRate: '记忆遗忘率 (λ_mem)', forgettingRateDesc: '控制记忆遗忘的速度,值越高遗忘越快', - offset: '最小保留度 (offset)', - offsetDesc: '控制记忆保留的最小保留阈值 遗忘这地方改个文字描述', + offset: '偏移量 (offset)', + offsetDesc: '最小保留度的偏移量', CurrentValue: '当前值', range: '范围', forgettingEngineConfigParams: '遗忘引擎配置参数', @@ -1038,6 +1117,14 @@ export const zh = { quicklyForget: '快速遗忘', slowForgetting: '缓慢遗忘', currentConfig: '当前配置', + + decay_constant: '衰减常数', + max_history_length: '访问历史最大长度', + forgetting_threshold: '遗忘阈值', + min_days_since_access: '最小未访问天数', + enable_llm_summary: '是否使用 LLM 生成摘要', + max_merge_batch_size: '单次最大融合节点对数', + forgetting_interval_hours: '遗忘周期间隔' }, userMemory: { userMemory: '用户记忆', @@ -1062,7 +1149,7 @@ export const zh = { drag: '拖放移动节点', zoom: '滚动缩放视图', memoryDetailEmpty: '请选择一个记忆节点', - memoryDetailEmptyDesc: '点击上方视图中的任何节点以查看详细信息', + memoryDetailEmptyDesc: '点击左侧图表中的节点查看实体记忆详情', totalNumOfMemories: '记忆总数', footprintCity: '足迹城市', @@ -1075,6 +1162,43 @@ export const zh = { occupation: '职业', memories: '记忆', expanded: '展开', + + description: '描述', + conversationMemory: '对话存储内容', + editConfig: '编辑模型', + chooseModel: '选择模型', + + nodeStatistics: '记忆分类', + total: '总计', + + Chunk: '长期记忆', + MemorySummary: '情景记忆', + Statement: '情绪记忆', + ExtractedEntity: '短期记忆', + + PERCEPTUAL_MEMORY: '感知记忆', + WORKING_MEMORY: '工作记忆', + SHORT_TERM_MEMORY: '短期记忆', + LONG_TERM_MEMORY: '长期记忆', + EXPLICIT_MEMORY: '显性记忆', + IMPLICIT_MEMORY: '隐性记忆', + EMOTIONAL_MEMORY: '情绪记忆', + EPISODIC_MEMORY: '情景记忆', + + endUserProfile: '核心档案', + editEndUserProfile: '编辑', + other_name: '姓名', + position: '职位', + department: '部门', + contact: '联系方式', + phone: '电话', + hire_date: '入职时间', + memoryContent: '记忆内容', + created_at: '创建时间', + updated_at: '最后更新时间', + fullScreen: '全屏', + + memoryWindow: "{{name}}的记忆之窗" }, space: { createSpace: '创建空间', @@ -1086,7 +1210,7 @@ export const zh = { storageType: '存储类型', rag: 'RAG存储', ragDesc: '基于向量检索,适合文档问答和语义搜索', - neo4j: '图谱存储', + neo4j: '图存储', neo4jDesc: '基于知识图谱,适合关系推理和路径查询', llmModel: 'LLM 模型', embeddingModel: 'Embedding 模型', @@ -1127,11 +1251,10 @@ export const zh = { exampleMemoryExtractionResults: '示例记忆提取结果', exampleMemoryExtractionResultsSubTitle: '(来自技术会议)', - warning: '当您修改左侧的配置项时,提取结论将在此处实时更新', extractTheNumberOfEntities: '提取实体数量', extractTheNumberOfEntitiesDesc: '去重后合并:{{num}}(精确:{{exact}},模糊:{{fuzzy}},LLM:{{llm}})', - + numberOfEntityDisambiguation: '实体消歧数量', numberOfEntityDisambiguationDesc: '总计{{num}}次(阻止:{{block_count}})', @@ -1166,7 +1289,7 @@ export const zh = { intelligentSemanticPruningSceneDesc: '选择智能语义修剪场景(education、online_service、outbound)。', intelligentSemanticPruningThreshold: '智能语义修剪阈值', intelligentSemanticPruningThresholdDesc: '设置智能语义修剪阈值(0-0.9)。', - selfReflexionEngine: '自我反思引擎', + reflectionEngine: '自我反思引擎', selfReflexionEngineSubTitle: '通过反思和精炼,将情节记忆转化为更深层的语义记忆。', enableSelfReflexion: '启用自我反思', iterationPeriod: '迭代周期', @@ -1217,7 +1340,27 @@ export const zh = { 记忆熊:秦国统一的原因包括:商鞅变法彻底,建立法律、户籍和军功爵制度,提升国家组织能力;旧贵族势力弱,中央集权程度高;关中地理优越,资源丰富且易守难攻;从孝公到秦始皇政策连续性强。 学生:那我换到唐朝史:安史之乱后,中央已开始整顿,为何藩镇割据反而加剧? -记忆熊:安史之乱后藩镇割据加剧的原因包括:节度使掌握募兵权、财政调度权与军事指挥权,形成地方军阀;中央财政因均田制瓦解和租庸调失效而衰退,难以支撑军队,导致地方军事力量依附节度使;募兵制使士兵效忠个人而非国家;宦官掌控禁军,文官集团失势,中央制衡能力削弱。` +记忆熊:安史之乱后藩镇割据加剧的原因包括:节度使掌握募兵权、财政调度权与军事指挥权,形成地方军阀;中央财政因均田制瓦解和租庸调失效而衰退,难以支撑军队,导致地方军事力量依附节度使;募兵制使士兵效忠个人而非国家;宦官掌控禁军,文官集团失势,中央制衡能力削弱。`, + warning: '当您修改左侧的配置项后,点击【调试】,提取结论将在此处实时更新', + processing: '配置已更新,正在重新萃取示例记忆...', + success: '记忆萃取完成!', + overallProgress: '整体进度', + text_preprocessing: '文本预处理', + fragment: '片段', + knowledge_extraction: '知识抽取', + creating_nodes_edges: '创建实体关系', + deduplication: '去重消歧', + status: { + pending: '等待中', + processing: '处理中', + completed: '已完成', + failed: '失败' + }, + time: '耗时: ', + text_preprocessing_desc: '文本切分为{{count}}个语义片段', + knowledge_extraction_desc: '知识抽取完成,共识别{{entities}}个实体,{{statements}}个句子, {{temporal_ranges_count}}个时间提取, {{triplets}}个三元组', + creating_nodes_edges_desc: '实体关系创建完成,共{{num}}条关系', + deduplication_desc: '去重消歧完成,最终{{count}}个唯一实体' }, memoryConversation: { searchPlaceholder: '输入用户ID...', @@ -1242,8 +1385,8 @@ export const zh = { startANewConversation: '开始新对话', normalReply: '正常回复', quickReply: '快速回复', - memoryConversationAnalysisEmpty: '当前没有可用的对话分析内容', - memoryConversationAnalysisEmptySubTitle: '输入用户ID后,单击“发送”查看对话记忆' + web_search: '联网搜索', + memory: '记忆', }, login: { title: '红熊记忆科学', @@ -1314,28 +1457,631 @@ export const zh = { websocketDemoCard: 'WebSocket 演示', sseDemoCard: 'SSE演示' }, - workflow: { - title: '工作流编辑器', - description: '拖拽节点创建连接,构建您的工作流程。点击节点可进行配置。', - addNode: '添加节点', - deleteNode: '删除选中', - saveWorkflow: '保存工作流', - startNode: '触发节点', - conditionNode: '条件判断', - actionNode: '执行动作', - endNode: '结束节点', - newNode: '新节点', - node: '节点', - nodesCreated: '已创建节点', - loadingNodes: '正在加载节点 {{progress}}%', - loadingFailed: '加载节点失败', - create5kNodes: '创建5000节点', - create10kNodes: '创建10000节点' - }, notFound: { title: '页面未找到', description: '请求的页面不存在。', backToHome: '返回首页' - } + }, + apiKey: { + name: '项目名称', + createApiKey: '创建API Key', + updateApiKey: '编辑API Key', + id: 'ID', + created_at: '创建时间', + description: '描述', + memoryEngine: '记忆引擎', + knowledgeBase: '知识库', + advancedSettings: '高级设置', + expires_at: '过期时间', + apiKey: 'API Key', + status: '状态', + createdAt: '创建时间', + expiresAt: '过期时间', + requestsPerMinute: '次/分钟', + viewDetail: '查看详情', + disable: '禁用', + enable: '启用', + baseInfo: '基础信息', + permissionInfo: '授权信息', + is_expired: '状态', + active: '活跃', + inactive: '过期' + }, + tool: { + mcp: 'MCP 服务', + inner: '内置工具', + custom: '自定义工具', + mcpSearchPlaceholder: '搜索MCP服务...', + innerSearchPlaceholder: '搜索工具...', + customSearchPlaceholder: '搜索自定义工具...', + addService: '添加MCP服务', + editService: '编辑MCP服务', + addServiceSuccess: '服务添加成功', + server_url: '服务地址', + last_health_check: '最后连接', + responseTime: '响应时间', + status: { + available: '可用', + unconfigured: '未配置', + configured_disabled: '已配置未启用', + error: '链接异常' + }, + available_desc: 'API 已配置并启用', + unconfigured_desc: '需要配置 API Key', + configured_disabled_desc: 'API 已配置但未启用', + error_desc: 'API 已配置但链接异常', + + testConnectionSuccess: '测试连接成功', + serviceEndpoint: '服务端点 URL', + serviceEndpointPlaceholder: '服务端点的 URL', + serviceEndpointExtra: 'MCP服务的完整访问地址', + nameAndIcon: '名称和图标', + namePlaceholder: '命名你的 MCP 服务', + serverIdentifier: '服务器标识符', + serverIdentifierPlaceholder: '服务器唯一标识,例如 my-mcp-server', + serverIdentifierLength: '最多 24 个字符', + serverIdentifierPattern: '支持小写字母、数字、下划线和连字符', + description: '描述信息', + auth: '认证', + requestHeader: '请求头', + config: '配置', + auth_type: '认证方式', + none: '无需认证', + api_key: 'API Key', + basic_auth: 'Basic Auth', + bearer_token: 'Bearer Token', + username: '用户名', + password: '密码', + key_name: 'Key Name', + requestHeaderDesc: '发送到 MCP 服务器的额外 HTTP 请求头', + addRequestHeader: '添加请求头', + editRequestHeader: '编辑请求头', + requestHeaderName: '请求头名称', + requestHeaderValue: '请求头值', + timeout: '超时时间(秒)', + sseReadTimeout: 'SSE 读取超时时间(秒)', + saveAndTest: '保存并测试', + + timeFormat: '时间格式化', + timeZoneConversion: '时区转换', + timestampConversion: '时间戳转换', + timeCalculation: '时间计算', + time_desc: '日期时间处理', + DateTimeTool_features: '提供时间格式转换、时区转换、时间戳计算等功能', + currentTime: '当前时间', + timestamp: '时间戳', + localTime: '本地时间', + utcTime: 'UTC时间', + secondsTimestamp: '时间戳(秒)', + millisecondsTimestamp: '时间戳(毫秒)', + enterTimestamp: '输入时间戳', + conversion: '转换', + conversionResult: '转换结果', + chooseFormatType: '选择格式', + + JsonTool_desc: '数据格式转换', + JsonTool_features: 'JSON格式化、压缩、验证和转换功能', + jsonFormat: 'JSON格式化', + jsonGzip: 'JSON压缩', + jsonCheck: 'JSON验证', + jsonConversion: '格式转换', + jsonEg: '示例JSON', + enterJson: '输入JSON', + jsonPlaceholder: '输入JSON数据,例如:{"name": "测试", "value": 123}', + clear: '清空', + parse: '粘贴', + format: '格式化', + minify: '压缩', + validate: '验证', + convert: '转义', + outputResult: '输出结果', + validJosn: 'JSON格式正确,验证通过!', + + BaiduSearchTool_desc: '搜索引擎服务', + BaiduSearchTool_features: '集成百度搜索API,提供网页搜索、新闻搜索等功能', + webSearch: '网页搜索', + newsSearch: '新闻搜索', + imageSearch: '图片搜索', + realTimeResults: '实时结果', + configStatus: '配置状态', + hasApiKey: 'API 已配置并启用', + needApiKey: '需要配置API Key', + + MinerUTool_desc: 'PDF解析工具', + MinerUTool_features: '高精度PDF文档解析工具,支持文字、表格、图片提取', + pdfParser: 'PDF解析', + tableExtraction: '表格提取', + imageRecognition: '图片识别', + textExtraction: '文本提取', + + TextInTool_desc: 'OCR文字识别', + TextInTool_features: '智能OCR文字识别服务,支持多语言、手写体识别', + universalOCR: '通用OCR', + handwritingRecognition: '手写识别', + multilingualSupport: '多语言支持', + highPrecisionRecognition: '高精度识别', + + configDesc: '配置说明', + BaiduSearchTool_config_desc: '使用百度搜索API需要先在百度开放平台申请API Key和Secret Key。', + MinerUTool_config_desc: 'MinerU是高精度PDF文档解析工具,需要API Key才能使用。', + TextInTool_config_desc: 'TextIn提供智能OCR文字识别服务,支持多语言识别。', + link: '申请地址', + BaiduSearchTool_api_key_desc: '从百度开放平台获取的API Key', + MinerUTool_api_key_desc: '从MinerU平台获取的API Key', + secret_key: 'Secret Key', + BaiduSearchTool_secret_key_desc: '从百度开放平台获取的Secret Key', + TextInTool_secret_key_desc: '从TextIn平台获取的Secret Key', + type: '搜索类型', + pagesize: '每页结果数', + pagesize_desc: '每次搜索返回的结果数量({{count1}}-{{count2}})', + BaiduSearchTool_enable: '启用百度搜索', + BaiduSearchTool_safe_enable: '启用安全搜索', + BaiduSearchTool_safe_enable_desc: '过滤不适宜内容', + api_address: 'API地址', + MinerUTool_api_address_desc: '默认使用官方API地址,如有私有部署可修改', + TextInTool_api_address_desc: '默认使用官方API地址', + parsing_mode: '解析模式', + auto_recognition: '自动识别', + pure_text_mode: '纯文本模式', + table_priority: '表格优先', + image_priority: '图片优先', + MinerUTool_timeout_desc: 'PDF解析超时时间(10-300秒)', + MinerUTool_enable: '启用MinerU', + MinerUTool_extract_images_enable: '提取图片', + MinerUTool_extract_images_enable_desc: '是否提取PDF中的图片内容', + app_id: 'APP ID', + TextInTool_app_id_desc: '从TextIn平台获取的App ID', + language_identification: '识别语言', + automatic_detection: '自动检测', + simplified_chinese: '简体中文', + traditional_chinese: '繁体中文', + english: '英文', + japanese: '日文', + korean_language: '韩文', + pattern_recognition: '识别模式', + universal_identification: '通用识别', + high_precision_identification: '高精度识别', + handwriting_recognition: '手写体识别', + formula_recognition: '公式识别', + TextInTool_enable: '启用TextIn', + return_text_position_enable: '返回文本位置信息', + return_text_position_enable_desc: '是否返回识别文字的坐标位置', + + addCustom: '添加自定义工具', + editCustom: '编辑自定义工具', + schema: 'Schema', + schemaPlaceholder: '在此处输入您的 OpenAPI schema', + authentication: '鉴权方式', + tags: '标签', + created_at: '创建时间', + headerName: 'Header 名称', + null: '无', + tagDesc: '多个标签用逗号分隔', + availableTools: '可用工具', + name: '名称', + desc: '描述', + method: '方法', + path: '路径', + viewDetail: '查看详情', + textLink: '测试连接', + noResult: '处理结果将显示在这里' + }, + workflow: { + coreNode: '核心节点', + start: '开始(Start)', + end: '结束(End)', + answer: '回复(Answer)', + aiAndCognitiveProcessing: 'AI与认知处理', + llm: '大语言模型 (LLM)', + model_selection: '多模型选择', + model_voting: '多模型投票', + 'knowledge-retrieval': '知识检索 (RAG)', + classification: '智能分类', + 'parameter-extractor': '参数提取', + flowControl: '流程控制', + 'if-else': '条件分支', + iteration: '迭代 (Iteration)', + loop: '循环 (Loop)', + parallel: '并行执行', + 'var-aggregator': '变量聚合器', + externalInteraction: '外部交互', + "http-request": 'HTTP请求', + tools: '工具 (Tools)', + code_execution: '代码执行', + "jinja-render": '模板渲染', + cognitiveUpgrading: '认知升级(创新)', + task_planning: '任务规划', + reasoning_control: '推理控制', + self_reflection: '自我反思', + memory_enhancement: '记忆增强', + agentCollaborationNode: 'Agent 协作节点', + agent_scheduling: 'Agent 调度', + agent_collaboration: 'Agent 协同', + agent_arbitration: 'Agent 仲裁', + safetyAndCompliance: '安全与合规', + sensitive_detection: '敏感识别', + output_audit: '输出审计', + evolutionAndGovernance: '演化与治理', + self_optimization: '自我优化', + process_evolution: '流程演化', + + clickToConfigure: '点击配置节点参数', + nodeProperties: '节点属性', + empty: "Emmm…盒子是空的,这里什么都没有~", + nodeName: '节点名称', + + + config: { + llm: { + model_id: '模型', + temperature: '温度', + max_tokens: '最大令牌数', + context: '上下文', + }, + start: { + variables: '输入字段', + + string: '文本', + number: '数字', + boolean: '复选框', + array: '下拉选项', + object: '对象', + + addVariable: '添加变量', + editVariable: '编辑变量', + variableType: '变量类型', + variableName: '变量名称', + invalidVariableName: '变量名只能以英文字母开头,包含英文字母、数字和下划线', + description: '显示名称', + default: '默认值', + required: '必填', + max_length: '最大长度', + defaultChecked: '选中', + notDefaultChecked: '不选中', + options: '选项', + }, + end: { + output: '回复' + }, + 'knowledge-retrieval': { + query: '查询变量', + knowledge_retrieval: '知识库', + recallConfig: '召回测试', + }, + 'parameter-extractor': { + model_id: '模型', + text: '输入变量', + params: '提取参数', + prompt: '指令', + + addParam: '添加提取参数', + editParam: '编辑提取参数', + + name: '名称', + invalidParamName: '提取参数名只能以英文字母开头,包含英文字母、数字和下划线', + type: '类型', + desc: '描述', + required: '必填', + + 'string': 'String', + 'number': 'Number', + 'boolean': 'Boolean', + 'array[string]': 'Array[String]', + 'array[number]': 'Array[Number]', + 'array[boolean]': 'Array[Boolean]', + 'array[object]': 'Array[Object]', + }, + 'var-aggregator': { + group: '聚合分组', + invalidVariableName: '变量名只能以英文字母开头,包含英文字母、数字和下划线', + addGroup: '添加分组', + variable: '变量赋值' + }, + 'if-else': { + "empty": '为空', + "not_empty": '不为空', + "contains": '包含', + "not_contains": '不包含', + "startwith": '开始是', + "endwith": '结束是', + "eq": '==', + "ne": '!=', + "lt": '<', + "le": '<=', + "gt": '>', + "ge": '>=', + else_desc: '用于定义当 if 条件不满足时应执行的逻辑。' + }, + 'http-request': { + auth: '鉴权', + authType: '鉴权类型', + apiKey: 'API Key', + basic: '基础', + bearer: 'Bearer', + custom: '自定义', + header: 'Header', + api_key: 'API Key', + timeouts: '超时设置', + "connect_timeout": '连接超时(秒)', + "read_timeout": '读取超时(秒)', + "write_timeout": '写入超时(秒)', + retry: '失败时重试', + error_handle: '异常处理', + verify_ssl: '验证 SSL 证书', + none: '无', + default: '默认值', + branch: '异常分支', + status_code: '状态码', + max_attempts: '最大重试次数', + retry_interval: '重试间隔', + }, + 'jinja-render': { + template: '代码', + mapping: '输入变量' + }, + name: '键', + type: '类型', + value: '值', + }, + + clear: '清空', + run: '运行', + save: '保存', + export: '导出', + variableConfig: '变量配置', + variableRequired: '必填', + addMessage: '添加消息', + answerDesc: '回复' + }, + emotionEngine: { + emotionEngineConfig: '情感引擎配置', + + emotion_enabled: '启用情感引擎', + emotion_enabled_desc: '自动分析对话中的情感倾向', + + emotion_model_id: '情感分析模型', + emotion_model_id_desc: '不同模型在准确度和速度上有所差异', + + emotion_extract_keywords: '情绪关键词提取', + emotion_extract_keywords_subTitle: '自动提取对话中的情绪相关关键词', + emotion_extract_keywords_desc: '提取如"开心"、"失望"、"期待"等情绪关键词,帮助更好地理解用户情绪', + emotion_min_intensity: '置信度阈值', + emotion_min_intensity_desc: '置信度越高,识别越准确,但可能遗漏部分信息', + + emotion_enable_subject: '情绪主体分类 ', + emotion_enable_subject_subTitle: '识别情绪归属(自己/他人/物体)', + emotion_enable_subject_desc: '区分情绪主体: self (我感到开心)、other (他很生气)、object (这个产品很棒)', + + currentValue: '当前值', + emotion_min_intensity_description: '置信度阈值说明', + question: '什么是置信度阈值?', + answer: '置信度阈值是情感引擎判断情绪时的"确定程度"标准。当 AI 分析出的情感置信度低于设定阈值时,该情感将不会被记录。', + differentTitle: '不同阈值的影响', + advantage: '优点', + shortcoming: '缺点', + scene: '适用场景', + low_title: '低阈值 (0.0 - 0.4)', + low_tag: '灵敏', + low_advantage: '能捕捉到更多细微的情感变化,不会遗漏潜在的情绪信号', + low_shortcoming: '可能产生误判,将中性或不明确的表达识别为特定情感', + low_scene: '需要全面了解用户情绪波动,对准确度要求不高的场景', + middle_title: '中阈值 (0.5 - 0.7)', + middle_tag: '推荐', + middle_advantage: '平衡准确度和覆盖率,既能识别明显情感,也不会过度敏感', + middle_shortcoming: '可能遗漏一些不太明显的情感表达', + middle_scene: '大多数日常对话场景,适合一般性情感分析需求', + high_title: '高阈值 (0.8 - 1.0)', + high_tag: '精准', + high_advantage: '只记录非常明确的情感表达,准确度极高,误判率低', + high_shortcoming: '会遗漏大量不够明显的情感信息,数据覆盖率低', + high_scene: '对准确度要求极高的场景,如情感危机预警、重要决策参考', + + configSuggest: '配置建议', + first: '初次使用', + first_desc: '建议从中等阈值(0.6-0.7)开始,观察一段时间后根据实际效果调整', + customer_service: '客服场景', + customer_service_desc: '建议使用较低阈值(0.4-0.6),及时捕捉用户的不满情绪', + data_analysis: '数据分析', + data_analysis_desc: '建议使用中等阈值(0.6-0.7),保证数据质量的同时有足够样本量', + risk_warning: '风险预警', + risk_warning_desc: '建议使用较高阈值(0.7-0.8),确保预警的准确性', + + actual_case: '实际案例', + user_input: '用户输入', + user_input_message: '"这个功能还行吧,不过有点小问题"', + neutral_emotion: '中性情感', + neutral_emotion_tag: '所有阈值都会记录', + minor_dissatisfaction: '轻微不满', + minor_dissatisfaction_tag: '仅低/中阈值会记录', + expect_improvement: '期待改进', + expect_improvement_tag: '仅低阈值会记录', + confidence: '置信度' + }, + statementDetail: { + wordCloud: '情感分布分析', + pieces: '条', + emotionTags: '高频情绪关键词', + joy: '喜悦', + anger: '愤怒', + sadness: '悲伤', + fear: '恐惧', + neutral: '中性', + surprise: '惊讶', + + health: '情绪健康指数', + positivity_rate: '积极率', + stability: '稳定性', + resilience: '恢复力', + suggestions: '个性化建议', + }, + reflectionEngine: { + reflectionEngineConfig: '反思引擎配置', + reflection_enabled: '启用反思引擎', + reflection_enabled_desc: '将情节记忆转化为语义记忆,形成长期认知', + reflection_model_id: '反思模型', + reflection_model_id_desc: '不同模型在准确度和速度上有所差异', + reflection_period_in_hours: '迭代周期', + reflection_period_in_hours_desc: '决定系统多久进行一次记忆反思和提炼', + reflexion_range: '反思范围', + partial: '部分反思 (仅新增记忆)', + all: '全部反思 (所有历史记忆)', + reflexion_range_desc: '', + baseline: '反思基线', + baseline_desc: '', + TIME: '基于时间(时序关系)', + FACT: '基于事实(知识点)', + HYBRID: '事实+时间(综合维度)', + quality_assessment: '启用质量评估', + quality_assessment_desc: '自动评估记忆的准确性、完整性和时效性', + memory_verify: '启用记忆审核', + memory_verify_desc: '检测敏感信息并过滤违规内容', + oneHour: '每1个小时', + threeHours: '每3个小时', + sixHours: '每6个小时', + twelveHours: '每12个小时', + daily: '每天', + run: '运行调试', + example: '原始数据', + exampleText: '我是 2023 年春天去北京工作的,后来基本一直都在北京上班,也没怎么换过城市。不过后来公司调整,2024 年上半年我被调到上海待了差不多半年,那段时间每天都是在上海办公室打卡。当时入职资料用的还是我之前的身份信息,身份证号是 11010119950308123X,银行卡是 6222023847595898,这些一直没变。对了,其实我 从 2023 年开始就一直在北京生活,从来没有长期离开过北京,上海那段更多算是远程配合', + runTitle: '反思试运行', + status: '状态', + message: '消息', + + conflictDetection: '冲突检测', + reason: '冲突原因', + solution: '解决方案', + + qualityAssessment: '质量评估', + qualityAssessmentObj: { + score: '质量评分', + summary: '评估摘要', + }, + + privacyAudit: '隐私审核', + privacyAuditObj: { + true: '是', + false: '否', + has_privacy: '包含隐私信息', + privacy_types: '隐私类型', + summary: '审核摘要', + } + }, + pricing: { + title: '灵活定价,满足各类团队需求', + desc: '透明的定价策略,助您轻松找到符合预算的方案', + solution: '方案定位', + targetAudience: '目标人群', + orderPayment: '订单支付', + orderPaymentDesc: '请确认订单信息并完成支付', + orderInformation: '订单信息', + paymentMethod: '支付方式', + paymentVoucher: '支付凭证', + corporateTransfer: '企业转账', + corporateTransferDesc: '通过企业对公账户转账支付', + receivingEntity: '收款单位', + bankName: '开户银行', + bankAccountNumber: '银行账号', + pay_txn_id: '支付流水号', + payer: '付款单位/个人', + transferDate: '转账日期', + payerAccount: '付款账号', + remark: '备注信息', + remarkPlaceholder: '如有其他说明,请在此填写(选填)', + confirm: '确认支付', + submitting: '提交中...', + payInfo: '提交后,我们将在1-3个工作日内核实您的付款信息', + paySuccess: '核实通过后,系统将自动开通您的套餐服务', + pay_txn_idPlaceholder: '请输入支付流水号', + pay_txn_idDesc: '请填写转账时的交易流水号,以便我们快速确认您的付款', + payerPlaceholder: '请输入付款单位或个人姓名', + redirectCountdown: '秒后跳转后台...', + confirmRedirect: '支付凭证已提交成功!', + confirmRedirectContent: '我们将在1-3个工作日内核实您的付款信息。是否立即跳转到记忆熊开始体验?', + stayCurrentPage: '留在当前页', + goBack: '跳转后台', + payeeInformation: '收款信息', + creationTime: '创建时间', + comboName: '套餐名称', + spAndTa: '方案定位与目标人群', + versionInformation: '版本信息', + orderCycle: '订购周期', + orderAmount: '订单金额', + personal: { + type: '个人版', + label: '当前安装包', + typeDesc: '个人玩家版本', + solution: '个人的第二大脑,最多可存储2000条记忆。', + targetAudience: '个人用户、学生及初次使用者', + priceDesc: '/永久免费', + supportServices: '社区论坛 + 邮件支持' + }, + team: { + type: '团队版', + label: '小型团队', + typeDesc: '小型团队版本', + solution: '让每一条业务记录瞬间成为团队的第二大脑。', + targetAudience: '初创团队、小微型企业、小型项目', + priceDesc: '/月', + supportServices: '标准客服支持' + }, + biz: { + type: '企业增长版', + label: '最受欢迎', + typeDesc: '企业增长版本', + solution: '让每一条业务记录瞬间成为团队的第二大脑。', + targetAudience: '初创团队、小微型企业、小型项目', + priceDesc: '/月/工作区', + supportServices: '优先客服支持' + }, + commerce: { + type: '商业OEM版', + label: '商业OEM', + typeDesc: '商业OEM版本', + solution: '将强大的记忆能力无缝嵌入您的SaaS产品中。', + targetAudience: '需要集成解决方案的大型企业、SaaS厂商及系统集成商。', + priceDesc: '本地化部署', + supportServices: '标准客服支持', + flexibleDeployment: '支持在数据中心进行本地化部署', + reliableGuarantee: '99.9% SLA保障' + }, + mostPopular: '最受欢迎', + startedBtn: '立即开始', + choosePlanBtn: '选择方案', + contactBtn: '联系我们', + memoryCapacity: '记忆容量:', + entries: '条', + intelligentSearchFrequency: '智能搜索次数:', + timesMonth: '次/月', + supportServices: '支持服务:', + flexibleDeployment: '灵活部署:', + reliableGuarantee: '可靠保障:', + alertTitle: '知识产权授权提醒', + alertContent: '请注意:使用某些AI模型(如GPT-4、Claude等)可能涉及第三方API调用费用,这些费用不包含在Memory Bear平台订阅费中。您需要单独向模型提供商支付相关费用。Memory Bear仅收取平台管理和服务费,不承担第三方API的使用费用。', + currentAccountType: '当前账户类型', + validUntil: '有效期至', + orderHistory: '订单记录', + order_no: '订单号', + product_type: '套餐名称', + payable_amount: '订单金额', + status: '订单状态', + pay_time: '支付时间', + viewDetail: '查看详情', + PENDING: '待审核', + APPROVED: '审核通过', + REJECTED: '审核不通过', + allStatus: '全部状态', + allTime: '全部时间', + today: '今天', + week: '最近一周', + month: '最近一月', + threeMonth: '最近三个月', + year: '最近一年', + searchPlaceholder: '搜索订单号', + allType: '全部套餐', + orderDetail: '订单详情', + orderInfo: '订单信息', + orderPayInfo: '支付信息', + create_time: '创建时间', + }, }, } \ No newline at end of file diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx index 4d7faf7a..9a2ea17d 100644 --- a/web/src/routes/index.tsx +++ b/web/src/routes/index.tsx @@ -10,19 +10,19 @@ import routesConfig from './routes.json'; // 递归收集所有路由中的element function collectElements(routes: RouteConfig[]): Set { const elements = new Set(); - + function traverse(routeList: RouteConfig[]) { routeList.forEach(route => { // 添加当前路由的element elements.add(route.element); - + // 递归处理子路由 if (route.children && route.children.length > 0) { traverse(route.children); } }); } - + traverse(routes); return elements; } @@ -38,6 +38,7 @@ const componentMap: Record>> = Home: lazy(() => import('@/views/Home')), UserMemory: lazy(() => import('@/views/UserMemory')), UserMemoryDetail: lazy(() => import('@/views/UserMemoryDetail')), + Neo4jUserMemoryDetail: lazy(() => import('@/views/UserMemoryDetail/Neo4j')), MemberManagement: lazy(() => import('@/views/MemberManagement')), MemoryManagement: lazy(() => import('@/views/MemoryManagement')), ForgettingEngine: lazy(() => import('@/views/ForgettingEngine')), @@ -54,10 +55,18 @@ const componentMap: Record>> = UserManagement: lazy(() => import('@/views/UserManagement')), ModelManagement: lazy(() => import('@/views/ModelManagement')), SpaceManagement: lazy(() => import('@/views/SpaceManagement')), + ApiKeyManagement: lazy(() => import('@/views/ApiKeyManagement')), + EmotionEngine: lazy(() => import('@/views/EmotionEngine')), + StatementDetail: lazy(() => import('@/views/UserMemoryDetail/pages/StatementDetail')), + SelfReflectionEngine: lazy(() => import('@/views/SelfReflectionEngine')), + OrderPayment: lazy(() => import('@/views/OrderPayment')), + OrderHistory: lazy(() => import('@/views/OrderHistory')), + Pricing: lazy(() => import('@/views/Pricing')), + ToolManagement: lazy(() => import('@/views/ToolManagement')), Login: lazy(() => import('@/views/Login')), InviteRegister: lazy(() => import('@/views/InviteRegister')), NoPermission: lazy(() => import('@/views/NoPermission')), - NotFound: lazy(() => import('@/views/NotFound')), + NotFound: lazy(() => import('@/views/NotFound')) }; // 检查并报告缺失的组件 @@ -87,12 +96,12 @@ const generateRoutes = (routes: RouteConfig[]): ReactNode => { // 获取组件 const componentKey = route.element as keyof typeof componentMap; const Component = componentMap[componentKey]; - + if (!Component) { console.error(`Component ${route.element} not found in componentMap`); return null; } - + // 如果有子路由 if (route.children) { return ( @@ -101,12 +110,12 @@ const generateRoutes = (routes: RouteConfig[]): ReactNode => { ); } - + // 如果有path属性,则为普通路由 if (route.path) { return } />; } - + return null; }); }; diff --git a/web/src/routes/routes.json b/web/src/routes/routes.json index 6c485a12..73431f67 100644 --- a/web/src/routes/routes.json +++ b/web/src/routes/routes.json @@ -5,6 +5,10 @@ { "path": "/user-management", "element": "UserManagement" }, { "path": "/model", "element": "ModelManagement" }, { "path": "/space", "element": "SpaceManagement" }, + { "path": "/tool", "element": "ToolManagement" }, + { "path": "/pricing", "element": "Pricing" }, + { "path": "/order-pay", "element": "OrderPayment" }, + { "path": "/orders", "element": "OrderHistory" }, { "path": "/no-permission", "element": "NoPermission" } ] }, @@ -25,6 +29,9 @@ { "path": "/knowledge-base/:knowledgeBaseId/share", "element": "Share" }, { "path": "/knowledge-base/:knowledgeBaseId/create-dataset", "element": "CreateDataset" }, { "path": "/knowledge-base/:knowledgeBaseId/DocumentDetails", "element": "DocumentDetails" }, + { "path": "/api-key", "element": "ApiKeyManagement" }, + { "path": "/emotion-engine/:id", "element": "EmotionEngine" }, + { "path": "/reflection-engine/:id", "element": "SelfReflectionEngine" }, { "path": "/no-permission", "element": "NoPermission" }, { "path": "/*", "element": "NotFound" } ] @@ -33,7 +40,9 @@ "element": "BasicLayout", "children": [ { "path": "/application/config/:id", "element": "ApplicationConfig" }, - { "path": "/conversation/:token", "element": "Conversation" } + { "path": "/conversation/:token", "element": "Conversation" }, + { "path": "/user-memory/neo4j/:id", "element": "Neo4jUserMemoryDetail" }, + { "path": "/statement/:id", "element": "StatementDetail" } ] }, { diff --git a/web/src/store/menu.json b/web/src/store/menu.json index 448400b0..8311e27d 100644 --- a/web/src/store/menu.json +++ b/web/src/store/menu.json @@ -26,6 +26,59 @@ "sort": 0, "subs": [] }, + { + "id": 7, + "parent": 0, + "code": "tool", + "label": "工具管理", + "i18nKey": "menu.toolManagement", + "path": "/tool", + "enable": true, + "display": true, + "level": 1, + "sort": 0, + "subs": [] + }, + { + "id": 6, + "parent": 0, + "code": "pricing", + "label": "收费管理", + "i18nKey": "menu.pricing", + "path": "/pricing", + "enable": true, + "display": true, + "level": 1, + "sort": 0, + "subs": [ + { + "id": 61, + "parent": 6, + "code": "order", + "label": "订单支付", + "i18nKey": "menu.orderPayment", + "path": "/order-pay", + "enable": true, + "display": false, + "level": 1, + "sort": 0, + "subs": [] + }, + { + "id": 62, + "parent": 6, + "code": "orderHistory", + "label": "订单记录", + "i18nKey": "menu.orderHistory", + "path": "/orders", + "enable": true, + "display": false, + "level": 1, + "sort": 0, + "subs": [] + } + ] + }, { "id": 3, "parent": 0, @@ -183,6 +236,32 @@ "level": 1, "sort": 0, "subs": null + }, + { + "id": 72, + "parent": 7, + "code": "emotionEngine", + "label": "情感引擎", + "i18nKey": "menu.emotionEngine", + "path": "/emotion-engine/:id", + "enable": true, + "display": false, + "level": 1, + "sort": 0, + "subs": null + }, + { + "id": 72, + "parent": 7, + "code": "selfReflectionEngine", + "label": "反思引擎", + "i18nKey": "menu.selfReflectionEngine", + "path": "/reflection-engine/:id", + "enable": true, + "display": false, + "level": 1, + "sort": 0, + "subs": null } ] }, @@ -205,12 +284,38 @@ "code": "userMemoryDetail", "label": "记忆详情", "i18nKey": "menu.userMemoryDetail", - "path": "/user-memory/:id", + "path": "/user-memory/neo4j/:id", "enable": true, "display": false, "level": 2, "sort": 0, - "subs": null + "subs": [ + { + "id": 811, + "parent": 81, + "code": "statementDetail", + "label": "记忆详情", + "i18nKey": "menu.statementDetail", + "path": "/statement/:id", + "enable": true, + "display": false, + "level": 3, + "sort": 0, + "subs": null + } + ] + }, + { + "id": 81, + "parent": 8, + "code": "userMemoryDetail", + "label": "记忆详情", + "i18nKey": "menu.userMemoryDetail", + "path": "/user-memory/:id", + "enable": true, + "display": false, + "level": 2, + "sort": 0 } ] }, @@ -243,6 +348,21 @@ "icon": null, "iconActive": null, "subs": null + }, + { + "id": 11, + "parent": 0, + "code": "apiKey", + "label": "API KEY管理", + "i18nKey": "menu.apiKeyManagement", + "path": "/api-key", + "enable": true, + "display": true, + "level": 1, + "sort": 0, + "icon": null, + "iconActive": null, + "subs": null } ] } \ No newline at end of file diff --git a/web/src/store/menu.ts b/web/src/store/menu.ts index 7e725921..63b9adf9 100644 --- a/web/src/store/menu.ts +++ b/web/src/store/menu.ts @@ -32,7 +32,7 @@ interface MenuState { allBreadcrumbs: Record<'space' | 'manage' | string, MenuItem[]>; loadMenus: (source: 'space' | 'manage') => void; updateBreadcrumbs: (keyPath: string[], source: 'space' | 'manage') => void; - setCustomBreadcrumbs: (breadcrumbs: MenuItem[], source: 'space' | 'manage') => void; + setCustomBreadcrumbs: (breadcrumbs: MenuItem[], source: string) => void; } const initBreadcrumbs = localStorage.getItem('breadcrumbs') || '[]' @@ -57,20 +57,47 @@ export const useMenu = create((set, get) => ({ const { allMenus } = get() const menus = allMenus[source] || [] let result: MenuItem[] = [] - const matchedMenu: MenuItem | undefined = menus.find(menu => menu.path === paths[paths.length - 1] || `${menu.id}` === paths[1]); - - if (matchedMenu) { - let matchedSubMenu: MenuItem | undefined = undefined; - if (paths.length > 1 && matchedMenu?.subs?.length) { - matchedSubMenu = matchedMenu.subs.find(menu => menu.path === paths[0]); + + console.log('updateBreadcrumbs paths:', paths); + + if (paths.length === 3) { + // 三级菜单:[subSubPath, subId, menuId] + const menuId = paths[2]; + const subId = paths[1]; + const subSubPath = paths[0]; + + const matchedMenu = menus.find(menu => `${menu.id}` === menuId); + if (matchedMenu && matchedMenu.subs) { + const matchedSub = matchedMenu.subs.find(sub => `${sub.id}` === subId); + if (matchedSub && matchedSub.subs) { + const matchedSubSub = matchedSub.subs.find(subSub => subSub.path === subSubPath); + if (matchedSubSub) { + result = [ + { ...matchedMenu, subs: null }, + { ...matchedSub, subs: null }, + { ...matchedSubSub, subs: null } + ]; + } + } } - result = [ - { ...matchedMenu, subs: null }, - matchedSubMenu - ].filter(item => item !== undefined) as MenuItem[] } else { - result = [] as MenuItem[] + // 原有逻辑处理一级和二级菜单 + const matchedMenu: MenuItem | undefined = menus.find(menu => menu.path === paths[paths.length - 1] || `${menu.id}` === paths[1]); + + if (matchedMenu) { + let matchedSubMenu: MenuItem | undefined = undefined; + if (paths.length > 1 && matchedMenu?.subs?.length) { + matchedSubMenu = matchedMenu.subs.find(menu => menu.path === paths[0]); + } + result = [ + { ...matchedMenu, subs: null }, + matchedSubMenu + ].filter(item => item !== undefined) as MenuItem[] + } else { + result = [] as MenuItem[] + } } + const allBreadcrumbs = { ...get().allBreadcrumbs, [source]: result } set({ allBreadcrumbs }) localStorage.setItem('breadcrumbs', JSON.stringify(allBreadcrumbs)) diff --git a/web/src/styles/index.css b/web/src/styles/index.css index eecd99f5..bbbe9cd9 100644 --- a/web/src/styles/index.css +++ b/web/src/styles/index.css @@ -174,4 +174,10 @@ body { } .ant-breadcrumb a:hover { background-color: transparent; +} + +/* X6 节点样式 */ +.x6-node foreignObject > body { + min-height: 100%; + max-height: 100%; } \ No newline at end of file diff --git a/web/src/utils/apiKeyReplacer.ts b/web/src/utils/apiKeyReplacer.ts new file mode 100644 index 00000000..a2914e46 --- /dev/null +++ b/web/src/utils/apiKeyReplacer.ts @@ -0,0 +1,46 @@ +/** + * API密钥替换工具 + */ + +const API_KEY_PATTERNS = { + service: /sk-service-[A-Za-z0-9_-]+/g, + agent: /sk-agent-[A-Za-z0-9_-]+/g, + multiAgent: /sk-multi_agent-[A-Za-z0-9_-]+/g, + workflow: /sk-workflow-[A-Za-z0-9_-]+/g +} +const API_KEY_PREFIX = { + service: 'sk-service-', + agent: 'sk-agent-', + multiAgent: 'sk-multi_agent-', + workflow: 'sk-workflow-' +} + +/** + * 替换文本中的API密钥为*号 + * @param text 原始文本 + * @returns 替换后的文本 + */ +export const maskApiKeys = (text: string): string => { + if (!text) return text + let result = text + + Object.keys(API_KEY_PREFIX).map(type => { + const key = type as keyof typeof API_KEY_PREFIX + result = result.replace(API_KEY_PATTERNS[key as keyof typeof API_KEY_PREFIX], (match) => { + const prefixLength = API_KEY_PREFIX[key].length + const prefix = match.substring(0, prefixLength) + return prefix + '*'.repeat(match.length - prefixLength) + }) + }) + + return result +} + +/** + * 检测文本中是否包含API密钥 + * @param text 待检测文本 + * @returns 是否包含API密钥 + */ +export const hasApiKeys = (text: string): boolean => { + return Object.values(API_KEY_PATTERNS).some(pattern => pattern.test(text)) +} \ No newline at end of file diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index 647b30f6..447ff88c 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -302,6 +302,10 @@ export const request = { // 获取父级域名 const getParentDomain = () => { const hostname = window.location.hostname + // 检查是否为IP地址 + if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { + return hostname + } const parts = hostname.split('.') return parts.length > 2 ? `.${parts.slice(-2).join('.')}` : hostname } diff --git a/web/src/utils/stream.ts b/web/src/utils/stream.ts index 2c827c2f..3ef1db39 100644 --- a/web/src/utils/stream.ts +++ b/web/src/utils/stream.ts @@ -3,7 +3,47 @@ import i18n from '@/i18n' import { cookieUtils } from './request' const API_PREFIX = '/api' -export const handleSSE = async (url: string, data: any, onMessage?: (data: string) => void, config = {}) => { +export interface SSEMessage { + event?: string + data?: string | object +} +export function parseSSEToJSON(sseString: string) { + const events: SSEMessage[] = [] + const lines = sseString.trim().split('\n') + + let currentEvent: SSEMessage = {} + + try { + for (const line of lines) { + if (line.startsWith('event:')) { + if (Object.keys(currentEvent).length > 0) { + events.push(currentEvent) + currentEvent = {} + } + currentEvent.event = line.substring(6).trim() + } else if (line.startsWith('data:')) { + const dataStr = line.substring(5).trim() + try { + currentEvent.data = JSON.parse(dataStr.replace(/"/g, '"')) + } catch { + currentEvent.data = dataStr + } + } + } + + if (Object.keys(currentEvent).length > 0) { + events.push(currentEvent) + } + + return events + } catch (error) { + console.error('Parse stream error:', error) + return [] + } +} + + +export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMessage[]) => void, config = { headers: {} }) => { try { const token = cookieUtils.get('authToken'); const response = await fetch(`${API_PREFIX}${url}`, { @@ -37,7 +77,7 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: strin const chunk = decoder.decode(value, { stream: true }); if (onMessage) { - onMessage(chunk); + onMessage(parseSSEToJSON(chunk) ?? {}); } } break; diff --git a/web/src/views/ApiKeyManagement/components/ApiKeyDetailModal.tsx b/web/src/views/ApiKeyManagement/components/ApiKeyDetailModal.tsx new file mode 100644 index 00000000..2899a306 --- /dev/null +++ b/web/src/views/ApiKeyManagement/components/ApiKeyDetailModal.tsx @@ -0,0 +1,102 @@ +import { forwardRef, useImperativeHandle, useState } from 'react'; +import { Switch, Button, Tooltip } from 'antd'; +import clsx from 'clsx'; +import { useTranslation } from 'react-i18next'; +import type { ApiKey, ApiKeyModalRef } from '../types'; +import RbModal from '@/components/RbModal' +import { getApiKey } from '@/api/apiKey'; +import { formatDateTime } from '@/utils/format' +import Tag from '@/components/Tag' +import { maskApiKeys } from '@/utils/apiKeyReplacer'; + +const ApiKeyDetailModal = forwardRef void }>(({ handleCopy }, ref) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + const [data, setData] = useState({} as ApiKey) + + // 封装取消方法,添加关闭弹窗逻辑 + const handleClose = () => { + setVisible(false); + }; + + const handleOpen = (apiKey?: ApiKey) => { + if (apiKey?.id) { + getApiKey(apiKey.id) + .then((res) => { + setVisible(true); + setData(res as ApiKey) + }) + } + }; + + // 暴露给父组件的方法 + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + return ( + +
    {t('apiKey.baseInfo')}
    + {['id', 'name', 'is_expired', 'created_at'].map((key, index) => ( +
    + {t(`apiKey.${key}`)} + + { key === 'created_at' + ? formatDateTime(data[key], 'YYYY-MM-DD HH:mm:ss') + : key === 'is_expired' + ? {data[key] ? t('apiKey.inactive') : t('apiKey.active')} + : {String(data[key as keyof ApiKey])} + } + +
    + ))} + +
    + {maskApiKeys(data.api_key)} + + +
    + +
    {t('apiKey.permissionInfo')}
    + +
    + {t(`apiKey.memoryEngine`)} + + + +
    +
    + {t(`apiKey.knowledgeBase`)} + + + +
    + + {/* 高级设置 */} + {data.expires_at && <> +
    {t('apiKey.advancedSettings')}
    + +
    + {t(`apiKey.expires_at`)} + + {data.expires_at ? formatDateTime(data.expires_at as number, 'YYYY-MM-DD HH:mm:ss') : '-'} + +
    + } +
    + ); +}); + +export default ApiKeyDetailModal; \ No newline at end of file diff --git a/web/src/views/ApiKeyManagement/components/ApiKeyModal.tsx b/web/src/views/ApiKeyManagement/components/ApiKeyModal.tsx new file mode 100644 index 00000000..f0bf4e11 --- /dev/null +++ b/web/src/views/ApiKeyManagement/components/ApiKeyModal.tsx @@ -0,0 +1,153 @@ +import { forwardRef, useImperativeHandle, useState } from 'react'; +import { Form, Input, Switch, App, DatePicker } from 'antd'; +import { useTranslation } from 'react-i18next'; +import type { ApiKey, ApiKeyModalRef } from '../types'; +import RbModal from '@/components/RbModal' +import dayjs from 'dayjs' +import { createApiKey, updateApiKey } from '@/api/apiKey'; + +const FormItem = Form.Item; + +interface CreateModalProps { + refresh: () => void; +} + +const ApiKeyModal = forwardRef(({ + refresh, +}, ref) => { + const { t } = useTranslation(); + const { message } = App.useApp(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [editVo, setEditVo] = useState(null); + + // 封装取消方法,添加关闭弹窗逻辑 + const handleClose = () => { + setVisible(false); + form.resetFields(); + setLoading(false); + setEditVo(null); + }; + + const handleOpen = (apiKey?: ApiKey) => { + if (apiKey?.id) { + const { scopes = [], expires_at } = apiKey + // 编辑模式,填充表单 + form.setFieldsValue({ + name: apiKey.name, + description: apiKey.description, + memory: scopes.includes('memory'), + rag: scopes.includes('rag'), + expires_at: expires_at ? dayjs(expires_at) : undefined + }); + setEditVo(apiKey); + } + setVisible(true); + }; + + // 封装保存方法,添加提交逻辑 + const handleSave = async () => { + form.validateFields() + .then((values) => { + const { memory, rag, expires_at, ...rest } = values + let scopes = [] + + if (memory) { + scopes.push('memory') + } + if (rag) { + scopes.push('rag') + } + // 准备新的/更新的API Key数据 + const apiKeyData = { + ...rest, + scopes, + expires_at: expires_at ? dayjs(expires_at.valueOf()).endOf('day').valueOf() : null, + type: 'service' + }; + setLoading(true) + const req = editVo?.id ? updateApiKey(editVo.id, apiKeyData as ApiKey) : createApiKey(apiKeyData as ApiKey) + + req.then(() => { + refresh(); + handleClose(); + message.success(t(editVo ? 'common.updateSuccess' : 'common.createSuccess')); + }) + .finally(() => setLoading(false)) + }) + } + + // 暴露给父组件的方法 + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + return ( + +
    +
    {t('apiKey.baseInfo')}
    + + + + + + + + +
    {t('apiKey.permissionInfo')}
    + + + + + + + + + + {/* 高级设置 */} +
    {t('apiKey.advancedSettings')}
    + + + current && current < dayjs().subtract(1, 'day').endOf('day')} + /> + +
    +
    + ); +}); + +export default ApiKeyModal; \ No newline at end of file diff --git a/web/src/views/ApiKeyManagement/index.tsx b/web/src/views/ApiKeyManagement/index.tsx new file mode 100644 index 00000000..eaed20c4 --- /dev/null +++ b/web/src/views/ApiKeyManagement/index.tsx @@ -0,0 +1,126 @@ +import React, { useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, App, Space } from 'antd'; +import clsx from 'clsx'; +import { DeleteOutlined, EditOutlined, EyeOutlined } from '@ant-design/icons'; +import type { ApiKey, ApiKeyModalRef } from './types'; +import ApiKeyModal from './components/ApiKeyModal'; +import ApiKeyDetailModal from './components/ApiKeyDetailModal'; +import RbCard from '@/components/RbCard/Card' +import { getApiKeyListUrl, deleteApiKey } from '@/api/apiKey'; +import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList' +import { formatDateTime } from '@/utils/format'; +import Tag from '@/components/Tag' +import copy from 'copy-to-clipboard' +import { maskApiKeys } from '@/utils/apiKeyReplacer'; + +const ApiKeyManagement: React.FC = () => { + const { t } = useTranslation(); + const { modal, message } = App.useApp(); + const apiKeyModalRef = useRef(null); + const apiKeyDetailModalRef = useRef(null) + const scrollListRef = useRef(null) + + const refresh = () => { + scrollListRef.current?.refresh(); + } + + const handleEdit = (item?: ApiKey) => { + apiKeyModalRef.current?.handleOpen(item); + } + const handleView = (item: ApiKey) => { + apiKeyDetailModalRef.current?.handleOpen(item); + } + const handleDelete = (item: ApiKey) => { + modal.confirm({ + title: t('common.confirmDeleteDesc', { name: item.name }), + okText: t('common.delete'), + cancelText: t('common.cancel'), + okType: 'danger', + onOk: () => { + deleteApiKey(item.id) + .then(() => { + refresh(); + message.success(t('common.deleteSuccess')) + }) + } + }) + } + const handleCopy = (content: string) => { + copy(content) + message.success(t('common.copySuccess')) + } + return ( + <> +
    + +
    + + ) => { + let apiKeyItem = item as unknown as ApiKey + return ( + + {['id', 'is_expired', 'created_at'].map((key, index) => ( +
    + {t(`apiKey.${key}`)} + + { key === 'created_at' + ? formatDateTime(apiKeyItem[key], 'YYYY-MM-DD HH:mm:ss') + : key === 'is_expired' + ? {apiKeyItem[key] ? t('apiKey.inactive') : t('apiKey.active')} + : String(apiKeyItem[key as keyof ApiKey]) + } + +
    + ))} + +
    + {maskApiKeys(apiKeyItem.api_key)} + + +
    + + + {apiKeyItem.scopes?.includes('memory') && {t('apiKey.memoryEngine')}} + {apiKeyItem.scopes?.includes('rag') && {t('apiKey.knowledgeBase')}} + + +
    + + + +
    +
    + ); + }} + /> + + + + + ); +}; + +export default ApiKeyManagement; \ No newline at end of file diff --git a/web/src/views/ApiKeyManagement/types.ts b/web/src/views/ApiKeyManagement/types.ts new file mode 100644 index 00000000..2df67193 --- /dev/null +++ b/web/src/views/ApiKeyManagement/types.ts @@ -0,0 +1,40 @@ +import type { Dayjs } from 'dayjs' +import { maskApiKeys } from '@/utils/apiKeyReplacer' + +export interface ApiKey { + id: string; + name: string; + description?: string; + type: 'agent' | 'multi_agent' | 'workflow' | 'service'; + scopes?: string[]; // 'memory' | 'rag' | 'app' + + api_key: string; + is_active: boolean; + is_expired: boolean; + created_at: number; + expires_at?: number | Dayjs; + memory?: boolean; + rag?: boolean; + + + updated_at: string; + qps_limit?: number; + daily_request_limit?: number; + + rate_limit?: number; + total_requests: number; + quota_used: number; + quota_limit: number; +} + +export interface ApiKeyModalRef { + handleOpen: (apiKey?: ApiKey) => void; + handleClose: () => void; +} + +/** + * 获取掩码后的API密钥 + */ +export const getMaskedApiKey = (apiKey: string): string => { + return maskApiKeys(apiKey) +} \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index dc600f1f..c6aa63e8 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -17,6 +17,8 @@ import type { KnowledgeConfig, Variable, MemoryConfig, + AiPromptModalRef, + Source } from './types' import type { Model } from '@/views/ModelManagement/types' import { getModelList } from '@/api/models'; @@ -27,19 +29,19 @@ import { getApplicationConfig } from '@/api/application' import { getKnowledgeBaseList } from '@/api/knowledgeBase' import { memoryConfigListUrl } from '@/api/memory' import CustomSelect from '@/components/CustomSelect' - - +import aiPrompt from '@/assets/images/application/aiPrompt.png' +import AiPromptModal from './components/AiPromptModal' const DescWrapper: FC<{desc: string, className?: string}> = ({desc, className}) => { return ( -
    +
    {desc}
    ) } const LabelWrapper: FC<{title: string, className?: string; children?: ReactNode}> = ({title, className, children}) => { return ( -
    +
    {title} {children}
    @@ -50,12 +52,12 @@ const SwitchWrapper: FC<{ title: string, desc: string, name: string }> = ({ titl return (
    - + @@ -66,11 +68,11 @@ const SelectWrapper: FC<{ title: string, desc: string, name: string, url: string const { t } = useTranslation(); return ( <> - + - + ) } @@ -199,7 +201,7 @@ const Agent = forwardRef((_props, ref) => { }) } - const refresh = (vo: ModelConfig, type: 'model' | 'chat') => { + const refresh = (vo: ModelConfig, type: Source) => { if (type === 'model') { const { default_model_config_id, ...rest } = vo form.setFieldsValue({ @@ -239,6 +241,7 @@ const Agent = forwardRef((_props, ref) => { return [ ...(prev || []).map(item => ({ ...item, + conversation_id: undefined, list: [] })), newChatItem @@ -306,13 +309,10 @@ const Agent = forwardRef((_props, ref) => { }) } const getModels = () => { - const requests = [getModelList({ type: 'llm', pagesize: 100, page: 1 }), getModelList({ type: 'chat', pagesize: 100, page: 1 })] - Promise.all(requests) - .then(responses => { - const [chatRes, modelRes] = responses as { items: Model[] }[] - const chatList = chatRes.items || [] - const modelList = modelRes.items || [] - setModelList([...chatList, ...modelList]) + getModelList({ type: 'llm,chat', pagesize: 100, page: 1 }) + .then(res => { + const response = res as { items: Model[] } + setModelList(response.items) }) } const handleAddModel = () => { @@ -335,15 +335,22 @@ const Agent = forwardRef((_props, ref) => { handleSave })) + const aiPromptModalRef = useRef(null) + const handlePrompt = () => { + aiPromptModalRef.current?.handleOpen() + } + const updatePrompt = (value: string) => { + form.setFieldValue('system_prompt', value) + } return ( <> {loading && } -
    +
    - + ((_props, ref) => { -
    +
    {t('application.debuggingAndPreview')} -
    +
    @@ -435,10 +446,14 @@ const Agent = forwardRef((_props, ref) => { + ); }); diff --git a/web/src/views/ApplicationConfig/Api.tsx b/web/src/views/ApplicationConfig/Api.tsx index 90612687..02c066e4 100644 --- a/web/src/views/ApplicationConfig/Api.tsx +++ b/web/src/views/ApplicationConfig/Api.tsx @@ -1,153 +1,195 @@ -import { type FC, useState } from 'react'; +import { type FC, useState, useRef, useEffect } from 'react'; +import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; -import { Button, Space, App - // Slider, Input, - // Form, - // Checkbox -} from 'antd'; +import { Button, Space, App, Statistic, Row, Col } from 'antd'; import copy from 'copy-to-clipboard' import Card from './components/Card'; -// import qpsRestrictions from '@/assets/images/application/qpsRestrictions.svg' -// import dailyAdjustmentDosage from '@/assets/images/application/dailyAdjustmentDosage.svg' -// import tokenCap from '@/assets/images/application/tokenCap.svg' +import type { Application } from '@/views/ApplicationManagement/types' +import type { ApiKeyModalRef, ApiKeyConfigModalRef } from './types' +import type { ApiKey } from '@/views/ApiKeyManagement/types' +import ApiKeyModal from './components/ApiKeyModal'; +import ApiKeyConfigModal from './components/ApiKeyConfigModal'; +import Tag from '@/components/Tag' +import { getApiKeyList, getApiKeyStats, deleteApiKey } from '@/api/apiKey'; +import { maskApiKeys } from '@/utils/apiKeyReplacer' -// const limitList = [ -// { key: 'qpsRestrictions', value: '10', icon: qpsRestrictions, unit: ' times/second' }, -// { key: 'dailyAdjustmentDosage', value: '1000', icon: dailyAdjustmentDosage, unit: ' times/day' }, -// { key: 'tokenCap', value: '10', icon: tokenCap, unit: 'M Tokens/day' }, -// ] -// const sdkList = ['pythonSDK', 'nodejsSDK', 'goSDK', 'curlExample'] - -const Api: FC<{apiKeyList?: string[]}> = ({apiKeyList = []}) => { +const Api: FC<{ application: Application | null }> = ({ application }) => { const { t } = useTranslation(); - const [activeMethods, setActiveMethod] = useState(['GET']); - const { message } = App.useApp() - // const [form] = Form.useForm(); + const activeMethods = ['GET']; + const { message, modal } = App.useApp() const copyContent = window.location.origin + '/v1/chat' + const apiKeyModalRef = useRef(null); + const apiKeyConfigModalRef = useRef(null); + const [apiKeyList, setApiKeyList] = useState([]) const handleCopy = (content: string) => { copy(content) message.success(t('common.copySuccess')) } - return ( -
    - {/*
    */} - - -
    - - {['GET', 'POST', 'PUT', 'DELETE'].map((method) => ( - - ))} - -
    - {copyContent} - - +
    +
    +
    + + {t('application.addApiKey')} + } + > +
    {t('application.apiKeySubTitle')}
    + {/* 总览数据 */} + + + + + + + + + {/* API Key 列表 */} + {apiKeyList.sort((a, b) => b.created_at - a.created_at).map(item => ( +
    +
    +
    +
    {item.name}
    + ID: {item.id} +
    +
    handleEdit(item)} + >
    +
    handleDelete(item)} + >
    +
    +
    +
    + {maskApiKeys(item.api_key)} + +
    + + + + + + + + +
    -
    - + {t('application.addApiKey')} - // } - > -
    - {t('application.apiKeyTitle')} -

    {t('application.apiKeyDesc')}

    -
    - {apiKeyList.map((item, index) => ( -
    - {item} + ))} + + - - - {/*
    handleDelete(index)} - >
    */} -
    -
    - ))} -
    - {/* -
    - {t('application.requestExample')} - -
    -
    - curl -X POST https://api.example.com/v1/agent/execute \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d -
    - -
    - {t('application.responseExample')} -
    -
    - curl -X POST https://api.example.com/v1/agent/execute \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d -
    -
    - -
    - {limitList.map(item => ( -
    -
    -
    - {t(`application.${item.key}`)} -
    {item.value}{item.unit}
    -
    - -
    - -
    - ))} -
    -
    - -
    - {sdkList.map(item => ( -
    - {t(`application.${item}`)} -
    - ))} -
    -
    - - {t('application.WebhookReturnsTimeout')} ({t('application.WebhookReturnsTimeoutDesc')})} - > - - - {t('application.whitelistIP')} ({t('application.whitelistIPDesc')})} - > - - - - {t('application.publicAPIDocumentation')} - - */} -
    - {/*
    */} + +
    ); } diff --git a/web/src/views/ApplicationConfig/Cluster.tsx b/web/src/views/ApplicationConfig/Cluster.tsx index 66e8f5a9..ec38c96a 100644 --- a/web/src/views/ApplicationConfig/Cluster.tsx +++ b/web/src/views/ApplicationConfig/Cluster.tsx @@ -1,27 +1,31 @@ -import { type FC, useEffect, useState, useRef, type Key } from 'react' +import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react' import { useTranslation } from 'react-i18next' import { useParams } from 'react-router-dom'; import Card from './components/Card' -import { Form, Space, Row, Col, Button, Flex, App } from 'antd' -import type { DefaultOptionType } from 'antd/es/select' +import { Form, Space, Row, Col, Button, Flex, App, Select } from 'antd' import Tag, { type TagProps } from './components/Tag' import CustomSelect from '@/components/CustomSelect'; -import { getApplicationListUrl, getMultiAgentConfig, saveMultiAgentConfig } from '@/api/application'; +import { getMultiAgentConfig, saveMultiAgentConfig } from '@/api/application'; import type { Config, SubAgentModalRef, ChatData, - SubAgentItem + SubAgentItem, + ClusterRef, + ModelConfigModalRef } from './types' import Chat from './components/Chat' import RbCard from '@/components/RbCard/Card' import SubAgentModal from './components/SubAgentModal' import Empty from '@/components/Empty' +import RadioGroupCard from '@/components/RadioGroupCard' +import { getModelListUrl } from '@/api/models' +import ModelConfigModal from './components/ModelConfigModal' const tagColors = ['processing', 'warning', 'default'] const MAX_LENGTH = 5; -const Cluster: FC<{application: SubAgentItem}> = ({application}) => { +const Cluster = forwardRef((_props, ref) => { const { t } = useTranslation() const { message } = App.useApp() const [form] = Form.useForm() @@ -37,7 +41,15 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => { ]) const handleSave = (flag = true) => { + if (!data) return Promise.resolve() + if (!values.default_model_config_id) { + message.warning(t('common.selectPlaceholder', { title: t('application.model') })) + return Promise.resolve() + } + const params = { + id: data.id, + app_id: data.app_id, ...values, sub_agents: (subAgents || []).map(item => ({ ...item, @@ -45,6 +57,8 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => { })) } + console.log('params', params) + return new Promise((resolve, reject) => { form.validateFields().then(() => { saveMultiAgentConfig(id as string, params) @@ -58,21 +72,14 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => { reject(error) }) }) - .catch(error => { - reject(error) - }) + .catch(error => { + reject(error) + }) }) } useEffect(() => { getData() }, [id]) - useEffect(() => { - if (application) { - form.setFieldsValue({ - name: application.name, - }) - } - }, [application]) const getData = () => { if (!id) { @@ -91,7 +98,6 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => { subAgentModalRef.current?.handleOpen(agent) } const refreshSubAgents = (agent: SubAgentItem) => { - // setSubAgents(subAgents) const index = subAgents.findIndex(item => item.agent_id === agent.agent_id) const newSubAgents = [...subAgents] if (index === -1) { @@ -108,87 +114,132 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => { const handleDeleteSubAgent = (agent: SubAgentItem) => { setSubAgents(prev => prev.filter(item => item.agent_id !== agent.agent_id)) } - const handleChange = (value: Key, option?: DefaultOptionType | DefaultOptionType[] | undefined) => { - if (option && !Array.isArray(option)) { - form.setFieldsValue({ master_agent_name: option.children }) - } + useImperativeHandle(ref, () => ({ + handleSave + })) + + const modelConfigModalRef = useRef(null) + const handleEditModelConfig = () => { + modelConfigModalRef.current?.handleOpen('multi_agent', values.model_parameters) + } + const handleSaveModelConfig = (values: Config['model_parameters']) => { + form.setFieldsValue({ + model_parameters: values + }) } return ( -
    +
    - - - - - {t('application.agentName')} -
    - } - className="rb:mb-[20px]!" - rules={[{ required: true, message: t('common.pleaseSelect') }]} - > - - -
    + + + ({ + value: type, + label: t(`application.${type}`), + labelDesc: t(`application.${type}Desc`), + disabled: type === 'handoffs' + }))} + allowClear={false} + /> + -
    {t('application.added')}: {subAgents.length}/{MAX_LENGTH}
    +
    {t('application.added')}: {subAgents.length}/{MAX_LENGTH}
    {subAgents.length === 0 ? : subAgents.map((agent, index) => ( - -
    +
    {agent.name?.[0]}
    {agent.name} - {agent.role &&
    {agent.role || '-'}
    } - {agent.capabilities && {agent.capabilities.map((tag, tagIndex) => {tag})}} + {agent.role &&
    {agent.role || '-'}
    } + {agent.capabilities && {agent.capabilities.map((tag, tagIndex) => {tag})}}
    -
    handleSubAgentModal(agent)} >
    -
    handleDeleteSubAgent(agent)} >
    ))} + + + + + + + + + + + + + + + + + + ({ + value: type, + label: t(`application.${type}`), + }))} + /> + + @@ -199,7 +250,7 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => { chatList={chatList} updateChatList={setChatList} handleSave={handleSave} - source="cluster" + source="multi_agent" /> @@ -208,8 +259,13 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => { ref={subAgentModalRef} refresh={refreshSubAgents} /> + ) -} +}) export default Cluster \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/AiPromptModal.tsx b/web/src/views/ApplicationConfig/components/AiPromptModal.tsx index 8bfa374c..a85f5cf1 100644 --- a/web/src/views/ApplicationConfig/components/AiPromptModal.tsx +++ b/web/src/views/ApplicationConfig/components/AiPromptModal.tsx @@ -1,43 +1,136 @@ -import { forwardRef, useImperativeHandle, useState } from 'react'; -import { Row, Col, Space, Button } from 'antd'; +import { forwardRef, useImperativeHandle, useState, useRef } from 'react'; +import { Button, Form, Input, App, Row, Col } from 'antd'; import { useTranslation } from 'react-i18next'; +import clsx from 'clsx' +import copy from 'copy-to-clipboard'; -import type { AiPromptModalRef } from '../types' -// import { request } from '@/utils/request' +import { updatePromptMessages, createPromptSessions } from '@/api/prompt' +import { getModelListUrl } from '@/api/models' +import type { AiPromptModalRef, AiPromptVariableModalRef, AiPromptForm } from '../types' import RbModal from '@/components/RbModal' -import Markdown from '@/components/Markdown'; +import type { Model } from '@/views/ModelManagement/types' +import ChatContent from '@/components/Chat/ChatContent' +import Empty from '@/components/Empty' +import ChatSendIcon from '@/assets/images/application/chatSend.svg' +import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg' +import type { ChatItem } from '@/components/Chat/types' +import CustomSelect from '@/components/CustomSelect' +import AiPromptVariableModal from './AiPromptVariableModal' interface AiPromptModalProps { - refresh: () => void; + refresh: (value: string) => void; + defaultModel: Model | null; } const AiPromptModal = forwardRef(({ - // refresh + refresh, + defaultModel, }, ref) => { const { t } = useTranslation(); + const { message } = App.useApp() const [visible, setVisible] = useState(false); const [loading, setLoading] = useState(false) - const [content, setContent] = useState(''); + const [form] = Form.useForm() + const [chatList, setChatList] = useState([]) + const [variables, setVariables] = useState([]) + const [promptSession, setPromptSession] = useState(null) + const aiPromptVariableModalRef = useRef(null) + const currentPromptRef = useRef(null) + const values = Form.useWatch([], form) // 封装取消方法,添加关闭弹窗逻辑 const handleClose = () => { setVisible(false); setLoading(false) + setChatList([]) + setVariables([]) + form.setFieldsValue({ + message: undefined, + current_prompt: undefined, + }) }; const handleOpen = () => { - setVisible(true); + createPromptSessions() + .then(res => { + const response = res as { id: string } + setPromptSession(response.id) + + if (!values.model_id && defaultModel?.id) { + form.setFieldValue('model_id', defaultModel?.id) + } + setVisible(true); + }) }; - // 封装保存方法,添加提交逻辑 - // const handleSave = () => { - // } + const handleSend = () => { + if (!promptSession) return + if (!values.model_id) { + message.warning(t('common.selectPlaceholder', { title: t('application.model') })) + return + } + if (!values.message) { + message.warning(t('application.promptChatPlaceholder')) + return + } + const messageContent = values.message + setLoading(true) + setChatList(prev => { + return [...prev, { role: 'user', content: messageContent}] + }) + form.setFieldsValue({ message: undefined }) + updatePromptMessages(promptSession, values) + .then(res => { + const response = res as { prompt: string; desc: string; variables: string[] } + form.setFieldsValue({ current_prompt: response.prompt }) + setChatList(prev => { + return [...prev, { role: 'assistant', content: response.desc }] + }) + setVariables(response.variables) + }) + .finally(() => { + setLoading(false) + }) + } + const handleCopy = () => { + if (!values.current_prompt || values?.current_prompt?.trim() === '') return + copy(values.current_prompt) + message.success(t('common.copySuccess')) + } + const handleAdd = () => { + aiPromptVariableModalRef.current?.handleOpen() + } + const handleVariableApply = (value: string) => { + const textArea = currentPromptRef.current?.resizableTextArea?.textArea + if (textArea) { + const cursorPosition = textArea.selectionStart + const currentValue = values.current_prompt || '' + const newValue = currentValue.slice(0, cursorPosition) + value + currentValue.slice(cursorPosition) + form.setFieldValue('current_prompt', newValue) + + // 设置新的光标位置 + setTimeout(() => { + textArea.focus() + textArea.setSelectionRange(cursorPosition + value.length, cursorPosition + value.length) + }, 0) + } else { + form.setFieldValue('current_prompt', (values.current_prompt || '') + value) + } + } + const handleApply = () => { + if (!values.current_prompt) { + return + } + refresh(values.current_prompt) + handleClose() + } // 暴露给父组件的方法 useImperativeHandle(ref, () => ({ handleOpen, })); + console.log(values) return ( (({ footer={null} width={1000} > - - -
    {t('application.generatedPrompt')}
    -
    -
    - +
    +
    + + + + + } + data={chatList || []} + streamLoading={false} + labelPosition="top" + labelFormat={(item) => item.role === 'user' ? t('application.you') : t('application.ai')} + /> + +
    + + + +
    - - -
    {t('application.conversationOptimizationPrompt')}
    -
    -
    - + +
    + + + + + + + + + + + +
    + +
    - - - - - - - - - - +
    + + + ); }); diff --git a/web/src/views/ApplicationConfig/components/AiPromptVariableModal.tsx b/web/src/views/ApplicationConfig/components/AiPromptVariableModal.tsx new file mode 100644 index 00000000..61847d9a --- /dev/null +++ b/web/src/views/ApplicationConfig/components/AiPromptVariableModal.tsx @@ -0,0 +1,107 @@ +import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; +import { Form, Input, App, Select, AutoComplete, type AutoCompleteProps } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import type { Application } from '@/views/ApplicationManagement/types' +import type { AiPromptVariableModalRef } from '../types' +import { createApiKey } from '@/api/apiKey'; +import RbModal from '@/components/RbModal' + +const FormItem = Form.Item; + +interface AiPromptVariableModalProps { + refresh: (value: string) => void; + variables: string[]; +} + +const AiPromptVariableModal = forwardRef(({ + refresh, + variables +}, ref) => { + const { t } = useTranslation(); + const { message } = App.useApp(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false) + const [options, setOptions] = useState([]) + + useEffect(() => { + setOptions(variables.map(key => ({ + value: key, + label: `{{${key}}}` + }))) + }, [variables]) + const handleSearch = (value: string) => { + const filterKeys = variables?.filter(key => key.includes(value)) + + if (filterKeys.length) { + setOptions(filterKeys.map(key => ({ + value: key, + label: `{{${key}}}` + }))) + } else { + setOptions([{ + value: value, + label: `{{${value}}}` + }]) + } + } + + // 封装取消方法,添加关闭弹窗逻辑 + const handleClose = () => { + setVisible(false); + form.resetFields(); + setLoading(false) + }; + + const handleOpen = () => { + setVisible(true); + form.resetFields(); + }; + // 封装保存方法,添加提交逻辑 + const handleSave = () => { + const variableName = form.getFieldValue('variableName') + + if (!variableName) return + + refresh(`{{${variableName}}}`) + handleClose() + } + + // 暴露给父组件的方法 + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + return ( + +
    + + + +
    +
    + ); +}); + +export default AiPromptVariableModal; \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/ApiKeyConfigModal.tsx b/web/src/views/ApplicationConfig/components/ApiKeyConfigModal.tsx new file mode 100644 index 00000000..1b4f3f6e --- /dev/null +++ b/web/src/views/ApplicationConfig/components/ApiKeyConfigModal.tsx @@ -0,0 +1,131 @@ +import { forwardRef, useImperativeHandle, useState } from 'react'; +import { Form, Slider } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import type { ApiKeyConfigModalRef } from '../types' +import RbModal from '@/components/RbModal' +import { updateApiKey } from '@/api/apiKey'; +import type { ApiKey } from '@/views/ApiKeyManagement/types' + +interface ApiKeyConfigModalProps { + refresh: () => void; +} +const ApiKeyConfigModal = forwardRef(({ + refresh +}, ref) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false) + const values = Form.useWatch([], form) + const [editVo, setEditVo] = useState(null) + + // 封装取消方法,添加关闭弹窗逻辑 + const handleClose = () => { + form.resetFields(); + setLoading(false) + setEditVo(null) + setVisible(false); + }; + + const handleOpen = (apiKey: ApiKey) => { + setVisible(true); + setEditVo(apiKey) + form.setFieldsValue({ + daily_request_limit: apiKey.daily_request_limit, + rate_limit: apiKey.rate_limit + }); + }; + // 封装保存方法,添加提交逻辑 + const handleSave = () => { + if (!editVo?.id) return + form.validateFields() + .then((values) => { + updateApiKey(editVo.id, { + ...editVo, + ...values + }) + handleClose() + setTimeout(() => { + refresh() + }, 50) + }) + } + + // 暴露给父组件的方法 + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + return ( + +
    + {/* QPS 限制(每秒请求数) */} + <> +
    + {t(`application.qpsLimit`)}({t('application.qpsLimitTip')}) +
    +
    + {t('application.qpsLimitDesc')} +
    +
    + + + +
    + 1 + {t('application.currentValue')}: {values?.rate_limit}{t('application.qpsLimitUnit')} +
    +
    + + {/* 日调用量限制 */} + <> +
    + {t(`application.dailyUsageLimit`)} +
    +
    + {t('application.dailyUsageLimitDesc')} +
    +
    + + + +
    + 100 + {t('application.currentValue')}: {values?.daily_request_limit}{t('application.dailyUsageLimitUnit')} +
    +
    + +
    +
    + ); +}); + +export default ApiKeyConfigModal; \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/ApiKeyModal.tsx b/web/src/views/ApplicationConfig/components/ApiKeyModal.tsx new file mode 100644 index 00000000..54740436 --- /dev/null +++ b/web/src/views/ApplicationConfig/components/ApiKeyModal.tsx @@ -0,0 +1,104 @@ +import { forwardRef, useImperativeHandle, useState } from 'react'; +import { Form, Input, App } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import type { Application } from '@/views/ApplicationManagement/types' +import type { ApiKeyModalRef } from '../types' +import { createApiKey } from '@/api/apiKey'; +import RbModal from '@/components/RbModal' + +const FormItem = Form.Item; + +interface ApiKeyModalProps { + refresh: () => void; + application?: Application | null; +} + +const ApiKeyModal = forwardRef(({ + refresh, + application +}, ref) => { + const { t } = useTranslation(); + const { message } = App.useApp(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false) + + // 封装取消方法,添加关闭弹窗逻辑 + const handleClose = () => { + setVisible(false); + form.resetFields(); + setLoading(false) + }; + + const handleOpen = () => { + setVisible(true); + form.resetFields(); + }; + // 封装保存方法,添加提交逻辑 + const handleSave = () => { + if (!application) return + form.validateFields() + .then((values) => { + setLoading(true) + createApiKey({ + ...values, + type: application.type, + resource_id: application.id, + scopes: ['app'] + }) + .then(() => { + handleClose() + refresh() + message.success(t('common.createSuccess')) + }) + .finally(() => { + setLoading(false) + }) + }) + } + + // 暴露给父组件的方法 + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + return ( + +
    + {/* Key 名称 */} + + + + {/* 描述 */} + + + +
    +
    + ); +}); + +export default ApiKeyModal; \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/Chat.tsx b/web/src/views/ApplicationConfig/components/Chat.tsx index 9a70b5f2..bd826ba1 100644 --- a/web/src/views/ApplicationConfig/components/Chat.tsx +++ b/web/src/views/ApplicationConfig/components/Chat.tsx @@ -1,46 +1,125 @@ -import { type FC, useRef, useEffect, useState } from 'react'; +import { type FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import clsx from 'clsx' import { Input, Form } from 'antd' import ChatIcon from '@/assets/images/application/chat.png' import ChatSendIcon from '@/assets/images/application/chatSend.svg' import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png' -import type { ChatItem, ChatData, Config } from '../types' +import type { ChatData, Config } from '../types' import { runCompare, draftRun } from '@/api/application' import Empty from '@/components/Empty' -import Markdown from '@/components/Markdown' +import ChatContent from '@/components/Chat/ChatContent' +import type { ChatItem } from '@/components/Chat/types' +import { type SSEMessage } from '@/utils/stream' interface ChatProps { chatList: ChatData[]; data: Config; - updateChatList: (list: ChatData[]) => void; - handleSave: (flag?: boolean) => Promise; - source?: 'cluster' | 'agent'; + updateChatList: React.Dispatch>; + handleSave: (flag?: boolean) => Promise; + source?: 'multi_agent' | 'agent'; } const Chat: FC = ({ chatList, data, updateChatList, handleSave, source = 'agent' }) => { const { t } = useTranslation(); const [form] = Form.useForm<{ message: string }>() - const scrollContainerRefs = useRef<(HTMLDivElement | null)[]>([]) const [loading, setLoading] = useState(false) - const [isCluster, setIsCluster] = useState(source === 'cluster') + const [isCluster, setIsCluster] = useState(source === 'multi_agent') const [conversationId, setConversationId] = useState(null) const [compareLoading, setCompareLoading] = useState(false) - - // 当聊天列表更新时,自动滚动到底部 + useEffect(() => { - // 延迟一下,确保DOM已经更新 - setTimeout(() => { - scrollContainerRefs.current.forEach(container => { - if (container) { - container.scrollTop = container.scrollHeight; - } - }); - }, 0); - }, [chatList]); - useEffect(() => { - setIsCluster(source === 'cluster') + setIsCluster(source === 'multi_agent') }, [source]) + const addUserMessage = (message: string) => { + const newUserMessage: ChatItem = { + role: 'user', + content: message, + created_at: Date.now(), + }; + updateChatList(prev => prev.map(item => ({ + ...item, + list: [...(item.list || []), newUserMessage] + }))) + } + const addAssistantMessage = () => { + const assistantMessage: ChatItem = { + role: 'assistant', + content: '', + created_at: Date.now(), + }; + + if (isCluster) { + updateChatList(prev => prev.map(item => ({ + ...item, + list: [...(item.list || []), assistantMessage] + }))) + } else { + const assistantMessages: Record = {} + chatList.forEach(item => { + assistantMessages[item.model_config_id as string] = assistantMessage + }) + updateChatList(prev => prev.map(item => ({ + ...item, + list: [...(item.list || []), assistantMessages[item.model_config_id as string]] + }))) + } + } + const updateAssistantMessage = (content?: string, model_config_id?: string, conversation_id?: string) => { + if (!content || !model_config_id) return + updateChatList(prev => { + const targetIndex = prev.findIndex(item => item.model_config_id === model_config_id); + if (targetIndex !== -1) { + const modelChatList = [...prev] + const curModelChat = modelChatList[targetIndex] + const curChatMsgList = curModelChat.list || [] + const lastMsg = curChatMsgList[curChatMsgList.length - 1] + if (lastMsg && lastMsg.role === 'assistant') { + modelChatList[targetIndex] = { + ...modelChatList[targetIndex], + conversation_id: conversation_id, + list: [ + ...curChatMsgList.slice(0, curChatMsgList.length - 1), + { + ...lastMsg, + content: lastMsg.content + content + } + ] + } + } + return [...modelChatList] + } + return prev; + }) + } + const updateErrorAssistantMessage = (message_length: number, model_config_id?: string) => { + if (message_length > 0 || !model_config_id) return + + updateChatList(prev => { + const targetIndex = prev.findIndex(item => item.model_config_id === model_config_id); + if (targetIndex > -1) { + const modelChatList = [...prev] + const curModelChat = modelChatList[targetIndex] + const curChatMsgList = curModelChat.list || [] + const lastMsg = curChatMsgList[curChatMsgList.length - 1] + if (lastMsg.role === 'assistant') { + modelChatList[targetIndex] = { + ...modelChatList[targetIndex], + list: [ + ...curChatMsgList.slice(0, curChatMsgList.length - 1), + { + ...lastMsg, + content: null + } + ] + } + } + return [...modelChatList] + } + + return prev + }) + } const handleSend = () => { if (loading) return setLoading(true) @@ -48,182 +127,47 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc handleSave(false) .then(() => { const message = form.getFieldValue('message') - if (!message || message.trim() === '') return - const newUserMessage: ChatItem = { - role: 'question', - content: message, - time: Date.now(), - }; - updateChatList((prev: ChatData[]) => { - return prev.map(item => ({ - ...item, - list: [ - ...(item.list || []), - newUserMessage - ] - })) - }) + if (!message?.trim()) return + + addUserMessage(message) form.setFieldsValue({ message: undefined }) - // 添加空的助手消息用于流式更新 - const assistantMessages: Record = {}; - if (isCluster) { - const assistantMessage: ChatItem = { - role: 'answer', - content: '', - time: Date.now(), - }; - assistantMessages['cluster'] = assistantMessage; - updateChatList((prev: ChatData[]) => prev.map(item => ({ - ...item, - list: [...(item.list || []), assistantMessage] - }))) - } else { - chatList.forEach(item => { - const assistantMessage: ChatItem = { - role: 'answer', - content: '', - time: Date.now(), - }; - assistantMessages[item.model_config_id] = assistantMessage; - }); - updateChatList((prev: ChatData[]) => prev.map(item => ({ - ...item, - list: [...(item.list || []), assistantMessages[item.model_config_id]] - }))) - } + addAssistantMessage() - const handleStreamMessage = (data: string) => { + const handleStreamMessage = (data: SSEMessage[]) => { setCompareLoading(false) - try { - const lines = data.split('\n'); - let currentEvent = ''; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - - if (line.startsWith('event:')) { - currentEvent = line.substring(6).trim(); - } else if (line.startsWith('data:') && (!isCluster && currentEvent === 'model_message')) { - const jsonData = line.substring(5).trim(); - const parsed = JSON.parse(jsonData); - - if (parsed.content && parsed.model_config_id) { - const targetIndex = chatList.findIndex(item => item.model_config_id === parsed.model_config_id); - if (targetIndex !== -1) { - updateChatList((prev: ChatData[]) => prev.map((item, index) => { - if (index === targetIndex) { - return { - ...item, - conversation_id: parsed.conversation_id, - list: item.list?.map((msg, msgIndex) => { - if (msgIndex === item.list!.length - 1 && msg.role === 'answer') { - return { ...msg, content: msg.content + parsed.content }; - } - return msg; - }) || [] - }; - } - return item; - })) - } - } - } else if (line.startsWith('data:') && (isCluster && currentEvent === 'message')) { - const jsonData = line.substring(5).trim(); - const parsed = JSON.parse(jsonData); - if (parsed.content) { - updateChatList((prev: ChatData[]) => prev.map((item, index) => { - if (index === 0) { - return { - ...item, - list: item.list?.map((msg, msgIndex) => { - if (msgIndex === item.list!.length - 1 && msg.role === 'answer') { - return { ...msg, content: (msg.content || '') + parsed.content }; - } - return msg; - }) || [] - }; - } - return item; - })) - } - if (parsed.conversation_id) { - setConversationId(parsed.conversation_id); - } - } else if (line.startsWith('data:') && (!isCluster && currentEvent === 'model_end')) { - const jsonData = line.substring(5).trim(); - const parsed = JSON.parse(jsonData); - - if (parsed.message_length === 0 && parsed.model_config_id) { - const targetIndex = chatList.findIndex(item => item.model_config_id === parsed.model_config_id); - if (targetIndex !== -1) { - updateChatList((prev: ChatData[]) => prev.map((item, index) => { - if (index === targetIndex) { - return { - ...item, - list: item.list?.map((msg, msgIndex) => { - if (msgIndex === item.list!.length - 1 && msg.role === 'answer') { - return { ...msg, content: null }; - } - return msg; - }) || [] - }; - } - return item; - })) - } - } - } else if (line.startsWith('data:') && (isCluster && currentEvent === 'model_end')) { - const jsonData = line.substring(5).trim(); - const parsed = JSON.parse(jsonData); - if (parsed.message_length === 0) { - updateChatList((prev: ChatData[]) => prev.map((item, index) => { - if (index === 0) { - return { - ...item, - list: item.list?.map((msg, msgIndex) => { - if (msgIndex === item.list!.length - 1 && msg.role === 'answer') { - return { ...msg, content: null }; - } - return msg; - }) || [] - }; - } - return item; - })) - } - if (parsed.conversation_id) { - setConversationId(parsed.conversation_id); - } - } else if (currentEvent === 'compare_end') { + data.map(item => { + const { model_config_id, conversation_id, content, message_length } = item.data as { model_config_id: string; conversation_id: string; content: string; message_length: number }; + + switch (item.event) { + case 'model_message': + updateAssistantMessage(content, model_config_id, conversation_id) + break; + case 'model_end': + updateErrorAssistantMessage(message_length, model_config_id) + break; + case 'compare_end': setLoading(false); - } + break; } - } catch (e) { - console.error('Parse stream data error:', e); - } + }) }; setTimeout(() => { - if (isCluster) { - draftRun(data.app_id, { message, conversation_id: conversationId, stream: true }, handleStreamMessage) - .finally(() => setLoading(false)) - } else { - runCompare(data.app_id, { - message, - models: chatList.map(item => ({ - model_config_id: item.model_config_id, - label: item.label, - model_parameters: item.model_parameters, - conversation_id: item.conversation_id - })), - variables: {}, - "parallel": true, - "stream": true, - "timeout": 60, - }, handleStreamMessage) - .finally(() => setLoading(false)); - } + runCompare(data.app_id, { + message, + models: chatList.map(item => ({ + model_config_id: item.model_config_id, + label: item.label, + model_parameters: item.model_parameters, + conversation_id: item.conversation_id + })), + variables: {}, + "parallel": true, + "stream": true, + "timeout": 60, + }, handleStreamMessage) + .finally(() => setLoading(false)); }, 0) }) .catch(() => { @@ -231,6 +175,122 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc setCompareLoading(false) }) } + + const addClusterAssistantMessage = () => { + const assistantMessage: ChatItem = { + role: 'assistant', + content: '', + created_at: Date.now(), + }; + updateChatList(prev => prev.map(item => ({ + ...item, + list: [...(item.list || []), assistantMessage] + }))) + } + const updateClusterAssistantMessage = (content?: string) => { + if (!content) return + updateChatList(prev => { + const modelChatList = [...prev] + const curModelChat = modelChatList[0] + const curChatMsgList = curModelChat.list || [] + const lastMsg = curChatMsgList[curChatMsgList.length - 1] + if (lastMsg.role === 'assistant') { + modelChatList[0] = { + ...modelChatList[0], + list: [ + ...curChatMsgList.slice(0, curChatMsgList.length - 1), + { + ...lastMsg, + content: lastMsg.content + content + } + ] + } + } + return [...modelChatList] + }) + } + const updateClusterErrorAssistantMessage = (message_length: number) => { + if (message_length > 0) return + + updateChatList(prev => { + const modelChatList = [...prev] + const curModelChat = modelChatList[0] + const curChatMsgList = curModelChat.list || [] + const lastMsg = curChatMsgList[curChatMsgList.length - 1] + if (lastMsg.role === 'assistant') { + modelChatList[0] = { + ...modelChatList[0], + list: [ + ...curChatMsgList.slice(0, curChatMsgList.length - 1), + { + ...lastMsg, + content: null + } + ] + } + } + return [...modelChatList] + }) + } + const handleClusterSend = () => { + if (loading) return + setLoading(true) + setCompareLoading(true) + handleSave(false) + .then(() => { + const message = form.getFieldValue('message') + if (!message || message.trim() === '') return + addUserMessage(message) + form.setFieldsValue({ message: undefined }) + addClusterAssistantMessage() + + const handleStreamMessage = (data: SSEMessage[]) => { + setCompareLoading(false) + + data.map(item => { + const { conversation_id, content, message_length } = item.data as { conversation_id: string, content: string, message_length: number }; + + switch(item.event) { + case 'start': + if (conversation_id && conversationId !== conversation_id) { + setConversationId(conversation_id); + } + break + case 'message': + updateClusterAssistantMessage(content) + if (conversation_id && conversationId !== conversation_id) { + setConversationId(conversation_id); + } + break; + case 'model_end': + updateClusterErrorAssistantMessage(message_length) + break; + case 'compare_end': + setLoading(false); + break; + } + }) + }; + + setTimeout(() => { + draftRun( + data.app_id, + { + message, + conversation_id: conversationId, + stream: true + }, + handleStreamMessage + ) + .finally(() => setLoading(false)) + }, 0) + }) + .catch(() => { + setLoading(false) + setCompareLoading(false) + }) + } + const handleDelete = (index: number) => { updateChatList(chatList.filter((_, voIndex) => voIndex !== index)) } @@ -258,69 +318,55 @@ const Chat: FC = ({ chatList, data, updateChatList, handleSave, sourc
    {chat.label}
    handleDelete(index)} >
    } - {!chat.list || chat.list.length === 0 - ? - : ( -
    scrollContainerRefs.current[index] = el} className={clsx(`rb:relative rb:overflow-y-auto rb:overflow-x-hidden`, { - 'rb:h-[calc(100vh-186px)]': isCluster, - 'rb:h-[calc(100vh-286px)]': !isCluster, - })}> - {chat.list?.map((vo, voIndex) => { - if (compareLoading && voIndex === chat.list?.length - 1) { - return null - } - return ( -
    -
    {vo.role === 'question' ? 'You' : chat.label}
    -
    - -
    -
    - ) - })} -
    - ) - } + } + data={chat.list || []} + streamLoading={compareLoading} + labelPosition="top" + labelFormat={(item) => item.role === 'user' ? t('application.you') : chat.label} + errorDesc={t('application.ReplyException')} + /> +
    ))}
    -
    +
    - + - + })} onClick={isCluster ? handleClusterSend : handleSend} />
    } diff --git a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx index 34d85aa0..ec899a32 100644 --- a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx +++ b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx @@ -1,6 +1,6 @@ -import { type FC, useRef } from 'react'; +import { type FC, useEffect, useRef } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { Layout, Tabs, Dropdown } from 'antd'; +import { Layout, Tabs, Dropdown, Button } from 'antd'; import type { MenuProps } from 'antd'; import { useTranslation } from 'react-i18next'; import styles from '../index.module.css' @@ -11,7 +11,7 @@ import exportIcon from '@/assets/images/export_hover.svg' import deleteIcon from '@/assets/images/delete_hover.svg' import type { Application, ApplicationModalRef } from '@/views/ApplicationManagement/types'; import ApplicationModal from '@/views/ApplicationManagement/components/ApplicationModal' -import type { CopyModalRef } from '../types' +import type { CopyModalRef, WorkflowRef } from '../types' import { deleteApplication } from '@/api/application' import CopyModal from './CopyModal' @@ -29,8 +29,12 @@ interface ConfigHeaderProps { activeTab: string; handleChangeTab: (key: string) => void; refresh: () => void; + workflowRef: React.RefObject } -const ConfigHeader: FC = ({ application, activeTab, handleChangeTab, refresh }) => { +const ConfigHeader: FC = ({ + application, activeTab, handleChangeTab, refresh, + workflowRef +}) => { const { t } = useTranslation(); const navigate = useNavigate(); const { id } = useParams(); @@ -46,7 +50,7 @@ const ConfigHeader: FC = ({ application, activeTab, handleCha const formatMenuItems = () => { const items = ['edit', 'copy', 'delete'].map(key => ({ key, - icon: , + icon: , label: t(`common.${key}`), })) return { @@ -85,12 +89,23 @@ const ConfigHeader: FC = ({ application, activeTab, handleCha const goToApplication = () => { navigate('/application', { replace: true }) } - + const save = () => { + workflowRef.current?.handleSave() + } + const run = () => { + workflowRef.current?.handleSave(false) + .then(() => { + workflowRef.current?.handleRun() + }) + } + const clear = () => { + workflowRef?.current?.graphRef?.current?.clearCells() + } return ( <> -
    -
    -
    +
    +
    +
    {application?.name[0]}
    @@ -101,7 +116,7 @@ const ConfigHeader: FC = ({ application, activeTab, handleCha placement="bottomRight" >
    @@ -114,10 +129,19 @@ const ConfigHeader: FC = ({ application, activeTab, handleCha className={styles.tabs} />
    -
    - + {application?.type === 'workflow' + ?
    + + + + {/* */} + +
    + :
    + {t('application.returnToApplicationList')}
    + }
    = () => { - const { t } = useTranslation(); - const [options, setOptions] = useState([]); - useEffect(() => { - getProviderList() - }, []); - - const getProviderList = () => { - getModelProviderList().then(res => { - const response = res as string[] - setOptions(response.map((key: string) => ({ - value: key, - label: t(`model.${key}`), - children: [], - isLeaf: false, - }))) - }) - } - const loadData = (selectedOptions: Option[]) => { - const targetOption = selectedOptions[selectedOptions.length - 1]; - console.log(targetOption) - } - return ( - - ); -} -export default CustomSelect; \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/components/ModelConfigModal.tsx b/web/src/views/ApplicationConfig/components/ModelConfigModal.tsx index 002d0b6b..67fd654c 100644 --- a/web/src/views/ApplicationConfig/components/ModelConfigModal.tsx +++ b/web/src/views/ApplicationConfig/components/ModelConfigModal.tsx @@ -2,7 +2,7 @@ import { forwardRef, useImperativeHandle, useState, useEffect } from 'react'; import { Form, Select } from 'antd'; import { useTranslation } from 'react-i18next'; -import type { ModelConfig, ModelConfigModalRef, Config, ChatData } from '../types' +import type { ModelConfig, ModelConfigModalRef, Config, Source } from '../types' import type { Model } from '@/views/ModelManagement/types' import RbModal from '@/components/RbModal' import RbSlider from '@/components/RbSlider' @@ -10,10 +10,9 @@ import RbSlider from '@/components/RbSlider' const FormItem = Form.Item; interface ModelConfigModalProps { - modelList: Model[]; - refresh: (values: ModelConfig, type: 'model') => void; + modelList?: Model[]; + refresh: (values: ModelConfig, type: Source) => void; data: Config; - chatList: ChatData[] } const configFields = [ @@ -28,12 +27,12 @@ const configFields = [ const ModelConfigModal = forwardRef(({ refresh, data, - modelList + modelList = [] }, ref) => { const { t } = useTranslation(); const [visible, setVisible] = useState(false); const [form] = Form.useForm(); - const [source, setSource] = useState<'chat' | 'model'>('model') + const [source, setSource] = useState('model') const values = Form.useWatch([], form); @@ -43,14 +42,14 @@ const ModelConfigModal = forwardRef( form.resetFields(); }; - const handleOpen = (source: 'chat' | 'model', model) => { + const handleOpen = (source: Source, model?: any) => { setSource(source) if (source === 'model') { form.setFieldsValue({ ...(data?.model_parameters || {}), default_model_config_id: data.default_model_config_id || '' }) - } else if (source === 'chat') { + } else if (source === 'chat' || source === 'multi_agent') { if (model) { form.setFieldsValue({ ...(model?.model_parameters || {}), @@ -77,9 +76,9 @@ const ModelConfigModal = forwardRef( console.log('err', err) }); } - const handleChange = (value: string, option: Model) => { + const handleChange = (_value: string, option: Model | Model[] | undefined) => { if (source === 'chat') { - form.setFieldValue('label', option.name) + form.setFieldValue('label', (option as Model).name) } } @@ -104,14 +103,15 @@ const ModelConfigModal = forwardRef(