Compare commits
9 Commits
5d956dd712
...
14d656eea3
| Author | SHA1 | Date | |
|---|---|---|---|
| 14d656eea3 | |||
| 765a968e63 | |||
| 8249f67351 | |||
| 85b4c51fd7 | |||
| 03540fb9e9 | |||
| 7791d198f1 | |||
| fdd6b2c17d | |||
| ecb885ee5e | |||
| e34d4bcd37 |
8
agent/app/core/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Core 模块 - AI 核心能力
|
||||
"""
|
||||
from . import tools
|
||||
|
||||
__all__ = [
|
||||
"tools",
|
||||
]
|
||||
130
agent/app/core/tools/__init__.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
Agent 工具模块
|
||||
"""
|
||||
from .registry import ToolRegistry, ToolMetadata, SecurityLevel, global_registry
|
||||
from . import impl
|
||||
|
||||
# 导入所有工具函数和定义
|
||||
from .impl import (
|
||||
# 文件操作
|
||||
read_file,
|
||||
write_file,
|
||||
list_dir,
|
||||
delete_file,
|
||||
search_files,
|
||||
READ_FILE_TOOL,
|
||||
WRITE_FILE_TOOL,
|
||||
LIST_DIR_TOOL,
|
||||
DELETE_FILE_TOOL,
|
||||
SEARCH_FILES_TOOL,
|
||||
|
||||
# 代码执行
|
||||
execute_python,
|
||||
execute_javascript,
|
||||
execute_bash,
|
||||
EXECUTE_PYTHON_TOOL,
|
||||
EXECUTE_JAVASCRIPT_TOOL,
|
||||
EXECUTE_BASH_TOOL,
|
||||
|
||||
# 网页
|
||||
web_fetch,
|
||||
web_search,
|
||||
WEB_FETCH_TOOL,
|
||||
WEB_SEARCH_TOOL,
|
||||
|
||||
# HTTP
|
||||
http_request,
|
||||
http_get,
|
||||
http_post,
|
||||
http_put,
|
||||
http_delete,
|
||||
HTTP_REQUEST_TOOL,
|
||||
|
||||
# 通知
|
||||
send_notification,
|
||||
send_email,
|
||||
send_webhook,
|
||||
SEND_NOTIFICATION_TOOL,
|
||||
|
||||
# 时间
|
||||
get_current_time,
|
||||
format_time,
|
||||
GET_CURRENT_TIME_TOOL,
|
||||
)
|
||||
|
||||
|
||||
def register_all_tools(registry: ToolRegistry = None):
|
||||
"""
|
||||
注册所有工具到注册表
|
||||
|
||||
Args:
|
||||
registry: 工具注册表,默认使用全局注册表
|
||||
"""
|
||||
reg = registry or global_registry
|
||||
|
||||
# 文件操作
|
||||
reg.register("read_file", read_file, READ_FILE_TOOL["description"], "safe", parameters=READ_FILE_TOOL["parameters"])
|
||||
reg.register("write_file", write_file, WRITE_FILE_TOOL["description"], "review", parameters=WRITE_FILE_TOOL["parameters"])
|
||||
reg.register("list_dir", list_dir, LIST_DIR_TOOL["description"], "safe", parameters=LIST_DIR_TOOL["parameters"])
|
||||
reg.register("delete_file", delete_file, DELETE_FILE_TOOL["description"], "danger", parameters=DELETE_FILE_TOOL["parameters"])
|
||||
reg.register("search_files", search_files, SEARCH_FILES_TOOL["description"], "safe", parameters=SEARCH_FILES_TOOL["parameters"])
|
||||
|
||||
# 代码执行
|
||||
reg.register("execute_python", execute_python, EXECUTE_PYTHON_TOOL["description"], "review", parameters=EXECUTE_PYTHON_TOOL["parameters"])
|
||||
reg.register("execute_javascript", execute_javascript, EXECUTE_JAVASCRIPT_TOOL["description"], "review", parameters=EXECUTE_JAVASCRIPT_TOOL["parameters"])
|
||||
reg.register("execute_bash", execute_bash, EXECUTE_BASH_TOOL["description"], "danger", parameters=EXECUTE_BASH_TOOL["parameters"])
|
||||
|
||||
# 网页
|
||||
reg.register("web_fetch", web_fetch, WEB_FETCH_TOOL["description"], "safe", parameters=WEB_FETCH_TOOL["parameters"])
|
||||
reg.register("web_search", web_search, WEB_SEARCH_TOOL["description"], "safe", parameters=WEB_SEARCH_TOOL["parameters"])
|
||||
|
||||
# HTTP
|
||||
reg.register("http_request", http_request, HTTP_REQUEST_TOOL["description"], "safe", parameters=HTTP_REQUEST_TOOL["parameters"])
|
||||
|
||||
# 通知
|
||||
reg.register("send_notification", send_notification, SEND_NOTIFICATION_TOOL["description"], "safe", parameters=SEND_NOTIFICATION_TOOL["parameters"])
|
||||
|
||||
# 时间
|
||||
reg.register("get_current_time", get_current_time, GET_CURRENT_TIME_TOOL["description"], "safe", parameters=GET_CURRENT_TIME_TOOL["parameters"])
|
||||
|
||||
return reg
|
||||
|
||||
|
||||
# 注册所有工具
|
||||
register_all_tools(global_registry)
|
||||
|
||||
__all__ = [
|
||||
"ToolRegistry",
|
||||
"ToolMetadata",
|
||||
"SecurityLevel",
|
||||
"global_registry",
|
||||
"register_all_tools",
|
||||
"impl",
|
||||
|
||||
# 所有工具函数
|
||||
"read_file",
|
||||
"write_file",
|
||||
"list_dir",
|
||||
"delete_file",
|
||||
"search_files",
|
||||
|
||||
"execute_python",
|
||||
"execute_javascript",
|
||||
"execute_bash",
|
||||
|
||||
"web_fetch",
|
||||
"web_search",
|
||||
|
||||
"http_request",
|
||||
"http_get",
|
||||
"http_post",
|
||||
"http_put",
|
||||
"http_delete",
|
||||
|
||||
"send_notification",
|
||||
"send_email",
|
||||
"send_webhook",
|
||||
|
||||
"get_current_time",
|
||||
"format_time",
|
||||
]
|
||||
100
agent/app/core/tools/impl/__init__.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
工具实现模块
|
||||
"""
|
||||
from .files import (
|
||||
read_file,
|
||||
write_file,
|
||||
list_dir,
|
||||
delete_file,
|
||||
search_files,
|
||||
READ_FILE_TOOL,
|
||||
WRITE_FILE_TOOL,
|
||||
LIST_DIR_TOOL,
|
||||
DELETE_FILE_TOOL,
|
||||
SEARCH_FILES_TOOL,
|
||||
)
|
||||
|
||||
from .executor import (
|
||||
execute_python,
|
||||
execute_javascript,
|
||||
execute_bash,
|
||||
EXECUTE_PYTHON_TOOL,
|
||||
EXECUTE_JAVASCRIPT_TOOL,
|
||||
EXECUTE_BASH_TOOL,
|
||||
)
|
||||
|
||||
from .web import (
|
||||
web_fetch,
|
||||
web_search,
|
||||
WEB_FETCH_TOOL,
|
||||
WEB_SEARCH_TOOL,
|
||||
)
|
||||
|
||||
from .http import (
|
||||
http_request,
|
||||
http_get,
|
||||
http_post,
|
||||
http_put,
|
||||
http_delete,
|
||||
HTTP_REQUEST_TOOL,
|
||||
)
|
||||
|
||||
from .notify import (
|
||||
send_notification,
|
||||
send_email,
|
||||
send_webhook,
|
||||
SEND_NOTIFICATION_TOOL,
|
||||
)
|
||||
|
||||
from .time_tool import (
|
||||
get_current_time,
|
||||
format_time,
|
||||
GET_CURRENT_TIME_TOOL,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# 文件操作
|
||||
"read_file",
|
||||
"write_file",
|
||||
"list_dir",
|
||||
"delete_file",
|
||||
"search_files",
|
||||
"READ_FILE_TOOL",
|
||||
"WRITE_FILE_TOOL",
|
||||
"LIST_DIR_TOOL",
|
||||
"DELETE_FILE_TOOL",
|
||||
"SEARCH_FILES_TOOL",
|
||||
|
||||
# 代码执行
|
||||
"execute_python",
|
||||
"execute_javascript",
|
||||
"execute_bash",
|
||||
"EXECUTE_PYTHON_TOOL",
|
||||
"EXECUTE_JAVASCRIPT_TOOL",
|
||||
"EXECUTE_BASH_TOOL",
|
||||
|
||||
# 网页
|
||||
"web_fetch",
|
||||
"web_search",
|
||||
"WEB_FETCH_TOOL",
|
||||
"WEB_SEARCH_TOOL",
|
||||
|
||||
# HTTP
|
||||
"http_request",
|
||||
"http_get",
|
||||
"http_post",
|
||||
"http_put",
|
||||
"http_delete",
|
||||
"HTTP_REQUEST_TOOL",
|
||||
|
||||
# 通知
|
||||
"send_notification",
|
||||
"send_email",
|
||||
"send_webhook",
|
||||
"SEND_NOTIFICATION_TOOL",
|
||||
|
||||
# 时间
|
||||
"get_current_time",
|
||||
"format_time",
|
||||
"GET_CURRENT_TIME_TOOL",
|
||||
]
|
||||
334
agent/app/core/tools/impl/executor.py
Normal file
@@ -0,0 +1,334 @@
|
||||
"""
|
||||
代码执行工具
|
||||
提供安全的Python、JavaScript、Bash代码执行
|
||||
"""
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
import shutil
|
||||
from typing import Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ExecutorConfig:
|
||||
"""执行器配置"""
|
||||
MAX_EXECUTION_TIME = 30 # 最大执行时间(秒)
|
||||
MAX_OUTPUT_SIZE = 1024 * 1024 # 最大输出大小(1MB)
|
||||
MAX_MEMORY_MB = 256 # 最大内存(MB)
|
||||
ALLOWED_PYTHON_PACKAGES = [] # 允许的Python包(空=仅标准库)
|
||||
|
||||
|
||||
class CodeExecutor:
|
||||
"""
|
||||
代码执行器 - 在沙盒环境中执行代码
|
||||
|
||||
安全特性:
|
||||
- 临时目录隔离
|
||||
- 超时控制
|
||||
- 输出大小限制
|
||||
- 环境变量限制
|
||||
"""
|
||||
|
||||
def __init__(self, config: Optional[ExecutorConfig] = None):
|
||||
self.config = config or ExecutorConfig()
|
||||
self.temp_dir: Optional[str] = None
|
||||
|
||||
def _setup_temp_dir(self) -> str:
|
||||
"""创建临时目录"""
|
||||
self.temp_dir = tempfile.mkdtemp(prefix="executor_")
|
||||
return self.temp_dir
|
||||
|
||||
def _cleanup(self):
|
||||
"""清理临时目录"""
|
||||
if self.temp_dir and os.path.exists(self.temp_dir):
|
||||
try:
|
||||
shutil.rmtree(self.temp_dir)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _get_safe_env(self) -> Dict[str, str]:
|
||||
"""获取安全的环境变量"""
|
||||
return {
|
||||
"PATH": os.environ.get("PATH", "/usr/bin:/bin"),
|
||||
"LANG": "en_US.UTF-8",
|
||||
"HOME": self.temp_dir or "/tmp",
|
||||
"TMPDIR": self.temp_dir or "/tmp",
|
||||
}
|
||||
|
||||
def execute_python(
|
||||
self,
|
||||
code: str,
|
||||
timeout: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
执行Python代码
|
||||
|
||||
Args:
|
||||
code: Python代码
|
||||
timeout: 超时时间(秒)
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
"""
|
||||
timeout = timeout or self.config.MAX_EXECUTION_TIME
|
||||
self._setup_temp_dir()
|
||||
|
||||
try:
|
||||
# 写入临时文件
|
||||
temp_file = os.path.join(self.temp_dir, "code.py")
|
||||
with open(temp_file, "w", encoding="utf-8") as f:
|
||||
f.write(code)
|
||||
|
||||
# 执行
|
||||
result = subprocess.run(
|
||||
["python", temp_file],
|
||||
capture_output=True,
|
||||
timeout=timeout,
|
||||
cwd=self.temp_dir,
|
||||
env=self._get_safe_env(),
|
||||
)
|
||||
|
||||
return self._process_result(result)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Execution timeout ({timeout}s)",
|
||||
"language": "python"
|
||||
}
|
||||
except FileNotFoundError:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Python not installed",
|
||||
"language": "python"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"language": "python"
|
||||
}
|
||||
finally:
|
||||
self._cleanup()
|
||||
|
||||
def execute_javascript(
|
||||
self,
|
||||
code: str,
|
||||
timeout: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
执行JavaScript代码
|
||||
|
||||
Args:
|
||||
code: JavaScript代码
|
||||
timeout: 超时时间(秒)
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
"""
|
||||
timeout = timeout or self.config.MAX_EXECUTION_TIME
|
||||
self._setup_temp_dir()
|
||||
|
||||
try:
|
||||
# 写入临时文件
|
||||
temp_file = os.path.join(self.temp_dir, "code.js")
|
||||
with open(temp_file, "w", encoding="utf-8") as f:
|
||||
f.write(code)
|
||||
|
||||
# 执行
|
||||
result = subprocess.run(
|
||||
["node", temp_file],
|
||||
capture_output=True,
|
||||
timeout=timeout,
|
||||
cwd=self.temp_dir,
|
||||
)
|
||||
|
||||
return self._process_result(result)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Execution timeout ({timeout}s)",
|
||||
"language": "javascript"
|
||||
}
|
||||
except FileNotFoundError:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Node.js not installed",
|
||||
"language": "javascript"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"language": "javascript"
|
||||
}
|
||||
finally:
|
||||
self._cleanup()
|
||||
|
||||
def execute_bash(
|
||||
self,
|
||||
command: str,
|
||||
timeout: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
执行Bash命令
|
||||
|
||||
Args:
|
||||
command: Bash命令
|
||||
timeout: 超时时间(秒)
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
"""
|
||||
timeout = timeout or self.config.MAX_EXECUTION_TIME
|
||||
self._setup_temp_dir()
|
||||
|
||||
# 安全检查:禁止的危险命令
|
||||
dangerous_patterns = [
|
||||
"rm -rf /",
|
||||
"mkfs",
|
||||
"dd if=",
|
||||
">:/dev/sd",
|
||||
"chmod 777 /",
|
||||
"chown -R",
|
||||
]
|
||||
|
||||
for pattern in dangerous_patterns:
|
||||
if pattern in command:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Dangerous command blocked: {pattern}",
|
||||
"language": "bash"
|
||||
}
|
||||
|
||||
try:
|
||||
# 执行
|
||||
result = subprocess.run(
|
||||
["bash", "-c", command],
|
||||
capture_output=True,
|
||||
timeout=timeout,
|
||||
cwd=self.temp_dir,
|
||||
env=self._get_safe_env(),
|
||||
)
|
||||
|
||||
return self._process_result(result)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Execution timeout ({timeout}s)",
|
||||
"language": "bash"
|
||||
}
|
||||
except FileNotFoundError:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Bash not installed",
|
||||
"language": "bash"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"language": "bash"
|
||||
}
|
||||
finally:
|
||||
self._cleanup()
|
||||
|
||||
def _process_result(self, result: subprocess.CompletedProcess) -> Dict[str, Any]:
|
||||
"""处理执行结果"""
|
||||
stdout = result.stdout.decode("utf-8", errors="replace")
|
||||
stderr = result.stderr.decode("utf-8", errors="replace")
|
||||
|
||||
# 截断输出
|
||||
if len(stdout) > self.config.MAX_OUTPUT_SIZE:
|
||||
stdout = stdout[:self.config.MAX_OUTPUT_SIZE] + "\n... (output truncated)"
|
||||
|
||||
return {
|
||||
"success": result.returncode == 0,
|
||||
"output": stdout,
|
||||
"error": stderr if result.returncode != 0 else None,
|
||||
"exit_code": result.returncode
|
||||
}
|
||||
|
||||
|
||||
# 全局执行器实例
|
||||
executor = CodeExecutor()
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def execute_python(code: str, timeout: int = 30) -> Dict[str, Any]:
|
||||
"""执行Python代码"""
|
||||
return executor.execute_python(code, timeout)
|
||||
|
||||
|
||||
def execute_javascript(code: str, timeout: int = 30) -> Dict[str, Any]:
|
||||
"""执行JavaScript代码"""
|
||||
return executor.execute_javascript(code, timeout)
|
||||
|
||||
|
||||
def execute_bash(command: str, timeout: int = 30) -> Dict[str, Any]:
|
||||
"""执行Bash命令"""
|
||||
return executor.execute_bash(command, timeout)
|
||||
|
||||
|
||||
# 工具定义
|
||||
EXECUTE_PYTHON_TOOL = {
|
||||
"name": "execute_python",
|
||||
"description": "Execute Python code in a sandboxed environment. Use this for Python programming tasks, calculations, and data processing.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "The Python code to execute"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Execution timeout in seconds (default: 30, max: 60)",
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
"required": ["code"]
|
||||
}
|
||||
}
|
||||
|
||||
EXECUTE_JAVASCRIPT_TOOL = {
|
||||
"name": "execute_javascript",
|
||||
"description": "Execute JavaScript code in a sandboxed environment. Use this for JavaScript programming tasks.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "The JavaScript code to execute"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Execution timeout in seconds (default: 30)",
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
"required": ["code"]
|
||||
}
|
||||
}
|
||||
|
||||
EXECUTE_BASH_TOOL = {
|
||||
"name": "execute_bash",
|
||||
"description": "Execute a bash command in a sandboxed environment. Use this for shell operations, file management, and system commands.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The bash command to execute"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Execution timeout in seconds (default: 30)",
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
"required": ["command"]
|
||||
}
|
||||
}
|
||||
444
agent/app/core/tools/impl/files.py
Normal file
@@ -0,0 +1,444 @@
|
||||
"""
|
||||
文件操作工具
|
||||
提供安全的文件读写、目录操作、搜索功能
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import glob as glob_module
|
||||
from typing import Optional, List, Dict, Any
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class FileToolConfig:
|
||||
"""文件工具配置"""
|
||||
# 允许访问的基础目录(限制在项目内)
|
||||
ALLOWED_BASE_DIRS = [
|
||||
"account", # 用户工作区
|
||||
"temp", # 临时文件
|
||||
]
|
||||
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
|
||||
MAX_SEARCH_RESULTS = 100
|
||||
|
||||
|
||||
def _resolve_safe_path(base_path: str, relative_path: str) -> str:
|
||||
"""
|
||||
解析安全的文件路径
|
||||
确保路径不会超出基础目录
|
||||
"""
|
||||
# 规范化路径
|
||||
full_path = os.path.normpath(os.path.join(base_path, relative_path))
|
||||
|
||||
# 检查是否在允许的基础目录内
|
||||
path_parts = Path(full_path).parts
|
||||
if len(path_parts) < 2:
|
||||
raise ValueError("Invalid path: too short")
|
||||
|
||||
base_dir = path_parts[0]
|
||||
if base_dir not in FileToolConfig.ALLOWED_BASE_DIRS and not base_dir.endswith(".py"):
|
||||
# 允许 account 下的子目录
|
||||
if len(path_parts) >= 2 and path_parts[0] != "account":
|
||||
raise ValueError(f"Path not in allowed directories: {base_dir}")
|
||||
|
||||
return full_path
|
||||
|
||||
|
||||
def read_file(file_path: str, encoding: str = "utf-8") -> Dict[str, Any]:
|
||||
"""
|
||||
读取文件内容
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
encoding: 文件编码
|
||||
|
||||
Returns:
|
||||
文件内容
|
||||
"""
|
||||
try:
|
||||
# 安全检查
|
||||
full_path = _resolve_safe_path("", file_path)
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"File not found: {file_path}"
|
||||
}
|
||||
|
||||
if not os.path.isfile(full_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Not a file: {file_path}"
|
||||
}
|
||||
|
||||
# 检查文件大小
|
||||
file_size = os.path.getsize(full_path)
|
||||
if file_size > FileToolConfig.MAX_FILE_SIZE:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"File too large: {file_size} bytes (max {FileToolConfig.MAX_FILE_SIZE})"
|
||||
}
|
||||
|
||||
# 读取内容
|
||||
with open(full_path, "r", encoding=encoding, errors="replace") as f:
|
||||
content = f.read()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"content": content,
|
||||
"file_path": file_path,
|
||||
"size": file_size,
|
||||
"encoding": encoding
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Read error: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
def write_file(file_path: str, content: str, encoding: str = "utf-8") -> Dict[str, Any]:
|
||||
"""
|
||||
写入文件内容
|
||||
|
||||
Args:
|
||||
file_path: 文件路径
|
||||
content: 文件内容
|
||||
encoding: 文件编码
|
||||
|
||||
Returns:
|
||||
写入结果
|
||||
"""
|
||||
try:
|
||||
# 安全检查
|
||||
full_path = _resolve_safe_path("", file_path)
|
||||
|
||||
# 检查内容大小
|
||||
if len(content.encode(encoding)) > FileToolConfig.MAX_FILE_SIZE:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Content too large: {len(content)} bytes"
|
||||
}
|
||||
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
|
||||
# 写入内容
|
||||
with open(full_path, "w", encoding=encoding) as f:
|
||||
f.write(content)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"file_path": file_path,
|
||||
"bytes_written": len(content.encode(encoding))
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Write error: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
def list_dir(dir_path: str = ".") -> Dict[str, Any]:
|
||||
"""
|
||||
列出目录内容
|
||||
|
||||
Args:
|
||||
dir_path: 目录路径
|
||||
|
||||
Returns:
|
||||
目录内容列表
|
||||
"""
|
||||
try:
|
||||
full_path = _resolve_safe_path("", dir_path)
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Directory not found: {dir_path}"
|
||||
}
|
||||
|
||||
if not os.path.isdir(full_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Not a directory: {dir_path}"
|
||||
}
|
||||
|
||||
items = []
|
||||
for item in os.listdir(full_path):
|
||||
item_path = os.path.join(full_path, item)
|
||||
is_dir = os.path.isdir(item_path)
|
||||
try:
|
||||
size = 0 if is_dir else os.path.getsize(item_path)
|
||||
except:
|
||||
size = 0
|
||||
|
||||
items.append({
|
||||
"name": item,
|
||||
"type": "directory" if is_dir else "file",
|
||||
"size": size
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"path": dir_path,
|
||||
"items": items,
|
||||
"count": len(items)
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"List error: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
def delete_file(file_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
删除文件或目录
|
||||
|
||||
Args:
|
||||
file_path: 文件或目录路径
|
||||
|
||||
Returns:
|
||||
删除结果
|
||||
"""
|
||||
try:
|
||||
full_path = _resolve_safe_path("", file_path)
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Path not found: {file_path}"
|
||||
}
|
||||
|
||||
# 删除
|
||||
if os.path.isfile(full_path):
|
||||
os.remove(full_path)
|
||||
elif os.path.isdir(full_path):
|
||||
shutil.rmtree(full_path)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"file_path": file_path,
|
||||
"deleted": True
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Delete error: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
def search_files(
|
||||
directory: str,
|
||||
pattern: str = "*",
|
||||
content_pattern: Optional[str] = None,
|
||||
file_only: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
搜索文件
|
||||
|
||||
Args:
|
||||
directory: 搜索目录
|
||||
pattern: 文件名匹配模式 (glob)
|
||||
content_pattern: 文件内容匹配模式 (可选)
|
||||
file_only: 是否只返回文件
|
||||
|
||||
Returns:
|
||||
搜索结果
|
||||
"""
|
||||
try:
|
||||
full_path = _resolve_safe_path("", directory)
|
||||
|
||||
if not os.path.exists(full_path) or not os.path.isdir(full_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Invalid directory: {directory}"
|
||||
}
|
||||
|
||||
results = []
|
||||
|
||||
# 按文件名搜索
|
||||
for match in glob_module.glob(os.path.join(full_path, "**", pattern), recursive=True):
|
||||
if file_only and os.path.isdir(match):
|
||||
continue
|
||||
|
||||
rel_path = os.path.relpath(match, full_path)
|
||||
|
||||
# 如果没有内容搜索,直接添加
|
||||
if not content_pattern:
|
||||
results.append({
|
||||
"path": rel_path,
|
||||
"name": os.path.basename(match),
|
||||
"type": "directory" if os.path.isdir(match) else "file"
|
||||
})
|
||||
continue
|
||||
|
||||
# 内容搜索
|
||||
if os.path.isfile(match):
|
||||
try:
|
||||
# 检查文件大小
|
||||
if os.path.getsize(match) > FileToolConfig.MAX_FILE_SIZE:
|
||||
continue
|
||||
|
||||
with open(match, "r", encoding="utf-8", errors="ignore") as f:
|
||||
content = f.read()
|
||||
if content_pattern.lower() in content.lower():
|
||||
results.append({
|
||||
"path": rel_path,
|
||||
"name": os.path.basename(match),
|
||||
"type": "file",
|
||||
"match": content_pattern
|
||||
})
|
||||
except:
|
||||
continue
|
||||
|
||||
# 限制结果数量
|
||||
if len(results) > FileToolConfig.MAX_SEARCH_RESULTS:
|
||||
results = results[:FileToolConfig.MAX_SEARCH_RESULTS]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"directory": directory,
|
||||
"pattern": pattern,
|
||||
"results": results,
|
||||
"count": len(results)
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Search error: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
# 工具定义
|
||||
READ_FILE_TOOL = {
|
||||
"name": "read_file",
|
||||
"description": "Read the contents of a file from the filesystem.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_path": {
|
||||
"type": "string",
|
||||
"description": "The path to the file to read"
|
||||
},
|
||||
"encoding": {
|
||||
"type": "string",
|
||||
"description": "File encoding (default: utf-8)",
|
||||
"default": "utf-8"
|
||||
}
|
||||
},
|
||||
"required": ["file_path"]
|
||||
}
|
||||
}
|
||||
|
||||
WRITE_FILE_TOOL = {
|
||||
"name": "write_file",
|
||||
"description": "Write content to a file. Creates the file if it doesn't exist, overwrites if it does.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_path": {
|
||||
"type": "string",
|
||||
"description": "The path to the file to write"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "The content to write to the file"
|
||||
},
|
||||
"encoding": {
|
||||
"type": "string",
|
||||
"description": "File encoding (default: utf-8)",
|
||||
"default": "utf-8"
|
||||
}
|
||||
},
|
||||
"required": ["file_path", "content"]
|
||||
}
|
||||
}
|
||||
|
||||
LIST_DIR_TOOL = {
|
||||
"name": "list_dir",
|
||||
"description": "List the contents of a directory.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dir_path": {
|
||||
"type": "string",
|
||||
"description": "The path to the directory to list (default: current directory)",
|
||||
"default": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DELETE_FILE_TOOL = {
|
||||
"name": "delete_file",
|
||||
"description": "Delete a file or directory.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_path": {
|
||||
"type": "string",
|
||||
"description": "The path to the file or directory to delete"
|
||||
}
|
||||
},
|
||||
"required": ["file_path"]
|
||||
}
|
||||
}
|
||||
|
||||
SEARCH_FILES_TOOL = {
|
||||
"name": "search_files",
|
||||
"description": "Search for files by name pattern or content.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "The directory to search in"
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Glob pattern for file names (e.g., '*.py', '*.txt')",
|
||||
"default": "*"
|
||||
},
|
||||
"content_pattern": {
|
||||
"type": "string",
|
||||
"description": "Optional: search for files containing this text in their content"
|
||||
},
|
||||
"file_only": {
|
||||
"type": "boolean",
|
||||
"description": "Only return files, not directories",
|
||||
"default": True
|
||||
}
|
||||
},
|
||||
"required": ["directory"]
|
||||
}
|
||||
}
|
||||
271
agent/app/core/tools/impl/http.py
Normal file
@@ -0,0 +1,271 @@
|
||||
"""
|
||||
HTTP请求工具
|
||||
提供通用的HTTP API调用功能
|
||||
"""
|
||||
import httpx
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
|
||||
class HTTPClientConfig:
|
||||
"""HTTP客户端配置"""
|
||||
DEFAULT_TIMEOUT = 30 # 默认超时(秒)
|
||||
MAX_RESPONSE_SIZE = 5 * 1024 * 1024 # 最大响应大小(5MB)
|
||||
MAX_REDIRECTS = 5 # 最大重定向次数
|
||||
ALLOWED_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
|
||||
|
||||
|
||||
class HTTPClient:
|
||||
"""
|
||||
HTTP客户端工具
|
||||
|
||||
安全特性:
|
||||
- 只允许特定HTTP方法
|
||||
- 响应大小限制
|
||||
- 超时控制
|
||||
- 请求/响应日志
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.default_timeout = HTTPClientConfig.DEFAULT_TIMEOUT
|
||||
|
||||
async def request(
|
||||
self,
|
||||
url: str,
|
||||
method: str = "GET",
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
json_data: Optional[Dict[str, Any]] = None,
|
||||
data: Optional[Any] = None,
|
||||
timeout: Optional[int] = None,
|
||||
allow_redirects: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
发送HTTP请求
|
||||
|
||||
Args:
|
||||
url: 目标URL
|
||||
method: HTTP方法
|
||||
params: 查询参数
|
||||
headers: 请求头
|
||||
json_data: JSON请求体
|
||||
data: 原始请求体
|
||||
timeout: 超时时间
|
||||
allow_redirects: 是否允许重定向
|
||||
|
||||
Returns:
|
||||
响应结果
|
||||
"""
|
||||
# 安全检查:方法
|
||||
method = method.upper()
|
||||
if method not in HTTPClientConfig.ALLOWED_METHODS:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Method '{method}' not allowed. Allowed: {HTTPClientConfig.ALLOWED_METHODS}"
|
||||
}
|
||||
|
||||
# 安全检查:协议
|
||||
if not url.startswith(("http://", "https://")):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Only HTTP and HTTPS protocols are allowed"
|
||||
}
|
||||
|
||||
timeout = timeout or self.default_timeout
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
timeout=timeout,
|
||||
max_redirects=HTTPClientConfig.MAX_REDIRECTS if allow_redirects else 0,
|
||||
follow_redirects=allow_redirects,
|
||||
) as client:
|
||||
response = await client.request(
|
||||
method=method,
|
||||
url=url,
|
||||
params=params,
|
||||
headers=headers,
|
||||
json=json_data,
|
||||
content=data,
|
||||
)
|
||||
|
||||
# 检查响应大小
|
||||
content_length = len(response.content)
|
||||
if content_length > HTTPClientConfig.MAX_RESPONSE_SIZE:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Response too large: {content_length} bytes"
|
||||
}
|
||||
|
||||
# 解析响应
|
||||
content_type = response.headers.get("content-type", "")
|
||||
|
||||
if "application/json" in content_type:
|
||||
try:
|
||||
return {
|
||||
"success": True,
|
||||
"status_code": response.status_code,
|
||||
"url": str(response.url),
|
||||
"headers": dict(response.headers),
|
||||
"json": response.json()
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
||||
# 文本响应
|
||||
return {
|
||||
"success": True,
|
||||
"status_code": response.status_code,
|
||||
"url": str(response.url),
|
||||
"headers": dict(response.headers),
|
||||
"text": response.text[:HTTPClientConfig.MAX_RESPONSE_SIZE]
|
||||
}
|
||||
|
||||
except httpx.TimeoutException:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Request timeout ({timeout}s)"
|
||||
}
|
||||
except httpx.InvalidURL:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Invalid URL"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
async def get(
|
||||
self,
|
||||
url: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
timeout: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送GET请求"""
|
||||
return await self.request(url, "GET", params, headers, timeout=timeout)
|
||||
|
||||
async def post(
|
||||
self,
|
||||
url: str,
|
||||
json_data: Optional[Dict[str, Any]] = None,
|
||||
data: Optional[Any] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
timeout: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送POST请求"""
|
||||
return await self.request(url, "POST", None, headers, json_data, data, timeout)
|
||||
|
||||
async def put(
|
||||
self,
|
||||
url: str,
|
||||
json_data: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
timeout: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送PUT请求"""
|
||||
return await self.request(url, "PUT", None, headers, json_data, None, timeout)
|
||||
|
||||
async def delete(
|
||||
self,
|
||||
url: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
timeout: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送DELETE请求"""
|
||||
return await self.request(url, "DELETE", None, headers, timeout=timeout)
|
||||
|
||||
|
||||
# 全局HTTP客户端
|
||||
http_client = HTTPClient()
|
||||
|
||||
|
||||
# 便捷函数
|
||||
async def http_request(
|
||||
url: str,
|
||||
method: str = "GET",
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
json_data: Optional[Dict[str, Any]] = None,
|
||||
timeout: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送HTTP请求"""
|
||||
return await http_client.request(url, method, params, headers, json_data, None, timeout)
|
||||
|
||||
|
||||
async def http_get(
|
||||
url: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
timeout: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送GET请求"""
|
||||
return await http_client.get(url, params, headers, timeout)
|
||||
|
||||
|
||||
async def http_post(
|
||||
url: str,
|
||||
json_data: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
timeout: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送POST请求"""
|
||||
return await http_client.post(url, json_data, None, headers, timeout)
|
||||
|
||||
|
||||
async def http_put(
|
||||
url: str,
|
||||
json_data: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
timeout: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送PUT请求"""
|
||||
return await http_client.put(url, json_data, headers, timeout)
|
||||
|
||||
|
||||
async def http_delete(
|
||||
url: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
timeout: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送DELETE请求"""
|
||||
return await http_client.delete(url, headers, timeout)
|
||||
|
||||
|
||||
# 工具定义
|
||||
HTTP_REQUEST_TOOL = {
|
||||
"name": "http_request",
|
||||
"description": "Make HTTP requests to APIs. Supports GET, POST, PUT, DELETE methods with JSON data.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The URL to request"
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"description": "HTTP method (GET, POST, PUT, DELETE, PATCH)",
|
||||
"default": "GET"
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"description": "Query parameters for GET requests"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"description": "Request headers"
|
||||
},
|
||||
"json_data": {
|
||||
"type": "object",
|
||||
"description": "JSON body for POST/PUT requests"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Request timeout in seconds",
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
}
|
||||
}
|
||||
379
agent/app/core/tools/impl/notify.py
Normal file
@@ -0,0 +1,379 @@
|
||||
"""
|
||||
通知工具
|
||||
提供发送通知的功能(邮件、Webhook等)
|
||||
"""
|
||||
import httpx
|
||||
from typing import Dict, Any, Optional, List
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class NotificationType(Enum):
|
||||
"""通知类型"""
|
||||
EMAIL = "email"
|
||||
WEBHOOK = "webhook"
|
||||
SMS = "sms"
|
||||
DINGTALK = "dingtalk"
|
||||
WECHAT = "wechat"
|
||||
SLACK = "slack"
|
||||
|
||||
|
||||
@dataclass
|
||||
class NotificationConfig:
|
||||
"""通知配置"""
|
||||
# Email配置
|
||||
smtp_host: str = ""
|
||||
smtp_port: int = 587
|
||||
smtp_user: str = ""
|
||||
smtp_password: str = ""
|
||||
from_email: str = ""
|
||||
|
||||
# Webhook配置
|
||||
webhook_url: str = ""
|
||||
webhook_secret: str = ""
|
||||
|
||||
# 钉钉配置
|
||||
dingtalk_webhook: str = ""
|
||||
|
||||
# Slack配置
|
||||
slack_webhook: str = ""
|
||||
|
||||
|
||||
class NotificationTool:
|
||||
"""
|
||||
通知工具
|
||||
|
||||
支持多种通知渠道:
|
||||
- Email (SMTP)
|
||||
- Webhook
|
||||
- 钉钉
|
||||
- Slack
|
||||
"""
|
||||
|
||||
def __init__(self, config: Optional[NotificationConfig] = None):
|
||||
self.config = config or NotificationConfig()
|
||||
|
||||
async def send_email(
|
||||
self,
|
||||
to: str,
|
||||
subject: str,
|
||||
body: str,
|
||||
cc: Optional[List[str]] = None,
|
||||
is_html: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
发送邮件
|
||||
|
||||
Args:
|
||||
to: 收件人
|
||||
subject: 主题
|
||||
body: 内容
|
||||
cc: 抄送列表
|
||||
is_html: 是否HTML格式
|
||||
|
||||
Returns:
|
||||
发送结果
|
||||
"""
|
||||
if not self.config.smtp_host:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Email not configured"
|
||||
}
|
||||
|
||||
try:
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
# 构建邮件
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = self.config.from_email or self.config.smtp_user
|
||||
msg['To'] = to
|
||||
|
||||
if cc:
|
||||
msg['Cc'] = ",".join(cc)
|
||||
|
||||
# 添加内容
|
||||
content_type = "html" if is_html else "plain"
|
||||
msg.attach(MIMEText(body, content_type))
|
||||
|
||||
# 发送
|
||||
with smtplib.SMTP(self.config.smtp_host, self.config.smtp_port) as server:
|
||||
server.starttls()
|
||||
server.login(self.config.smtp_user, self.config.smtp_password)
|
||||
server.send_message(msg)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"type": "email",
|
||||
"to": to,
|
||||
"subject": subject
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"type": "email"
|
||||
}
|
||||
|
||||
async def send_webhook(
|
||||
self,
|
||||
url: str,
|
||||
data: Dict[str, Any],
|
||||
method: str = "POST",
|
||||
headers: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
发送Webhook
|
||||
|
||||
Args:
|
||||
url: Webhook URL
|
||||
data: 请求数据
|
||||
method: HTTP方法
|
||||
headers: 请求头
|
||||
|
||||
Returns:
|
||||
发送结果
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
response = await client.request(
|
||||
method=method,
|
||||
url=url,
|
||||
json=data,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
return {
|
||||
"success": response.status_code < 400,
|
||||
"status_code": response.status_code,
|
||||
"type": "webhook",
|
||||
"url": url
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"type": "webhook"
|
||||
}
|
||||
|
||||
async def send_dingtalk(
|
||||
self,
|
||||
message: str,
|
||||
webhook: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
发送钉钉消息
|
||||
|
||||
Args:
|
||||
message: 消息内容
|
||||
webhook: 自定义webhook URL
|
||||
|
||||
Returns:
|
||||
发送结果
|
||||
"""
|
||||
url = webhook or self.config.dingtalk_webhook
|
||||
if not url:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Dingtalk webhook not configured"
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
response = await client.post(
|
||||
url,
|
||||
json={
|
||||
"msgtype": "text",
|
||||
"text": {
|
||||
"content": message
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
result = response.json()
|
||||
return {
|
||||
"success": result.get("errcode") == 0,
|
||||
"type": "dingtalk",
|
||||
"response": result
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"type": "dingtalk"
|
||||
}
|
||||
|
||||
async def send_slack(
|
||||
self,
|
||||
message: str,
|
||||
channel: Optional[str] = None,
|
||||
webhook: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
发送Slack消息
|
||||
|
||||
Args:
|
||||
message: 消息内容
|
||||
channel: 频道
|
||||
webhook: 自定义webhook URL
|
||||
|
||||
Returns:
|
||||
发送结果
|
||||
"""
|
||||
url = webhook or self.config.slack_webhook
|
||||
if not url:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Slack webhook not configured"
|
||||
}
|
||||
|
||||
try:
|
||||
payload = {"text": message}
|
||||
if channel:
|
||||
payload["channel"] = channel
|
||||
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
response = await client.post(url, json=payload)
|
||||
|
||||
return {
|
||||
"success": response.status_code == 200,
|
||||
"type": "slack",
|
||||
"status_code": response.status_code
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"type": "slack"
|
||||
}
|
||||
|
||||
async def send(
|
||||
self,
|
||||
type: str,
|
||||
message: str,
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
统一发送接口
|
||||
|
||||
Args:
|
||||
type: 通知类型 (email, webhook, dingtalk, slack)
|
||||
message: 消息内容
|
||||
**kwargs: 其他参数
|
||||
|
||||
Returns:
|
||||
发送结果
|
||||
"""
|
||||
type = type.lower()
|
||||
|
||||
if type == "email":
|
||||
return await self.send_email(
|
||||
to=kwargs.get("to", ""),
|
||||
subject=kwargs.get("subject", "Notification"),
|
||||
body=message,
|
||||
cc=kwargs.get("cc")
|
||||
)
|
||||
elif type == "webhook":
|
||||
return await self.send_webhook(
|
||||
url=kwargs.get("url", ""),
|
||||
data=kwargs.get("data", {"message": message})
|
||||
)
|
||||
elif type == "dingtalk":
|
||||
return await self.send_dingtalk(
|
||||
message=message,
|
||||
webhook=kwargs.get("webhook")
|
||||
)
|
||||
elif type == "slack":
|
||||
return await self.send_slack(
|
||||
message=message,
|
||||
channel=kwargs.get("channel"),
|
||||
webhook=kwargs.get("webhook")
|
||||
)
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Unknown notification type: {type}"
|
||||
}
|
||||
|
||||
|
||||
# 全局通知工具
|
||||
notification_tool = NotificationTool()
|
||||
|
||||
|
||||
# 便捷函数
|
||||
async def send_notification(
|
||||
type: str,
|
||||
message: str,
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""发送通知"""
|
||||
return await notification_tool.send(type, message, **kwargs)
|
||||
|
||||
|
||||
async def send_email(
|
||||
to: str,
|
||||
subject: str,
|
||||
body: str
|
||||
) -> Dict[str, Any]:
|
||||
"""发送邮件"""
|
||||
return await notification_tool.send_email(to, subject, body)
|
||||
|
||||
|
||||
async def send_webhook(
|
||||
url: str,
|
||||
data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""发送Webhook"""
|
||||
return await notification_tool.send_webhook(url, data)
|
||||
|
||||
|
||||
# 工具定义
|
||||
SEND_NOTIFICATION_TOOL = {
|
||||
"name": "send_notification",
|
||||
"description": "Send notifications via email, webhook, dingtalk, or slack.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Notification type: email, webhook, dingtalk, slack",
|
||||
"enum": ["email", "webhook", "dingtalk", "slack"]
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "The notification message"
|
||||
},
|
||||
"to": {
|
||||
"type": "string",
|
||||
"description": "For email: recipient email address"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"description": "For email: email subject"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "For webhook: webhook URL"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "For webhook: JSON data to send"
|
||||
},
|
||||
"webhook": {
|
||||
"type": "string",
|
||||
"description": "Custom webhook URL for dingtalk/slack"
|
||||
},
|
||||
"channel": {
|
||||
"type": "string",
|
||||
"description": "For slack: channel name"
|
||||
}
|
||||
},
|
||||
"required": ["type", "message"]
|
||||
}
|
||||
}
|
||||
70
agent/app/core/tools/impl/time_tool.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
时间工具
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
|
||||
def get_current_time(timezone: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
获取当前时间
|
||||
|
||||
Args:
|
||||
timezone: 时区名称,如 "UTC", "Asia/Shanghai"
|
||||
|
||||
Returns:
|
||||
当前时间信息
|
||||
"""
|
||||
now = datetime.now()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"datetime": now.isoformat(),
|
||||
"timestamp": now.timestamp(),
|
||||
"date": now.strftime("%Y-%m-%d"),
|
||||
"time": now.strftime("%H:%M:%S"),
|
||||
"weekday": now.strftime("%A"),
|
||||
"timezone": timezone or "Local Time"
|
||||
}
|
||||
|
||||
|
||||
def format_time(timestamp: float, format_str: str = "%Y-%m-%d %H:%M:%S") -> Dict[str, Any]:
|
||||
"""
|
||||
格式化时间戳
|
||||
|
||||
Args:
|
||||
timestamp: Unix 时间戳
|
||||
format_str: 格式字符串
|
||||
|
||||
Returns:
|
||||
格式化后的时间
|
||||
"""
|
||||
try:
|
||||
dt = datetime.fromtimestamp(timestamp)
|
||||
return {
|
||||
"success": True,
|
||||
"formatted": dt.strftime(format_str),
|
||||
"datetime": dt.isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
# 工具定义
|
||||
GET_CURRENT_TIME_TOOL = {
|
||||
"name": "get_current_time",
|
||||
"description": "Get the current date and time. Useful for timestamps or scheduling.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"description": "Optional timezone (e.g., 'UTC', 'Asia/Shanghai')",
|
||||
"default": "Local"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
233
agent/app/core/tools/impl/web.py
Normal file
@@ -0,0 +1,233 @@
|
||||
"""
|
||||
网页获取工具
|
||||
提供安全的网页内容抓取功能
|
||||
"""
|
||||
import httpx
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class WebToolConfig:
|
||||
"""网页工具配置"""
|
||||
REQUEST_TIMEOUT = 30 # 请求超时(秒)
|
||||
MAX_RESPONSE_SIZE = 2 * 1024 * 1024 # 最大响应大小(2MB)
|
||||
MAX_REDIRECTS = 5 # 最大重定向次数
|
||||
ALLOWED_PROTOCOLS = ["http", "https"] # 允许的协议
|
||||
|
||||
|
||||
async def web_fetch(
|
||||
url: str,
|
||||
method: str = "GET",
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
body: Optional[str] = None,
|
||||
timeout: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取网页内容
|
||||
|
||||
Args:
|
||||
url: 目标URL
|
||||
method: HTTP方法
|
||||
params: 查询参数
|
||||
headers: 请求头
|
||||
body: 请求体
|
||||
timeout: 超时时间
|
||||
|
||||
Returns:
|
||||
网页内容
|
||||
"""
|
||||
timeout = timeout or WebToolConfig.REQUEST_TIMEOUT
|
||||
|
||||
# 安全检查:协议
|
||||
if not url.startswith(("http://", "https://")):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Only HTTP and HTTPS protocols are allowed"
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
timeout=timeout,
|
||||
max_redirects=WebToolConfig.MAX_REDIRECTS,
|
||||
follow_redirects=True,
|
||||
) as client:
|
||||
# 发送请求
|
||||
response = await client.request(
|
||||
method=method,
|
||||
url=url,
|
||||
params=params,
|
||||
headers=headers,
|
||||
content=body,
|
||||
)
|
||||
|
||||
# 检查响应大小
|
||||
if len(response.content) > WebToolConfig.MAX_RESPONSE_SIZE:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Response too large: {len(response.content)} bytes (max {WebToolConfig.MAX_RESPONSE_SIZE})"
|
||||
}
|
||||
|
||||
# 尝试解析JSON
|
||||
content_type = response.headers.get("content-type", "")
|
||||
if "application/json" in content_type:
|
||||
try:
|
||||
data = response.json()
|
||||
return {
|
||||
"success": True,
|
||||
"url": str(response.url),
|
||||
"status_code": response.status_code,
|
||||
"content_type": content_type,
|
||||
"data": data,
|
||||
"headers": dict(response.headers)
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
||||
# 返回文本
|
||||
return {
|
||||
"success": True,
|
||||
"url": str(response.url),
|
||||
"status_code": response.status_code,
|
||||
"content_type": content_type,
|
||||
"content": response.text[:WebToolConfig.MAX_RESPONSE_SIZE],
|
||||
"headers": dict(response.headers)
|
||||
}
|
||||
|
||||
except httpx.TimeoutException:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Request timeout ({timeout}s)"
|
||||
}
|
||||
except httpx.RedirectLoop:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Too many redirects"
|
||||
}
|
||||
except httpx.InvalidURL:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Invalid URL"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
async def web_search(
|
||||
query: str,
|
||||
max_results: int = 5
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
搜索网页
|
||||
|
||||
Args:
|
||||
query: 搜索关键词
|
||||
max_results: 最大结果数
|
||||
|
||||
Returns:
|
||||
搜索结果
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(
|
||||
"https://api.duckduckgo.com/",
|
||||
params={
|
||||
"q": query,
|
||||
"format": "json",
|
||||
"no_html": 1,
|
||||
"skip_disambig": 1
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
results = []
|
||||
|
||||
if "RelatedTopics" in data:
|
||||
for item in data["RelatedTopics"][:max_results]:
|
||||
if "Text" in item:
|
||||
text = item.get("Text", "")
|
||||
results.append({
|
||||
"title": text.split(" - ")[0] if " - " in text else "",
|
||||
"content": text,
|
||||
"url": item.get("URL", "")
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"query": query,
|
||||
"results": results,
|
||||
"count": len(results)
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Search API returned status {response.status_code}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
# 工具定义
|
||||
WEB_FETCH_TOOL = {
|
||||
"name": "web_fetch",
|
||||
"description": "Fetch content from a web URL. Supports GET, POST methods and can return JSON or text content.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The URL to fetch"
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"description": "HTTP method (GET, POST)",
|
||||
"default": "GET"
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"description": "Query parameters"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"description": "Request headers"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "Request body (for POST)"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Request timeout in seconds",
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
}
|
||||
}
|
||||
|
||||
WEB_SEARCH_TOOL = {
|
||||
"name": "web_search",
|
||||
"description": "Search the web for information. Use this when you need to find current information or facts.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The search query"
|
||||
},
|
||||
"max_results": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of results to return",
|
||||
"default": 5
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
107
agent/app/core/tools/registry.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
工具注册表 - 管理所有可用工具(白名单机制)
|
||||
"""
|
||||
from typing import Any, Callable, Optional, Dict
|
||||
from dataclasses import dataclass, asdict
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class SecurityLevel(Enum):
|
||||
"""工具安全等级"""
|
||||
SAFE = "safe" # 安全操作
|
||||
REVIEW = "review" # 需要审核
|
||||
DANGER = "danger" # 危险操作
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolMetadata:
|
||||
"""工具元数据"""
|
||||
name: str
|
||||
description: str
|
||||
security_level: str
|
||||
require_approval: bool = False
|
||||
allowed_roles: list = None
|
||||
|
||||
def dict(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"security_level": self.security_level,
|
||||
"require_approval": self.require_approval
|
||||
}
|
||||
|
||||
|
||||
class ToolRegistry:
|
||||
"""工具注册表"""
|
||||
|
||||
def __init__(self):
|
||||
self._tools: Dict[str, tuple[Callable, ToolMetadata]] = {}
|
||||
self._definitions: Dict[str, dict] = {}
|
||||
|
||||
def register(
|
||||
self,
|
||||
name: str,
|
||||
func: Callable,
|
||||
description: str = "",
|
||||
security_level: str = "safe",
|
||||
require_approval: bool = False,
|
||||
allowed_roles: list = None,
|
||||
parameters: dict = None
|
||||
):
|
||||
"""注册工具到白名单"""
|
||||
metadata = ToolMetadata(
|
||||
name=name,
|
||||
description=description,
|
||||
security_level=security_level,
|
||||
require_approval=require_approval,
|
||||
allowed_roles=allowed_roles or ["user", "admin"]
|
||||
)
|
||||
|
||||
self._tools[name] = (func, metadata)
|
||||
|
||||
# 生成工具定义(用于 LLM 调用)
|
||||
self._definitions[name] = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"parameters": parameters or {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
|
||||
def get_tool(self, name: str) -> tuple[Callable, ToolMetadata]:
|
||||
"""获取工具函数和元数据"""
|
||||
if name not in self._tools:
|
||||
raise ValueError(f"Tool '{name}' not found in whitelist")
|
||||
return self._tools[name]
|
||||
|
||||
def get_tool_definition(self, name: str) -> Optional[dict]:
|
||||
"""获取工具定义(用于 LLM)"""
|
||||
return self._definitions.get(name)
|
||||
|
||||
def list_tools(self) -> list[ToolMetadata]:
|
||||
"""列出所有已注册工具"""
|
||||
return [meta for _, meta in self._tools.values()]
|
||||
|
||||
def list_definitions(self) -> list[dict]:
|
||||
"""列出所有工具定义(用于LLM)"""
|
||||
return list(self._definitions.values())
|
||||
|
||||
def check_permission(self, tool_name: str, user_role: str) -> bool:
|
||||
"""检查用户权限"""
|
||||
if tool_name not in self._tools:
|
||||
return False
|
||||
_, metadata = self._tools[tool_name]
|
||||
return user_role in metadata.allowed_roles
|
||||
|
||||
def need_approval(self, tool_name: str) -> bool:
|
||||
"""判断是否需要审批"""
|
||||
if tool_name not in self._tools:
|
||||
return False
|
||||
_, metadata = self._tools[tool_name]
|
||||
return metadata.require_approval
|
||||
|
||||
|
||||
# 全局工具注册表
|
||||
global_registry = ToolRegistry()
|
||||
16
agent/app/core/tools/sandbox/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
沙盒模块
|
||||
"""
|
||||
from .sandbox import (
|
||||
Sandbox,
|
||||
SandboxConfig,
|
||||
SafeEval,
|
||||
sandbox,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Sandbox",
|
||||
"SandboxConfig",
|
||||
"SafeEval",
|
||||
"sandbox",
|
||||
]
|
||||
267
agent/app/core/tools/sandbox/sandbox.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""
|
||||
沙盒执行环境 - 在项目内构建,不依赖 Docker
|
||||
提供安全的代码执行环境
|
||||
"""
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
import shutil
|
||||
from typing import Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class SandboxConfig:
|
||||
"""沙盒配置"""
|
||||
# 资源限制
|
||||
MAX_MEMORY_MB = 256 # 最大内存 (MB)
|
||||
MAX_CPU_PERCENT = 50 # 最大 CPU 百分比
|
||||
MAX_EXECUTION_TIME = 30 # 最大执行时间 (秒)
|
||||
MAX_OUTPUT_SIZE = 1024 * 1024 # 最大输出大小 (bytes)
|
||||
|
||||
|
||||
class Sandbox:
|
||||
"""
|
||||
沙盒执行器 - 使用 subprocess 隔离执行
|
||||
|
||||
安全特性:
|
||||
- 内存限制
|
||||
- CPU限制
|
||||
- 超时控制
|
||||
- 网络隔离(可选)
|
||||
- 临时文件隔离
|
||||
"""
|
||||
|
||||
def __init__(self, config: Optional[SandboxConfig] = None):
|
||||
self.config = config or SandboxConfig()
|
||||
self.temp_dir = None
|
||||
|
||||
def _setup_temp_dir(self) -> str:
|
||||
"""创建临时目录"""
|
||||
self.temp_dir = tempfile.mkdtemp(prefix="sandbox_")
|
||||
return self.temp_dir
|
||||
|
||||
def _cleanup(self):
|
||||
"""清理临时目录"""
|
||||
if self.temp_dir and os.path.exists(self.temp_dir):
|
||||
try:
|
||||
shutil.rmtree(self.temp_dir)
|
||||
except Exception as e:
|
||||
print(f"Cleanup error: {e}")
|
||||
|
||||
def execute(
|
||||
self,
|
||||
code: str,
|
||||
language: str = "python",
|
||||
timeout: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
在沙盒中执行代码
|
||||
|
||||
Args:
|
||||
code: 要执行的代码
|
||||
language: 语言类型 (python, javascript)
|
||||
timeout: 超时时间(秒)
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
"""
|
||||
timeout = timeout or self.config.MAX_EXECUTION_TIME
|
||||
|
||||
self._setup_temp_dir()
|
||||
|
||||
try:
|
||||
if language == "python":
|
||||
return self._execute_python(code, timeout)
|
||||
elif language == "javascript":
|
||||
return self._execute_javascript(code, timeout)
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Unsupported language: {language}"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
finally:
|
||||
self._cleanup()
|
||||
|
||||
def _execute_python(self, code: str, timeout: int) -> Dict[str, Any]:
|
||||
"""执行 Python 代码"""
|
||||
# 创建临时文件
|
||||
temp_file = os.path.join(self.temp_dir, "code.py")
|
||||
with open(temp_file, "w", encoding="utf-8") as f:
|
||||
f.write(code)
|
||||
|
||||
try:
|
||||
# 构建命令
|
||||
cmd = ["python", temp_file]
|
||||
|
||||
# 执行
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
timeout=timeout,
|
||||
cwd=self.temp_dir, # 限制工作目录
|
||||
env=self._get_restricted_env(), # 限制环境变量
|
||||
)
|
||||
|
||||
# 检查输出大小
|
||||
stdout = result.stdout.decode("utf-8", errors="replace")
|
||||
stderr = result.stderr.decode("utf-8", errors="replace")
|
||||
|
||||
if len(stdout) > self.config.MAX_OUTPUT_SIZE:
|
||||
stdout = stdout[:self.config.MAX_OUTPUT_SIZE] + "\n... (output truncated)"
|
||||
|
||||
return {
|
||||
"success": result.returncode == 0,
|
||||
"output": stdout,
|
||||
"error": stderr if result.returncode != 0 else None,
|
||||
"exit_code": result.returncode
|
||||
}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Execution timeout ({timeout}s)",
|
||||
"output": None
|
||||
}
|
||||
|
||||
def _execute_javascript(self, code: str, timeout: int) -> Dict[str, Any]:
|
||||
"""执行 JavaScript 代码"""
|
||||
temp_file = os.path.join(self.temp_dir, "code.js")
|
||||
with open(temp_file, "w", encoding="utf-8") as f:
|
||||
f.write(code)
|
||||
|
||||
try:
|
||||
# 尝试使用 node
|
||||
cmd = ["node", temp_file]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
timeout=timeout,
|
||||
cwd=self.temp_dir,
|
||||
)
|
||||
|
||||
stdout = result.stdout.decode("utf-8", errors="replace")
|
||||
stderr = result.stderr.decode("utf-8", errors="replace")
|
||||
|
||||
return {
|
||||
"success": result.returncode == 0,
|
||||
"output": stdout,
|
||||
"error": stderr if result.returncode != 0 else None,
|
||||
"exit_code": result.returncode
|
||||
}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Execution timeout ({timeout}s)",
|
||||
"output": None
|
||||
}
|
||||
except FileNotFoundError:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Node.js not installed",
|
||||
"output": None
|
||||
}
|
||||
|
||||
def _get_restricted_env(self) -> Dict[str, str]:
|
||||
"""
|
||||
获取受限的环境变量
|
||||
移除敏感变量,保留必要的 PATH
|
||||
"""
|
||||
# 保留 PATH,移除其他敏感变量
|
||||
safe_env = {
|
||||
"PATH": os.environ.get("PATH", "/usr/bin:/bin"),
|
||||
"LANG": "en_US.UTF-8",
|
||||
"HOME": self.temp_dir,
|
||||
"TMPDIR": self.temp_dir,
|
||||
}
|
||||
|
||||
# 移除可能不安全的变量
|
||||
unsafe_vars = [
|
||||
"PYTHONPATH",
|
||||
"PYTHONHOME",
|
||||
"LD_PRELOAD",
|
||||
"LD_LIBRARY_PATH",
|
||||
]
|
||||
|
||||
for var in unsafe_vars:
|
||||
if var in os.environ:
|
||||
del os.environ[var]
|
||||
|
||||
return safe_env
|
||||
|
||||
|
||||
class SafeEval:
|
||||
"""
|
||||
安全求值器 - 用于简单表达式计算
|
||||
比沙盒更轻量,适用于不需要完全隔离的场景
|
||||
"""
|
||||
|
||||
# 安全函数白名单
|
||||
SAFE_BUILTINS = {
|
||||
"abs": abs,
|
||||
"min": min,
|
||||
"max": max,
|
||||
"sum": sum,
|
||||
"len": len,
|
||||
"round": round,
|
||||
"pow": pow,
|
||||
"print": print,
|
||||
"str": str,
|
||||
"int": int,
|
||||
"float": float,
|
||||
"bool": bool,
|
||||
"list": list,
|
||||
"dict": dict,
|
||||
"tuple": tuple,
|
||||
"set": set,
|
||||
"range": range,
|
||||
"enumerate": enumerate,
|
||||
"zip": zip,
|
||||
"map": map,
|
||||
"filter": filter,
|
||||
"sorted": sorted,
|
||||
"reversed": reversed,
|
||||
}
|
||||
|
||||
# 安全数学常量
|
||||
SAFE_MATH = {
|
||||
"pi": 3.14159265359,
|
||||
"e": 2.71828182846,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def eval(cls, expression: str) -> Any:
|
||||
"""
|
||||
安全地求值表达式
|
||||
|
||||
Args:
|
||||
expression: 数学表达式
|
||||
|
||||
Returns:
|
||||
计算结果
|
||||
"""
|
||||
# 预处理表达式
|
||||
expression = expression.replace("sqrt", "**0.5")
|
||||
|
||||
# 构建安全命名空间
|
||||
safe_namespace = {
|
||||
**cls.SAFE_BUILTINS,
|
||||
**cls.SAFE_MATH,
|
||||
"__builtins__": {} # 禁用__builtins__
|
||||
}
|
||||
|
||||
try:
|
||||
result = eval(expression, safe_namespace)
|
||||
return result
|
||||
except Exception as e:
|
||||
raise ValueError(f"Evaluation error: {e}")
|
||||
|
||||
|
||||
# 全局沙盒实例
|
||||
sandbox = Sandbox()
|
||||
347
agent/app/core/tools/tools.json
Normal file
@@ -0,0 +1,347 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"tools": [
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read the contents of a file from the filesystem.",
|
||||
"category": "file",
|
||||
"security_level": "safe",
|
||||
"require_approval": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_path": {
|
||||
"type": "string",
|
||||
"description": "The path to the file to read"
|
||||
},
|
||||
"encoding": {
|
||||
"type": "string",
|
||||
"description": "File encoding (default: utf-8)",
|
||||
"default": "utf-8"
|
||||
}
|
||||
},
|
||||
"required": ["file_path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"description": "Write content to a file. Creates the file if it doesn't exist, overwrites if it does.",
|
||||
"category": "file",
|
||||
"security_level": "review",
|
||||
"require_approval": true,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_path": {
|
||||
"type": "string",
|
||||
"description": "The path to the file to write"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "The content to write to the file"
|
||||
},
|
||||
"encoding": {
|
||||
"type": "string",
|
||||
"description": "File encoding (default: utf-8)",
|
||||
"default": "utf-8"
|
||||
}
|
||||
},
|
||||
"required": ["file_path", "content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "list_dir",
|
||||
"description": "List the contents of a directory.",
|
||||
"category": "file",
|
||||
"security_level": "safe",
|
||||
"require_approval": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dir_path": {
|
||||
"type": "string",
|
||||
"description": "The path to the directory to list (default: current directory)",
|
||||
"default": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "delete_file",
|
||||
"description": "Delete a file or directory.",
|
||||
"category": "file",
|
||||
"security_level": "danger",
|
||||
"require_approval": true,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_path": {
|
||||
"type": "string",
|
||||
"description": "The path to the file or directory to delete"
|
||||
}
|
||||
},
|
||||
"required": ["file_path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "search_files",
|
||||
"description": "Search for files by name pattern or content.",
|
||||
"category": "file",
|
||||
"security_level": "safe",
|
||||
"require_approval": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "The directory to search in"
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Glob pattern for file names (e.g., '*.py', '*.txt')",
|
||||
"default": "*"
|
||||
},
|
||||
"content_pattern": {
|
||||
"type": "string",
|
||||
"description": "Optional: search for files containing this text in their content"
|
||||
},
|
||||
"file_only": {
|
||||
"type": "boolean",
|
||||
"description": "Only return files, not directories",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"required": ["directory"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "execute_python",
|
||||
"description": "Execute Python code in a sandboxed environment. Use this for Python programming tasks, calculations, and data processing.",
|
||||
"category": "executor",
|
||||
"security_level": "review",
|
||||
"require_approval": true,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "The Python code to execute"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Execution timeout in seconds (default: 30, max: 60)",
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
"required": ["code"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "execute_javascript",
|
||||
"description": "Execute JavaScript code in a sandboxed environment. Use this for JavaScript programming tasks.",
|
||||
"category": "executor",
|
||||
"security_level": "review",
|
||||
"require_approval": true,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "The JavaScript code to execute"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Execution timeout in seconds (default: 30)",
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
"required": ["code"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "execute_bash",
|
||||
"description": "Execute a bash command in a sandboxed environment. Use this for shell operations, file management, and system commands.",
|
||||
"category": "executor",
|
||||
"security_level": "danger",
|
||||
"require_approval": true,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The bash command to execute"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Execution timeout in seconds (default: 30)",
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
"required": ["command"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "web_fetch",
|
||||
"description": "Fetch content from a web URL. Supports GET, POST methods and can return JSON or text content.",
|
||||
"category": "web",
|
||||
"security_level": "safe",
|
||||
"require_approval": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The URL to fetch"
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"description": "HTTP method (GET, POST)",
|
||||
"default": "GET"
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"description": "Query parameters"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"description": "Request headers"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "Request body (for POST)"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Request timeout in seconds",
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "web_search",
|
||||
"description": "Search the web for information. Use this when you need to find current information or facts.",
|
||||
"category": "web",
|
||||
"security_level": "safe",
|
||||
"require_approval": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The search query"
|
||||
},
|
||||
"max_results": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of results to return",
|
||||
"default": 5
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "http_request",
|
||||
"description": "Make HTTP requests to APIs. Supports GET, POST, PUT, DELETE methods with JSON data.",
|
||||
"category": "http",
|
||||
"security_level": "safe",
|
||||
"require_approval": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The URL to request"
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"description": "HTTP method (GET, POST, PUT, DELETE, PATCH)",
|
||||
"default": "GET"
|
||||
},
|
||||
"params": {
|
||||
"type": "object",
|
||||
"description": "Query parameters for GET requests"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"description": "Request headers"
|
||||
},
|
||||
"json_data": {
|
||||
"type": "object",
|
||||
"description": "JSON body for POST/PUT requests"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Request timeout in seconds",
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "send_notification",
|
||||
"description": "Send notifications via email, webhook, dingtalk, or slack.",
|
||||
"category": "notification",
|
||||
"security_level": "safe",
|
||||
"require_approval": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Notification type: email, webhook, dingtalk, slack",
|
||||
"enum": ["email", "webhook", "dingtalk", "slack"]
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "The notification message"
|
||||
},
|
||||
"to": {
|
||||
"type": "string",
|
||||
"description": "For email: recipient email address"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"description": "For email: email subject"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "For webhook: webhook URL"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "For webhook: JSON data to send"
|
||||
},
|
||||
"webhook": {
|
||||
"type": "string",
|
||||
"description": "Custom webhook URL for dingtalk/slack"
|
||||
},
|
||||
"channel": {
|
||||
"type": "string",
|
||||
"description": "For slack: channel name"
|
||||
}
|
||||
},
|
||||
"required": ["type", "message"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_current_time",
|
||||
"description": "Get the current date and time. Useful for timestamps or scheduling.",
|
||||
"category": "system",
|
||||
"security_level": "safe",
|
||||
"require_approval": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"description": "Optional timezone (e.g., 'UTC', 'Asia/Shanghai')",
|
||||
"default": "Local"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,835 +0,0 @@
|
||||
# X-Agents 智能体平台架构设计
|
||||
|
||||
## 一、整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 用户层 │
|
||||
│ Web / App / API Consumer │
|
||||
└─────────────────────────────────┬───────────────────────────────────────────┘
|
||||
│ HTTP / WebSocket
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Go API Gateway │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────┐│
|
||||
│ │ • HTTP Server (Gin) • 认证鉴权 (JWT) ││
|
||||
│ │ • 路由管理 • 限流熔断 ││
|
||||
│ │ • 业务逻辑 • 日志监控 ││
|
||||
│ │ • 数据库操作 • 权限控制 (RBAC) ││
|
||||
│ └─────────────────────────────────────────────────────────────────────────┘│
|
||||
└─────────────────────────────────┬───────────────────────────────────────────┘
|
||||
│ HTTP JSON API
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Python Agent Engine │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────────┐│
|
||||
│ │ • FastAPI Server • Agent Core (LangChain/AutoGen) ││
|
||||
│ │ • LLM Adapter • Tool Registry (白名单) ││
|
||||
│ │ • Memory Manager • Sandbox Executor (沙盒) ││
|
||||
│ │ • RAG Pipeline • Audit Logger ││
|
||||
│ └─────────────────────────────────────────────────────────────────────────┘│
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、系统分层
|
||||
|
||||
### 2.1 Go 后端层 (server/)
|
||||
|
||||
```
|
||||
server/ # Go API Gateway 服务
|
||||
├── cmd/api/ # 程序入口
|
||||
│ └── main.go
|
||||
├── internal/
|
||||
│ ├── config/ # 配置管理
|
||||
│ │ └── config.go
|
||||
│ ├── handler/ # HTTP处理器
|
||||
│ │ ├── auth_handler.go # 认证接口
|
||||
│ │ ├── chat_handler.go # 聊天接口
|
||||
│ │ └── approval_handler.go # 审批接口
|
||||
│ ├── service/ # 业务逻辑
|
||||
│ │ ├── auth_service.go
|
||||
│ │ ├── chat_service.go
|
||||
│ │ └── approval_service.go
|
||||
│ ├── repository/ # 数据访问层
|
||||
│ │ ├── user_repo.go
|
||||
│ │ ├── agent_repo.go
|
||||
│ │ └── audit_repo.go
|
||||
│ ├── middleware/ # 中间件
|
||||
│ │ └── auth.go
|
||||
│ └── model/ # 数据模型
|
||||
│ ├── user.go
|
||||
│ ├── agent.go
|
||||
│ └── audit.go
|
||||
├── config/ # 配置文件
|
||||
│ └── config.yaml
|
||||
├── Dockerfile
|
||||
├── go.mod
|
||||
└── go.sum
|
||||
```
|
||||
|
||||
### 2.2 Python Agent 层 (agent/)
|
||||
|
||||
```
|
||||
agent/ # Python Agent Engine
|
||||
├── app/
|
||||
│ ├── main.py # FastAPI入口
|
||||
│ ├── api/
|
||||
│ │ └── routes.py # API路由
|
||||
│ ├── agent/
|
||||
│ │ ├── core/
|
||||
│ │ │ ├── agent.py # Agent管理器
|
||||
│ │ │ └── executor.py # Agent执行器
|
||||
│ │ ├── tools/
|
||||
│ │ │ ├── registry.py # 工具注册表(白名单)
|
||||
│ │ │ └── impl/ # 工具实现
|
||||
│ │ │ ├── search.py
|
||||
│ │ │ ├── calculator.py
|
||||
│ │ │ └── time_tool.py
|
||||
│ │ └── memory/
|
||||
│ │ └── session.py # 会话管理
|
||||
│ ├── llm/
|
||||
│ │ └── factory.py # LLM工厂
|
||||
│ └── security/
|
||||
│ ├── audit.py # 审计日志
|
||||
│ └── approval.py # 审批服务
|
||||
├── requirements.txt
|
||||
├── Dockerfile
|
||||
└── pyproject.toml
|
||||
```
|
||||
|
||||
### 2.3 根目录结构
|
||||
|
||||
```
|
||||
X-Agents/
|
||||
├── server/ # Go API Gateway
|
||||
├── agent/ # Python Agent Engine
|
||||
├── web/ # 前端 (Vue.js)
|
||||
├── docs/
|
||||
│ └── ARCHITECTURE.md # 架构文档
|
||||
├── docker-compose.yml # 容器编排
|
||||
├── .env.example # 环境变量模板
|
||||
└── README.md
|
||||
│ ├── service/ # 业务逻辑
|
||||
│ │ ├── chat_service.go
|
||||
│ │ ├── agent_service.go
|
||||
│ │ └── approval_service.go # 审批服务
|
||||
│ ├── repository/ # 数据访问层
|
||||
│ │ ├── user_repo.go
|
||||
│ │ ├── agent_repo.go
|
||||
│ │ └── audit_repo.go
|
||||
│ ├── middleware/ # 中间件
|
||||
│ │ ├── auth.go # 认证中间件
|
||||
│ │ ├── rbac.go # 权限中间件
|
||||
│ │ └── audit.go # 审计中间件
|
||||
│ ├── client/ # 外部服务客户端
|
||||
│ │ └── python_client.go # Python服务HTTP客户端
|
||||
│ └── model/ # 数据模型
|
||||
│ ├── user.go
|
||||
│ ├── agent.go
|
||||
│ └── audit.go
|
||||
├── pkg/
|
||||
│ ├── utils/ # 工具函数
|
||||
│ └── errors/ # 错误定义
|
||||
└── go.mod
|
||||
```
|
||||
|
||||
### 2.2 Python AI 层 (智能逻辑)
|
||||
|
||||
```
|
||||
python/
|
||||
├── app/
|
||||
│ ├── main.py # FastAPI入口
|
||||
│ ├── api/
|
||||
│ │ ├── routes.py # 路由定义
|
||||
│ │ └── dependencies.py # 依赖注入
|
||||
│ ├── agent/
|
||||
│ │ ├── core/
|
||||
│ │ │ ├── agent.py # Agent核心
|
||||
│ │ │ ├── executor.py # 执行器
|
||||
│ │ │ └── memory.py # 记忆管理
|
||||
│ │ ├── tools/
|
||||
│ │ │ ├── registry.py # 工具注册表
|
||||
│ │ │ ├── base.py # 工具基类
|
||||
│ │ │ ├── security.py # 安全检查
|
||||
│ │ │ └── impl/ # 具体工具实现
|
||||
│ │ │ ├── search.py
|
||||
│ │ │ ├── calculator.py
|
||||
│ │ │ ├── database.py
|
||||
│ │ │ └── sandbox.py # 沙盒执行
|
||||
│ │ └── sandbox/
|
||||
│ │ ├── docker_sandbox.py
|
||||
│ │ └── wasm_sandbox.py
|
||||
│ ├── llm/
|
||||
│ │ ├── factory.py # LLM工厂
|
||||
│ │ ├── openai_adapter.py
|
||||
│ │ ├── anthropic_adapter.py
|
||||
│ │ └── base.py
|
||||
│ ├── rag/
|
||||
│ │ ├── vector_store.py # 向量存储
|
||||
│ │ ├── retriever.py # 检索器
|
||||
│ │ └── pipeline.py # RAG流程
|
||||
│ └── security/
|
||||
│ ├── permission.py # 权限检查
|
||||
│ ├── approval.py # 审批管理
|
||||
│ └── audit.py # 安全审计
|
||||
├── requirements.txt
|
||||
└── pyproject.toml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、通信机制
|
||||
|
||||
### 3.1 HTTP API 通信
|
||||
|
||||
```
|
||||
┌──────────────────┐ HTTP POST ┌──────────────────┐
|
||||
│ │ ─────────────▶ │ │
|
||||
│ Go Service │ JSON Request │ Python Service │
|
||||
│ (Port: 8080) │ │ (Port: 8081) │
|
||||
│ │ ◀───────────── │ │
|
||||
└──────────────────┘ JSON Response └──────────────────┘
|
||||
```
|
||||
|
||||
#### 接口设计
|
||||
|
||||
**1. Agent 聊天接口**
|
||||
|
||||
```
|
||||
POST /api/v1/agent/chat
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <token>
|
||||
|
||||
Request:
|
||||
{
|
||||
"agent_id": "agent_001",
|
||||
"message": "帮我查询用户数据",
|
||||
"session_id": "session_xxx",
|
||||
"context": {} // 额外上下文
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"reply": "查询结果...",
|
||||
"session_id": "session_xxx",
|
||||
"tools_used": ["query_database"],
|
||||
"metadata": {}
|
||||
}
|
||||
```
|
||||
|
||||
**2. 工具执行审批接口**
|
||||
|
||||
```
|
||||
POST /api/v1/tool/approve
|
||||
Request:
|
||||
{
|
||||
"request_id": "req_001",
|
||||
"tool_name": "execute_sql",
|
||||
"params": {"sql": "SELECT * FROM users"},
|
||||
"reason": "用户查询自己的订单",
|
||||
"approved": true // true=批准, false=拒绝
|
||||
}
|
||||
```
|
||||
|
||||
**3. 工具执行状态查询**
|
||||
|
||||
```
|
||||
GET /api/v1/tool/request/{request_id}
|
||||
Response:
|
||||
{
|
||||
"status": "pending|approved|rejected|completed",
|
||||
"tool_name": "execute_sql",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"result": null // 如果已完成
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Go → Python 客户端
|
||||
|
||||
```go
|
||||
// internal/client/python_client.go
|
||||
|
||||
package client
|
||||
|
||||
type PythonAgentClient struct {
|
||||
baseURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type ChatRequest struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
Message string `json:"message"`
|
||||
SessionID string `json:"session_id"`
|
||||
Context map[string]interface{} `json:"context"`
|
||||
}
|
||||
|
||||
type ChatResponse struct {
|
||||
Reply string `json:"reply"`
|
||||
SessionID string `json:"session_id"`
|
||||
ToolsUsed []string `json:"tools_used"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
// Chat 调用Python Agent服务
|
||||
func (c *PythonAgentClient) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
|
||||
// 1. 构建请求
|
||||
// 2. 添加超时
|
||||
// 3. 发送请求
|
||||
// 4. 处理响应
|
||||
// 5. 错误处理
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、沙盒安全机制
|
||||
|
||||
### 4.1 安全架构总览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ 安全控制层 │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ │
|
||||
│ │ 权限管理 │ │ 工具分级 │ │ 人工审批 │ │ 审计日志 │ │
|
||||
│ │ (RBAC) │ │ (白名单) │ │ (Approval) │ │ (Audit) │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └────┬─────┘ │
|
||||
└─────────┼─────────────────┼─────────────────┼─────────────────┼────────┘
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Agent 执行层 │
|
||||
│ │
|
||||
│ User Request ─▶ Permission Check ─▶ Tool Lookup ─▶ Execute ─▶ Result │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ [Need Approval] ──▶ [Pending Queue] ──▶ [Notify] │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 工具安全等级
|
||||
|
||||
```python
|
||||
# python/app/agent/tools/security.py
|
||||
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Callable
|
||||
|
||||
class SecurityLevel(Enum):
|
||||
"""工具安全等级"""
|
||||
SAFE = "safe" # 安全操作:搜索、计算、读取公开数据
|
||||
REVIEW = "review" # 需要审核:修改数据、发送消息
|
||||
DANGER = "danger" # 危险操作:删除数据、执行代码、敏感API
|
||||
|
||||
@dataclass
|
||||
class ToolMetadata:
|
||||
"""工具元数据"""
|
||||
name: str
|
||||
description: str
|
||||
security_level: SecurityLevel
|
||||
require_approval: bool # 是否需要人工审批
|
||||
allowed_roles: List[str] # 允许调用的角色
|
||||
rate_limit: int # 调用频率限制
|
||||
timeout: int # 超时时间(秒)
|
||||
|
||||
class ToolSecurity:
|
||||
"""工具安全管理"""
|
||||
|
||||
# 安全等级阈值
|
||||
APPROVAL_THRESHOLD = SecurityLevel.REVIEW
|
||||
|
||||
@staticmethod
|
||||
def check_permission(tool: ToolMetadata, user_role: str) -> bool:
|
||||
"""检查用户权限"""
|
||||
if user_role in tool.allowed_roles:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def need_approval(tool: ToolMetadata) -> bool:
|
||||
"""判断是否需要审批"""
|
||||
return tool.security_level.value >= ToolSecurity.APPROVAL_THRESHOLD.value
|
||||
```
|
||||
|
||||
### 4.3 工具注册与执行
|
||||
|
||||
```python
|
||||
# python/app/agent/tools/registry.py
|
||||
|
||||
from typing import Dict, Callable, Any
|
||||
from .security import ToolMetadata, SecurityLevel
|
||||
|
||||
class ToolRegistry:
|
||||
"""工具注册表 - 白名单机制"""
|
||||
|
||||
def __init__(self):
|
||||
self._tools: Dict[str, tuple[Callable, ToolMetadata]] = {}
|
||||
|
||||
def register(
|
||||
self,
|
||||
name: str,
|
||||
func: Callable,
|
||||
security_level: SecurityLevel = SecurityLevel.SAFE,
|
||||
require_approval: bool = False,
|
||||
allowed_roles: List[str] = None,
|
||||
description: str = ""
|
||||
):
|
||||
"""注册工具到白名单"""
|
||||
metadata = ToolMetadata(
|
||||
name=name,
|
||||
description=description,
|
||||
security_level=security_level,
|
||||
require_approval=require_approval or security_level == SecurityLevel.REVIEW,
|
||||
allowed_roles=allowed_roles or ["user", "admin"],
|
||||
rate_limit=100,
|
||||
timeout=30
|
||||
)
|
||||
self._tools[name] = (func, metadata)
|
||||
|
||||
def get_tool(self, name: str) -> tuple[Callable, ToolMetadata]:
|
||||
"""获取工具(必须在白名单中)"""
|
||||
if name not in self._tools:
|
||||
raise ValueError(f"Tool '{name}' not found in whitelist")
|
||||
return self._tools[name]
|
||||
|
||||
def list_tools(self) -> List[ToolMetadata]:
|
||||
"""列出所有可用工具"""
|
||||
return [meta for _, meta in self._tools.values()]
|
||||
```
|
||||
|
||||
### 4.4 沙盒执行
|
||||
|
||||
```python
|
||||
# python/app/agent/tools/sandbox/docker_sandbox.py
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
import shutil
|
||||
import os
|
||||
from typing import Any, Dict
|
||||
|
||||
class DockerSandbox:
|
||||
"""Docker沙盒执行环境"""
|
||||
|
||||
def __init__(self, image: str = "python-sandbox:latest", timeout: int = 30):
|
||||
self.image = image
|
||||
self.timeout = timeout
|
||||
|
||||
def execute(self, code: str, language: str = "python") -> Dict[str, Any]:
|
||||
"""在沙盒中执行代码"""
|
||||
|
||||
# 1. 创建临时文件
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode='w',
|
||||
suffix=f'.{language}',
|
||||
delete=False
|
||||
) as f:
|
||||
f.write(code)
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
# 2. Docker容器执行
|
||||
result = subprocess.run(
|
||||
[
|
||||
"docker", "run",
|
||||
"--rm",
|
||||
"--network", "none", # 断网
|
||||
"--memory", "256m", # 内存限制
|
||||
"--cpus", "0.5", # CPU限制
|
||||
"-v", f"{temp_path}:/code/{os.path.basename(temp_path)}",
|
||||
self.image,
|
||||
"python", f"/code/{os.path.basename(temp_path)}"
|
||||
],
|
||||
capture_output=True,
|
||||
timeout=self.timeout
|
||||
)
|
||||
|
||||
return {
|
||||
"success": result.returncode == 0,
|
||||
"output": result.stdout.decode(),
|
||||
"error": result.stderr.decode()
|
||||
}
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return {
|
||||
"success": False,
|
||||
"output": "",
|
||||
"error": "Execution timeout"
|
||||
}
|
||||
finally:
|
||||
# 3. 清理临时文件
|
||||
os.unlink(temp_path)
|
||||
|
||||
# 使用示例
|
||||
@sandbox.execute
|
||||
def execute_code(code: str) -> str:
|
||||
"""安全执行用户代码"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 4.5 Human in the Loop (人工审批)
|
||||
|
||||
```python
|
||||
# python/app/security/approval.py
|
||||
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
|
||||
class ApprovalStatus(Enum):
|
||||
PENDING = "pending"
|
||||
APPROVED = "approved"
|
||||
REJECTED = "rejected"
|
||||
|
||||
@dataclass
|
||||
class ApprovalRequest:
|
||||
"""审批请求"""
|
||||
request_id: str
|
||||
tool_name: str
|
||||
params: dict
|
||||
user_id: str
|
||||
reason: str
|
||||
status: ApprovalStatus
|
||||
created_at: datetime
|
||||
reviewed_at: Optional[datetime]
|
||||
reviewed_by: Optional[str]
|
||||
|
||||
class ApprovalService:
|
||||
"""审批服务"""
|
||||
|
||||
def __init__(self, http_client):
|
||||
self.client = http_client
|
||||
self.pending: Dict[str, ApprovalRequest] = {}
|
||||
|
||||
async def request_approval(
|
||||
self,
|
||||
tool_name: str,
|
||||
params: dict,
|
||||
user_id: str,
|
||||
reason: str
|
||||
) -> str:
|
||||
"""请求审批"""
|
||||
request_id = generate_uuid()
|
||||
|
||||
approval_req = ApprovalRequest(
|
||||
request_id=request_id,
|
||||
tool_name=tool_name,
|
||||
params=params,
|
||||
user_id=user_id,
|
||||
reason=reason,
|
||||
status=ApprovalStatus.PENDING,
|
||||
created_at=datetime.now(),
|
||||
reviewed_at=None,
|
||||
reviewed_by=None
|
||||
)
|
||||
|
||||
self.pending[request_id] = approval_req
|
||||
|
||||
# 通知Go后端有新审批
|
||||
await self.notify_go_service(approval_req)
|
||||
|
||||
return request_id
|
||||
|
||||
async def wait_for_approval(self, request_id: str, timeout: int = 300) -> bool:
|
||||
"""等待审批结果"""
|
||||
start = datetime.now()
|
||||
|
||||
while (datetime.now() - start).seconds < timeout:
|
||||
if request_id in self.pending:
|
||||
status = self.pending[request_id].status
|
||||
if status == ApprovalStatus.APPROVED:
|
||||
return True
|
||||
elif status == ApprovalStatus.REJECTED:
|
||||
return False
|
||||
await asyncio.sleep(1)
|
||||
|
||||
raise TimeoutError("Approval request timeout")
|
||||
```
|
||||
|
||||
### 4.6 全链路审计
|
||||
|
||||
```python
|
||||
# python/app/security/audit.py
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict
|
||||
import json
|
||||
|
||||
class AuditLogger:
|
||||
"""审计日志"""
|
||||
|
||||
def __init__(self, log_file: str = "audit.log"):
|
||||
self.log_file = log_file
|
||||
|
||||
def log(
|
||||
self,
|
||||
action: str,
|
||||
user_id: str,
|
||||
agent_id: str,
|
||||
details: Dict[str, Any],
|
||||
result: str = "success"
|
||||
):
|
||||
"""记录审计日志"""
|
||||
entry = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"action": action,
|
||||
"user_id": user_id,
|
||||
"agent_id": agent_id,
|
||||
"details": details,
|
||||
"result": result
|
||||
}
|
||||
|
||||
# 写入日志文件
|
||||
with open(self.log_file, 'a') as f:
|
||||
f.write(json.dumps(entry) + '\n')
|
||||
|
||||
# 发送到Go后端
|
||||
self.send_to_backend(entry)
|
||||
|
||||
def log_tool_execution(
|
||||
self,
|
||||
user_id: str,
|
||||
tool_name: str,
|
||||
params: Dict[str, Any],
|
||||
approved: bool,
|
||||
result: Any
|
||||
):
|
||||
"""记录工具执行"""
|
||||
self.log(
|
||||
action="tool_execution",
|
||||
user_id=user_id,
|
||||
agent_id="",
|
||||
details={
|
||||
"tool_name": tool_name,
|
||||
"params": params,
|
||||
"approved": approved,
|
||||
"result_preview": str(result)[:100]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、权限模型 (Go端)
|
||||
|
||||
### 5.1 用户角色
|
||||
|
||||
```go
|
||||
// go/internal/model/user.go
|
||||
|
||||
package model
|
||||
|
||||
// 权限级别
|
||||
type PermissionLevel int
|
||||
|
||||
const (
|
||||
PermissionRead PermissionLevel = 1 // 只读
|
||||
PermissionWrite PermissionLevel = 2 // 读写
|
||||
PermissionExecute PermissionLevel = 3 // 可执行工具
|
||||
PermissionAdmin PermissionLevel = 4 // 管理员
|
||||
)
|
||||
|
||||
// 角色定义
|
||||
type Role struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Permissions []PermissionLevel `json:"permissions"`
|
||||
}
|
||||
|
||||
// 用户
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
RoleID string `json:"role_id"`
|
||||
Role *Role `json:"role,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Agent定义
|
||||
|
||||
```go
|
||||
// go/internal/model/agent.go
|
||||
|
||||
package model
|
||||
|
||||
type Agent struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
|
||||
// Agent能力配置
|
||||
Capabilities []string `json:"capabilities"` // 可用工具列表
|
||||
MemoryLimit int64 `json:"memory_limit"` // 内存限制
|
||||
Timeout int `json:"timeout"` // 超时时间
|
||||
|
||||
// 安全配置
|
||||
SecurityLevel SecurityLevel `json:"security_level"`
|
||||
AllowDangerousTools bool `json:"allow_dangerous_tools"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、部署架构
|
||||
|
||||
### 6.1 Docker Compose
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Go API 服务
|
||||
go-api:
|
||||
build: ./go
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- DATABASE_URL=postgres://user:pass@db:5432/agents
|
||||
- PYTHON_SERVICE_URL=http://python-agent:8081
|
||||
- JWT_SECRET=your-secret
|
||||
depends_on:
|
||||
- db
|
||||
- python-agent
|
||||
|
||||
# Python Agent 服务
|
||||
python-agent:
|
||||
build: ./python
|
||||
ports:
|
||||
- "8081:8081"
|
||||
environment:
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
volumes:
|
||||
- ./python/app:/app
|
||||
- /var/run/docker.sock:/var/run/docker.sock # 如果需要Docker沙盒
|
||||
|
||||
# 数据库
|
||||
db:
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: pass
|
||||
POSTGRES_DB: agents
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
|
||||
# Redis (缓存/会话)
|
||||
redis:
|
||||
image: redis:7
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
# 向量数据库 (可选)
|
||||
qdrant:
|
||||
image: qdrant/qdrant
|
||||
volumes:
|
||||
- qdrant-data:/qdrant/storage
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
redis-data:
|
||||
qdrant-data:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、开发流程
|
||||
|
||||
### 7.1 请求流程图
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ 用户 │────▶│ Go │────▶│ Python │────▶│ LLM │────▶│ 返回 │
|
||||
│ 请求 │ │ 鉴权 │ │ Agent │ │ +Tools │ │ 结果 │
|
||||
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
|
||||
│ │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────┐ ┌─────────┐
|
||||
│ 检查 │ │ 权限 │
|
||||
│ 权限 │ │ 检查 │
|
||||
└─────────┘ └─────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 工具安全等级判断 │
|
||||
└─────────────────────┘
|
||||
│
|
||||
┌─────────────────┼─────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Safe │ │ Review │ │ Danger │
|
||||
│ 直接执行 │ │ 等待审批 │ │ 拒绝执行 │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
### 7.2 目录结构总览
|
||||
|
||||
```
|
||||
X-Agents/
|
||||
├── docs/
|
||||
│ └── ARCHITECTURE.md # 本文档
|
||||
│
|
||||
├── go/ # Go 后端
|
||||
│ ├── cmd/
|
||||
│ ├── internal/
|
||||
│ ├── pkg/
|
||||
│ ├── go.mod
|
||||
│ └── Dockerfile
|
||||
│
|
||||
├── python/ # Python AI 层
|
||||
│ ├── app/
|
||||
│ │ ├── api/
|
||||
│ │ ├── agent/
|
||||
│ │ ├── llm/
|
||||
│ │ ├── rag/
|
||||
│ │ └── security/
|
||||
│ ├── requirements.txt
|
||||
│ └── Dockerfile
|
||||
│
|
||||
├── web/ # 前端 (Vue)
|
||||
│ ├── src/
|
||||
│ └── package.json
|
||||
│
|
||||
├── docker-compose.yml # 容器编排
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、总结
|
||||
|
||||
### 架构核心原则
|
||||
|
||||
| 原则 | 实现方式 |
|
||||
|------|----------|
|
||||
| **分层治理** | Go负责业务/权限,Python负责AI逻辑 |
|
||||
| **安全优先** | 工具分级+权限控制+人工审批+审计日志 |
|
||||
| **通信简洁** | HTTP JSON API,后续可升级gRPC |
|
||||
| **可扩展** | 模块化设计,支持多Agent/多Python服务 |
|
||||
| **可观测** | 全链路日志+监控 |
|
||||
|
||||
### 安全特性
|
||||
|
||||
- [x] 工具白名单机制
|
||||
- [x] 安全等级分级 (Safe/Review/Danger)
|
||||
- [x] RBAC权限控制
|
||||
- [x] Human in the Loop 人工审批
|
||||
- [x] 沙盒执行环境 (Docker)
|
||||
- [x] 全链路审计日志
|
||||
|
||||
---
|
||||
|
||||
*本文档将随项目开发持续更新*
|
||||
337
docs/agents.html
@@ -1,337 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Remo MVP Design Interface</title>
|
||||
<!-- 引入Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- 引入Font Awesome图标 -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<!-- 自定义Tailwind配置 -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#1E6BF9',
|
||||
figma: '#F24E1E',
|
||||
action: '#000000',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/tailwindcss">
|
||||
@layer utilities {
|
||||
.node-shadow {
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
.flow-line {
|
||||
@apply bg-gray-300 w-0.5;
|
||||
}
|
||||
.flow-line-h {
|
||||
@apply bg-gray-300 h-0.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 font-sans h-screen flex flex-col overflow-hidden">
|
||||
<!-- 顶部浏览器模拟栏 -->
|
||||
<div class="bg-white border-b border-gray-200 px-4 py-2 flex items-center gap-2">
|
||||
<div class="flex gap-1.5">
|
||||
<div class="w-3 h-3 rounded-full bg-red-400"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-yellow-400"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-green-400"></div>
|
||||
</div>
|
||||
<div class="flex-1 mx-4 bg-gray-100 rounded-md px-3 py-1 text-sm text-gray-500 flex items-center gap-2">
|
||||
<i class="fa-solid fa-lock text-xs"></i>
|
||||
<span>Remo MVP Design</span>
|
||||
</div>
|
||||
<div class="flex gap-2 text-gray-500">
|
||||
<i class="fa-solid fa-chevron-left"></i>
|
||||
<i class="fa-solid fa-chevron-right opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容三栏布局 -->
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<!-- 左侧边栏 -->
|
||||
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full">
|
||||
<!-- 顶部导航信息 -->
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<div class="flex items-center gap-1">
|
||||
<i class="fa-solid fa-th text-gray-600"></i>
|
||||
<span class="font-medium text-gray-800">Designera</span>
|
||||
<span class="text-gray-400">|</span>
|
||||
<span class="text-gray-600 text-sm">Web Design</span>
|
||||
</div>
|
||||
<i class="fa-solid fa-window-maximize text-gray-500"></i>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">Drafts</p>
|
||||
</div>
|
||||
|
||||
<!-- 文件/资产标签栏 -->
|
||||
<div class="px-4 py-2 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex gap-4">
|
||||
<button class="text-sm font-medium text-gray-800 border-b-2 border-primary pb-1">File</button>
|
||||
<button class="text-sm font-medium text-gray-400 pb-1">Assets</button>
|
||||
</div>
|
||||
<i class="fa-solid fa-magnifying-glass text-gray-500 text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pages区域 -->
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<span class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Pages</span>
|
||||
<button class="text-gray-500 hover:text-gray-800">
|
||||
<i class="fa-solid fa-plus text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 bg-gray-100 rounded px-3 py-2">
|
||||
<i class="fa-solid fa-palette text-sm text-gray-600"></i>
|
||||
<span class="text-sm font-medium text-gray-800">Design</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layers区域 -->
|
||||
<div class="p-4 flex-1 overflow-y-auto">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<i class="fa-solid fa-layer-group text-xs text-gray-500"></i>
|
||||
<span class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Layers</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 中间画布区域 -->
|
||||
<main class="flex-1 bg-gray-50 relative overflow-auto">
|
||||
<!-- 流程图容器 -->
|
||||
<div class="min-h-full w-full flex flex-col items-center py-10 px-4">
|
||||
<!-- 节点1 -->
|
||||
<div class="node bg-white rounded-lg node-shadow w-72 p-3 relative z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="checkbox" checked class="w-5 h-5 rounded text-primary focus:ring-primary">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<i class="fa-brands fa-figma text-figma text-lg"></i>
|
||||
<span class="font-medium text-gray-800">Figma</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-gray-500">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square text-sm"></i>
|
||||
<i class="fa-solid fa-pen text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-700 ml-8 mt-1">1. Start your project from sketch</p>
|
||||
</div>
|
||||
|
||||
<!-- 连接线1 -->
|
||||
<div class="flow-line h-6"></div>
|
||||
|
||||
<!-- 节点2 Action黑色节点 -->
|
||||
<div class="node bg-action rounded-lg node-shadow w-72 p-3 relative z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-5 h-5 flex items-center justify-center">
|
||||
<i class="fa-solid fa-bolt text-white text-sm"></i>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="font-medium text-white">Action</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-white/70">
|
||||
<i class="fa-solid fa-pen text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-white/90 ml-8 mt-1">2. Take a frame and start designing</p>
|
||||
</div>
|
||||
|
||||
<!-- 连接线2 -->
|
||||
<div class="flow-line h-6"></div>
|
||||
|
||||
<!-- 节点3 -->
|
||||
<div class="node bg-white rounded-lg node-shadow w-72 p-3 relative z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="checkbox" checked class="w-5 h-5 rounded text-primary focus:ring-primary">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<i class="fa-brands fa-figma text-figma text-lg"></i>
|
||||
<span class="font-medium text-gray-800">Figma</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-gray-500">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square text-sm"></i>
|
||||
<i class="fa-solid fa-pen text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-700 ml-8 mt-1">3. Add few shapes</p>
|
||||
</div>
|
||||
|
||||
<!-- 连接线3 -->
|
||||
<div class="flow-line h-6"></div>
|
||||
|
||||
<!-- 节点4 -->
|
||||
<div class="node bg-white rounded-lg node-shadow w-72 p-3 relative z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="checkbox" checked class="w-5 h-5 rounded text-primary focus:ring-primary">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<i class="fa-brands fa-figma text-figma text-lg"></i>
|
||||
<span class="font-medium text-gray-800">Figma</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-gray-500">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square text-sm"></i>
|
||||
<i class="fa-solid fa-pen text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-700 ml-8 mt-1">4. Split into paths</p>
|
||||
</div>
|
||||
|
||||
<!-- 分支连接线 -->
|
||||
<div class="w-full max-w-2xl relative flex justify-center">
|
||||
<div class="flow-line h-6"></div>
|
||||
<div class="absolute top-6 w-full flex justify-center">
|
||||
<div class="flow-line-h w-2/3"></div>
|
||||
</div>
|
||||
<div class="absolute top-6 w-full flex justify-between px-[12.5%]">
|
||||
<div class="flow-line h-6"></div>
|
||||
<div class="flow-line h-6"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分支标签 -->
|
||||
<div class="w-full max-w-2xl flex justify-between px-[12.5%] mb-6">
|
||||
<span class="bg-primary text-white text-xs font-medium px-3 py-1 rounded-full">Path A</span>
|
||||
<span class="bg-primary text-white text-xs font-medium px-3 py-1 rounded-full">Path B</span>
|
||||
</div>
|
||||
|
||||
<!-- 双分支节点 -->
|
||||
<div class="w-full max-w-2xl flex justify-between px-4">
|
||||
<!-- 节点5 Path A -->
|
||||
<div class="node bg-white rounded-lg node-shadow w-72 p-3 relative z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="checkbox" checked class="w-5 h-5 rounded text-primary focus:ring-primary">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<i class="fa-brands fa-figma text-figma text-lg"></i>
|
||||
<span class="font-medium text-gray-800">Figma</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-gray-500">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square text-sm"></i>
|
||||
<i class="fa-solid fa-pen text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-700 ml-8 mt-1">5. Split into paths</p>
|
||||
</div>
|
||||
|
||||
<!-- 节点6 Path B -->
|
||||
<div class="node bg-white rounded-lg node-shadow w-72 p-3 relative z-10">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="checkbox" checked class="w-5 h-5 rounded text-primary focus:ring-primary">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<i class="fa-brands fa-figma text-figma text-lg"></i>
|
||||
<span class="font-medium text-gray-800">Figma</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-gray-500">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square text-sm"></i>
|
||||
<i class="fa-solid fa-pen text-sm"></i>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-700 ml-8 mt-1">6. Split into paths</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部工具栏 -->
|
||||
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-white rounded-full shadow-lg px-4 py-2 flex items-center gap-4">
|
||||
<button class="text-primary p-1.5 rounded hover:bg-gray-100">
|
||||
<i class="fa-solid fa-cursor-pointer"></i>
|
||||
</button>
|
||||
<button class="text-gray-600 p-1.5 rounded hover:bg-gray-100">
|
||||
<i class="fa-solid fa-hand"></i>
|
||||
</button>
|
||||
<button class="text-gray-600 p-1.5 rounded hover:bg-gray-100">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
</button>
|
||||
<button class="text-gray-600 p-1.5 rounded hover:bg-gray-100">
|
||||
<i class="fa-regular fa-square"></i>
|
||||
</button>
|
||||
<button class="text-gray-600 p-1.5 rounded hover:bg-gray-100">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
<button class="text-gray-600 p-1.5 rounded hover:bg-gray-100">
|
||||
<i class="fa-solid fa-t"></i>
|
||||
</button>
|
||||
<button class="text-gray-600 p-1.5 rounded hover:bg-gray-100">
|
||||
<i class="fa-regular fa-circle"></i>
|
||||
</button>
|
||||
<div class="w-px h-5 bg-gray-200"></div>
|
||||
<button class="text-gray-600 p-1.5 rounded hover:bg-gray-100">
|
||||
<i class="fa-solid fa-code"></i>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 右侧Copilot面板 -->
|
||||
<aside class="w-96 bg-white border-l border-gray-200 flex flex-col h-full">
|
||||
<!-- 顶部Copilot品牌区 -->
|
||||
<div class="p-6 flex flex-col items-center text-center border-b border-gray-200">
|
||||
<div class="w-16 h-16 rounded-xl bg-gradient-to-tr from-blue-500 via-purple-500 to-orange-400 mb-4 flex items-center justify-center">
|
||||
<div class="w-10 h-10 bg-white/20 backdrop-blur-sm rounded-lg"></div>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-gray-800 mb-2">Hey, what's on your mind today?</h2>
|
||||
<button class="bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium px-4 py-2 rounded-lg transition">
|
||||
Get advice
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 聊天对话区 -->
|
||||
<div class="flex-1 overflow-y-auto p-4 flex flex-col gap-6">
|
||||
<!-- Copilot消息 -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<div class="w-5 h-5 rounded bg-gradient-to-tr from-blue-500 to-purple-500 flex items-center justify-center">
|
||||
<i class="fa-brands fa-microsoft text-white text-xs"></i>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700">Copilot</span>
|
||||
</div>
|
||||
<div class="bg-gray-100 rounded-2xl rounded-tl-none px-4 py-3 text-gray-700 ml-7">
|
||||
Advice on what, exactly? Whether it's life decisions, career choices, relationships, or something entirely random, I'm here to help. Let me know the topic, and we'll dive into it together!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户消息 -->
|
||||
<div class="flex flex-col gap-2 items-end">
|
||||
<div class="bg-gray-800 rounded-2xl rounded-tr-none px-4 py-3 text-white max-w-[80%]">
|
||||
What are some common life decisions people seek advice on?
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部输入框 -->
|
||||
<div class="p-4 border-t border-gray-200">
|
||||
<div class="relative">
|
||||
<input type="text" placeholder="Ask anything..." class="w-full border border-gray-300 rounded-full px-4 py-3 pr-12 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
|
||||
<button class="absolute right-3 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center">
|
||||
<i class="fa-solid fa-arrow-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 mt-2 ml-2 text-xs text-gray-500">
|
||||
<i class="fa-brands fa-microsoft"></i>
|
||||
<span>Copilot by Microsoft</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,692 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Saasfactor Dashboard</title>
|
||||
<!-- 引入Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- 引入Font Awesome图标 -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<!-- 自定义Tailwind配置 -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
dark: {
|
||||
950: '#0a0c10', // 侧边栏背景
|
||||
900: '#0f1115', // 主页面背景
|
||||
800: '#121419',
|
||||
700: '#171922', // 卡片背景
|
||||
600: '#1a1c25',
|
||||
500: '#2a2c36', // 次要背景/边框
|
||||
},
|
||||
primary: {
|
||||
orange: '#ff9500',
|
||||
yellow: '#ffc247',
|
||||
cyan: '#36bffa',
|
||||
purple: '#a78bfa',
|
||||
danger: '#ef4444',
|
||||
success: '#10b981',
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/tailwindcss">
|
||||
@layer utilities {
|
||||
.content-auto {
|
||||
content-visibility: auto;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-dark-900 text-gray-100 font-sans min-h-screen flex overflow-x-hidden">
|
||||
<!-- 左侧侧边栏 -->
|
||||
<aside class="w-64 bg-dark-950 h-screen flex flex-col fixed left-0 top-0 overflow-y-auto scrollbar-hide z-10">
|
||||
<!-- 顶部Logo与组织信息 -->
|
||||
<div class="p-5 flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-primary-orange to-red-500 flex items-center justify-center">
|
||||
<i class="fa-solid fa-basketball text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-lg flex items-center gap-1">
|
||||
Organization
|
||||
<i class="fa-solid fa-chevron-down text-xs text-gray-400"></i>
|
||||
</div>
|
||||
<div class="text-sm text-gray-400">Saasfactor</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<nav class="flex-1 px-3 py-2">
|
||||
<ul class="space-y-1">
|
||||
<!-- Dashboard 激活项 -->
|
||||
<li>
|
||||
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-dark-600 text-white">
|
||||
<i class="fa-solid fa-gauge w-5 text-center"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Agents -->
|
||||
<li>
|
||||
<a href="#" class="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fa-solid fa-robot w-5 text-center"></i>
|
||||
<span>Agents</span>
|
||||
</div>
|
||||
<span class="bg-dark-500 text-xs px-2 py-0.5 rounded-full">3</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- MCP -->
|
||||
<li>
|
||||
<a href="#" class="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fa-solid fa-code w-5 text-center"></i>
|
||||
<span>MCP</span>
|
||||
</div>
|
||||
<span class="bg-dark-500 text-xs px-2 py-0.5 rounded-full">21</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Model APIs -->
|
||||
<li>
|
||||
<a href="#" class="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fa-solid fa-cube w-5 text-center"></i>
|
||||
<span>Model APIs</span>
|
||||
</div>
|
||||
<span class="bg-dark-500 text-xs px-2 py-0.5 rounded-full">13</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Policies -->
|
||||
<li>
|
||||
<a href="#" class="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fa-solid fa-shield-halved w-5 text-center"></i>
|
||||
<span>Policies</span>
|
||||
</div>
|
||||
<span class="bg-dark-500 text-xs px-2 py-0.5 rounded-full">1</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<li class="my-4 border-t border-dark-500"></li>
|
||||
|
||||
<!-- API Keys -->
|
||||
<li>
|
||||
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-key w-5 text-center"></i>
|
||||
<span>API Keys</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Settings -->
|
||||
<li>
|
||||
<a href="#" class="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fa-solid fa-gear w-5 text-center"></i>
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<span class="w-2 h-2 rounded-full bg-primary-orange"></span>
|
||||
<span class="w-2 h-2 rounded-full bg-yellow-500"></span>
|
||||
<span class="w-2 h-2 rounded-full bg-gray-500"></span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Team -->
|
||||
<li>
|
||||
<a href="#" class="flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fa-solid fa-users w-5 text-center"></i>
|
||||
<span>Team</span>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<span class="w-2 h-2 rounded-full bg-primary-orange"></span>
|
||||
<span class="w-2 h-2 rounded-full bg-blue-500"></span>
|
||||
<span class="w-2 h-2 rounded-full bg-gray-500"></span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Service Accounts -->
|
||||
<li>
|
||||
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-user-shield w-5 text-center"></i>
|
||||
<span>Service Accounts</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Integrations -->
|
||||
<li>
|
||||
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-plug w-5 text-center"></i>
|
||||
<span>Integrations</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<li class="my-4 border-t border-dark-500"></li>
|
||||
|
||||
<!-- Information -->
|
||||
<li>
|
||||
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-circle-info w-5 text-center"></i>
|
||||
<span>Information</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Account -->
|
||||
<li>
|
||||
<a href="#" class="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-dark-600 text-gray-300 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-user w-5 text-center"></i>
|
||||
<span>Account</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- 底部用户信息 -->
|
||||
<div class="p-4 border-t border-dark-500">
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="https://picsum.photos/id/64/40/40" alt="User Avatar" class="w-8 h-8 rounded-full object-cover">
|
||||
<div>
|
||||
<div class="font-medium text-sm">Alex Smith</div>
|
||||
<div class="text-xs text-gray-400">alex@gmail.com</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="ml-64 flex-1 p-6">
|
||||
<!-- 顶部导航与日期选择区 -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-gauge text-gray-400"></i>
|
||||
<span class="font-medium">Dashboard</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-gray-400">Date Range</span>
|
||||
<div class="flex items-center gap-2 bg-dark-600 rounded-lg px-3 py-2">
|
||||
<span>10 December</span>
|
||||
<span class="text-gray-400">To</span>
|
||||
<span>12 December</span>
|
||||
<i class="fa-solid fa-calendar text-gray-400"></i>
|
||||
</div>
|
||||
<button class="text-primary-orange hover:text-orange-400 transition-colors">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片网格布局 -->
|
||||
<div class="grid grid-cols-3 gap-6">
|
||||
<!-- 第一行:3个状态卡片 -->
|
||||
<!-- Active Agents 卡片 -->
|
||||
<div class="bg-dark-700 rounded-xl p-5">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold text-lg">Active Agents</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-400">Errors</span>
|
||||
<!-- 开启状态开关 -->
|
||||
<div class="w-10 h-5 rounded-full bg-primary-orange relative cursor-pointer">
|
||||
<div class="absolute right-0.5 top-0.5 w-4 h-4 rounded-full bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-4xl font-bold mb-4">3</div>
|
||||
<!-- 进度条 -->
|
||||
<div class="w-full h-2 rounded-full bg-dark-500 overflow-hidden mb-4">
|
||||
<div class="flex h-full">
|
||||
<div class="w-[33%] bg-primary-yellow"></div>
|
||||
<div class="w-[33%] bg-primary-cyan"></div>
|
||||
<div class="w-[34%] bg-primary-purple"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 明细列表 -->
|
||||
<ul class="space-y-2">
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm bg-primary-yellow"></span>
|
||||
<span class="text-sm text-gray-300">template-google-adk-api</span>
|
||||
</div>
|
||||
<span class="text-sm">1</span>
|
||||
</li>
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm bg-primary-cyan"></span>
|
||||
<span class="text-sm text-gray-300">mcp-google-adk-api</span>
|
||||
<span class="bg-primary-danger text-white text-xs px-1.5 py-0.5 rounded">Error</span>
|
||||
</div>
|
||||
<span class="text-sm">1</span>
|
||||
</li>
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm bg-primary-purple"></span>
|
||||
<span class="text-sm text-gray-300">template-openai-api</span>
|
||||
<span class="bg-primary-danger text-white text-xs px-1.5 py-0.5 rounded">Error</span>
|
||||
</div>
|
||||
<span class="text-sm">1</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Active MCP Servers 卡片 -->
|
||||
<div class="bg-dark-700 rounded-xl p-5">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold text-lg">Active MCP Servers</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-400">Errors</span>
|
||||
<!-- 关闭状态开关 -->
|
||||
<div class="w-10 h-5 rounded-full bg-dark-500 relative cursor-pointer">
|
||||
<div class="absolute left-0.5 top-0.5 w-4 h-4 rounded-full bg-gray-400"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-4xl font-bold mb-4">21</div>
|
||||
<!-- 进度条 -->
|
||||
<div class="w-full h-2 rounded-full bg-dark-500 overflow-hidden mb-4">
|
||||
<div class="flex h-full">
|
||||
<div class="w-[71%] bg-primary-yellow"></div>
|
||||
<div class="w-[19%] bg-primary-cyan"></div>
|
||||
<div class="w-[10%] bg-primary-purple"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 明细列表 -->
|
||||
<ul class="space-y-2">
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm bg-primary-yellow"></span>
|
||||
<span class="text-sm text-gray-300">linear-demo</span>
|
||||
</div>
|
||||
<span class="text-sm">15</span>
|
||||
</li>
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm bg-primary-cyan"></span>
|
||||
<span class="text-sm text-gray-300">google-maps</span>
|
||||
</div>
|
||||
<span class="text-sm">4</span>
|
||||
</li>
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm bg-primary-purple"></span>
|
||||
<span class="text-sm text-gray-300">explorer-mcp</span>
|
||||
</div>
|
||||
<span class="text-sm">2</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Active Models 卡片 -->
|
||||
<div class="bg-dark-700 rounded-xl p-5">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold text-lg">Active Models</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-gray-400">Errors</span>
|
||||
<!-- 关闭状态开关 -->
|
||||
<div class="w-10 h-5 rounded-full bg-dark-500 relative cursor-pointer">
|
||||
<div class="absolute left-0.5 top-0.5 w-4 h-4 rounded-full bg-gray-400"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-4xl font-bold mb-4">13</div>
|
||||
<!-- 进度条 -->
|
||||
<div class="w-full h-2 rounded-full bg-dark-500 overflow-hidden mb-4">
|
||||
<div class="flex h-full">
|
||||
<div class="w-[46%] bg-primary-yellow"></div>
|
||||
<div class="w-[15%] bg-primary-cyan"></div>
|
||||
<div class="w-[39%] bg-primary-purple"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 明细列表 -->
|
||||
<ul class="space-y-2">
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm bg-primary-yellow"></span>
|
||||
<span class="text-sm text-gray-300">gpt-40-2024-08-12</span>
|
||||
</div>
|
||||
<span class="text-sm">2</span>
|
||||
</li>
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm bg-primary-cyan"></span>
|
||||
<span class="text-sm text-gray-300">cerebras-sandbox</span>
|
||||
</div>
|
||||
<span class="text-sm">6</span>
|
||||
</li>
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm bg-primary-purple"></span>
|
||||
<span class="text-sm text-gray-300">sandbox-openai</span>
|
||||
</div>
|
||||
<span class="text-sm">5</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 第二行 -->
|
||||
<!-- All deployment request Insights 卡片(跨2列)【已修复对齐问题】 -->
|
||||
<div class="bg-dark-700 rounded-xl p-5 col-span-2">
|
||||
<h3 class="font-semibold text-lg mb-4">All deployment request Insights</h3>
|
||||
<!-- 数据概览 -->
|
||||
<div class="grid grid-cols-4 gap-4 mb-6">
|
||||
<div>
|
||||
<div class="text-sm text-gray-400 mb-1">Requests</div>
|
||||
<div class="text-2xl font-bold">36</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-400 mb-1">Agents calls</div>
|
||||
<div class="text-2xl font-bold">3</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-400 mb-1">MCP servers calls</div>
|
||||
<div class="text-2xl font-bold">21</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm text-gray-400 mb-1">Models requests</div>
|
||||
<div class="text-2xl font-bold">13</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 【重构后】图表容器 完美对齐0刻度 -->
|
||||
<div class="relative h-52 w-full">
|
||||
<!-- 绘图区(网格线+Y轴+柱子) 预留底部20px给时间标签 -->
|
||||
<div class="relative h-[calc(100%-20px)] w-full">
|
||||
<!-- 横向网格线 与Y轴刻度完全对齐 0刻度线在绘图区最底部 -->
|
||||
<div class="absolute left-0 top-0 w-full h-full z-0">
|
||||
<div class="absolute w-full h-[1px] bg-white/[0.06] top-0"></div>
|
||||
<div class="absolute w-full h-[1px] bg-white/[0.06] top-[25%]"></div>
|
||||
<div class="absolute w-full h-[1px] bg-white/[0.06] top-[50%]"></div>
|
||||
<div class="absolute w-full h-[1px] bg-white/[0.06] top-[75%]"></div>
|
||||
<div class="absolute w-full h-[1px] bg-white/[0.06] top-[100%]"></div>
|
||||
</div>
|
||||
|
||||
<!-- Y轴刻度 0刻度与绘图区最底部的0网格线完美垂直对齐 -->
|
||||
<div class="absolute left-0 top-0 h-full flex flex-col justify-between text-xs text-gray-500 z-10">
|
||||
<span>8</span>
|
||||
<span>6</span>
|
||||
<span>4</span>
|
||||
<span>2</span>
|
||||
<span>0</span>
|
||||
</div>
|
||||
|
||||
<!-- 柱状图容器 柱子底部严丝合缝对齐0网格线 -->
|
||||
<div class="ml-6 h-full flex items-end justify-between gap-1 z-10 relative">
|
||||
<!-- 3:02 PM 柱子 -->
|
||||
<div class="flex items-end gap-1 h-full w-full justify-center">
|
||||
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 12.5%"></div>
|
||||
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 25%"></div>
|
||||
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 18.75%"></div>
|
||||
</div>
|
||||
<!-- 3:07 PM 柱子 -->
|
||||
<div class="flex items-end gap-1 h-full w-full justify-center">
|
||||
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 25%"></div>
|
||||
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 31.25%"></div>
|
||||
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 25%"></div>
|
||||
</div>
|
||||
<!-- 3:12 PM 柱子 -->
|
||||
<div class="flex items-end gap-1 h-full w-full justify-center">
|
||||
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 31.25%"></div>
|
||||
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 62.5%"></div>
|
||||
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 50%"></div>
|
||||
</div>
|
||||
<!-- 3:17 PM 柱子 -->
|
||||
<div class="flex items-end gap-1 h-full w-full justify-center">
|
||||
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 31.25%"></div>
|
||||
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 37.5%"></div>
|
||||
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 25%"></div>
|
||||
</div>
|
||||
<!-- 3:22 PM 柱子 -->
|
||||
<div class="flex items-end gap-1 h-full w-full justify-center">
|
||||
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 18.75%"></div>
|
||||
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 31.25%"></div>
|
||||
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 18.75%"></div>
|
||||
</div>
|
||||
<!-- 3:27 PM 柱子 -->
|
||||
<div class="flex items-end gap-1 h-full w-full justify-center">
|
||||
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 18.75%"></div>
|
||||
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 37.5%"></div>
|
||||
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 31.25%"></div>
|
||||
</div>
|
||||
<!-- 3:32 PM 柱子 -->
|
||||
<div class="flex items-end gap-1 h-full w-full justify-center">
|
||||
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 12.5%"></div>
|
||||
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 25%"></div>
|
||||
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 31.25%"></div>
|
||||
</div>
|
||||
<!-- 3:37 PM 柱子 -->
|
||||
<div class="flex items-end gap-1 h-full w-full justify-center">
|
||||
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 31.25%"></div>
|
||||
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 100%"></div>
|
||||
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 37.5%"></div>
|
||||
</div>
|
||||
<!-- 3:42 PM 柱子 -->
|
||||
<div class="flex items-end gap-1 h-full w-full justify-center">
|
||||
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 12.5%"></div>
|
||||
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 62.5%"></div>
|
||||
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 31.25%"></div>
|
||||
</div>
|
||||
<!-- 3:47 PM 柱子 -->
|
||||
<div class="flex items-end gap-1 h-full w-full justify-center">
|
||||
<div class="w-3 bg-primary-purple rounded-t-sm" style="height: 31.25%"></div>
|
||||
<div class="w-3 bg-primary-yellow rounded-t-sm" style="height: 37.5%"></div>
|
||||
<div class="w-3 bg-primary-cyan rounded-t-sm" style="height: 25%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间标签区 统一放在0刻度线下方 与对应柱子居中对齐 -->
|
||||
<div class="ml-6 w-full flex justify-between gap-1 mt-1">
|
||||
<span class="text-xs text-gray-500 w-full text-center">3:02 PM</span>
|
||||
<span class="text-xs text-gray-500 w-full text-center">3:07 PM</span>
|
||||
<span class="text-xs text-gray-500 w-full text-center">3:12 PM</span>
|
||||
<span class="text-xs text-gray-500 w-full text-center">3:17 PM</span>
|
||||
<span class="text-xs text-gray-500 w-full text-center">3:22 PM</span>
|
||||
<span class="text-xs text-gray-500 w-full text-center">3:27 PM</span>
|
||||
<span class="text-xs text-gray-500 w-full text-center">3:32 PM</span>
|
||||
<span class="text-xs text-gray-500 w-full text-center">3:37 PM</span>
|
||||
<span class="text-xs text-gray-500 w-full text-center">3:42 PM</span>
|
||||
<span class="text-xs text-gray-500 w-full text-center">3:47 PM</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图例 -->
|
||||
<div class="flex justify-center gap-6 mt-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm bg-primary-purple"></span>
|
||||
<span class="text-xs text-gray-400">Agents calls</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm bg-primary-yellow"></span>
|
||||
<span class="text-xs text-gray-400">MCP servers calls</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm bg-primary-cyan"></span>
|
||||
<span class="text-xs text-gray-400">Models requests</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top 10 requests 卡片 -->
|
||||
<div class="bg-dark-700 rounded-xl p-5">
|
||||
<h3 class="font-semibold text-lg mb-4">Top 10 requests</h3>
|
||||
<!-- 标签切换 -->
|
||||
<div class="flex mb-4">
|
||||
<button class="flex-1 bg-dark-500 text-white py-1.5 rounded-l-lg text-sm">General</button>
|
||||
<button class="flex-1 bg-transparent text-gray-400 py-1.5 rounded-r-lg text-sm hover:bg-dark-600 transition-colors">Errors</button>
|
||||
</div>
|
||||
<!-- 列表 -->
|
||||
<ul class="space-y-3">
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-cube text-gray-400"></i>
|
||||
<span class="text-sm text-gray-300">gpt-40-2024-08-12</span>
|
||||
</div>
|
||||
<span class="text-sm">7</span>
|
||||
</li>
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-code text-gray-400"></i>
|
||||
<span class="text-sm text-gray-300">google-maps</span>
|
||||
</div>
|
||||
<span class="text-sm">4</span>
|
||||
</li>
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-code text-gray-400"></i>
|
||||
<span class="text-sm text-gray-300">explorer-mcp</span>
|
||||
</div>
|
||||
<span class="text-sm">2</span>
|
||||
</li>
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-cube text-gray-400"></i>
|
||||
<span class="text-sm text-gray-300">template-google-adk-api</span>
|
||||
</div>
|
||||
<span class="text-sm">4</span>
|
||||
</li>
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-cube text-gray-400"></i>
|
||||
<span class="text-sm text-gray-300">linear-demo</span>
|
||||
</div>
|
||||
<span class="text-sm">2</span>
|
||||
</li>
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-code text-gray-400"></i>
|
||||
<span class="text-sm text-gray-300">cerebras-sandbox</span>
|
||||
</div>
|
||||
<span class="text-sm">1</span>
|
||||
</li>
|
||||
<li class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-cube text-gray-400"></i>
|
||||
<span class="text-sm text-gray-300">sandbox-openai</span>
|
||||
</div>
|
||||
<span class="text-sm">2</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 第三行 -->
|
||||
<!-- What's new 卡片(跨2列) -->
|
||||
<div class="bg-dark-700 rounded-xl p-5 col-span-2">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold text-lg">What's new</h3>
|
||||
<a href="#" class="text-primary-orange text-sm flex items-center gap-1 hover:text-orange-400 transition-colors">
|
||||
Full change log
|
||||
<i class="fa-solid fa-arrow-up-right-from-square"></i>
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400 mb-4">Stay up to date with our latest feature and improvements</p>
|
||||
<!-- 更新列表 -->
|
||||
<ul class="space-y-4">
|
||||
<li class="flex justify-between items-start">
|
||||
<div>
|
||||
<h4 class="font-medium mb-1">New framework supported: PydanticAI</h4>
|
||||
<p class="text-sm text-gray-400">Added support for PydanticAI framework</p>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">2025-04-12</span>
|
||||
</li>
|
||||
<li class="flex justify-between items-start">
|
||||
<div>
|
||||
<h4 class="font-medium mb-1">New framework supported: Google ADK</h4>
|
||||
<p class="text-sm text-gray-400">Added support for Google ADK (Agent Development Kit) framework</p>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">2025-04-07</span>
|
||||
</li>
|
||||
<li class="flex justify-between items-start">
|
||||
<div>
|
||||
<h4 class="font-medium mb-1">Improved Analytics Dashboard</h4>
|
||||
<p class="text-sm text-gray-400">Enhanced real-time monitoring with faster data refresh</p>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">2025-04-15</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Recent requests (10) 卡片 -->
|
||||
<div class="bg-dark-700 rounded-xl p-5">
|
||||
<h3 class="font-semibold text-lg mb-4">Recent requests (10)</h3>
|
||||
<!-- 列表 -->
|
||||
<ul class="space-y-3">
|
||||
<li class="flex items-center gap-3">
|
||||
<i class="fa-solid fa-cube text-gray-400"></i>
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">linear-demo</span>
|
||||
<span class="text-xs text-gray-500">in 21 hours</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-xs text-primary-success">
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
<span>Success</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center gap-3">
|
||||
<i class="fa-solid fa-robot text-gray-400"></i>
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">myagent</span>
|
||||
<span class="text-xs text-gray-500">in 21 hours</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-xs text-primary-success">
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
<span>Success</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center gap-3">
|
||||
<i class="fa-solid fa-cube text-gray-400"></i>
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">linear-demo</span>
|
||||
<span class="text-xs text-gray-500">in 21 hours</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-xs text-primary-success">
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
<span>Success</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center gap-3">
|
||||
<i class="fa-solid fa-code text-gray-400"></i>
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">gpt-40</span>
|
||||
<span class="text-xs text-gray-500">in 21 hours</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-xs text-primary-success">
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
<span>Success</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex items-center gap-3">
|
||||
<i class="fa-solid fa-cube text-gray-400"></i>
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-sm font-medium">linear-demo</span>
|
||||
<span class="text-xs text-gray-500">in 21 hours</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-xs text-primary-success">
|
||||
<i class="fa-solid fa-circle-check"></i>
|
||||
<span>Success</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
909
docs/graph.html
@@ -1,909 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>思维本体元模型 — 知识图谱</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||
<script src="./graph-data.js"></script>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', 'PingFang SC', -apple-system, sans-serif;
|
||||
background: #0a0e1a;
|
||||
color: #e6edf3;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
height: 52px;
|
||||
background: linear-gradient(90deg, #0f1628, #1a1040);
|
||||
border-bottom: 1px solid #2d2d55;
|
||||
flex-shrink: 0;
|
||||
z-index: 100;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#topbar .title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#topbar .title .icon {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
#topbar .title h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(90deg, #8B5CF6, #3B82F6, #10B981);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
#topbar .subtitle {
|
||||
font-size: 12px;
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
#topbar .controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ctrl-btn {
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #2d2d55;
|
||||
color: #c9d1d9;
|
||||
padding: 5px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all .2s;
|
||||
}
|
||||
|
||||
.ctrl-btn:hover {
|
||||
background: #2d2d55;
|
||||
border-color: #8B5CF6;
|
||||
color: #8B5CF6;
|
||||
}
|
||||
|
||||
.ctrl-btn.active {
|
||||
background: #4C1D95;
|
||||
border-color: #8B5CF6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#main-layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#left-panel {
|
||||
width: 230px;
|
||||
background: #0f1628;
|
||||
border-right: 1px solid #2d2d55;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
padding: 14px 14px 10px;
|
||||
border-bottom: 1px solid #1a1a2e;
|
||||
}
|
||||
|
||||
.panel-section-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
color: #8b949e;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background .15s;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.legend-item:hover {
|
||||
background: #1a1a2e;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 6px currentColor;
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
font-size: 13px;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.legend-count {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
color: #8b949e;
|
||||
background: #1a1a2e;
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.scenario-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 4px;
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #2d2d55;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: #c9d1d9;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
transition: all .2s;
|
||||
}
|
||||
|
||||
.scenario-btn:hover {
|
||||
border-color: #8B5CF6;
|
||||
color: #8B5CF6;
|
||||
background: #1a1040;
|
||||
}
|
||||
|
||||
.scenario-btn.active {
|
||||
border-color: var(--sc-color);
|
||||
color: var(--sc-color);
|
||||
background: rgba(139, 92, 246, .08);
|
||||
box-shadow: 0 0 8px rgba(139, 92, 246, .2);
|
||||
}
|
||||
|
||||
.scenario-btn .sc-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--sc-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.scenario-btn .sc-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.scenario-btn .sc-count {
|
||||
font-size: 10px;
|
||||
color: #8b949e;
|
||||
background: #0f1628;
|
||||
padding: 1px 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.rel-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 3px 0;
|
||||
font-size: 11px;
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.rel-line {
|
||||
width: 28px;
|
||||
height: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#chart-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#graph-hint {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(15, 22, 40, 0.85);
|
||||
border: 1px solid #2d2d55;
|
||||
border-radius: 8px;
|
||||
padding: 6px 14px;
|
||||
font-size: 11px;
|
||||
color: #8b949e;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
#right-panel {
|
||||
width: 260px;
|
||||
background: #0f1628;
|
||||
border-left: 1px solid #2d2d55;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#detail-header {
|
||||
padding: 14px 16px 10px;
|
||||
border-bottom: 1px solid #1a1a2e;
|
||||
}
|
||||
|
||||
#detail-type-badge {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#detail-name {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #e6edf3;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#detail-tag {
|
||||
font-size: 11px;
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
#detail-body {
|
||||
padding: 12px 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#detail-desc {
|
||||
font-size: 12px;
|
||||
color: #8b949e;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
background: #0a0e1a;
|
||||
border: 1px solid #1a1a2e;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#detail-connections {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.conn-title {
|
||||
font-size: 11px;
|
||||
color: #6e7681;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.conn-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 2px;
|
||||
font-size: 11px;
|
||||
color: #8b949e;
|
||||
background: #0a0e1a;
|
||||
border: 1px solid #1a1a2e;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.conn-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.conn-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.conn-rel {
|
||||
font-size: 10px;
|
||||
color: #6e7681;
|
||||
}
|
||||
|
||||
#detail-placeholder {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #484f58;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#detail-placeholder .ph-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
#detail-placeholder .ph-text {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
#stats-bar {
|
||||
display: flex;
|
||||
padding: 10px 16px;
|
||||
gap: 10px;
|
||||
border-top: 1px solid #1a1a2e;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-num {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 10px;
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
/* 模拟控制栏 */
|
||||
#sim-bar {
|
||||
position: fixed;
|
||||
bottom: -160px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: min(820px, 94vw);
|
||||
background: linear-gradient(135deg, #1a1040, #0f1628);
|
||||
border: 1px solid #2d2d55;
|
||||
border-bottom: none;
|
||||
border-radius: 14px 14px 0 0;
|
||||
box-shadow: 0 -4px 32px rgba(0, 0, 0, 0.6);
|
||||
z-index: 500;
|
||||
transition: bottom .4s cubic-bezier(.4, 0, .2, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#sim-bar.visible {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#sim-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 18px 8px;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #1a1a2e;
|
||||
}
|
||||
|
||||
#sim-header .sim-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#sim-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #c084fc;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#sim-step-badge {
|
||||
font-size: 11px;
|
||||
background: #1a1a2e;
|
||||
color: #8b949e;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #2d2d55;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#sim-phase-badge {
|
||||
font-size: 10px;
|
||||
padding: 3px 9px;
|
||||
border-radius: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#sim-progress {
|
||||
height: 3px;
|
||||
background: #1a1a2e;
|
||||
margin: 0 18px;
|
||||
}
|
||||
|
||||
#sim-progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #8B5CF6, #c084fc);
|
||||
border-radius: 2px;
|
||||
transition: width .4s ease;
|
||||
}
|
||||
|
||||
#sim-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 18px 14px;
|
||||
}
|
||||
|
||||
#sim-desc {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: #c9d1d9;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
#sim-desc .sim-step-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #e6edf3;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
#sim-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sim-btn {
|
||||
padding: 7px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #2d2d55;
|
||||
background: #1a1a2e;
|
||||
color: #c9d1d9;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transition: all .2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sim-btn:hover {
|
||||
background: #2d2d55;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sim-btn:disabled {
|
||||
opacity: .35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.sim-btn.primary {
|
||||
background: linear-gradient(135deg, #7C3AED, #8B5CF6);
|
||||
border-color: #7C3AED;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 12px rgba(124, 58, 237, .4);
|
||||
}
|
||||
|
||||
.sim-btn.stop {
|
||||
background: #2d1f1f;
|
||||
border-color: #EF4444;
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.sim-btn.stop:hover {
|
||||
background: #EF4444;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#sim-auto-btn.playing {
|
||||
background: linear-gradient(135deg, #059669, #10B981);
|
||||
border-color: #10B981;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #2d2d55;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="topbar">
|
||||
<div class="title">
|
||||
<span class="icon">🧠</span>
|
||||
<div>
|
||||
<h1>思维本体元模型 — 知识图谱</h1>
|
||||
<div class="subtitle">Thinking Ontology Meta-Model · ECharts Force-Directed Graph</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button class="ctrl-btn" id="btn-reset">↺ 复位</button>
|
||||
<button class="ctrl-btn active" id="btn-labels">标签 ON</button>
|
||||
<button class="ctrl-btn active" id="btn-rules">规则边</button>
|
||||
<button class="ctrl-btn" id="btn-fullscreen">⛶ 全屏</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="main-layout">
|
||||
<div id="left-panel">
|
||||
<div class="panel-section">
|
||||
<div class="panel-section-title">节点类型图例</div>
|
||||
<div class="legend-item" onclick="filterByCategory(0)">
|
||||
<div class="legend-dot" style="background:#3B82F6;color:#3B82F6"></div>
|
||||
<span class="legend-label">语义对象</span>
|
||||
<span class="legend-count" id="cnt-0">—</span>
|
||||
</div>
|
||||
<div class="legend-item" onclick="filterByCategory(1)">
|
||||
<div class="legend-dot" style="background:#10B981;color:#10B981"></div>
|
||||
<span class="legend-label">对象行为</span>
|
||||
<span class="legend-count" id="cnt-1">—</span>
|
||||
</div>
|
||||
<div class="legend-item" onclick="filterByCategory(2)">
|
||||
<div class="legend-dot" style="background:#F59E0B;color:#F59E0B"></div>
|
||||
<span class="legend-label">约束规则</span>
|
||||
<span class="legend-count" id="cnt-2">—</span>
|
||||
</div>
|
||||
<div class="legend-item" onclick="filterByCategory(3)">
|
||||
<div class="legend-dot" style="background:#8B5CF6;color:#8B5CF6"></div>
|
||||
<span class="legend-label">编排流程</span>
|
||||
<span class="legend-count" id="cnt-3">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-section">
|
||||
<div class="panel-section-title">场景联动筛选</div>
|
||||
<div id="scenario-buttons"></div>
|
||||
<button class="ctrl-btn" style="width:100%;margin-top:6px;font-size:11px" onclick="clearScenario()">✕
|
||||
清除筛选</button>
|
||||
</div>
|
||||
<div class="panel-section">
|
||||
<div class="panel-section-title">关系类型</div>
|
||||
<div class="rel-item">
|
||||
<div class="rel-line" style="background:#3B82F6"></div><span>语义对象关系</span>
|
||||
</div>
|
||||
<div class="rel-item">
|
||||
<div class="rel-line" style="background:#10B981"></div><span>行为操作关系</span>
|
||||
</div>
|
||||
<div class="rel-item">
|
||||
<div class="rel-line"
|
||||
style="background:repeating-linear-gradient(90deg,#F59E0B 0,#F59E0B 4px,transparent 4px,transparent 8px)">
|
||||
</div><span>规则引用关系</span>
|
||||
</div>
|
||||
<div class="rel-item">
|
||||
<div class="rel-line" style="background:#8B5CF6"></div><span>流程调用关系</span>
|
||||
</div>
|
||||
<div class="rel-item">
|
||||
<div class="rel-line" style="background:#EF4444"></div><span>⭐ 核心关系</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stats-bar">
|
||||
<div class="stat-item">
|
||||
<div class="stat-num" style="color:#3B82F6" id="s-nodes">—</div>
|
||||
<div class="stat-label">节点</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-num" style="color:#10B981" id="s-links">—</div>
|
||||
<div class="stat-label">关系</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="chart-container">
|
||||
<div id="chart"></div>
|
||||
<div id="graph-hint">🖱 拖拽节点 · 滚轮缩放 · 点击查看详情 · 双击流程触发场景</div>
|
||||
<div id="sim-bar">
|
||||
<div id="sim-header">
|
||||
<span class="sim-icon">🎬</span>
|
||||
<span id="sim-title">问题分析与解决 — 动态模拟</span>
|
||||
<span id="sim-phase-badge"></span>
|
||||
<span id="sim-step-badge">0 / 14</span>
|
||||
</div>
|
||||
<div id="sim-progress">
|
||||
<div id="sim-progress-bar" style="width:0%"></div>
|
||||
</div>
|
||||
<div id="sim-body">
|
||||
<div id="sim-desc">
|
||||
<div class="sim-step-title" id="sim-step-title">点击「下一步」开始模拟</div>
|
||||
<div id="sim-step-desc">逐步展示问题分析与解决的完整调用链路。</div>
|
||||
</div>
|
||||
<div id="sim-controls">
|
||||
<button class="sim-btn" id="sim-prev-btn" disabled onclick="simStep(-1)">◀ 上一步</button>
|
||||
<button class="sim-btn primary" id="sim-next-btn" onclick="simStep(1)">下一步 ▶</button>
|
||||
<button class="sim-btn" id="sim-auto-btn" onclick="simToggleAuto()">⏵ 自动</button>
|
||||
<button class="sim-btn stop" onclick="simStop()">■ 停止</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="right-panel">
|
||||
<div id="detail-placeholder">
|
||||
<div class="ph-icon">🔍</div>
|
||||
<div class="ph-text">点击图谱中任意节点<br>查看详细元数据信息</div>
|
||||
</div>
|
||||
<div id="detail-content" style="display:none;flex-direction:column;flex:1">
|
||||
<div id="detail-header">
|
||||
<div id="detail-type-badge"></div>
|
||||
<div id="detail-name"></div>
|
||||
<div id="detail-tag"></div>
|
||||
</div>
|
||||
<div id="detail-body">
|
||||
<div class="conn-title">节点描述</div>
|
||||
<div id="detail-desc"></div>
|
||||
<div id="detail-connections">
|
||||
<div class="conn-title" style="margin-top:14px">关联节点</div>
|
||||
<div id="conn-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { CATEGORIES, NODES, LINKS, SCENARIOS, SIMULATION_STEPS } = ONTOLOGY_DATA;
|
||||
let chart = null, activeScenario = null, showLabels = true, showRuleEdges = true, catFilter = null;
|
||||
const CAT_COLORS = ['#3B82F6', '#10B981', '#F59E0B', '#8B5CF6'];
|
||||
const CAT_NAMES = ['语义对象', '对象行为', '约束规则', '编排流程'];
|
||||
const nodeMap = {}; NODES.forEach(n => nodeMap[n.id] = n);
|
||||
|
||||
document.getElementById('s-nodes').textContent = NODES.length;
|
||||
document.getElementById('s-links').textContent = LINKS.length;
|
||||
for (let i = 0; i < 4; i++) document.getElementById('cnt-' + i).textContent = NODES.filter(n => n.category === i).length;
|
||||
|
||||
// 场景按钮
|
||||
const scColors = { 'WF.ProblemSolving': '#F59E0B', 'WF.ArticleGen': '#3B82F6', 'WF.KnowledgeUpdate': '#10B981', 'WF.CognitionUpgrade': '#EF4444' };
|
||||
Object.entries(SCENARIOS).forEach(([key, sc]) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'scenario-btn'; btn.dataset.key = key;
|
||||
btn.style.setProperty('--sc-color', scColors[key] || '#8B5CF6');
|
||||
btn.innerHTML = `<span class="sc-dot"></span><span class="sc-label">${sc.label}</span><span class="sc-count">${sc.nodes.length}节点</span>`;
|
||||
btn.onclick = () => activateScenario(key);
|
||||
document.getElementById('scenario-buttons').appendChild(btn);
|
||||
});
|
||||
|
||||
// 构建 ECharts 选项
|
||||
function buildOption(highlightIds = null) {
|
||||
const filteredLinks = showRuleEdges ? LINKS : LINKS.filter(l => l.relType !== 'reference');
|
||||
const nodes = NODES.map(n => {
|
||||
const hi = highlightIds ? highlightIds.includes(n.id) : true;
|
||||
const cv = catFilter === null || n.category === catFilter;
|
||||
const op = (hi && cv) ? 1 : 0.08;
|
||||
return {
|
||||
id: n.id, name: n.name, category: n.category,
|
||||
symbolSize: n.symbolSize * (hi ? 1 : 0.85),
|
||||
label: { show: showLabels && hi && cv, fontSize: n.category === 3 ? 11 : (n.category === 0 ? 10 : 9), fontWeight: n.category <= 1 ? 'bold' : 'normal' },
|
||||
itemStyle: {
|
||||
color: CAT_COLORS[n.category], opacity: op,
|
||||
shadowBlur: hi && n.tag && n.tag.includes('⭐') ? 20 : (hi ? 8 : 0),
|
||||
shadowColor: hi ? CAT_COLORS[n.category] : 'transparent',
|
||||
borderColor: hi && n.tag && n.tag.includes('⭐') ? '#FFD700' : CAT_COLORS[n.category],
|
||||
borderWidth: hi && n.tag && n.tag.includes('⭐') ? 2.5 : 0,
|
||||
}, _data: n,
|
||||
};
|
||||
});
|
||||
const links = filteredLinks.map(l => {
|
||||
const sv = highlightIds ? highlightIds.includes(l.source) : true;
|
||||
const tv = highlightIds ? highlightIds.includes(l.target) : true;
|
||||
const v = sv && tv;
|
||||
return {
|
||||
source: l.source, target: l.target,
|
||||
label: { show: v && showLabels && l.relType !== 'reference', formatter: l.label, fontSize: 9, color: '#8b949e' },
|
||||
lineStyle: { ...l.lineStyle, opacity: v ? (l.relType === 'reference' ? 0.5 : 0.75) : 0.03, curveness: 0.2, type: l.lineStyle.type || 'solid' },
|
||||
symbol: ['none', 'arrow'], symbolSize: [0, 7],
|
||||
};
|
||||
});
|
||||
return {
|
||||
backgroundColor: '#0a0e1a',
|
||||
tooltip: {
|
||||
trigger: 'item', backgroundColor: 'rgba(15,22,40,0.95)', borderColor: '#2d2d55', textStyle: { color: '#e6edf3', fontSize: 12 },
|
||||
formatter: p => { if (p.dataType !== 'node') return ''; const n = nodeMap[p.data.id]; if (!n) return ''; return `<div style="max-width:260px"><div style="color:${CAT_COLORS[n.category]};font-weight:bold;font-size:13px;margin-bottom:4px">${n.name.replace(/\n/g, ' ')}</div><div style="color:#8b949e;font-size:10px;margin-bottom:6px">[${CAT_NAMES[n.category]}] ${n.tag || ''}</div><div style="color:#c9d1d9;font-size:11px;line-height:1.6;white-space:pre-wrap">${n.desc || ''}</div></div>`; }
|
||||
},
|
||||
legend: { show: false },
|
||||
series: [{
|
||||
type: 'graph', layout: 'force', animation: true, animationDuration: 800, animationEasingUpdate: 'quinticInOut',
|
||||
data: nodes, links: links,
|
||||
categories: CATEGORIES.map((c, i) => ({ name: c.name, itemStyle: { color: CAT_COLORS[i] } })),
|
||||
roam: true, draggable: true, focusNodeAdjacency: false,
|
||||
symbol: (v, p) => { const c = p.data.category; return c === 3 ? 'diamond' : c === 2 ? 'roundRect' : 'circle'; },
|
||||
label: { show: showLabels, position: 'bottom', formatter: '{b}', color: '#c9d1d9', textBorderColor: '#0a0e1a', textBorderWidth: 3 },
|
||||
edgeLabel: { show: showLabels, fontSize: 9, color: '#8b949e' },
|
||||
force: { repulsion: 320, edgeLength: [80, 200], gravity: 0.06, layoutAnimation: true, friction: 0.6 },
|
||||
lineStyle: { curveness: 0.2 },
|
||||
emphasis: { focus: 'adjacency', scale: true, itemStyle: { shadowBlur: 20 }, lineStyle: { width: 3, opacity: 1 }, label: { show: true } },
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
function initChart() {
|
||||
chart = echarts.init(document.getElementById('chart'), 'dark');
|
||||
chart.setOption(buildOption());
|
||||
chart.on('click', p => { if (p.dataType === 'node') showDetail(p.data.id); });
|
||||
chart.on('dblclick', p => { if (p.dataType === 'node' && SCENARIOS[p.data.id]) activateScenario(p.data.id); });
|
||||
window.addEventListener('resize', () => chart.resize());
|
||||
}
|
||||
|
||||
function showDetail(nodeId) {
|
||||
const n = nodeMap[nodeId]; if (!n) return;
|
||||
document.getElementById('detail-placeholder').style.display = 'none';
|
||||
document.getElementById('detail-content').style.display = 'flex';
|
||||
const color = CAT_COLORS[n.category];
|
||||
const badge = document.getElementById('detail-type-badge');
|
||||
badge.textContent = CAT_NAMES[n.category]; badge.style.background = color + '22'; badge.style.color = color; badge.style.border = `1px solid ${color}55`;
|
||||
document.getElementById('detail-name').textContent = n.name.replace(/\n/g, ' ');
|
||||
document.getElementById('detail-name').style.color = color;
|
||||
document.getElementById('detail-tag').textContent = n.tag || '';
|
||||
document.getElementById('detail-desc').textContent = n.desc || '暂无描述';
|
||||
const connList = document.getElementById('conn-list'); connList.innerHTML = '';
|
||||
const rels = LINKS.filter(l => l.source === nodeId || l.target === nodeId);
|
||||
if (!rels.length) { connList.innerHTML = '<div style="color:#484f58;font-size:11px">无直接关联</div>'; return; }
|
||||
rels.slice(0, 12).forEach(l => {
|
||||
const isOut = l.source === nodeId, otherId = isOut ? l.target : l.source, o = nodeMap[otherId];
|
||||
if (!o) return;
|
||||
const div = document.createElement('div'); div.className = 'conn-item';
|
||||
div.innerHTML = `<div class="conn-dot" style="background:${CAT_COLORS[o.category]}"></div><div class="conn-text">${o.name.replace(/\n/g, ' ')}</div><div class="conn-rel">${isOut ? '→' : '←'} ${l.label}</div>`;
|
||||
div.onclick = () => showDetail(otherId); connList.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function activateScenario(key) {
|
||||
activeScenario = key;
|
||||
document.querySelectorAll('.scenario-btn').forEach(b => b.classList.toggle('active', b.dataset.key === key));
|
||||
chart.setOption(buildOption(SCENARIOS[key].nodes));
|
||||
showDetail(key);
|
||||
if (SIMULATION_STEPS[key]) simInit(key); else simHide();
|
||||
}
|
||||
function clearScenario() {
|
||||
activeScenario = null; catFilter = null; simHide(); simClearTimer(); simCurrentStep = -1;
|
||||
document.querySelectorAll('.scenario-btn').forEach(b => b.classList.remove('active'));
|
||||
chart.setOption(buildOption());
|
||||
}
|
||||
function filterByCategory(cat) {
|
||||
catFilter = catFilter === cat ? null : cat;
|
||||
chart.setOption(buildOption(activeScenario ? SCENARIOS[activeScenario].nodes : null));
|
||||
}
|
||||
|
||||
document.getElementById('btn-reset').onclick = () => { clearScenario(); chart.dispatchAction({ type: 'restore' }); };
|
||||
document.getElementById('btn-labels').onclick = function () { showLabels = !showLabels; this.textContent = showLabels ? '标签 ON' : '标签 OFF'; this.classList.toggle('active', showLabels); chart.setOption(buildOption(activeScenario ? SCENARIOS[activeScenario].nodes : null)); };
|
||||
document.getElementById('btn-rules').onclick = function () { showRuleEdges = !showRuleEdges; this.textContent = showRuleEdges ? '规则边' : '规则边 OFF'; this.classList.toggle('active', showRuleEdges); chart.setOption(buildOption(activeScenario ? SCENARIOS[activeScenario].nodes : null)); };
|
||||
document.getElementById('btn-fullscreen').onclick = () => { if (!document.fullscreenElement) document.documentElement.requestFullscreen(); else document.exitFullscreen(); };
|
||||
|
||||
initChart();
|
||||
|
||||
// ================================================================
|
||||
// 动态模拟系统
|
||||
// ================================================================
|
||||
let simCurrentStep = -1, simScenarioKey = null, simAutoTimer = null, simIsPlaying = false;
|
||||
const PHASE_META = {
|
||||
process: { color: '#8B5CF6', bg: 'rgba(139,92,246,0.15)', label: '🟣 流程启动' },
|
||||
behavior: { color: '#10B981', bg: 'rgba(16,185,129,0.15)', label: '🟢 行为调用' },
|
||||
rule: { color: '#F59E0B', bg: 'rgba(245,158,11,0.15)', label: '🟠 规则引用' },
|
||||
entity: { color: '#3B82F6', bg: 'rgba(59,130,246,0.15)', label: '🔵 对象生成' },
|
||||
complete: { color: '#FFD700', bg: 'rgba(255,215,0,0.15)', label: '✅ 完成' },
|
||||
};
|
||||
|
||||
function simInit(key) {
|
||||
simScenarioKey = key; simCurrentStep = -1; simClearTimer(); simIsPlaying = false;
|
||||
document.getElementById('sim-auto-btn').classList.remove('playing');
|
||||
document.getElementById('sim-auto-btn').textContent = '⏵ 自动';
|
||||
document.getElementById('sim-title').textContent = (SCENARIOS[key] ? SCENARIOS[key].label : key) + ' — 动态模拟';
|
||||
document.getElementById('sim-step-badge').textContent = '0 / ' + SIMULATION_STEPS[key].length;
|
||||
document.getElementById('sim-step-title').textContent = '点击「下一步」开始模拟';
|
||||
document.getElementById('sim-step-desc').textContent = '逐步展示问题分析与解决的完整调用链路。';
|
||||
document.getElementById('sim-progress-bar').style.width = '0%';
|
||||
document.getElementById('sim-phase-badge').textContent = '';
|
||||
document.getElementById('sim-phase-badge').style.cssText = '';
|
||||
document.getElementById('sim-prev-btn').disabled = true;
|
||||
document.getElementById('sim-next-btn').disabled = false;
|
||||
document.getElementById('sim-next-btn').textContent = '下一步 ▶';
|
||||
document.getElementById('sim-bar').classList.add('visible');
|
||||
}
|
||||
function simHide() { document.getElementById('sim-bar').classList.remove('visible'); }
|
||||
function simClearTimer() { if (simAutoTimer) { clearInterval(simAutoTimer); simAutoTimer = null; } }
|
||||
|
||||
function simStep(dir) {
|
||||
if (!simScenarioKey) return;
|
||||
const steps = SIMULATION_STEPS[simScenarioKey], next = simCurrentStep + dir;
|
||||
if (next < 0 || next >= steps.length) return;
|
||||
simCurrentStep = next; simRenderStep(steps[simCurrentStep]);
|
||||
}
|
||||
|
||||
function simRenderStep(step) {
|
||||
const steps = SIMULATION_STEPS[simScenarioKey], total = steps.length;
|
||||
document.getElementById('sim-progress-bar').style.width = Math.round((step.stepNo / total) * 100) + '%';
|
||||
document.getElementById('sim-step-badge').textContent = step.stepNo + ' / ' + total;
|
||||
const pm = PHASE_META[step.phase] || PHASE_META.process;
|
||||
const badge = document.getElementById('sim-phase-badge');
|
||||
badge.textContent = pm.label; badge.style.cssText = `color:${pm.color};background:${pm.bg};border:1px solid ${pm.color}44;font-size:10px;padding:3px 9px;border-radius:10px;font-weight:700;`;
|
||||
document.getElementById('sim-step-title').textContent = step.title;
|
||||
document.getElementById('sim-step-desc').textContent = step.desc;
|
||||
document.getElementById('sim-prev-btn').disabled = simCurrentStep <= 0;
|
||||
const isLast = simCurrentStep >= total - 1;
|
||||
document.getElementById('sim-next-btn').disabled = isLast;
|
||||
document.getElementById('sim-next-btn').textContent = isLast ? '已完成 ✓' : '下一步 ▶';
|
||||
if (isLast && simIsPlaying) simToggleAuto();
|
||||
|
||||
const allNodeIds = step.allNodes, newNodeIds = step.newNodes || [];
|
||||
const activeEdgeSet = new Set(step.activeEdges.map(e => e.source + '__' + e.target));
|
||||
const edgeSeqMap = {}; step.activeEdges.forEach(e => { edgeSeqMap[e.source + '__' + e.target] = e; });
|
||||
|
||||
const simNodes = NODES.map(n => {
|
||||
const isActive = allNodeIds.includes(n.id), isNew = newNodeIds.includes(n.id) && !step.isComplete, isComp = step.isComplete;
|
||||
let sb = 0, sc = 'transparent', bw = 0, bc = CAT_COLORS[n.category];
|
||||
if (isNew) { sb = 30; sc = CAT_COLORS[n.category]; bw = 3; bc = '#fff'; }
|
||||
else if (isActive && isComp) { sb = 16; sc = CAT_COLORS[n.category]; }
|
||||
else if (isActive) { sb = 10; sc = CAT_COLORS[n.category]; }
|
||||
return {
|
||||
id: n.id, name: n.name, category: n.category,
|
||||
symbolSize: n.symbolSize * (isNew ? 1.22 : (isActive ? 1 : 0.8)),
|
||||
label: { show: isActive, fontSize: n.category === 3 ? 11 : (n.category === 0 ? 10 : 9), fontWeight: isNew ? 'bold' : 'normal', color: isNew ? '#fff' : '#c9d1d9', textBorderColor: '#0a0e1a', textBorderWidth: 3 },
|
||||
itemStyle: { color: CAT_COLORS[n.category], opacity: isActive ? 1 : 0.05, shadowBlur: sb, shadowColor: sc, borderWidth: bw, borderColor: bc },
|
||||
};
|
||||
});
|
||||
|
||||
const simLinks = LINKS.map(l => {
|
||||
const ek = l.source + '__' + l.target, ia = activeEdgeSet.has(ek), ei = edgeSeqMap[ek];
|
||||
return {
|
||||
source: l.source, target: l.target,
|
||||
label: { show: ia && !!ei, formatter: ei ? ei.label : l.label, fontSize: 10, fontWeight: 'bold', color: '#FFD700', textBorderColor: '#0a0e1a', textBorderWidth: 2, backgroundColor: 'rgba(0,0,0,0.55)', padding: [2, 5], borderRadius: 4 },
|
||||
lineStyle: { color: ia ? (pm.color || '#10B981') : l.lineStyle.color, width: ia ? (l.lineStyle.width || 1.5) + 1.5 : (l.lineStyle.width || 1.5), opacity: ia ? 0.92 : 0.03, curveness: 0.2, type: 'solid', shadowBlur: ia ? 12 : 0, shadowColor: ia ? (pm.color || '#10B981') : 'transparent' },
|
||||
symbol: ['none', 'arrow'], symbolSize: ia ? [0, 9] : [0, 6],
|
||||
};
|
||||
});
|
||||
|
||||
chart.setOption({ series: [{ data: simNodes, links: simLinks, force: { repulsion: 320, edgeLength: [80, 200], gravity: 0.06, layoutAnimation: false } }] }, false);
|
||||
}
|
||||
|
||||
function simToggleAuto() {
|
||||
simIsPlaying = !simIsPlaying; const btn = document.getElementById('sim-auto-btn');
|
||||
if (simIsPlaying) {
|
||||
btn.classList.add('playing'); btn.textContent = '⏸ 暂停';
|
||||
simAutoTimer = setInterval(() => { const s = SIMULATION_STEPS[simScenarioKey]; if (simCurrentStep >= s.length - 1) simToggleAuto(); else simStep(1); }, 2200);
|
||||
} else { btn.classList.remove('playing'); btn.textContent = '⏵ 自动'; simClearTimer(); }
|
||||
}
|
||||
function simStop() {
|
||||
simClearTimer(); simIsPlaying = false; simCurrentStep = -1; simScenarioKey = null; simHide();
|
||||
if (activeScenario) chart.setOption(buildOption(SCENARIOS[activeScenario].nodes)); else chart.setOption(buildOption());
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
304
docs/memory.html
@@ -1,304 +0,0 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-[#121212] text-gray-200 p-6">
|
||||
<!-- 顶部开关 -->
|
||||
<div class="flex justify-end items-center mb-6">
|
||||
<span class="text-gray-400 mr-3">记忆管理 已启用</span>
|
||||
<button class="w-12 h-6 bg-green-500 rounded-full relative">
|
||||
<span class="absolute right-1 top-1 w-4 h-4 bg-white rounded-full"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片区域 -->
|
||||
<div class="grid grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-[#1e1e1e] rounded-xl p-5 text-center">
|
||||
<div class="text-3xl font-bold text-white mb-1">15</div>
|
||||
<div class="text-sm text-gray-400">总记忆数</div>
|
||||
</div>
|
||||
<div class="bg-[#1e1e1e] rounded-xl p-5 text-center">
|
||||
<div class="text-3xl font-bold text-white mb-1">0.78</div>
|
||||
<div class="text-sm text-gray-400">平均分数</div>
|
||||
</div>
|
||||
<div class="bg-[#1e1e1e] rounded-xl p-5 text-center">
|
||||
<div class="text-3xl font-bold text-cyan-400 mb-1">6</div>
|
||||
<div class="text-sm text-gray-400">经验</div>
|
||||
</div>
|
||||
<div class="bg-[#1e1e1e] rounded-xl p-5 text-center">
|
||||
<div class="text-3xl font-bold text-red-400 mb-1">9</div>
|
||||
<div class="text-sm text-gray-400">经验教训</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="mb-4">
|
||||
<div class="relative">
|
||||
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索记忆内容..."
|
||||
class="w-full bg-[#0a0a0a] border border-gray-700 rounded-lg py-3 pl-10 pr-4 text-gray-200 focus:outline-none focus:border-gray-600"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选与操作栏 -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center space-x-3">
|
||||
<select class="bg-[#0a0a0a] border border-gray-700 rounded-lg py-2 px-4 text-gray-200 focus:outline-none">
|
||||
<option>全部类型</option>
|
||||
<option>经验</option>
|
||||
<option>经验教训</option>
|
||||
</select>
|
||||
<button class="flex items-center space-x-2 bg-[#1e1e1e] border border-gray-700 rounded-lg py-2 px-4 text-gray-200 hover:bg-[#2a2a2a]">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
<span>刷新</span>
|
||||
</button>
|
||||
<button class="flex items-center space-x-2 bg-indigo-600 rounded-lg py-2 px-4 text-white hover:bg-indigo-700">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
|
||||
</svg>
|
||||
<span>LLM 智能审查</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<div class="bg-[#1e1e1e] rounded-xl overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-[#0a0a0a]">
|
||||
<tr>
|
||||
<th class="w-10 px-4 py-3">
|
||||
<input type="checkbox" class="rounded bg-gray-700 border-gray-600">
|
||||
</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium text-gray-400">类型</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium text-gray-400">内容</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium text-gray-400">分数</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium text-gray-400">创建时间</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium text-gray-400">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-800">
|
||||
<!-- 表格行 1 -->
|
||||
<tr class="hover:bg-[#252525]">
|
||||
<td class="px-4 py-4">
|
||||
<input type="checkbox" class="rounded bg-gray-700 border-gray-600">
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<span class="px-2 py-1 bg-cyan-900/30 text-cyan-400 text-xs rounded-full">经验</span>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-gray-300 max-w-xl">
|
||||
当任务明确要求获取外部信息(搜索最新数据、新闻、财报、行业报告等)时,模型必须主动调用搜索工具而非仅依赖内部知识。应在系统提示中明确要求「必须使用搜索工具获取XX最新数据」,首次迭代即调用搜索工具,采用「搜索→分析→输出」的递进式流程,避免模型陷入纯文字生成的空转循环。
|
||||
<div class="text-xs text-gray-500 mt-1">主体: 外部信息获取规范 · 属性: 工具调用原则</div>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<span class="text-emerald-400 font-medium">0.95</span>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-gray-400">3/10 15:35</td>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex space-x-2">
|
||||
<button class="text-gray-400 hover:text-white">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="text-red-400 hover:text-red-300">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 表格行 2 -->
|
||||
<tr class="hover:bg-[#252525]">
|
||||
<td class="px-4 py-4">
|
||||
<input type="checkbox" class="rounded bg-gray-700 border-gray-600">
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<span class="px-2 py-1 bg-cyan-900/30 text-cyan-400 text-xs rounded-full">经验</span>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-gray-300 max-w-xl">
|
||||
当任务明确要求获取外部信息(搜索最新数据、新闻、财报、行业报告等)时,模型必须调用搜索工具而非仅依赖内部知识。应在系统提示中明确要求「必须使用搜索工具获取XX最新数据」,并建立工具调用检测机制,确保任务执行路径正确。
|
||||
<div class="text-xs text-gray-500 mt-1">主体: 工具调用 · 属性: 错误教训</div>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<span class="text-emerald-400 font-medium">0.95</span>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-gray-400">3/10 12:34</td>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex space-x-2">
|
||||
<button class="text-gray-400 hover:text-white">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="text-red-400 hover:text-red-300">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 表格行 3 -->
|
||||
<tr class="hover:bg-[#252525]">
|
||||
<td class="px-4 py-4">
|
||||
<input type="checkbox" class="rounded bg-gray-700 border-gray-600">
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<span class="px-2 py-1 bg-cyan-900/30 text-cyan-400 text-xs rounded-full">经验</span>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-gray-300 max-w-xl">
|
||||
任务执行应采用「一次性完整输出」策略,避免分批次小幅输出导致的迭代空转。对于代码生成、文件写入等任务,应要求模型一次性完成方案设计与工具执行的完整流程,将确认性回复并入上一次响应,将冗余迭代压缩合并,复杂任务控制在2-3次迭代内完成。
|
||||
<div class="text-xs text-gray-500 mt-1">主体: 任务执行效率优化 · 属性: 迭代策略原则</div>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<span class="text-emerald-400 font-medium">0.92</span>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-gray-400">3/10 15:35</td>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex space-x-2">
|
||||
<button class="text-gray-400 hover:text-white">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="text-red-400 hover:text-red-300">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 表格行 4 -->
|
||||
<tr class="hover:bg-[#252525]">
|
||||
<td class="px-4 py-4">
|
||||
<input type="checkbox" class="rounded bg-gray-700 border-gray-600">
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<span class="px-2 py-1 bg-cyan-900/30 text-cyan-400 text-xs rounded-full">经验</span>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-gray-300 max-w-xl">
|
||||
任务执行过程中应建立工具调用检测机制,监控模型是否按要求调用了必要的工具。若迭代中工具调用数为0,说明执行路径不正确,应触发异常处理或提示,而非继续无效迭代。
|
||||
<div class="text-xs text-gray-500 mt-1">主体: 执行路径监控 · 属性: 质量控制机制</div>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<span class="text-emerald-400 font-medium">0.88</span>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-gray-400">3/10 15:35</td>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex space-x-2">
|
||||
<button class="text-gray-400 hover:text-white">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="text-red-400 hover:text-red-300">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 表格行 5 -->
|
||||
<tr class="hover:bg-[#252525]">
|
||||
<td class="px-4 py-4">
|
||||
<input type="checkbox" class="rounded bg-gray-700 border-gray-600">
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<span class="px-2 py-1 bg-cyan-900/30 text-cyan-400 text-xs rounded-full">经验</span>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-gray-300 max-w-xl">
|
||||
任务执行应采用「搜索→分析→输出」的递进式流程,避免无效迭代和空转。纯文字生成的重复迭代应压缩或合并,确认性回复应并入上一次响应。简单任务应在1-2次迭代内完成,避免模型陷入重复思考或无意义的自我确认。
|
||||
<div class="text-xs text-gray-500 mt-1">主体: 迭代效率 · 属性: 错误教训</div>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<span class="text-emerald-400 font-medium">0.85</span>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-gray-400">3/10 12:34</td>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex space-x-2">
|
||||
<button class="text-gray-400 hover:text-white">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="text-red-400 hover:text-red-300">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 表格行 6 -->
|
||||
<tr class="hover:bg-[#252525]">
|
||||
<td class="px-4 py-4">
|
||||
<input type="checkbox" class="rounded bg-gray-700 border-gray-600">
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<span class="px-2 py-1 bg-cyan-900/30 text-cyan-400 text-xs rounded-full">经验</span>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-gray-300 max-w-xl">
|
||||
对于代码生成或文件写入类任务,应要求模型一次性完整输出或按模块批量输出,避免分批次的小幅输出导致迭代次数过多、执行效率低下。
|
||||
<div class="text-xs text-gray-500 mt-1">主体: 代码生成策略 · 属性: 错误教训</div>
|
||||
</td>
|
||||
<td class="px-4 py-4">
|
||||
<span class="text-amber-400 font-medium">0.80</span>
|
||||
</td>
|
||||
<td class="px-4 py-4 text-sm text-gray-400">3/10 12:34</td>
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex space-x-2">
|
||||
<button class="text-gray-400 hover:text-white">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="text-red-400 hover:text-red-300">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 底部文本区域 -->
|
||||
<div class="mt-6 px-4 text-gray-400 text-sm">
|
||||
<div class="mb-2">任务执行复盘发现问题:## 任务执行分析</div>
|
||||
<div class="mb-2">### 问题诊断</div>
|
||||
<div class="mb-2">**核心问题:模型在9次迭代中未执行任何工具调用**</div>
|
||||
<div>从迭代记录看,前9次迭代工具调用数均为0,说明模型一直在"空转"生成文字,未实际搜索最新数据。直到第10次才产出完整报告(包含2025年9月销量等最新数据),说明最后一次才真正调用了搜索工具。</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 这里可以添加交互逻辑
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #333;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #444;
|
||||
}
|
||||
</style>
|
||||
212
docs/plan.html
@@ -1,212 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>计划任务管理</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
/* 自定义滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #333;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #444;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-[#0f0f0f] text-gray-200 min-h-screen font-sans">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="flex items-center justify-between px-4 py-2 border-b border-gray-800 bg-[#0a0a0a]">
|
||||
<!-- 左侧状态区 -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="text-gray-300 font-medium">default</span>
|
||||
<span class="flex items-center text-green-400 text-sm">
|
||||
<span class="w-2 h-2 rounded-full bg-green-400 mr-1.5"></span>
|
||||
运行中
|
||||
</span>
|
||||
<span class="bg-blue-600/20 text-blue-400 text-xs px-2 py-0.5 rounded">网页访问</span>
|
||||
<span class="text-gray-400 text-sm">远程地址</span>
|
||||
<span class="text-gray-400 text-sm">1 端点</span>
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作区 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<button class="flex items-center space-x-1 border border-blue-500 bg-[#0a0a0a] text-white px-3 py-1 rounded text-sm hover:bg-blue-900/20 transition">
|
||||
<span>×</span>
|
||||
<span>断开</span>
|
||||
</button>
|
||||
<button class="text-gray-400 hover:text-white transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="text-gray-400 hover:text-white transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="text-gray-400 hover:text-white transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="flex items-center ml-4">
|
||||
<span class="text-gray-400 mr-2 text-sm">计划任务 已启用</span>
|
||||
<button class="w-10 h-5 bg-green-500 rounded-full relative">
|
||||
<span class="absolute right-0.5 top-0.5 w-4 h-4 bg-white rounded-full"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主体内容区 -->
|
||||
<main class="px-6 py-5">
|
||||
<!-- 标题与操作栏 -->
|
||||
<div class="flex justify-between items-start mb-5">
|
||||
<!-- 标题+标签页 -->
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-white mb-3">计划任务</h1>
|
||||
<div class="flex items-center space-x-6">
|
||||
<button class="text-blue-400 border-b-2 border-blue-500 px-1 py-2 font-medium text-sm -mb-[1px]">进行中 3</button>
|
||||
<button class="text-gray-400 hover:text-white px-1 py-2 text-sm transition">已完成 0</button>
|
||||
<button class="text-gray-400 hover:text-white px-1 py-2 text-sm transition">全部 3</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作+搜索区 -->
|
||||
<div class="flex flex-col items-end space-y-2">
|
||||
<div class="flex items-center space-x-3">
|
||||
<button class="flex items-center space-x-1 text-gray-400 hover:text-white text-sm transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
<span>刷新</span>
|
||||
</button>
|
||||
<button class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded text-sm flex items-center space-x-1 transition">
|
||||
<span>+</span>
|
||||
<span>新建任务</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索任务..."
|
||||
class="w-full bg-[#0a0a0a] border border-gray-700 rounded-lg py-2 pl-10 pr-4 text-sm text-gray-200 focus:outline-none focus:border-gray-600"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<div class="space-y-3">
|
||||
<!-- 任务1:活人感心跳 -->
|
||||
<div class="bg-[#1a1a1a] rounded-lg p-4 hover:bg-[#1e1e1e] transition">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="w-2 h-2 rounded-full bg-green-500"></span>
|
||||
<span class="text-white font-medium">活人感心跳</span>
|
||||
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">系统任务</span>
|
||||
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">Agent 任务</span>
|
||||
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">间隔重复</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<button class="bg-gray-800 hover:bg-gray-700 text-gray-300 text-xs px-3 py-1 rounded transition">暂停</button>
|
||||
<button class="text-gray-400 hover:text-white p-1 transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center flex-wrap gap-x-8 gap-y-1 text-xs text-gray-400 mb-2">
|
||||
<span>状态: 已调度</span>
|
||||
<span>触发方式: 间隔重复 30 分钟</span>
|
||||
<span>下次执行: 2026/03/10 16:26</span>
|
||||
<span>上次执行: 2026/03/10 15:56</span>
|
||||
<span>通知通道: -</span>
|
||||
<span>执行次数: 13</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-300">
|
||||
检查是否需要发送主动消息 (问候/提醒/跟进)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务2:记忆整理 -->
|
||||
<div class="bg-[#1a1a1a] rounded-lg p-4 hover:bg-[#1e1e1e] transition">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="w-2 h-2 rounded-full bg-green-500"></span>
|
||||
<span class="text-white font-medium">记忆整理</span>
|
||||
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">系统任务</span>
|
||||
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">Agent 任务</span>
|
||||
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">间隔重复</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<button class="bg-gray-800 hover:bg-gray-700 text-gray-300 text-xs px-3 py-1 rounded transition">暂停</button>
|
||||
<button class="text-gray-400 hover:text-white p-1 transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center flex-wrap gap-x-8 gap-y-1 text-xs text-gray-400 mb-2">
|
||||
<span>状态: 已调度</span>
|
||||
<span>触发方式: 间隔重复 3 小时</span>
|
||||
<span>下次执行: 2026/03/10 18:35</span>
|
||||
<span>上次执行: 2026/03/10 15:35</span>
|
||||
<span>通知通道: -</span>
|
||||
<span>执行次数: 2</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-300">
|
||||
执行记忆整理: 整理对话历史, 提取精华记忆, 刷新 MEMORY.md
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务3:系统自检 -->
|
||||
<div class="bg-[#1a1a1a] rounded-lg p-4 hover:bg-[#1e1e1e] transition">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="w-2 h-2 rounded-full bg-green-500"></span>
|
||||
<span class="text-white font-medium">系统自检</span>
|
||||
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">系统任务</span>
|
||||
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">Agent 任务</span>
|
||||
<span class="bg-gray-700 text-gray-300 text-xs px-2 py-0.5 rounded-full">每天</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<button class="bg-gray-800 hover:bg-gray-700 text-gray-300 text-xs px-3 py-1 rounded transition">暂停</button>
|
||||
<button class="text-gray-400 hover:text-white p-1 transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center flex-wrap gap-x-8 gap-y-1 text-xs text-gray-400 mb-2">
|
||||
<span>状态: 已调度</span>
|
||||
<span>触发方式: 每天 04:00</span>
|
||||
<span>下次执行: 2026/03/11 04:00</span>
|
||||
<span>上次执行: 从未</span>
|
||||
<span>通知通道: -</span>
|
||||
<span>执行次数: 0</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-300">
|
||||
执行系统自检: 分析 ERROR 日志, 尝试修复工具问题, 生成报告
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 332 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 261 KiB |
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
@@ -15,6 +16,8 @@ import (
|
||||
"x-agents/server/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
// Logger 日志记录器
|
||||
@@ -56,6 +59,83 @@ func (l *Logger) LogRequest(method, path, body string, status int, duration time
|
||||
|
||||
var logger *Logger
|
||||
|
||||
// initDefaultAdmin 初始化默认管理员用户
|
||||
func initDefaultAdmin(userRepo *repository.UserRepository) {
|
||||
// 确保 admin 用户的工作空间目录存在
|
||||
ensureAdminWorkspace()
|
||||
|
||||
// 检查 admin 用户是否已存在
|
||||
_, err := userRepo.FindByUsername("admin")
|
||||
if err == nil {
|
||||
log.Println("Admin user already exists")
|
||||
return
|
||||
}
|
||||
|
||||
// 使用 AuthService 创建用户(会自动加密密码)
|
||||
authService := service.NewAuthService("", userRepo)
|
||||
adminUser, err := authService.Register("admin", "admin", "admin@example.com")
|
||||
if err != nil {
|
||||
log.Printf("Failed to create admin user: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新角色为管理员
|
||||
adminUser.RoleID = "admin"
|
||||
userRepo.Update(adminUser)
|
||||
|
||||
// 创建管理员角色
|
||||
perms, _ := json.Marshal([]int{int(model.PermissionRead), int(model.PermissionWrite), int(model.PermissionExecute), int(model.PermissionAdmin)})
|
||||
adminRole := &model.Role{
|
||||
ID: "admin",
|
||||
Name: "admin",
|
||||
Permissions: string(perms),
|
||||
}
|
||||
userRepo.CreateRole(adminRole)
|
||||
|
||||
log.Printf("Default admin user created: id=%s, username=admin", adminUser.ID)
|
||||
}
|
||||
|
||||
// ensureAdminWorkspace 确保 admin 用户工作空间目录存在
|
||||
func ensureAdminWorkspace() {
|
||||
execPath, _ := os.Getwd()
|
||||
projectRoot := execPath
|
||||
|
||||
// 如果当前目录名为 server,向上找一级
|
||||
baseName := filepath.Base(execPath)
|
||||
if baseName == "server" {
|
||||
projectRoot = filepath.Dir(execPath)
|
||||
}
|
||||
|
||||
// 尝试向上查找包含 .git 的目录
|
||||
if _, err := os.Stat(filepath.Join(projectRoot, ".git")); os.IsNotExist(err) {
|
||||
for i := 0; i < 3; i++ {
|
||||
parent := filepath.Dir(projectRoot)
|
||||
if parent == projectRoot {
|
||||
break
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(parent, ".git")); err == nil {
|
||||
projectRoot = parent
|
||||
break
|
||||
}
|
||||
projectRoot = parent
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 admin 工作空间
|
||||
workspacePath := filepath.Join(projectRoot, "account", "admin")
|
||||
if err := os.MkdirAll(workspacePath, 0755); err != nil {
|
||||
log.Printf("Warning: failed to create admin workspace: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建子目录
|
||||
for _, dir := range []string{"projects", "files", "temp"} {
|
||||
os.MkdirAll(filepath.Join(workspacePath, dir), 0755)
|
||||
}
|
||||
|
||||
log.Printf("Admin workspace created at: %s", workspacePath)
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 初始化日志
|
||||
logger = NewLogger()
|
||||
@@ -71,7 +151,66 @@ func main() {
|
||||
}
|
||||
|
||||
// 3. 自动迁移表
|
||||
db.AutoMigrate(&model.DatabaseInfo{}, &model.SubTableInfo{}, &model.ModelInfo{}, &model.KnowledgeBase{}, &model.KnowledgeDocument{}, &model.User{}, &model.Role{})
|
||||
db.AutoMigrate(&model.DatabaseInfo{}, &model.SubTableInfo{}, &model.ModelInfo{}, &model.KnowledgeBase{}, &model.KnowledgeDocument{}, &model.User{}, &model.Role{}, &model.Tool{})
|
||||
|
||||
// 3.1 确保 users 和 roles 表存在(使用 SQL 强制创建)
|
||||
db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id VARCHAR(191) PRIMARY KEY,
|
||||
name VARCHAR(191) UNIQUE,
|
||||
permissions TEXT,
|
||||
created_at DATETIME(3),
|
||||
updated_at DATETIME(3)
|
||||
)
|
||||
`)
|
||||
db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR(191) PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password VARCHAR(191) NOT NULL,
|
||||
email VARCHAR(191),
|
||||
role_id VARCHAR(50) NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at DATETIME(3),
|
||||
updated_at DATETIME(3),
|
||||
INDEX idx_users_username (username),
|
||||
INDEX idx_users_email (email)
|
||||
)
|
||||
`)
|
||||
|
||||
// 3.2 确保 tools 表存在(使用 SQL 强制创建)
|
||||
db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS tools (
|
||||
id VARCHAR(191) PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
category VARCHAR(50) NOT NULL,
|
||||
provider VARCHAR(100),
|
||||
security_level VARCHAR(20) DEFAULT 'safe',
|
||||
require_approval BOOLEAN DEFAULT FALSE,
|
||||
parameters TEXT,
|
||||
status VARCHAR(20) DEFAULT 'active',
|
||||
created_at DATETIME(3),
|
||||
updated_at DATETIME(3),
|
||||
INDEX idx_tools_name (name),
|
||||
INDEX idx_tools_category (category),
|
||||
INDEX idx_tools_status (status)
|
||||
)
|
||||
`)
|
||||
|
||||
// 使用GORM Migrator添加缺失的列
|
||||
migrator := db.Migrator()
|
||||
if !migrator.HasColumn(&model.Tool{}, "security_level") {
|
||||
migrator.AddColumn(&model.Tool{}, "security_level")
|
||||
}
|
||||
if !migrator.HasColumn(&model.Tool{}, "require_approval") {
|
||||
migrator.AddColumn(&model.Tool{}, "require_approval")
|
||||
}
|
||||
if !migrator.HasColumn(&model.Tool{}, "parameters") {
|
||||
migrator.AddColumn(&model.Tool{}, "parameters")
|
||||
}
|
||||
|
||||
log.Println("Database tables verified/created")
|
||||
|
||||
// 4. 初始化 Repository
|
||||
dbRepo := repository.NewDatabaseRepository(db)
|
||||
@@ -79,6 +218,10 @@ func main() {
|
||||
modelRepo := repository.NewModelRepository(db)
|
||||
knowledgeRepo := repository.NewKnowledgeRepository(db)
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
toolRepo := repository.NewToolRepository(db)
|
||||
|
||||
// 4.1 初始化默认管理员用户
|
||||
initDefaultAdmin(userRepo)
|
||||
|
||||
// 5. 初始化 Service
|
||||
dbService := service.NewDatabaseService(dbRepo, subTableRepo)
|
||||
@@ -91,6 +234,14 @@ func main() {
|
||||
}
|
||||
knowledgeService := service.NewKnowledgeService(knowledgeRepo, modelRepo, uploadService, cfg.PythonServiceURL, cfg.AICoreServiceAddr, cfg.MarkdownLocalPath)
|
||||
authService := service.NewAuthService(cfg.JWTSecret, userRepo)
|
||||
toolService := service.NewToolService(toolRepo)
|
||||
|
||||
// 4.2 初始化默认工具
|
||||
if err := toolService.InitDefaultTools(); err != nil {
|
||||
log.Printf("Warning: Failed to init default tools: %v", err)
|
||||
} else {
|
||||
log.Println("Default tools initialized")
|
||||
}
|
||||
|
||||
// 6. 初始化 Handler
|
||||
dbHandler := handler.NewDatabaseHandler(dbService)
|
||||
@@ -100,6 +251,7 @@ func main() {
|
||||
systemHandler := handler.NewSystemHandler()
|
||||
knowledgeHandler := handler.NewKnowledgeHandler(knowledgeService)
|
||||
authHandler := handler.NewAuthHandler(authService)
|
||||
toolHandler := handler.NewToolHandler(toolService)
|
||||
var uploadHandler *handler.UploadHandler
|
||||
if uploadService != nil {
|
||||
uploadHandler = handler.NewUploadHandler(uploadService, knowledgeRepo)
|
||||
@@ -137,8 +289,8 @@ func main() {
|
||||
// CORS 中间件
|
||||
r.Use(func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
@@ -162,6 +314,14 @@ func main() {
|
||||
authProtectedGroup.GET("/me", authHandler.GetCurrentUser)
|
||||
}
|
||||
|
||||
// 用户管理模块(需要登录)
|
||||
userGroup := r.Group("/user")
|
||||
userGroup.Use(middleware.Auth(cfg.JWTSecret))
|
||||
{
|
||||
userGroup.GET("/list", authHandler.ListUsers)
|
||||
userGroup.GET("/:id", authHandler.GetUserByID)
|
||||
}
|
||||
|
||||
// 数据库管理模块
|
||||
databaseGroup := r.Group("/database")
|
||||
{
|
||||
@@ -225,6 +385,20 @@ func main() {
|
||||
// 系统信息模块
|
||||
r.GET("/system/info", systemHandler.GetSystemInfo)
|
||||
|
||||
// 工具管理模块
|
||||
toolGroup := r.Group("/tool")
|
||||
{
|
||||
toolGroup.GET("/list", toolHandler.List)
|
||||
toolGroup.GET("/sync", toolHandler.Sync) // 手动同步
|
||||
toolGroup.GET("/:id", toolHandler.GetByID)
|
||||
toolGroup.POST("/add", toolHandler.Create)
|
||||
toolGroup.PUT("/:id", toolHandler.Update)
|
||||
toolGroup.DELETE("/:id", toolHandler.Delete)
|
||||
}
|
||||
|
||||
// Swagger 文档
|
||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
|
||||
// 文件上传模块
|
||||
if uploadHandler != nil {
|
||||
// 本地文件静态服务
|
||||
|
||||
2936
server/docs/docs.go
Normal file
2907
server/docs/swagger.json
Normal file
1960
server/docs/swagger.yaml
Normal file
@@ -1,78 +1,115 @@
|
||||
module x-agents/server
|
||||
|
||||
go 1.25
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/go-sql-driver/mysql v1.7.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/spf13/viper v1.18.2
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
gorm.io/driver/mysql v1.5.2
|
||||
gorm.io/gorm v1.25.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.5 // indirect
|
||||
github.com/go-openapi/spec v0.22.4 // indirect
|
||||
github.com/go-openapi/swag v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mailru/easyjson v0.9.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.99 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/swaggo/files v1.0.1 // indirect
|
||||
github.com/swaggo/gin-swagger v1.6.1 // indirect
|
||||
github.com/swaggo/swag v1.16.6 // indirect
|
||||
github.com/tinylib/msgp v1.6.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.7 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/arch v0.25.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/grpc v1.79.2 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
146
server/go.sum
@@ -1,9 +1,29 @@
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28=
|
||||
github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -16,14 +36,42 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
|
||||
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
|
||||
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
|
||||
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
|
||||
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
|
||||
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
|
||||
github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU=
|
||||
github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA=
|
||||
github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
|
||||
github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
|
||||
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
|
||||
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
|
||||
github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
|
||||
github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
|
||||
github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
|
||||
github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
|
||||
github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
|
||||
github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -32,10 +80,16 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
@@ -56,6 +110,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
@@ -66,22 +122,31 @@ github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZX
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
||||
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
@@ -97,6 +162,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -104,16 +171,24 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
@@ -127,6 +202,7 @@ github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMV
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -136,8 +212,16 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
@@ -148,30 +232,66 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
|
||||
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -181,10 +301,29 @@ golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
@@ -196,11 +335,15 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -209,4 +352,7 @@ gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb
|
||||
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
@@ -2,12 +2,57 @@ package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"x-agents/server/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// maskEmail 邮箱脱敏,只显示前后各1个字符
|
||||
func maskEmail(email string) string {
|
||||
if email == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.Split(email, "@")
|
||||
if len(parts) != 2 {
|
||||
return "***"
|
||||
}
|
||||
local := parts[0]
|
||||
domain := parts[1]
|
||||
if len(local) <= 2 {
|
||||
local = "**"
|
||||
} else {
|
||||
local = string(local[0]) + "***" + string(local[len(local)-1])
|
||||
}
|
||||
if len(domain) <= 2 {
|
||||
domain = "***"
|
||||
} else {
|
||||
domain = string(domain[0]) + "***" + string(domain[len(domain)-1])
|
||||
}
|
||||
return local + "@" + domain
|
||||
}
|
||||
|
||||
// @title X-Agents API
|
||||
// @version 1.0
|
||||
// @description X-Agents 后端 API 文档
|
||||
// @termsOfService http://swagger.io/terms/
|
||||
|
||||
// @contact.name API Support
|
||||
// @contact.url http://www.example.com/support
|
||||
// @contact.email support@example.com
|
||||
|
||||
// @license.name Apache 2.0
|
||||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
// @host localhost:8080
|
||||
// @BasePath /
|
||||
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @description Type "Bearer" followed by a space and JWT token.
|
||||
|
||||
type AuthHandler struct {
|
||||
authService *service.AuthService
|
||||
}
|
||||
@@ -26,7 +71,23 @@ type LoginResponse struct {
|
||||
User interface{} `json:"user"`
|
||||
}
|
||||
|
||||
// Login 处理登录
|
||||
// RegisterRequest 注册请求
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// @Summary 用户登录
|
||||
// @Description 用户登录并获取 JWT Token
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body LoginRequest true "登录请求"
|
||||
// @Success 200 {object} LoginResponse
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/login [post]
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -54,13 +115,17 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// Register 处理注册
|
||||
// @Summary 用户注册
|
||||
// @Description 新用户注册账号
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param RegisterRequest body handler.RegisterRequest true "注册请求"
|
||||
// @Success 201 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /auth/register [post]
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
var req RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -75,11 +140,19 @@ func (h *AuthHandler) Register(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"email": maskEmail(user.Email),
|
||||
})
|
||||
}
|
||||
|
||||
// GetCurrentUser 获取当前登录用户信息
|
||||
// @Summary 获取当前用户信息
|
||||
// @Description 获取已登录用户的详细信息
|
||||
// @Tags 认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /auth/me [get]
|
||||
func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
@@ -96,7 +169,71 @@ func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"email": maskEmail(user.Email),
|
||||
"role_id": user.RoleID,
|
||||
"is_active": user.IsActive,
|
||||
"created_at": user.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary 获取所有用户
|
||||
// @Description 获取所有用户列表(需要管理员权限)
|
||||
// @Tags 用户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 401 {object} map[string]string
|
||||
// @Router /user/list [get]
|
||||
func (h *AuthHandler) ListUsers(c *gin.Context) {
|
||||
users, err := h.authService.GetAllUsers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var userList []gin.H
|
||||
for _, user := range users {
|
||||
userList = append(userList, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": maskEmail(user.Email),
|
||||
"role_id": user.RoleID,
|
||||
"is_active": user.IsActive,
|
||||
"created_at": user.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"list": userList})
|
||||
}
|
||||
|
||||
// @Summary 获取用户详情
|
||||
// @Description 根据ID获取用户详情
|
||||
// @Tags 用户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "用户ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /user/{id} [get]
|
||||
func (h *AuthHandler) GetUserByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "user id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.authService.GetUserByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": maskEmail(user.Email),
|
||||
"role_id": user.RoleID,
|
||||
"is_active": user.IsActive,
|
||||
"created_at": user.CreatedAt,
|
||||
|
||||
@@ -17,7 +17,16 @@ func NewDatabaseHandler(svc *service.DatabaseService) *DatabaseHandler {
|
||||
return &DatabaseHandler{service: svc}
|
||||
}
|
||||
|
||||
// Check 检查数据库连接
|
||||
// @Summary 检查数据库连接
|
||||
// @Description 测试数据库连接是否正常
|
||||
// @Tags 数据库管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.CheckRequest true "数据库连接信息"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /database/check [post]
|
||||
func (h *DatabaseHandler) Check(c *gin.Context) {
|
||||
var req model.CheckRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -34,7 +43,16 @@ func (h *DatabaseHandler) Check(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// Create 创建数据库信息
|
||||
// @Summary 创建数据库
|
||||
// @Description 添加新的数据库连接信息
|
||||
// @Tags 数据库管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.CreateDatabaseRequest true "数据库信息"
|
||||
// @Success 201 {object} model.DatabaseInfo
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /database/add [post]
|
||||
func (h *DatabaseHandler) Create(c *gin.Context) {
|
||||
var req model.CreateDatabaseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -51,7 +69,15 @@ func (h *DatabaseHandler) Create(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, info)
|
||||
}
|
||||
|
||||
// GetByID 获取详情
|
||||
// @Summary 获取数据库详情
|
||||
// @Description 根据ID获取数据库详细信息
|
||||
// @Tags 数据库管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "数据库ID"
|
||||
// @Success 200 {object} model.DatabaseInfo
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /database/{id} [get]
|
||||
func (h *DatabaseHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
@@ -64,7 +90,14 @@ func (h *DatabaseHandler) GetByID(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, info)
|
||||
}
|
||||
|
||||
// List 获取列表
|
||||
// @Summary 获取数据库列表
|
||||
// @Description 获取所有已添加的数据库列表
|
||||
// @Tags 数据库管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /database/list [get]
|
||||
func (h *DatabaseHandler) List(c *gin.Context) {
|
||||
list, err := h.service.List()
|
||||
if err != nil {
|
||||
@@ -79,7 +112,17 @@ func (h *DatabaseHandler) List(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"list": list})
|
||||
}
|
||||
|
||||
// Update 更新
|
||||
// @Summary 更新数据库信息
|
||||
// @Description 更新指定数据库的信息
|
||||
// @Tags 数据库管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "数据库ID"
|
||||
// @Param request body model.UpdateDatabaseRequest true "更新信息"
|
||||
// @Success 200 {object} model.DatabaseInfo
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /database/{id} [put]
|
||||
func (h *DatabaseHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
@@ -98,7 +141,16 @@ func (h *DatabaseHandler) Update(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, info)
|
||||
}
|
||||
|
||||
// SaveGraph 保存图谱信息
|
||||
// @Summary 保存图谱信息
|
||||
// @Description 保存数据库的图谱结构信息
|
||||
// @Tags 数据库管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.SaveGraphRequest true "图谱信息"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /database/graph/save [post]
|
||||
func (h *DatabaseHandler) SaveGraph(c *gin.Context) {
|
||||
var req model.SaveGraphRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -115,7 +167,15 @@ func (h *DatabaseHandler) SaveGraph(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// Delete 删除
|
||||
// @Summary 删除数据库
|
||||
// @Description 删除指定的数据库连接
|
||||
// @Tags 数据库管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "数据库ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /database/{id} [delete]
|
||||
func (h *DatabaseHandler) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
|
||||
@@ -17,7 +17,15 @@ func NewKnowledgeHandler(s *service.KnowledgeService) *KnowledgeHandler {
|
||||
return &KnowledgeHandler{service: s}
|
||||
}
|
||||
|
||||
// Create 创建知识库
|
||||
// @Summary 创建知识库
|
||||
// @Description 创建一个新的知识库
|
||||
// @Tags 知识库
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.CreateKnowledgeRequest true "知识库信息"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Router /api/knowledge/create [post]
|
||||
func (h *KnowledgeHandler) Create(c *gin.Context) {
|
||||
var req model.CreateKnowledgeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -38,7 +46,14 @@ func (h *KnowledgeHandler) Create(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// List 获取知识库列表
|
||||
// @Summary 获取知识库列表
|
||||
// @Description 获取所有知识库列表
|
||||
// @Tags 知识库
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/knowledge/list [get]
|
||||
func (h *KnowledgeHandler) List(c *gin.Context) {
|
||||
list, err := h.service.List()
|
||||
if err != nil {
|
||||
@@ -49,7 +64,16 @@ func (h *KnowledgeHandler) List(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
}
|
||||
|
||||
// GetByID 获取知识库详情
|
||||
// @Summary 获取知识库详情
|
||||
// @Description 根据ID获取知识库详细信息
|
||||
// @Tags 知识库
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "知识库ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/knowledge/{id} [get]
|
||||
func (h *KnowledgeHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
@@ -66,7 +90,17 @@ func (h *KnowledgeHandler) GetByID(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": kb})
|
||||
}
|
||||
|
||||
// Update 更新知识库
|
||||
// @Summary 更新知识库
|
||||
// @Description 更新指定知识库的信息
|
||||
// @Tags 知识库
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "知识库ID"
|
||||
// @Param request body model.UpdateKnowledgeRequest true "更新信息"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/knowledge/{id} [put]
|
||||
func (h *KnowledgeHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
@@ -88,7 +122,16 @@ func (h *KnowledgeHandler) Update(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Knowledge base updated"})
|
||||
}
|
||||
|
||||
// Delete 删除知识库
|
||||
// @Summary 删除知识库
|
||||
// @Description 删除指定的知识库
|
||||
// @Tags 知识库
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "知识库ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/knowledge/{id} [delete]
|
||||
func (h *KnowledgeHandler) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
@@ -104,7 +147,17 @@ func (h *KnowledgeHandler) Delete(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Knowledge base deleted"})
|
||||
}
|
||||
|
||||
// ListDocuments 获取知识库下的文档列表
|
||||
// @Summary 获取知识库文档列表
|
||||
// @Description 获取指定知识库下的所有文档
|
||||
// @Tags 知识库
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "知识库ID"
|
||||
// @Param status query string false "文档状态筛选"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/knowledge/{id}/documents [get]
|
||||
func (h *KnowledgeHandler) ListDocuments(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
@@ -122,7 +175,17 @@ func (h *KnowledgeHandler) ListDocuments(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
}
|
||||
|
||||
// UploadDocument 上传文档到知识库
|
||||
// @Summary 上传文档
|
||||
// @Description 上传文档到指定知识库
|
||||
// @Tags 知识库
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param id path string true "知识库ID"
|
||||
// @Param file formData file true "文档文件"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/knowledge/{id}/documents [post]
|
||||
func (h *KnowledgeHandler) UploadDocument(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
@@ -161,7 +224,17 @@ func (h *KnowledgeHandler) UploadDocument(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteDocument 删除文档
|
||||
// @Summary 删除文档
|
||||
// @Description 删除知识库中的指定文档
|
||||
// @Tags 知识库
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "知识库ID"
|
||||
// @Param doc_id path string true "文档ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/knowledge/{id}/documents/{doc_id} [delete]
|
||||
func (h *KnowledgeHandler) DeleteDocument(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
docID := c.Param("doc_id")
|
||||
@@ -179,7 +252,17 @@ func (h *KnowledgeHandler) DeleteDocument(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Document deleted"})
|
||||
}
|
||||
|
||||
// ReparseDocument 重新解析文档
|
||||
// @Summary 重新解析文档
|
||||
// @Description 重新解析指定文档(用于更新解析结果)
|
||||
// @Tags 知识库
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "知识库ID"
|
||||
// @Param doc_id path string true "文档ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/knowledge/{id}/documents/{doc_id}/reparse [post]
|
||||
func (h *KnowledgeHandler) ReparseDocument(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
docID := c.Param("doc_id")
|
||||
@@ -197,7 +280,18 @@ func (h *KnowledgeHandler) ReparseDocument(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Document reparse started"})
|
||||
}
|
||||
|
||||
// GetDocumentPreview 获取文档预览
|
||||
// @Summary 获取文档预览
|
||||
// @Description 获取文档的解析预览内容
|
||||
// @Tags 知识库
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "知识库ID"
|
||||
// @Param doc_id path string true "文档ID"
|
||||
// @Param page query int false "页码"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/knowledge/{id}/documents/{doc_id}/preview [get]
|
||||
func (h *KnowledgeHandler) GetDocumentPreview(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
docID := c.Param("doc_id")
|
||||
|
||||
@@ -17,7 +17,14 @@ func NewModelHandler(svc *service.ModelService) *ModelHandler {
|
||||
return &ModelHandler{service: svc}
|
||||
}
|
||||
|
||||
// List 获取列表
|
||||
// @Summary 获取模型列表
|
||||
// @Description 获取所有已添加的AI模型列表
|
||||
// @Tags 模型管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /model/list [get]
|
||||
func (h *ModelHandler) List(c *gin.Context) {
|
||||
list, err := h.service.List()
|
||||
if err != nil {
|
||||
@@ -32,7 +39,15 @@ func (h *ModelHandler) List(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"list": list})
|
||||
}
|
||||
|
||||
// GetByID 获取详情
|
||||
// @Summary 获取模型详情
|
||||
// @Description 根据ID获取模型详细信息
|
||||
// @Tags 模型管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "模型ID"
|
||||
// @Success 200 {object} model.ModelInfo
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /model/{id} [get]
|
||||
func (h *ModelHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
model, err := h.service.GetByID(id)
|
||||
@@ -43,7 +58,16 @@ func (h *ModelHandler) GetByID(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, model)
|
||||
}
|
||||
|
||||
// Create 创建
|
||||
// @Summary 添加模型
|
||||
// @Description 添加新的AI模型配置
|
||||
// @Tags 模型管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.CreateModelRequest true "模型信息"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /model/add [post]
|
||||
func (h *ModelHandler) Create(c *gin.Context) {
|
||||
var req model.CreateModelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -60,7 +84,17 @@ func (h *ModelHandler) Create(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// Update 更新
|
||||
// @Summary 更新模型
|
||||
// @Description 更新指定模型的信息
|
||||
// @Tags 模型管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "模型ID"
|
||||
// @Param request body model.UpdateModelRequest true "更新信息"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /model/{id} [put]
|
||||
func (h *ModelHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req model.UpdateModelRequest
|
||||
@@ -78,7 +112,15 @@ func (h *ModelHandler) Update(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// Delete 删除
|
||||
// @Summary 删除模型
|
||||
// @Description 删除指定的AI模型
|
||||
// @Tags 模型管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "模型ID"
|
||||
// @Success 200 {object} map[string]bool
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /model/{id} [delete]
|
||||
func (h *ModelHandler) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
err := h.service.Delete(id)
|
||||
@@ -90,7 +132,16 @@ func (h *ModelHandler) Delete(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// Test 测试连接
|
||||
// @Summary 测试模型连接
|
||||
// @Description 测试AI模型连接是否正常
|
||||
// @Tags 模型管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.TestModelRequest true "模型测试请求"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /model/test [post]
|
||||
func (h *ModelHandler) Test(c *gin.Context) {
|
||||
var req model.TestModelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
||||
@@ -17,7 +17,16 @@ func NewNeo4jHandler(svc *service.Neo4jService) *Neo4jHandler {
|
||||
return &Neo4jHandler{service: svc}
|
||||
}
|
||||
|
||||
// Check 检查 Neo4j 连接
|
||||
// @Summary 检查 Neo4j 连接
|
||||
// @Description 测试 Neo4j 数据库连接是否正常
|
||||
// @Tags Neo4j
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.Neo4jCheckRequest true "Neo4j连接信息"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /neo4j/check [post]
|
||||
func (h *Neo4jHandler) Check(c *gin.Context) {
|
||||
var req model.Neo4jCheckRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -34,7 +43,16 @@ func (h *Neo4jHandler) Check(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GetGraphs 获取图谱概览数据
|
||||
// @Summary 获取图谱概览
|
||||
// @Description 获取 Neo4j 中的图谱概览数据
|
||||
// @Tags Neo4j
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.Neo4jGraphRequest true "图谱查询请求"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /neo4j/graphs [post]
|
||||
func (h *Neo4jHandler) GetGraphs(c *gin.Context) {
|
||||
var req model.Neo4jGraphRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -51,7 +69,16 @@ func (h *Neo4jHandler) GetGraphs(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GetNodes 获取节点详情
|
||||
// @Summary 获取节点列表
|
||||
// @Description 获取 Neo4j 中的节点详情
|
||||
// @Tags Neo4j
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.Neo4jNodeRequest true "节点查询请求"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /neo4j/nodes [post]
|
||||
func (h *Neo4jHandler) GetNodes(c *gin.Context) {
|
||||
var req model.Neo4jNodeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -68,7 +95,16 @@ func (h *Neo4jHandler) GetNodes(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GetRelationships 获取关系详情
|
||||
// @Summary 获取关系列表
|
||||
// @Description 获取 Neo4j 中的关系详情
|
||||
// @Tags Neo4j
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.Neo4jRelRequest true "关系查询请求"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /neo4j/relationships [post]
|
||||
func (h *Neo4jHandler) GetRelationships(c *gin.Context) {
|
||||
var req model.Neo4jRelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
||||
@@ -17,7 +17,16 @@ func NewSubTableHandler(svc *service.SubTableService) *SubTableHandler {
|
||||
return &SubTableHandler{service: svc}
|
||||
}
|
||||
|
||||
// Create 创建子表信息
|
||||
// @Summary 创建子表映射
|
||||
// @Description 添加数据库的子表映射关系
|
||||
// @Tags 子表管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body model.CreateSubTableRequest true "子表信息"
|
||||
// @Success 201 {object} model.SubTableInfo
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /sub-table/add [post]
|
||||
func (h *SubTableHandler) Create(c *gin.Context) {
|
||||
var req model.CreateSubTableRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -34,7 +43,15 @@ func (h *SubTableHandler) Create(c *gin.Context) {
|
||||
c.JSON(http.StatusCreated, info)
|
||||
}
|
||||
|
||||
// GetByID 获取详情
|
||||
// @Summary 获取子表详情
|
||||
// @Description 根据ID获取子表映射详情
|
||||
// @Tags 子表管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "子表ID"
|
||||
// @Success 200 {object} model.SubTableInfo
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /sub-table/{id} [get]
|
||||
func (h *SubTableHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
@@ -47,7 +64,15 @@ func (h *SubTableHandler) GetByID(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, info)
|
||||
}
|
||||
|
||||
// ListByDatabase 获取数据库下所有子表
|
||||
// @Summary 获取数据库下所有子表
|
||||
// @Description 获取指定数据库的所有子表映射列表
|
||||
// @Tags 子表管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param database_id path string true "数据库ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /sub-table/database/{database_id} [get]
|
||||
func (h *SubTableHandler) ListByDatabase(c *gin.Context) {
|
||||
databaseID := c.Param("database_id")
|
||||
|
||||
@@ -64,7 +89,15 @@ func (h *SubTableHandler) ListByDatabase(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"list": list})
|
||||
}
|
||||
|
||||
// GetMappingFromFile 从文件获取映射
|
||||
// @Summary 从文件获取映射
|
||||
// @Description 从文件中读取子表映射关系
|
||||
// @Tags 子表管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param database_id path string true "数据库ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /sub-table/mapping/{database_id} [get]
|
||||
func (h *SubTableHandler) GetMappingFromFile(c *gin.Context) {
|
||||
databaseID := c.Param("database_id")
|
||||
|
||||
@@ -82,7 +115,17 @@ func (h *SubTableHandler) GetMappingFromFile(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"mapping": mapping})
|
||||
}
|
||||
|
||||
// Update 更新
|
||||
// @Summary 更新子表映射
|
||||
// @Description 更新子表映射信息
|
||||
// @Tags 子表管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "子表ID"
|
||||
// @Param request body model.UpdateSubTableRequest true "更新信息"
|
||||
// @Success 200 {object} model.SubTableInfo
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /sub-table/{id} [put]
|
||||
func (h *SubTableHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
@@ -101,7 +144,15 @@ func (h *SubTableHandler) Update(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, info)
|
||||
}
|
||||
|
||||
// Delete 删除
|
||||
// @Summary 删除子表映射
|
||||
// @Description 删除指定的子表映射
|
||||
// @Tags 子表管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "子表ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /sub-table/{id} [delete]
|
||||
func (h *SubTableHandler) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
@@ -114,7 +165,15 @@ func (h *SubTableHandler) Delete(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
|
||||
}
|
||||
|
||||
// GetTablesDDL 获取数据库下所有表及DDL
|
||||
// @Summary 获取表结构DDL
|
||||
// @Description 获取数据库下所有表的DDL语句
|
||||
// @Tags 子表管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param database_id path string true "数据库ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /sub-table/ddl/{database_id} [get]
|
||||
func (h *SubTableHandler) GetTablesDDL(c *gin.Context) {
|
||||
databaseID := c.Param("database_id")
|
||||
|
||||
|
||||
@@ -14,7 +14,14 @@ func NewSystemHandler() *SystemHandler {
|
||||
return &SystemHandler{}
|
||||
}
|
||||
|
||||
// GetSystemInfo 获取系统信息
|
||||
// @Summary 获取系统信息
|
||||
// @Description 获取服务器系统信息(CPU、内存等)
|
||||
// @Tags 系统
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.SystemInfo
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /system/info [get]
|
||||
func (h *SystemHandler) GetSystemInfo(c *gin.Context) {
|
||||
info, err := getSystemInfo()
|
||||
if err != nil {
|
||||
|
||||
166
server/internal/handler/tool_handler.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"x-agents/server/internal/model"
|
||||
"x-agents/server/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ToolHandler 工具处理器
|
||||
type ToolHandler struct {
|
||||
toolService *service.ToolService
|
||||
}
|
||||
|
||||
// NewToolHandler 创建工具处理器
|
||||
func NewToolHandler(toolService *service.ToolService) *ToolHandler {
|
||||
return &ToolHandler{toolService: toolService}
|
||||
}
|
||||
|
||||
// List 获取工具列表
|
||||
// @Summary 获取工具列表
|
||||
// @Description 获取所有工具列表,支持按分类和状态筛选
|
||||
// @Tags 工具管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param category query string false "工具分类"
|
||||
// @Param status query string false "工具状态"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /tool/list [get]
|
||||
func (h *ToolHandler) List(c *gin.Context) {
|
||||
category := c.Query("category")
|
||||
status := c.Query("status")
|
||||
|
||||
tools, err := h.toolService.GetTools(category, status)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"list": tools, "total": len(tools)})
|
||||
}
|
||||
|
||||
// Sync 手动同步工具
|
||||
// @Summary 手动同步工具
|
||||
// @Description 从代码中的默认配置同步工具到数据库
|
||||
// @Tags 工具管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /tool/sync [get]
|
||||
func (h *ToolHandler) Sync(c *gin.Context) {
|
||||
if err := h.toolService.InitDefaultTools(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
tools, _ := h.toolService.GetTools("", "")
|
||||
c.JSON(http.StatusOK, gin.H{"message": "tools synced", "count": len(tools)})
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取工具
|
||||
// @Summary 获取工具详情
|
||||
// @Description 根据ID获取工具详情
|
||||
// @Tags 工具管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "工具ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /tool/{id} [get]
|
||||
func (h *ToolHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tool id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
tool, err := h.toolService.GetToolByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "tool not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"tool": tool})
|
||||
}
|
||||
|
||||
// Create 创建工具
|
||||
// @Summary 创建工具
|
||||
// @Description 创建新的工具
|
||||
// @Tags 工具管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param tool body model.Tool true "工具信息"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /tool/add [post]
|
||||
func (h *ToolHandler) Create(c *gin.Context) {
|
||||
var tool model.Tool
|
||||
if err := c.ShouldBindJSON(&tool); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.toolService.CreateTool(&tool); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "tool created", "tool": tool})
|
||||
}
|
||||
|
||||
// Update 更新工具
|
||||
// @Summary 更新工具
|
||||
// @Description 更新工具信息
|
||||
// @Tags 工具管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "工具ID"
|
||||
// @Param tool body model.Tool true "工具信息"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /tool/{id} [put]
|
||||
func (h *ToolHandler) Update(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tool id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
var tool model.Tool
|
||||
if err := c.ShouldBindJSON(&tool); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
tool.ID = id
|
||||
if err := h.toolService.UpdateTool(&tool); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "tool updated"})
|
||||
}
|
||||
|
||||
// Delete 删除工具
|
||||
// @Summary 删除工具
|
||||
// @Description 删除工具
|
||||
// @Tags 工具管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "工具ID"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /tool/{id} [delete]
|
||||
func (h *ToolHandler) Delete(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tool id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.toolService.DeleteTool(id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "tool deleted"})
|
||||
}
|
||||
@@ -20,7 +20,16 @@ func NewUploadHandler(uploadService *service.UploadService, knowledgeRepo *repos
|
||||
return &UploadHandler{uploadService: uploadService, knowledgeRepo: knowledgeRepo}
|
||||
}
|
||||
|
||||
// Upload 上传文件
|
||||
// @Summary 上传文件
|
||||
// @Description 上传文件到服务器(本地存储或MinIO)
|
||||
// @Tags 文件上传
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param file formData file true "要上传的文件"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/file_upload [post]
|
||||
func (h *UploadHandler) Upload(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
@@ -48,7 +57,16 @@ func (h *UploadHandler) Upload(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// Delete 删除文件
|
||||
// @Summary 删除文件
|
||||
// @Description 删除指定文件
|
||||
// @Tags 文件上传
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param filename path string true "文件名"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/file_upload/{filename} [delete]
|
||||
func (h *UploadHandler) Delete(c *gin.Context) {
|
||||
filename := c.Param("filename")
|
||||
if filename == "" {
|
||||
@@ -64,7 +82,17 @@ func (h *UploadHandler) Delete(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "File deleted"})
|
||||
}
|
||||
|
||||
// ProxyFile 代理文件访问(解决 MinIO 内网地址和 HTTPS 问题)
|
||||
// @Summary 代理文件访问
|
||||
// @Description 代理访问文件,解决 MinIO 内网和 HTTPS 问题
|
||||
// @Tags 文件上传
|
||||
// @Accept json
|
||||
// @Produce octet-stream
|
||||
// @Param key query string true "文件Key"
|
||||
// @Param kb_id query string false "知识库ID"
|
||||
// @Success 200 {file} binary
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/file_proxy [get]
|
||||
func (h *UploadHandler) ProxyFile(c *gin.Context) {
|
||||
fileKey := c.Query("key")
|
||||
kbID := c.Query("kb_id")
|
||||
|
||||
31
server/internal/model/tool.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Tool 工具
|
||||
type Tool struct {
|
||||
ID string `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"uniqueIndex;size:100;not null"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
Category string `json:"category" gorm:"size:50;not null"`
|
||||
Provider string `json:"provider" gorm:"size:100"`
|
||||
SecurityLevel string `json:"security_level" gorm:"size:20;default:'safe'"`
|
||||
RequireApproval bool `json:"require_approval" gorm:"default:false"`
|
||||
Parameters string `json:"parameters" gorm:"type:text"` // JSON格式存储
|
||||
Status string `json:"status" gorm:"size:20;default:'active'"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// BeforeCreate 创建前自动生成ID
|
||||
func (t *Tool) BeforeCreate(tx *gorm.DB) error {
|
||||
if t.ID == "" {
|
||||
t.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -18,7 +19,7 @@ const (
|
||||
type Role struct {
|
||||
ID string `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"uniqueIndex"`
|
||||
Permissions []PermissionLevel `json:"permissions" gorm:"type:int[]"`
|
||||
Permissions string `json:"permissions" gorm:"type:text"` // 存储 JSON 格式的权限数组
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -41,8 +42,13 @@ func (u *User) HasPermission(level PermissionLevel) bool {
|
||||
if u.Role == nil {
|
||||
return false
|
||||
}
|
||||
for _, p := range u.Role.Permissions {
|
||||
if p >= level {
|
||||
// 解析 JSON 格式的权限
|
||||
var perms []int
|
||||
if err := json.Unmarshal([]byte(u.Role.Permissions), &perms); err != nil {
|
||||
return false
|
||||
}
|
||||
for _, p := range perms {
|
||||
if PermissionLevel(p) >= level {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
83
server/internal/repository/tool_repo.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"x-agents/server/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ToolRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewToolRepository(db *gorm.DB) *ToolRepository {
|
||||
return &ToolRepository{db: db}
|
||||
}
|
||||
|
||||
// DB 获取数据库连接
|
||||
func (r *ToolRepository) DB() *gorm.DB {
|
||||
return r.db
|
||||
}
|
||||
|
||||
func (r *ToolRepository) Create(tool *model.Tool) error {
|
||||
return r.db.Create(tool).Error
|
||||
}
|
||||
|
||||
func (r *ToolRepository) FindAll() ([]model.Tool, error) {
|
||||
var tools []model.Tool
|
||||
err := r.db.Order("category, name").Find(&tools).Error
|
||||
return tools, err
|
||||
}
|
||||
|
||||
func (r *ToolRepository) FindByCategory(category string) ([]model.Tool, error) {
|
||||
var tools []model.Tool
|
||||
err := r.db.Where("category = ?", category).Order("name").Find(&tools).Error
|
||||
return tools, err
|
||||
}
|
||||
|
||||
func (r *ToolRepository) FindByID(id string) (*model.Tool, error) {
|
||||
var tool model.Tool
|
||||
err := r.db.First(&tool, "id = ?", id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tool, nil
|
||||
}
|
||||
|
||||
func (r *ToolRepository) Update(tool *model.Tool) error {
|
||||
return r.db.Save(tool).Error
|
||||
}
|
||||
|
||||
func (r *ToolRepository) Delete(id string) error {
|
||||
return r.db.Delete(&model.Tool{}, "id = ?", id).Error
|
||||
}
|
||||
|
||||
func (r *ToolRepository) Count() (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.Tool{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// UpsertBatch 批量upsert工具
|
||||
func (r *ToolRepository) UpsertBatch(tools []model.Tool) error {
|
||||
for _, tool := range tools {
|
||||
var existing model.Tool
|
||||
err := r.db.First(&existing, "name = ?", tool.Name).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
if err := r.db.Create(&tool).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else {
|
||||
existing.Description = tool.Description
|
||||
existing.Category = tool.Category
|
||||
existing.Provider = tool.Provider
|
||||
existing.Status = tool.Status
|
||||
if err := r.db.Save(&existing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"x-agents/server/internal/model"
|
||||
@@ -123,10 +128,11 @@ func (s *AuthService) Register(username, password, email string) (*model.User, e
|
||||
role, err := s.userRepo.FindRoleByID(user.RoleID)
|
||||
if err != nil {
|
||||
// 创建默认角色
|
||||
perms, _ := json.Marshal([]int{int(model.PermissionRead), int(model.PermissionWrite)})
|
||||
role = &model.Role{
|
||||
ID: "user",
|
||||
Name: "user",
|
||||
Permissions: []model.PermissionLevel{model.PermissionRead, model.PermissionWrite},
|
||||
Permissions: string(perms),
|
||||
}
|
||||
s.userRepo.CreateRole(role)
|
||||
user.Role = role
|
||||
@@ -136,10 +142,74 @@ func (s *AuthService) Register(username, password, email string) (*model.User, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建用户工作空间目录
|
||||
if err := s.createUserWorkspace(username); err != nil {
|
||||
// 工作空间创建失败不影响注册成功,仅记录日志
|
||||
println("Warning: failed to create user workspace:", err.Error())
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// createUserWorkspace 创建用户工作空间目录
|
||||
func (s *AuthService) createUserWorkspace(username string) error {
|
||||
// 获取当前可执行文件所在目录
|
||||
execPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 尝试多种方式获取项目根目录
|
||||
projectRoot := execPath
|
||||
|
||||
// 方式1: 如果当前目录名为 server,向上找一级
|
||||
baseName := filepath.Base(execPath)
|
||||
// 处理 Windows 和 Unix 风格路径
|
||||
if baseName == "server" || strings.HasSuffix(execPath, "/server") || strings.HasSuffix(execPath, "\\server") {
|
||||
projectRoot = filepath.Dir(execPath)
|
||||
}
|
||||
|
||||
// 方式2: 尝试向上查找包含 .git 或 package.json 的目录
|
||||
if _, err := os.Stat(filepath.Join(projectRoot, ".git")); os.IsNotExist(err) {
|
||||
// 继续向上查找
|
||||
for i := 0; i < 3; i++ {
|
||||
parent := filepath.Dir(projectRoot)
|
||||
if parent == projectRoot {
|
||||
break
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(parent, ".git")); err == nil {
|
||||
projectRoot = parent
|
||||
break
|
||||
}
|
||||
projectRoot = parent
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[Workspace] Creating workspace at: %s", filepath.Join(projectRoot, "account", username))
|
||||
|
||||
// 创建 account 目录和用户目录
|
||||
workspacePath := filepath.Join(projectRoot, "account", username)
|
||||
if err := os.MkdirAll(workspacePath, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建子目录
|
||||
subDirs := []string{"projects", "files", "temp"}
|
||||
for _, dir := range subDirs {
|
||||
if err := os.MkdirAll(filepath.Join(workspacePath, dir), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserByID 根据ID获取用户
|
||||
func (s *AuthService) GetUserByID(id string) (*model.User, error) {
|
||||
return s.userRepo.FindByID(id)
|
||||
}
|
||||
|
||||
// GetAllUsers 获取所有用户
|
||||
func (s *AuthService) GetAllUsers() ([]model.User, error) {
|
||||
return s.userRepo.FindAll()
|
||||
}
|
||||
|
||||
@@ -155,9 +155,9 @@ func (s *KnowledgeService) Delete(id string) error {
|
||||
|
||||
// 删除每个文档的 MinIO 文件和本地 Markdown 文件
|
||||
for _, doc := range docs {
|
||||
// 删除 MinIO 文件
|
||||
if doc.FileKey != "" && kb.StorageConfig.Type == "minio" {
|
||||
s.uploadService.DeleteFileWithConfig(doc.FileKey, kb.StorageConfig)
|
||||
// 删除存储文件(MinIO 或本地)
|
||||
if doc.FileKey != "" {
|
||||
s.uploadService.DeleteFileWithConfig(doc.FileKey, kb.StorageConfig, kb.Name)
|
||||
}
|
||||
// 删除本地 Markdown 文件
|
||||
if s.markdownLocalPath != "" {
|
||||
@@ -215,15 +215,15 @@ func (s *KnowledgeService) UploadDocument(kbID string, file *multipart.FileHeade
|
||||
|
||||
// 上传文件(根据知识库的 storage_config 选择存储方式)
|
||||
var result *UploadResponse
|
||||
if kb.StorageConfig.Type != "" {
|
||||
// 使用知识库的存储配置
|
||||
if kb.StorageConfig.Type != "" && kb.StorageConfig.Type != "local" {
|
||||
// 使用知识库的存储配置(MinIO)
|
||||
knowledgeDebugLog.Printf("[Knowledge Upload] 使用知识库存储配置: type=%s, endpoint=%s, bucket=%s",
|
||||
kb.StorageConfig.Type, kb.StorageConfig.Endpoint, kb.StorageConfig.Bucket)
|
||||
result, err = s.uploadService.UploadWithConfig(file, kb.StorageConfig)
|
||||
} else {
|
||||
// 使用全局配置
|
||||
knowledgeDebugLog.Printf("[Knowledge Upload] 使用全局存储配置")
|
||||
result, err = s.uploadService.Upload(file)
|
||||
// 本地存储,使用知识库名称作为子目录
|
||||
knowledgeDebugLog.Printf("[Knowledge Upload] 使用本地存储,路径: resources/%s/", kb.Name)
|
||||
result, err = s.uploadService.UploadToKnowledgeBase(file, kb.StorageConfig, kb.Name)
|
||||
}
|
||||
if err != nil {
|
||||
knowledgeDebugLog.Printf("[Knowledge Upload] 错误: 上传失败, err=%v", err)
|
||||
@@ -419,13 +419,8 @@ func (s *KnowledgeService) DeleteDocument(kbID, docID string) error {
|
||||
if doc.FileKey != "" {
|
||||
knowledgeDebugLog.Printf("[Knowledge DeleteDocument] 删除文件: kbID=%s, docID=%s, fileKey=%s, storageType=%s",
|
||||
kbID, docID, doc.FileKey, kb.StorageConfig.Type)
|
||||
if kb.StorageConfig.Type != "" {
|
||||
// 使用知识库的存储配置删除
|
||||
s.uploadService.DeleteFileWithConfig(doc.FileKey, kb.StorageConfig)
|
||||
} else {
|
||||
// 使用全局配置删除
|
||||
s.uploadService.DeleteFile(doc.FileKey)
|
||||
}
|
||||
// 使用知识库的存储配置删除(传入知识库名称)
|
||||
s.uploadService.DeleteFileWithConfig(doc.FileKey, kb.StorageConfig, kb.Name)
|
||||
}
|
||||
|
||||
// 删除文档记录
|
||||
|
||||
219
server/internal/service/tool_service.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"log"
|
||||
"x-agents/server/internal/model"
|
||||
"x-agents/server/internal/repository"
|
||||
)
|
||||
|
||||
type ToolService struct {
|
||||
toolRepo *repository.ToolRepository
|
||||
}
|
||||
|
||||
func NewToolService(toolRepo *repository.ToolRepository) *ToolService {
|
||||
return &ToolService{toolRepo: toolRepo}
|
||||
}
|
||||
|
||||
func (s *ToolService) GetAllTools() ([]model.Tool, error) {
|
||||
return s.toolRepo.FindAll()
|
||||
}
|
||||
|
||||
func (s *ToolService) GetToolsByCategory(category string) ([]model.Tool, error) {
|
||||
return s.toolRepo.FindByCategory(category)
|
||||
}
|
||||
|
||||
func (s *ToolService) GetToolByID(id string) (*model.Tool, error) {
|
||||
return s.toolRepo.FindByID(id)
|
||||
}
|
||||
|
||||
// GetTools 根据条件获取工具列表
|
||||
func (s *ToolService) GetTools(category string, status string) ([]model.Tool, error) {
|
||||
var tools []model.Tool
|
||||
query := s.toolRepo.DB()
|
||||
|
||||
if category != "" {
|
||||
query = query.Where("category = ?", category)
|
||||
}
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
|
||||
err := query.Find(&tools).Error
|
||||
return tools, err
|
||||
}
|
||||
|
||||
func (s *ToolService) CreateTool(tool *model.Tool) error {
|
||||
return s.toolRepo.Create(tool)
|
||||
}
|
||||
|
||||
func (s *ToolService) UpdateTool(tool *model.Tool) error {
|
||||
return s.toolRepo.Update(tool)
|
||||
}
|
||||
|
||||
func (s *ToolService) DeleteTool(id string) error {
|
||||
return s.toolRepo.Delete(id)
|
||||
}
|
||||
|
||||
// InitDefaultTools 初始化默认工具到数据库
|
||||
func (s *ToolService) InitDefaultTools() error {
|
||||
log.Println("[ToolService] Starting init default tools...")
|
||||
|
||||
// 获取默认工具
|
||||
tools := s.getDefaultTools()
|
||||
|
||||
// 删除现有的系统工具,重新插入
|
||||
s.toolRepo.DB().Where("provider = ?", "system").Delete(&model.Tool{})
|
||||
log.Printf("[ToolService] Deleted existing system tools, inserting %d default tools...", len(tools))
|
||||
|
||||
for _, tool := range tools {
|
||||
if err := s.toolRepo.Create(&tool); err != nil {
|
||||
log.Printf("[ToolService] Create tool error: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[ToolService] Default tools initialized successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDefaultTools 获取默认工具列表
|
||||
func (s *ToolService) getDefaultTools() []model.Tool {
|
||||
return []model.Tool{
|
||||
// 文件操作
|
||||
{
|
||||
Name: "read_file",
|
||||
Description: "Read the contents of a file from the filesystem.",
|
||||
Category: "file",
|
||||
SecurityLevel: "safe",
|
||||
RequireApproval: false,
|
||||
Provider: "system",
|
||||
Status: "active",
|
||||
Parameters: `{"type":"object","properties":{"file_path":{"type":"string","description":"The path to the file to read"},"encoding":{"type":"string","description":"File encoding (default: utf-8)","default":"utf-8"}},"required":["file_path"]}`,
|
||||
},
|
||||
{
|
||||
Name: "write_file",
|
||||
Description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does.",
|
||||
Category: "file",
|
||||
SecurityLevel: "review",
|
||||
RequireApproval: true,
|
||||
Provider: "system",
|
||||
Status: "active",
|
||||
Parameters: `{"type":"object","properties":{"file_path":{"type":"string","description":"The path to the file to write"},"content":{"type":"string","description":"The content to write to the file"}},"required":["file_path","content"]}`,
|
||||
},
|
||||
{
|
||||
Name: "list_dir",
|
||||
Description: "List the contents of a directory.",
|
||||
Category: "file",
|
||||
SecurityLevel: "safe",
|
||||
RequireApproval: false,
|
||||
Provider: "system",
|
||||
Status: "active",
|
||||
Parameters: `{"type":"object","properties":{"dir_path":{"type":"string","description":"The path to the directory to list","default":"."}}}`,
|
||||
},
|
||||
{
|
||||
Name: "delete_file",
|
||||
Description: "Delete a file or directory.",
|
||||
Category: "file",
|
||||
SecurityLevel: "danger",
|
||||
RequireApproval: true,
|
||||
Provider: "system",
|
||||
Status: "active",
|
||||
Parameters: `{"type":"object","properties":{"file_path":{"type":"string","description":"The path to the file or directory to delete"}},"required":["file_path"]}`,
|
||||
},
|
||||
{
|
||||
Name: "search_files",
|
||||
Description: "Search for files by name pattern or content.",
|
||||
Category: "file",
|
||||
SecurityLevel: "safe",
|
||||
RequireApproval: false,
|
||||
Provider: "system",
|
||||
Status: "active",
|
||||
Parameters: `{"type":"object","properties":{"directory":{"type":"string","description":"The directory to search in"},"pattern":{"type":"string","description":"Glob pattern for file names","default":"*"},"content_pattern":{"type":"string","description":"Search for files containing this text"}},"required":["directory"]}`,
|
||||
},
|
||||
// 代码执行
|
||||
{
|
||||
Name: "execute_python",
|
||||
Description: "Execute Python code in a sandboxed environment.",
|
||||
Category: "executor",
|
||||
SecurityLevel: "review",
|
||||
RequireApproval: true,
|
||||
Provider: "system",
|
||||
Status: "active",
|
||||
Parameters: `{"type":"object","properties":{"code":{"type":"string","description":"The Python code to execute"},"timeout":{"type":"integer","description":"Execution timeout in seconds","default":30}},"required":["code"]}`,
|
||||
},
|
||||
{
|
||||
Name: "execute_javascript",
|
||||
Description: "Execute JavaScript code in a sandboxed environment.",
|
||||
Category: "executor",
|
||||
SecurityLevel: "review",
|
||||
RequireApproval: true,
|
||||
Provider: "system",
|
||||
Status: "active",
|
||||
Parameters: `{"type":"object","properties":{"code":{"type":"string","description":"The JavaScript code to execute"},"timeout":{"type":"integer","description":"Execution timeout in seconds","default":30}},"required":["code"]}`,
|
||||
},
|
||||
{
|
||||
Name: "execute_bash",
|
||||
Description: "Execute a bash command in a sandboxed environment.",
|
||||
Category: "executor",
|
||||
SecurityLevel: "danger",
|
||||
RequireApproval: true,
|
||||
Provider: "system",
|
||||
Status: "active",
|
||||
Parameters: `{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute"},"timeout":{"type":"integer","description":"Execution timeout in seconds","default":30}},"required":["command"]}`,
|
||||
},
|
||||
// 网页
|
||||
{
|
||||
Name: "web_fetch",
|
||||
Description: "Fetch content from a web URL.",
|
||||
Category: "web",
|
||||
SecurityLevel: "safe",
|
||||
RequireApproval: false,
|
||||
Provider: "system",
|
||||
Status: "active",
|
||||
Parameters: `{"type":"object","properties":{"url":{"type":"string","description":"The URL to fetch"},"method":{"type":"string","description":"HTTP method","default":"GET"},"timeout":{"type":"integer","description":"Request timeout in seconds","default":30}},"required":["url"]}`,
|
||||
},
|
||||
{
|
||||
Name: "web_search",
|
||||
Description: "Search the web for information.",
|
||||
Category: "web",
|
||||
SecurityLevel: "safe",
|
||||
RequireApproval: false,
|
||||
Provider: "system",
|
||||
Status: "active",
|
||||
Parameters: `{"type":"object","properties":{"query":{"type":"string","description":"The search query"},"max_results":{"type":"integer","description":"Maximum number of results","default":5}},"required":["query"]}`,
|
||||
},
|
||||
// HTTP
|
||||
{
|
||||
Name: "http_request",
|
||||
Description: "Make HTTP requests to APIs.",
|
||||
Category: "http",
|
||||
SecurityLevel: "safe",
|
||||
RequireApproval: false,
|
||||
Provider: "system",
|
||||
Status: "active",
|
||||
Parameters: `{"type":"object","properties":{"url":{"type":"string","description":"The URL to request"},"method":{"type":"string","description":"HTTP method","default":"GET"},"params":{"type":"object","description":"Query parameters"},"json_data":{"type":"object","description":"JSON body"},"timeout":{"type":"integer","description":"Request timeout","default":30}},"required":["url"]}`,
|
||||
},
|
||||
// 通知
|
||||
{
|
||||
Name: "send_notification",
|
||||
Description: "Send notifications via email, webhook, dingtalk, or slack.",
|
||||
Category: "notification",
|
||||
SecurityLevel: "safe",
|
||||
RequireApproval: false,
|
||||
Provider: "system",
|
||||
Status: "active",
|
||||
Parameters: `{"type":"object","properties":{"type":{"type":"string","description":"Notification type: email, webhook, dingtalk, slack"},"message":{"type":"string","description":"The notification message"}},"required":["type","message"]}`,
|
||||
},
|
||||
// 系统
|
||||
{
|
||||
Name: "get_current_time",
|
||||
Description: "Get the current date and time.",
|
||||
Category: "system",
|
||||
SecurityLevel: "safe",
|
||||
RequireApproval: false,
|
||||
Provider: "system",
|
||||
Status: "active",
|
||||
Parameters: `{"type":"object","properties":{"timezone":{"type":"string","description":"Optional timezone (e.g., UTC, Asia/Shanghai)"}}}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func (s *UploadService) Upload(file *multipart.FileHeader) (*UploadResponse, err
|
||||
if s.cfg.UploadMode == "minio" {
|
||||
return s.uploadToMinIO(f, filename, fileKey, file.Size)
|
||||
}
|
||||
return s.uploadToLocal(f, filename, fileKey)
|
||||
return s.uploadToLocal(f, filename, fileKey, "")
|
||||
}
|
||||
|
||||
// UploadWithConfig 上传文件(使用指定配置)
|
||||
@@ -116,7 +116,29 @@ func (s *UploadService) UploadWithConfig(file *multipart.FileHeader, storageConf
|
||||
return s.uploadToMinIOWithConfig(f, filename, fileKey, file.Size, storageConfig)
|
||||
}
|
||||
// 默认使用本地存储
|
||||
return s.uploadToLocal(f, filename, fileKey)
|
||||
return s.uploadToLocal(f, filename, fileKey, "")
|
||||
}
|
||||
|
||||
// UploadToKnowledgeBase 上传文件到知识库(本地存储时按知识库名称分类)
|
||||
func (s *UploadService) UploadToKnowledgeBase(file *multipart.FileHeader, storageConfig model.StorageConfig, knowledgeBaseName string) (*UploadResponse, error) {
|
||||
// 打开文件
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
return &UploadResponse{Success: false, Message: err.Error()}, nil
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// 生成唯一文件名
|
||||
ext := filepath.Ext(file.Filename)
|
||||
filename := fmt.Sprintf("%s%s", uuid.New().String(), ext)
|
||||
fileKey := strings.TrimSuffix(filename, ext)
|
||||
|
||||
// 根据配置选择存储方式
|
||||
if storageConfig.Type == "minio" {
|
||||
return s.uploadToMinIOWithConfig(f, filename, fileKey, file.Size, storageConfig)
|
||||
}
|
||||
// 本地存储,使用知识库名称作为子目录
|
||||
return s.uploadToLocal(f, filename, fileKey, knowledgeBaseName)
|
||||
}
|
||||
|
||||
// uploadToMinIOWithConfig 使用指定配置上传到 MinIO
|
||||
@@ -217,9 +239,18 @@ func (s *UploadService) uploadToMinIO(f multipart.File, filename, fileKey string
|
||||
}
|
||||
|
||||
// uploadToLocal 上传到本地
|
||||
func (s *UploadService) uploadToLocal(f multipart.File, filename, fileKey string) (*UploadResponse, error) {
|
||||
// 确保目录存在
|
||||
uploadPath := s.cfg.UploadLocalPath
|
||||
// knowledgeBaseName: 知识库名称,如果为空则使用全局配置目录,否则使用 resources/{知识库名称}/
|
||||
func (s *UploadService) uploadToLocal(f multipart.File, filename, fileKey string, knowledgeBaseName string) (*UploadResponse, error) {
|
||||
// 确定存储路径
|
||||
var uploadPath string
|
||||
if knowledgeBaseName != "" {
|
||||
// 使用 resources/{知识库名称}/ 路径
|
||||
uploadPath = filepath.Join("resources", knowledgeBaseName)
|
||||
} else {
|
||||
// 使用全局配置路径
|
||||
uploadPath = s.cfg.UploadLocalPath
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(uploadPath, 0755); err != nil {
|
||||
return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to create directory: %v", err)}, nil
|
||||
}
|
||||
@@ -259,7 +290,8 @@ func (s *UploadService) DeleteFile(filename string) error {
|
||||
}
|
||||
|
||||
// DeleteFileWithConfig 使用指定配置删除文件
|
||||
func (s *UploadService) DeleteFileWithConfig(filename string, storageConfig model.StorageConfig) error {
|
||||
// 注意:此方法需要知识库名称才能正确删除本地文件
|
||||
func (s *UploadService) DeleteFileWithConfig(filename string, storageConfig model.StorageConfig, knowledgeBaseName string) error {
|
||||
if storageConfig.Type == "minio" {
|
||||
// 创建 MinIO 客户端
|
||||
client, err := minio.New(storageConfig.Endpoint, &minio.Options{
|
||||
@@ -275,7 +307,12 @@ func (s *UploadService) DeleteFileWithConfig(filename string, storageConfig mode
|
||||
}
|
||||
|
||||
// 本地删除
|
||||
path := filepath.Join(s.cfg.UploadLocalPath, filename)
|
||||
var path string
|
||||
if knowledgeBaseName != "" {
|
||||
path = filepath.Join("resources", knowledgeBaseName, filename)
|
||||
} else {
|
||||
path = filepath.Join(s.cfg.UploadLocalPath, filename)
|
||||
}
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
|
||||
76
web/package-lock.json
generated
@@ -12,7 +12,9 @@
|
||||
"@vue-office/excel": "^1.7.14",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.13.3",
|
||||
"lucide-vue-next": "^0.577.0",
|
||||
"marked": "^17.0.4",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"papaparse": "^5.5.3",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
@@ -27,6 +29,7 @@
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vue-tsc": "^2.0.7"
|
||||
}
|
||||
},
|
||||
@@ -978,7 +981,6 @@
|
||||
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
@@ -989,7 +991,6 @@
|
||||
"integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
@@ -1004,6 +1005,13 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.20",
|
||||
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
||||
@@ -1419,7 +1427,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -1559,6 +1566,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.2.7.tgz",
|
||||
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.0.0.tgz",
|
||||
@@ -1864,7 +1880,6 @@
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -1893,15 +1908,13 @@
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz",
|
||||
"integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-unified": {
|
||||
"version": "1.0.3",
|
||||
@@ -1914,6 +1927,15 @@
|
||||
"lodash-es": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-vue-next": {
|
||||
"version": "0.577.0",
|
||||
"resolved": "https://registry.npmmirror.com/lucide-vue-next/-/lucide-vue-next-0.577.0.tgz",
|
||||
"integrity": "sha512-py05bAfv9SHVJqscbiOnjcnLlEmOffA58a+7XhZuFxrs6txe1E8VoR1ngWGTYO+9aVKABAz8l3ee3PqiQN9QPA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@@ -1981,6 +2003,28 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.55.1",
|
||||
"resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dompurify": "3.2.7",
|
||||
"marked": "14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor/node_modules/marked": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/marked/-/marked-14.0.0.tgz",
|
||||
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/muggle-string": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz",
|
||||
@@ -2161,7 +2205,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -2597,7 +2640,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -2637,7 +2679,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -2697,7 +2738,6 @@
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
@@ -2752,6 +2792,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-monaco-editor": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.1.0.tgz",
|
||||
"integrity": "sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"monaco-editor": ">=0.33.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||
@@ -2764,7 +2814,6 @@
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.29.tgz",
|
||||
"integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.29",
|
||||
"@vue/compiler-sfc": "3.5.29",
|
||||
@@ -2787,7 +2836,6 @@
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
"@vue-office/excel": "^1.7.14",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.13.3",
|
||||
"lucide-vue-next": "^0.577.0",
|
||||
"marked": "^17.0.4",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"papaparse": "^5.5.3",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
@@ -28,6 +30,7 @@
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vue-tsc": "^2.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ElConfigProvider } from 'element-plus'
|
||||
import Sidebar from '@/components/Sidebar.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const showSidebar = computed(() => route.path !== '/')
|
||||
const showSidebar = computed(() => route.path !== '/login' && route.path !== '/' && route.path !== '/signup')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { fetchKnowledgeBases } from '@/views/knowledge/useKnowledge'
|
||||
import { useDatabase } from '@/views/database/useDatabase'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
// 下拉菜单展开状态
|
||||
const userDropdownVisible = ref(false)
|
||||
|
||||
// 退出确认弹窗状态
|
||||
const showLogoutConfirm = ref(false)
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 获取当前用户信息
|
||||
const { getUser, logout: authLogout } = useAuth()
|
||||
const currentUser = ref(getUser())
|
||||
|
||||
// 计算用户首字母缩写
|
||||
const userInitials = computed(() => {
|
||||
const name = currentUser.value?.username || currentUser.value?.name || 'User'
|
||||
const parts = name.split(' ')
|
||||
if (parts.length >= 2) {
|
||||
return (parts[0][0] + parts[1][0]).toUpperCase()
|
||||
}
|
||||
return name.substring(0, 2).toUpperCase()
|
||||
})
|
||||
|
||||
// 获取 Knowledge 数量
|
||||
const knowledgeCount = ref(0)
|
||||
const fetchKnowledgeCount = async () => {
|
||||
@@ -60,11 +77,11 @@ const group3 = computed(() => [
|
||||
{ name: 'Memory', icon: 'fa-brain', path: '/memory' },
|
||||
])
|
||||
|
||||
// 第4组: Dashboard, Account, Settings
|
||||
// 第4组: Dashboard, Models, Logs
|
||||
const group4 = computed(() => [
|
||||
{ name: 'Dashboard', icon: 'fa-gauge', path: '/dashboard' },
|
||||
{ name: 'Account', icon: 'fa-user', path: '/account' },
|
||||
{ name: 'Settings', icon: 'fa-gear', path: '/settings' },
|
||||
{ name: 'Models', icon: 'fa-brain', path: '/settings' },
|
||||
{ name: 'Logs', icon: 'fa-file-lines', path: '/logs' },
|
||||
])
|
||||
|
||||
const activeMenu = computed(() => {
|
||||
@@ -83,31 +100,43 @@ const navigateTo = (item: MenuItem) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 通知设置弹窗状态
|
||||
const showNotificationSettings = ref(false)
|
||||
|
||||
// 用户菜单操作
|
||||
const handleUserCommand = (command: string) => {
|
||||
switch (command) {
|
||||
case 'settings':
|
||||
// 全局设置
|
||||
router.push('/settings')
|
||||
case 'notifications':
|
||||
// 通知设置
|
||||
showNotificationSettings.value = true
|
||||
break
|
||||
case 'account':
|
||||
// 账户设置
|
||||
router.push('/account')
|
||||
break
|
||||
case 'userManagement':
|
||||
// 用户管理
|
||||
router.push('/user-management')
|
||||
break
|
||||
case 'logout':
|
||||
// 退出登录
|
||||
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
// 清除登录状态
|
||||
localStorage.removeItem('token')
|
||||
router.push('/login')
|
||||
}).catch(() => {})
|
||||
// 退出登录 - 显示自定义确认弹窗
|
||||
showLogoutConfirm.value = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 确认退出登录
|
||||
const confirmLogout = () => {
|
||||
showLogoutConfirm.value = false
|
||||
userDropdownVisible.value = false
|
||||
authLogout()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// 取消退出
|
||||
const cancelLogout = () => {
|
||||
showLogoutConfirm.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -186,7 +215,7 @@ const handleUserCommand = (command: string) => {
|
||||
<!-- 分隔线3 -->
|
||||
<li class="my-4 border-t border-dark-500"></li>
|
||||
|
||||
<!-- 第4组: Dashboard, Account, Settings -->
|
||||
<!-- 第4组: Dashboard, Notifications, Account, Model Settings, Logs -->
|
||||
<li v-for="item in group4" :key="item.name">
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
@@ -198,11 +227,6 @@ const handleUserCommand = (command: string) => {
|
||||
<i :class="['fa-solid', item.icon, 'w-5', 'text-center']"></i>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
<div v-if="item.name === 'Settings'" class="flex gap-1">
|
||||
<span class="w-2 h-2 rounded-full bg-primary-orange"></span>
|
||||
<span class="w-2 h-2 rounded-full bg-yellow-500"></span>
|
||||
<span class="w-2 h-2 rounded-full bg-gray-500"></span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -210,65 +234,218 @@ const handleUserCommand = (command: string) => {
|
||||
|
||||
<!-- 底部用户信息 -->
|
||||
<div class="border-t border-dark-500">
|
||||
<el-dropdown trigger="click" @command="handleUserCommand" class="user-dropdown" @visible-change="(v: boolean) => userDropdownVisible = v">
|
||||
<div class="w-full flex items-center justify-between cursor-pointer hover:bg-dark-600 px-4 py-3 transition-colors">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="w-full flex items-center justify-between cursor-pointer hover:bg-dark-600 px-4 py-3 transition-colors"
|
||||
@click="userDropdownVisible = !userDropdownVisible"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="https://picsum.photos/id/64/40/40" alt="User Avatar" class="w-8 h-8 rounded-full object-cover">
|
||||
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-primary-orange to-red-500 flex items-center justify-center text-white font-medium text-xs">
|
||||
{{ userInitials }}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium text-sm text-gray-300 truncate">Alex Smith</div>
|
||||
<div class="text-xs text-gray-500 truncate">alex@gmail.com</div>
|
||||
<div class="font-medium text-sm text-gray-200 truncate">{{ currentUser?.username || currentUser?.name || 'User' }}</div>
|
||||
<div class="text-xs text-gray-500 truncate">{{ currentUser?.email || 'user@example.com' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<i :class="['fa-solid', userDropdownVisible ? 'fa-chevron-up' : 'fa-chevron-down', 'text-xs', 'text-gray-500']"></i>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="settings">
|
||||
<i class="fa-solid fa-gear w-4 text-center"></i>
|
||||
Settings
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="userManagement">
|
||||
<i class="fa-solid fa-user w-4 text-center"></i>
|
||||
Users
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="logout">
|
||||
<i class="fa-solid fa-arrow-right-from-bracket w-4 text-center"></i>
|
||||
Sign Out
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<!-- 简洁下拉弹窗 -->
|
||||
<Transition name="dropdown-fade">
|
||||
<div v-if="userDropdownVisible" class="user-dropdown-panel" @click.stop>
|
||||
<div class="dropdown-menu">
|
||||
<div class="menu-item" @click="handleUserCommand('notifications')">
|
||||
<i class="fa-solid fa-bell text-gray-400"></i>
|
||||
<span class="text-gray-300">Notifications</span>
|
||||
</div>
|
||||
|
||||
<div class="menu-item" @click="handleUserCommand('account')">
|
||||
<i class="fa-solid fa-user text-gray-400"></i>
|
||||
<span class="text-gray-300">Account</span>
|
||||
</div>
|
||||
|
||||
<div class="menu-item">
|
||||
<i class="fa-solid fa-circle-question text-gray-400"></i>
|
||||
<span class="text-gray-300">Help & Support</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
<div class="dropdown-footer">
|
||||
<div class="menu-item logout" @click="handleUserCommand('logout')">
|
||||
<i class="fa-solid fa-arrow-right-from-bracket text-gray-400"></i>
|
||||
<span class="text-gray-300">Sign Out</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<!-- 点击外部关闭 -->
|
||||
<div v-if="userDropdownVisible" class="fixed inset-0 z-40" @click="userDropdownVisible = false"></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 退出确认弹窗 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-if="showLogoutConfirm" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click="cancelLogout">
|
||||
<div class="bg-dark-700 rounded-xl w-full max-w-sm border border-dark-500 shadow-2xl" @click.stop>
|
||||
<div class="p-6 text-center">
|
||||
<div class="w-14 h-14 rounded-full bg-red-500/20 flex items-center justify-center mx-auto mb-4">
|
||||
<i class="fa-solid fa-arrow-right-from-bracket text-red-400 text-xl"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-white mb-2">Sign Out</h3>
|
||||
<p class="text-gray-400 text-sm">Are you sure you want to sign out?</p>
|
||||
</div>
|
||||
<div class="flex border-t border-dark-500">
|
||||
<button @click="cancelLogout" class="flex-1 py-3 text-gray-400 hover:text-white hover:bg-dark-600 transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button @click="confirmLogout" class="flex-1 py-3 text-red-400 hover:bg-red-500/10 transition-colors border-l border-dark-500">
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<!-- 通知设置弹窗 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-if="showNotificationSettings" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click="showNotificationSettings = false">
|
||||
<div class="bg-dark-700 rounded-xl w-full max-w-md border border-dark-500 shadow-2xl" @click.stop>
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">Notification Settings</h3>
|
||||
<button @click="showNotificationSettings = false" class="text-gray-400 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<div class="flex items-center justify-between py-3 border-b border-dark-500">
|
||||
<div>
|
||||
<div class="font-medium text-white">Email Notifications</div>
|
||||
<div class="text-sm text-gray-400">Receive email for important updates</div>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" class="sr-only peer" checked>
|
||||
<div class="w-11 h-6 bg-dark-500 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-orange"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-3 border-b border-dark-500">
|
||||
<div>
|
||||
<div class="font-medium text-white">Push Notifications</div>
|
||||
<div class="text-sm text-gray-400">Receive push notifications in browser</div>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" class="sr-only peer" checked>
|
||||
<div class="w-11 h-6 bg-dark-500 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-orange"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-3 border-b border-dark-500">
|
||||
<div>
|
||||
<div class="font-medium text-white">System Alerts</div>
|
||||
<div class="text-sm text-gray-400">Get notified about system events</div>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" class="sr-only peer" checked>
|
||||
<div class="w-11 h-6 bg-dark-500 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-orange"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-3">
|
||||
<div>
|
||||
<div class="font-medium text-white">Agent Updates</div>
|
||||
<div class="text-sm text-gray-400">Notifications about agent activities</div>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" class="sr-only peer">
|
||||
<div class="w-11 h-6 bg-dark-500 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-orange"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button @click="showNotificationSettings = false" class="px-4 py-2 rounded-lg bg-primary-orange text-white hover:bg-orange-600 transition-colors">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.user-dropdown {
|
||||
width: 100%;
|
||||
}
|
||||
.user-dropdown .el-dropdown-menu {
|
||||
background-color: #262626;
|
||||
border: none;
|
||||
padding: 6px;
|
||||
/* 用户下拉弹窗 */
|
||||
.user-dropdown-panel {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-bottom: 8px;
|
||||
background: #1f1f1f;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 10px;
|
||||
min-width: 200px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
|
||||
z-index: 50;
|
||||
}
|
||||
.user-dropdown .el-dropdown-menu__item {
|
||||
color: white;
|
||||
padding: 8px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
|
||||
.dropdown-menu {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
font-size: 13px;
|
||||
}
|
||||
.user-dropdown .el-dropdown-menu__item:hover {
|
||||
background-color: #F97316;
|
||||
color: white;
|
||||
|
||||
.menu-item:hover {
|
||||
background: #333;
|
||||
}
|
||||
.user-dropdown .el-dropdown-menu__item--divided {
|
||||
border-top: 1px solid #404040;
|
||||
margin-top: 4px;
|
||||
padding-top: 8px;
|
||||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background: #333;
|
||||
margin: 4px 8px;
|
||||
}
|
||||
|
||||
.dropdown-footer {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.menu-item.logout:hover {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
/* 下拉动画 */
|
||||
.dropdown-fade-enter-active,
|
||||
.dropdown-fade-leave-active {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.dropdown-fade-enter-from,
|
||||
.dropdown-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
|
||||
/* 退出确认弹窗动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
114
web/src/composables/useAuth.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
const API_BASE = 'http://localhost:8082'
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
username: string
|
||||
password: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string
|
||||
user: any
|
||||
}
|
||||
|
||||
export const useAuth = () => {
|
||||
const token = localStorage.getItem('token')
|
||||
const user = localStorage.getItem('user')
|
||||
|
||||
const login = async (data: LoginRequest): Promise<LoginResponse> => {
|
||||
const response = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.message || 'Login failed')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
// Save token and user info
|
||||
localStorage.setItem('token', result.token)
|
||||
localStorage.setItem('user', JSON.stringify(result.user))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const register = async (data: RegisterRequest): Promise<any> => {
|
||||
const response = await fetch(`${API_BASE}/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.message || 'Registration failed')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
}
|
||||
|
||||
const getToken = () => {
|
||||
return localStorage.getItem('token')
|
||||
}
|
||||
|
||||
const getUser = () => {
|
||||
const userStr = localStorage.getItem('user')
|
||||
return userStr ? JSON.parse(userStr) : null
|
||||
}
|
||||
|
||||
const isLoggedIn = () => {
|
||||
return !!token
|
||||
}
|
||||
|
||||
const getCurrentUser = async () => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}/auth/me`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.message || 'Failed to get user')
|
||||
}
|
||||
|
||||
const user = await response.json()
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
return user
|
||||
}
|
||||
|
||||
return {
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
getToken,
|
||||
getUser,
|
||||
isLoggedIn,
|
||||
getCurrentUser,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createApp, type Component } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
@@ -6,8 +6,18 @@ import router from './router'
|
||||
import './assets/styles/index.css'
|
||||
import App from './App.vue'
|
||||
|
||||
// 全局注册 Lucide 图标
|
||||
import * as iconModule from 'lucide-vue-next'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册所有图标为全局组件(过滤非组件)
|
||||
for (const [name, icon] of Object.entries(iconModule)) {
|
||||
if (typeof icon === 'object' && icon !== null && 'render' in icon) {
|
||||
app.component(name, icon as Component)
|
||||
}
|
||||
}
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
|
||||
@@ -16,11 +16,23 @@ import Settings from '@/views/Settings.vue'
|
||||
import Account from '@/views/Account.vue'
|
||||
import Logs from '@/views/Logs.vue'
|
||||
|
||||
// 需要登录才能访问的路由
|
||||
const protectedRoutes = ['/dashboard', '/chat', '/agents', '/team', '/mcp', '/tools', '/database', '/script', '/plan', '/memory', '/knowledge', '/settings', '/account', '/logs']
|
||||
|
||||
// 检查是否已登录
|
||||
const isAuthenticated = () => {
|
||||
return !!localStorage.getItem('token')
|
||||
}
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
redirect: () => isAuthenticated() ? '/dashboard' : '/login'
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login
|
||||
},
|
||||
@@ -102,4 +114,24 @@ const router = createRouter({
|
||||
]
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const isAuth = isAuthenticated()
|
||||
const isLoginPage = to.path === '/login' || to.path === '/signup'
|
||||
|
||||
// 如果未登录且不是访问登录/注册页,跳转到登录页
|
||||
if (!isAuth && !isLoginPage) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果已登录且访问登录/注册页,跳转到仪表盘
|
||||
if (isAuth && isLoginPage) {
|
||||
next('/dashboard')
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,15 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import './database/database.css'
|
||||
|
||||
const { getCurrentUser, logout } = useAuth()
|
||||
|
||||
// 菜单类型
|
||||
type MenuKey = 'users' | 'roles' | 'permissions'
|
||||
type MenuKey = 'profile' | 'users' | 'roles' | 'permissions'
|
||||
|
||||
// 当前选中的菜单
|
||||
const activeMenu = ref<MenuKey>('users')
|
||||
const activeMenu = ref<MenuKey>('profile')
|
||||
|
||||
// 当前登录用户信息
|
||||
const currentUser = ref<any>(null)
|
||||
const userLoading = ref(false)
|
||||
const userError = ref('')
|
||||
|
||||
// 获取当前用户
|
||||
const fetchCurrentUser = async () => {
|
||||
userLoading.value = true
|
||||
userError.value = ''
|
||||
try {
|
||||
currentUser.value = await getCurrentUser()
|
||||
} catch (error: any) {
|
||||
userError.value = error.message
|
||||
} finally {
|
||||
userLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchCurrentUser()
|
||||
})
|
||||
|
||||
// 菜单列表
|
||||
const menuItems = [
|
||||
{ key: 'profile', label: 'Profile', icon: 'fa-user' },
|
||||
{ key: 'users', label: 'Users', icon: 'fa-users' },
|
||||
{ key: 'roles', label: 'Roles', icon: 'fa-user-shield' },
|
||||
{ key: 'permissions', label: 'Permissions', icon: 'fa-lock' },
|
||||
@@ -193,6 +225,61 @@ const statusClass = (status: string) => {
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="flex-1">
|
||||
<!-- Profile -->
|
||||
<div v-if="activeMenu === 'profile'" class="space-y-4">
|
||||
<h2 class="text-xl font-semibold">My Profile</h2>
|
||||
|
||||
<div v-if="userLoading" class="flex items-center justify-center py-12">
|
||||
<i class="fa-solid fa-circle-notch fa-spin text-2xl text-primary-orange"></i>
|
||||
</div>
|
||||
|
||||
<div v-else-if="userError" class="bg-red-500/10 border border-red-500/30 rounded-lg p-4 text-red-400">
|
||||
{{ userError }}
|
||||
</div>
|
||||
|
||||
<div v-else class="bg-dark-700 rounded-xl p-6 border border-dark-500">
|
||||
<div class="flex items-start gap-6">
|
||||
<!-- 头像 -->
|
||||
<div class="w-20 h-20 rounded-full bg-gradient-to-br from-primary-orange to-red-500 flex items-center justify-center text-white text-3xl font-bold">
|
||||
{{ currentUser?.username?.charAt(0)?.toUpperCase() || 'U' }}
|
||||
</div>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="flex-1">
|
||||
<h3 class="text-xl font-semibold">{{ currentUser?.username || 'User' }}</h3>
|
||||
<p class="text-gray-400">{{ currentUser?.email || 'No email' }}</p>
|
||||
|
||||
<div class="mt-4 flex gap-4">
|
||||
<div class="bg-dark-600 px-4 py-2 rounded-lg">
|
||||
<div class="text-sm text-gray-400">Role</div>
|
||||
<div class="font-medium">{{ currentUser?.role_id || 'User' }}</div>
|
||||
</div>
|
||||
<div class="bg-dark-600 px-4 py-2 rounded-lg">
|
||||
<div class="text-sm text-gray-400">Status</div>
|
||||
<div class="font-medium text-green-400">{{ currentUser?.is_active ? 'Active' : 'Inactive' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-gray-500">
|
||||
Member since: {{ currentUser?.created_at ? new Date(currentUser.created_at).toLocaleDateString() : 'N/A' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mt-6 pt-6 border-t border-dark-500 flex gap-3">
|
||||
<button class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all">
|
||||
<i class="fa-solid fa-pen mr-2"></i>
|
||||
Edit Profile
|
||||
</button>
|
||||
<button @click="handleLogout" class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors">
|
||||
<i class="fa-solid fa-sign-out-alt mr-2"></i>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users -->
|
||||
<div v-if="activeMenu === 'users'" class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
@@ -1,6 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// 创建智能体弹窗状态
|
||||
const showCreateModal = ref(false)
|
||||
const isCreating = ref(false)
|
||||
const newAgent = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
skills: '',
|
||||
knowledge: '',
|
||||
prompt: '',
|
||||
})
|
||||
|
||||
// Skills 选项
|
||||
const skillsOptions = [
|
||||
{ value: 'research', label: 'Research' },
|
||||
{ value: 'coder', label: 'Coder' },
|
||||
{ value: 'review', label: 'Code Review' },
|
||||
{ value: 'writer', label: 'Writer' },
|
||||
{ value: 'analyst', label: 'Analyst' },
|
||||
{ value: 'assistant', label: 'Assistant' },
|
||||
]
|
||||
|
||||
// Knowledge 选项
|
||||
const knowledgeOptions = [
|
||||
{ value: 'general', label: 'General Knowledge' },
|
||||
{ value: 'codebase', label: 'Codebase' },
|
||||
{ value: 'docs', label: 'Documentation' },
|
||||
{ value: 'api', label: 'API Reference' },
|
||||
]
|
||||
|
||||
// 打开创建弹窗
|
||||
const openCreateModal = () => {
|
||||
newAgent.value = { name: '', description: '', skills: '', knowledge: '', prompt: '' }
|
||||
showCreateModal.value = true
|
||||
}
|
||||
|
||||
// 创建智能体
|
||||
const createAgent = async () => {
|
||||
if (!newAgent.value.name || !newAgent.value.skills || !newAgent.value.knowledge) {
|
||||
return
|
||||
}
|
||||
|
||||
isCreating.value = true
|
||||
try {
|
||||
// 模拟创建
|
||||
const newId = Math.max(...agents.value.map(a => a.id)) + 1
|
||||
agents.value.unshift({
|
||||
id: newId,
|
||||
name: newAgent.value.name,
|
||||
avatar: '🤖',
|
||||
description: newAgent.value.description,
|
||||
accentColor: '#f97316',
|
||||
gradient: 'from-orange-500/20 to-amber-500/20',
|
||||
status: 'stopped',
|
||||
framework: skillsOptions.find(f => f.value === newAgent.value.skills)?.label || newAgent.value.skills,
|
||||
model: knowledgeOptions.find(k => k.value === newAgent.value.knowledge)?.label || newAgent.value.knowledge,
|
||||
mcpServers: 0,
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
})
|
||||
showCreateModal.value = false
|
||||
} finally {
|
||||
isCreating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
interface Agent {
|
||||
id: number
|
||||
name: string
|
||||
@@ -73,7 +137,7 @@ const deleteAgent = (id: number) => {
|
||||
<i class="fa-solid fa-robot text-orange-500"></i>
|
||||
<span class="font-medium">Agents</span>
|
||||
</div>
|
||||
<button class="btn-primary">
|
||||
<button @click="openCreateModal" class="btn-primary">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
New Agent
|
||||
</button>
|
||||
@@ -224,4 +288,81 @@ const deleteAgent = (id: number) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建智能体弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showCreateModal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||||
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">Create New Agent</h3>
|
||||
<button @click="showCreateModal = false" class="text-gray-400 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Agent Name *</label>
|
||||
<input
|
||||
v-model="newAgent.name"
|
||||
type="text"
|
||||
placeholder="Enter agent name..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<textarea
|
||||
v-model="newAgent.description"
|
||||
rows="3"
|
||||
placeholder="Describe what this agent does..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Skills *</label>
|
||||
<el-select v-model="newAgent.skills" placeholder="Select skills" class="w-full" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option v-for="s in skillsOptions" :key="s.value" :label="s.label" :value="s.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Knowledge *</label>
|
||||
<el-select v-model="newAgent.knowledge" placeholder="Select knowledge" class="w-full" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option v-for="k in knowledgeOptions" :key="k.value" :label="k.label" :value="k.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Custom Prompt</label>
|
||||
<textarea
|
||||
v-model="newAgent.prompt"
|
||||
rows="4"
|
||||
placeholder="Define the agent's behavior and instructions..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button
|
||||
@click="showCreateModal = false"
|
||||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="createAgent"
|
||||
:disabled="isCreating || !newAgent.name || !newAgent.skills || !newAgent.knowledge"
|
||||
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
<i v-if="isCreating" class="fa-solid fa-circle-notch fa-spin"></i>
|
||||
{{ isCreating ? 'Creating...' : 'Create Agent' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
@@ -53,6 +53,76 @@ const chatSessions = ref<ChatSession[]>([
|
||||
{ id: 3, title: '数据分析咨询', agentId: 4, lastMessage: 'DeepSeek: 好的', timestamp: new Date(Date.now() - 86400000) },
|
||||
])
|
||||
|
||||
// 群聊数据
|
||||
interface GroupChat {
|
||||
id: number
|
||||
name: string
|
||||
members: string[]
|
||||
lastMessage: string
|
||||
timestamp: Date
|
||||
}
|
||||
|
||||
const groupChats = ref<GroupChat[]>([
|
||||
{ id: 1, name: 'AI 讨论组', members: ['Claude', 'GPT-4', 'Gemini'], lastMessage: '我们来讨论一下...', timestamp: new Date(Date.now() - 1800000) },
|
||||
{ id: 2, name: '编程助手', members: ['Claude', 'DeepSeek'], lastMessage: '这段代码有问题吗?', timestamp: new Date(Date.now() - 3600000) },
|
||||
{ id: 3, name: '创意头脑风暴', members: ['GPT-4', 'Claude', 'Kimi'], lastMessage: '有个新想法...', timestamp: new Date(Date.now() - 7200000) },
|
||||
])
|
||||
|
||||
// 智能体选择弹窗状态
|
||||
const showAgentSelector = ref(false)
|
||||
const selectMode = ref<'single' | 'group'>('single')
|
||||
const selectedAgents = ref<Agent[]>([])
|
||||
const groupChatName = ref('')
|
||||
|
||||
// 打开智能体选择器
|
||||
const openAgentSelector = (mode: 'single' | 'group') => {
|
||||
selectMode.value = mode
|
||||
selectedAgents.value = []
|
||||
groupChatName.value = ''
|
||||
showAgentSelector.value = true
|
||||
}
|
||||
|
||||
// 切换智能体选择(群聊模式)
|
||||
const toggleAgentSelection = (agent: Agent) => {
|
||||
const index = selectedAgents.value.findIndex(a => a.id === agent.id)
|
||||
if (index > -1) {
|
||||
selectedAgents.value.splice(index, 1)
|
||||
} else {
|
||||
selectedAgents.value.push(agent)
|
||||
}
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const confirmAgentSelection = () => {
|
||||
if (selectMode.value === 'single') {
|
||||
// 单聊模式:选择一个智能体开始对话
|
||||
if (selectedAgents.value.length > 0) {
|
||||
selectedAgent.value = selectedAgents.value[0]
|
||||
messages.value = [
|
||||
{ id: 1, role: 'assistant', content: `你好!我是 ${selectedAgent.value.name},你的 AI 助手。有什么我可以帮助你的吗?`, timestamp: new Date() }
|
||||
]
|
||||
}
|
||||
} else {
|
||||
// 群聊模式:选择多个智能体
|
||||
const name = groupChatName.value.trim() || `群聊 (${selectedAgents.value.length}人)`
|
||||
console.log('创建群聊:', { name, members: selectedAgents.value })
|
||||
// 添加到群聊列表
|
||||
groupChats.value.unshift({
|
||||
id: Date.now(),
|
||||
name: name,
|
||||
members: selectedAgents.value.map(a => a.name),
|
||||
lastMessage: 'New group created',
|
||||
timestamp: new Date()
|
||||
})
|
||||
}
|
||||
showAgentSelector.value = false
|
||||
}
|
||||
|
||||
// 取消选择
|
||||
const cancelAgentSelection = () => {
|
||||
showAgentSelector.value = false
|
||||
}
|
||||
|
||||
// 侧边栏展开/收起状态
|
||||
const sidebarCollapsed = ref(false)
|
||||
|
||||
@@ -398,15 +468,26 @@ const toggleSidebar = () => {
|
||||
|
||||
<!-- 新建对话按钮 -->
|
||||
<div class="p-3">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="newChat"
|
||||
class="w-full flex items-center gap-2 px-3 py-2.5 bg-orange-500 hover:bg-orange-400 rounded-lg text-white text-sm font-medium transition-all duration-200"
|
||||
@click="openAgentSelector('single')"
|
||||
class="flex-1 flex items-center justify-center gap-2 px-3 py-2.5 bg-orange-500 hover:bg-orange-400 rounded-lg text-white text-sm font-medium transition-all duration-200"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
<span>新建对话</span>
|
||||
</button>
|
||||
<button
|
||||
@click="openAgentSelector('group')"
|
||||
class="flex-1 flex items-center justify-center gap-2 px-3 py-2.5 bg-dark-700 hover:bg-dark-600 border border-dark-500 rounded-lg text-white/80 text-sm font-medium transition-all duration-200"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||
</svg>
|
||||
<span>新建群聊</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI 助手选择 -->
|
||||
@@ -432,27 +513,115 @@ const toggleSidebar = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史对话列表 -->
|
||||
<!-- 群聊列表 -->
|
||||
<div class="flex-1 overflow-y-auto px-3 pb-3">
|
||||
<div class="text-xs text-white/40 uppercase tracking-wider mb-2 px-1">最近对话</div>
|
||||
<div class="text-xs text-white/40 uppercase tracking-wider mb-2 px-1">群聊</div>
|
||||
<div class="space-y-1">
|
||||
<button
|
||||
v-for="session in chatSessions"
|
||||
:key="session.id"
|
||||
@click="selectSession(session)"
|
||||
v-for="group in groupChats"
|
||||
:key="group.id"
|
||||
class="w-full text-left px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all duration-200 group"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-white/30 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||
</svg>
|
||||
<span class="text-sm text-white/70 group-hover:text-white truncate">{{ session.title }}</span>
|
||||
<span class="text-sm text-white/70 group-hover:text-white truncate">{{ group.name }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-white/30 mt-1 pl-6">{{ formatRelativeTime(session.timestamp) }}</div>
|
||||
<div class="text-xs text-white/30 mt-1 pl-6">{{ group.members.length }} members</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 智能体选择弹窗 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-if="showAgentSelector" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click="cancelAgentSelection">
|
||||
<div class="bg-dark-800 rounded-xl w-full max-w-md border border-dark-600 shadow-2xl" @click.stop>
|
||||
<div class="p-4 border-b border-dark-600">
|
||||
<h3 class="text-lg font-semibold text-white">
|
||||
{{ selectMode === 'single' ? '选择智能体' : '选择群聊成员' }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-400 mt-1">
|
||||
{{ selectMode === 'single' ? '选择一个智能体开始对话' : '选择多个智能体创建群聊' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 max-h-80 overflow-y-auto">
|
||||
<!-- 群聊名称输入框 -->
|
||||
<div v-if="selectMode === 'group'" class="mb-4">
|
||||
<input
|
||||
v-model="groupChatName"
|
||||
type="text"
|
||||
placeholder="Enter group name..."
|
||||
class="w-full bg-dark-700 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
v-for="agent in chatAgents"
|
||||
:key="agent.id"
|
||||
@click="selectMode === 'group' ? toggleAgentSelection(agent) : (selectedAgents = [agent], confirmAgentSelection())"
|
||||
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200"
|
||||
:class="selectedAgents.some(a => a.id === agent.id)
|
||||
? 'bg-orange-500/20 border border-orange-500/50'
|
||||
: 'bg-dark-700 hover:bg-dark-600 border border-transparent'"
|
||||
>
|
||||
<span class="text-xl">{{ agent.avatar }}</span>
|
||||
<div class="flex-1 text-left">
|
||||
<div class="text-white font-medium">{{ agent.name }}</div>
|
||||
<div class="text-xs text-gray-400">{{ agent.description }}</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="agent.status === 'online'"
|
||||
class="w-2 h-2 rounded-full bg-emerald-400"
|
||||
></span>
|
||||
<span
|
||||
v-if="selectMode === 'group' && selectedAgents.some(a => a.id === agent.id)"
|
||||
class="w-5 h-5 rounded-full bg-orange-500 flex items-center justify-center"
|
||||
>
|
||||
<svg class="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t border-dark-600 flex gap-3">
|
||||
<button
|
||||
@click="cancelAgentSelection"
|
||||
class="flex-1 py-2.5 bg-dark-700 hover:bg-dark-600 text-gray-300 rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
v-if="selectMode === 'group'"
|
||||
@click="confirmAgentSelection"
|
||||
:disabled="selectedAgents.length < 2"
|
||||
class="flex-1 py-2.5 bg-orange-500 hover:bg-orange-400 text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Create Group ({{ selectedAgents.length }})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { login } = useAuth()
|
||||
|
||||
// 表单数据
|
||||
const username = ref('')
|
||||
@@ -11,23 +14,38 @@ const rememberMe = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const showPassword = ref(false)
|
||||
const errorMsg = ref('')
|
||||
const successMsg = ref('')
|
||||
|
||||
// 模拟登录验证
|
||||
const handleLogin = () => {
|
||||
// 检查是否从注册页跳转过来
|
||||
onMounted(() => {
|
||||
if (route.query.registered === 'true') {
|
||||
successMsg.value = 'Account created successfully! Please sign in.'
|
||||
}
|
||||
})
|
||||
|
||||
// 登录处理
|
||||
const handleLogin = async () => {
|
||||
errorMsg.value = ''
|
||||
|
||||
if (username.value !== 'admin' || password.value !== 'admin') {
|
||||
errorMsg.value = 'Invalid username or password'
|
||||
if (!username.value || !password.value) {
|
||||
errorMsg.value = 'Please enter username and password'
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
// 模拟登录
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
|
||||
try {
|
||||
await login({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
})
|
||||
// 登录成功,跳转到 Dashboard
|
||||
router.push('/dashboard')
|
||||
}, 1500)
|
||||
router.push('/chat')
|
||||
} catch (error: any) {
|
||||
errorMsg.value = error.message || 'Login failed, please check your credentials'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -52,6 +70,12 @@ const handleLogin = () => {
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<div class="bg-dark-700 rounded-2xl p-8 shadow-xl border border-dark-500/50">
|
||||
<!-- 成功提示 -->
|
||||
<div v-if="successMsg" class="mb-4 p-3 bg-green-500/10 border border-green-500/30 rounded-lg text-green-400 text-sm flex items-center gap-2">
|
||||
<i class="fa-solid fa-check-circle"></i>
|
||||
{{ successMsg }}
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="errorMsg" class="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm flex items-center gap-2">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
|
||||
@@ -41,15 +41,13 @@ const tasks = ref([
|
||||
},
|
||||
])
|
||||
|
||||
const activeTab = ref('running') // running, completed, all
|
||||
const filterStatus = ref('all') // running, stopped, all
|
||||
const searchQuery = ref('')
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
let result = tasks.value
|
||||
if (activeTab.value === 'running') {
|
||||
result = result.filter(t => t.status === 'running')
|
||||
} else if (activeTab.value === 'completed') {
|
||||
result = result.filter(t => t.status === 'stopped')
|
||||
if (filterStatus.value !== 'all') {
|
||||
result = result.filter(t => t.status === filterStatus.value)
|
||||
}
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
@@ -63,7 +61,7 @@ const filteredTasks = computed(() => {
|
||||
|
||||
const getTaskCount = (status: string) => {
|
||||
if (status === 'running') return tasks.value.filter(t => t.status === 'running').length
|
||||
if (status === 'completed') return tasks.value.filter(t => t.status === 'stopped').length
|
||||
if (status === 'stopped') return tasks.value.filter(t => t.status === 'stopped').length
|
||||
return tasks.value.length
|
||||
}
|
||||
|
||||
@@ -101,28 +99,11 @@ const getStatusClass = (status: string) => {
|
||||
class="search-input w-full"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="flex gap-6 mb-4">
|
||||
<button
|
||||
:class="['tab-item', { active: activeTab === 'running' }]"
|
||||
@click="activeTab = 'running'"
|
||||
>
|
||||
Running {{ getTaskCount('running') }}
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-item', { active: activeTab === 'completed' }]"
|
||||
@click="activeTab = 'completed'"
|
||||
>
|
||||
Completed {{ getTaskCount('completed') }}
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-item', { active: activeTab === 'all' }]"
|
||||
@click="activeTab = 'all'"
|
||||
>
|
||||
All {{ getTaskCount('all') }}
|
||||
</button>
|
||||
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
|
||||
<el-option label="All Status" value="all" />
|
||||
<el-option label="Running" value="running" />
|
||||
<el-option label="Stopped" value="stopped" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- Task List Table -->
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import * as monaco from 'monaco-editor'
|
||||
|
||||
interface Script {
|
||||
id: number
|
||||
name: string
|
||||
type: string
|
||||
description: string
|
||||
code: string
|
||||
status: 'running' | 'stopped'
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// 模拟脚本数据
|
||||
const scripts = ref<Script[]>([
|
||||
{ id: 1, name: 'Data Processing', type: 'Python', description: 'Process and transform data', status: 'running', createdAt: '2025-04-10' },
|
||||
{ id: 2, name: 'Report Generator', type: 'Python', description: 'Generate weekly reports', status: 'stopped', createdAt: '2025-04-08' },
|
||||
{ id: 3, name: 'Backup Script', type: 'Shell', description: 'Database backup automation', status: 'running', createdAt: '2025-04-05' },
|
||||
{ id: 4, name: 'Data Sync', type: 'Python', description: 'Sync data between systems', status: 'stopped', createdAt: '2025-04-12' },
|
||||
{ id: 1, name: 'Data Processing', type: 'Python', description: 'Process and transform data', code: 'print("hello")', status: 'running', createdAt: '2025-04-10' },
|
||||
{ id: 2, name: 'Report Generator', type: 'Python', description: 'Generate weekly reports', code: '', status: 'stopped', createdAt: '2025-04-08' },
|
||||
{ id: 3, name: 'Backup Script', type: 'Shell', description: 'Database backup automation', code: '', status: 'running', createdAt: '2025-04-05' },
|
||||
{ id: 4, name: 'Data Sync', type: 'Python', description: 'Sync data between systems', code: '', status: 'stopped', createdAt: '2025-04-12' },
|
||||
])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const filterStatus = ref('all')
|
||||
const isCreating = ref(false)
|
||||
const isCreatingCode = ref(false)
|
||||
const isEditing = ref(false)
|
||||
const editingScript = ref<Script | null>(null)
|
||||
|
||||
@@ -28,6 +31,90 @@ const newScriptForm = ref({
|
||||
name: '',
|
||||
type: 'Python',
|
||||
description: '',
|
||||
code: '',
|
||||
})
|
||||
|
||||
// Monaco Editor 实例
|
||||
const codeEditorRef = ref<HTMLElement | null>(null)
|
||||
let codeEditor: monaco.editor.IStandaloneCodeEditor | null = null
|
||||
|
||||
// 定义自定义主题(匹配弹窗背景色)
|
||||
const defineCustomTheme = () => {
|
||||
monaco.editor.defineTheme('custom-dark', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#1f2937', // dark-700
|
||||
'editor.lineHighlightBackground': '#374151',
|
||||
'editorLineNumber.foreground': '#6b7280',
|
||||
'editorLineNumber.activeForeground': '#9ca3af',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Monaco 语言映射
|
||||
const getMonacoLanguage = (type: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'Python': 'python',
|
||||
'Shell': 'shell',
|
||||
'JavaScript': 'javascript',
|
||||
'Go': 'go',
|
||||
}
|
||||
return map[type] || 'plaintext'
|
||||
}
|
||||
|
||||
// 初始化代码编辑器
|
||||
const initCodeEditor = () => {
|
||||
if (!codeEditorRef.value) return
|
||||
|
||||
// 如果已存在,先销毁
|
||||
if (codeEditor) {
|
||||
codeEditor.dispose()
|
||||
}
|
||||
|
||||
// 定义自定义主题
|
||||
defineCustomTheme()
|
||||
|
||||
codeEditor = monaco.editor.create(codeEditorRef.value, {
|
||||
value: newScriptForm.value.code || '',
|
||||
language: getMonacoLanguage(newScriptForm.value.type),
|
||||
theme: 'custom-dark',
|
||||
minimap: { enabled: false },
|
||||
lineNumbers: 'on',
|
||||
fontSize: 14,
|
||||
automaticLayout: true,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: 'on',
|
||||
roundedSelection: false,
|
||||
padding: { top: 16, bottom: 16 },
|
||||
})
|
||||
|
||||
// 监听内容变化
|
||||
codeEditor.onDidChangeModelContent(() => {
|
||||
newScriptForm.value.code = codeEditor?.getValue() || ''
|
||||
})
|
||||
}
|
||||
|
||||
// 监听脚本类型变化,更新语言
|
||||
watch(() => newScriptForm.value.type, (newType) => {
|
||||
if (codeEditor) {
|
||||
monaco.editor.setModelLanguage(codeEditor.getModel()!, getMonacoLanguage(newType))
|
||||
}
|
||||
})
|
||||
|
||||
// 监听弹窗打开
|
||||
watch(isCreatingCode, (val) => {
|
||||
if (val) {
|
||||
nextTick(() => {
|
||||
initCodeEditor()
|
||||
})
|
||||
} else {
|
||||
if (codeEditor) {
|
||||
codeEditor.dispose()
|
||||
codeEditor = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 过滤后的脚本
|
||||
@@ -86,6 +173,7 @@ const openCreate = () => {
|
||||
name: '',
|
||||
type: 'Python',
|
||||
description: '',
|
||||
code: '',
|
||||
}
|
||||
isCreating.value = true
|
||||
}
|
||||
@@ -94,6 +182,19 @@ const closeCreate = () => {
|
||||
isCreating.value = false
|
||||
}
|
||||
|
||||
// 跳转到代码编辑
|
||||
const goToCodeEditor = () => {
|
||||
if (!newScriptForm.value.name || !newScriptForm.value.type) {
|
||||
return
|
||||
}
|
||||
isCreating.value = false
|
||||
isCreatingCode.value = true
|
||||
}
|
||||
|
||||
const closeCreateCode = () => {
|
||||
isCreatingCode.value = false
|
||||
}
|
||||
|
||||
const saveNewScript = () => {
|
||||
const newId = Math.max(...scripts.value.map(s => s.id), 0) + 1
|
||||
scripts.value.push({
|
||||
@@ -101,10 +202,11 @@ const saveNewScript = () => {
|
||||
name: newScriptForm.value.name || 'Untitled Script',
|
||||
type: newScriptForm.value.type,
|
||||
description: newScriptForm.value.description,
|
||||
code: newScriptForm.value.code,
|
||||
status: 'stopped',
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
})
|
||||
isCreating.value = false
|
||||
isCreatingCode.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -254,6 +356,43 @@ const saveNewScript = () => {
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="goToCodeEditor"
|
||||
:disabled="!newScriptForm.name || !newScriptForm.type"
|
||||
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 代码编辑弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="isCreatingCode" class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-8">
|
||||
<div class="bg-dark-700 rounded-2xl w-full max-w-6xl h-[90vh] border border-dark-500 shadow-2xl flex flex-col">
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Edit Code - {{ newScriptForm.name }}</h3>
|
||||
<p class="text-sm text-gray-400">{{ newScriptForm.type }}</p>
|
||||
</div>
|
||||
<button @click="closeCreateCode" class="text-gray-400 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div ref="codeEditorRef" class="w-full h-full"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button
|
||||
@click="closeCreateCode"
|
||||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
@click="saveNewScript"
|
||||
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all"
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted } from 'vue'
|
||||
import { useModelSettings } from './settings/useModelSettings'
|
||||
import FormDialog from '@/components/FormDialog.vue'
|
||||
|
||||
// 当前选中的设置菜单
|
||||
const activeMenu = ref('general')
|
||||
|
||||
// 导入 Model Settings 逻辑
|
||||
const {
|
||||
models,
|
||||
@@ -32,248 +28,26 @@ const {
|
||||
testConnectionEdit,
|
||||
} = useModelSettings()
|
||||
|
||||
// 监听菜单切换,获取模型列表
|
||||
watch(activeMenu, (newVal) => {
|
||||
if (newVal === 'modelSettings') {
|
||||
// 页面加载时获取模型列表
|
||||
onMounted(() => {
|
||||
fetchModels()
|
||||
}
|
||||
})
|
||||
|
||||
// 设置菜单列表
|
||||
const menuItems = [
|
||||
{ key: 'general', label: 'General', icon: 'fa-gear' },
|
||||
{ key: 'members', label: 'Members', icon: 'fa-users' },
|
||||
{ key: 'notifications', label: 'Notifications', icon: 'fa-bell' },
|
||||
{ key: 'modelSettings', label: 'Model Settings', icon: 'fa-brain' },
|
||||
{ key: 'logs', label: 'Logs', icon: 'fa-file-lines' },
|
||||
]
|
||||
|
||||
// General 设置表单
|
||||
const generalForm = ref({
|
||||
name: 'Alex Smith',
|
||||
email: 'alex@gmail.com',
|
||||
password: '********',
|
||||
language: 'English',
|
||||
timezone: 'UTC +08:00 Beijing',
|
||||
})
|
||||
|
||||
// 语言选项
|
||||
const languageOptions = [
|
||||
{ value: 'English', label: 'English' },
|
||||
{ value: 'Chinese', label: '中文' },
|
||||
{ value: 'Japanese', label: '日本語' },
|
||||
]
|
||||
|
||||
// 时区选项
|
||||
const timezoneOptions = [
|
||||
{ value: 'UTC +08:00 Beijing', label: 'UTC +08:00 Beijing' },
|
||||
{ value: 'UTC +00:00 London', label: 'UTC +00:00 London' },
|
||||
{ value: 'UTC -05:00 New York', label: 'UTC -05:00 New York' },
|
||||
{ value: 'UTC -08:00 Los Angeles', label: 'UTC -08:00 Los Angeles' },
|
||||
]
|
||||
|
||||
// 保存设置
|
||||
const saveChanges = () => {
|
||||
ElMessage.success('Settings saved successfully')
|
||||
}
|
||||
|
||||
// 显示密码修改弹窗
|
||||
const showChangePassword = () => {
|
||||
ElMessage.info('Password change dialog would open here')
|
||||
}
|
||||
|
||||
// ========== Logs 功能 ==========
|
||||
interface Log {
|
||||
id: number
|
||||
level: 'info' | 'warning' | 'error' | 'debug'
|
||||
source: string
|
||||
message: string
|
||||
timestamp: string
|
||||
user?: string
|
||||
}
|
||||
|
||||
const logs = ref<Log[]>([
|
||||
{ id: 1, level: 'info', source: 'System', message: 'User logged in successfully', timestamp: '2025-03-10 14:35:22', user: 'alex@example.com' },
|
||||
{ id: 2, level: 'warning', source: 'API', message: 'Rate limit approaching for API key', timestamp: '2025-03-10 14:32:15', user: 'john@example.com' },
|
||||
{ id: 3, level: 'error', source: 'Database', message: 'Connection timeout to primary database', timestamp: '2025-03-10 14:30:45' },
|
||||
{ id: 4, level: 'info', source: 'Skill', message: 'MCP Server started successfully', timestamp: '2025-03-10 14:28:10' },
|
||||
{ id: 5, level: 'debug', source: 'Auth', message: 'Token refresh initiated', timestamp: '2025-03-10 14:25:33', user: 'jane@example.com' },
|
||||
{ id: 6, level: 'error', source: 'Script', message: 'Failed to execute backup script', timestamp: '2025-03-10 14:20:18' },
|
||||
{ id: 7, level: 'info', source: 'Account', message: 'User role updated', timestamp: '2025-03-10 14:15:42', user: 'admin@example.com' },
|
||||
{ id: 8, level: 'warning', source: 'Memory', message: 'Memory usage exceeds 80% threshold', timestamp: '2025-03-10 14:10:55' },
|
||||
{ id: 9, level: 'info', source: 'Knowledge', message: 'Document indexed successfully', timestamp: '2025-03-10 14:05:30' },
|
||||
{ id: 10, level: 'error', source: 'API', message: 'Invalid API key provided', timestamp: '2025-03-10 14:00:12' },
|
||||
])
|
||||
|
||||
const logSearchQuery = ref('')
|
||||
const logFilterLevel = ref('')
|
||||
const logFilterSource = ref('')
|
||||
|
||||
const logLevelOptions = [
|
||||
{ value: '', label: 'All Levels' },
|
||||
{ value: 'info', label: 'Info' },
|
||||
{ value: 'warning', label: 'Warning' },
|
||||
{ value: 'error', label: 'Error' },
|
||||
{ value: 'debug', label: 'Debug' },
|
||||
]
|
||||
|
||||
const logSourceOptions = computed(() => {
|
||||
const sources = [...new Set(logs.value.map(l => l.source))]
|
||||
return [{ value: '', label: 'All Sources' }, ...sources.map(s => ({ value: s, label: s }))]
|
||||
})
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
return logs.value.filter(log => {
|
||||
const matchSearch = logSearchQuery.value === '' ||
|
||||
log.message.toLowerCase().includes(logSearchQuery.value.toLowerCase()) ||
|
||||
log.source.toLowerCase().includes(logSearchQuery.value.toLowerCase())
|
||||
const matchLevel = logFilterLevel.value === '' || log.level === logFilterLevel.value
|
||||
const matchSource = logFilterSource.value === '' || log.source === logFilterSource.value
|
||||
return matchSearch && matchLevel && matchSource
|
||||
})
|
||||
})
|
||||
|
||||
const logLevelClass = (level: string) => {
|
||||
switch (level) {
|
||||
case 'info': return 'bg-blue-500/20 text-blue-400'
|
||||
case 'warning': return 'bg-yellow-500/20 text-yellow-400'
|
||||
case 'error': return 'bg-red-500/20 text-red-400'
|
||||
case 'debug': return 'bg-gray-500/20 text-gray-400'
|
||||
default: return 'bg-gray-500/20 text-gray-400'
|
||||
}
|
||||
}
|
||||
|
||||
const selectedLog = ref<Log | null>(null)
|
||||
const showLogDetail = ref(false)
|
||||
|
||||
const viewLogDetail = (log: Log) => {
|
||||
selectedLog.value = log
|
||||
showLogDetail.value = true
|
||||
}
|
||||
|
||||
const closeLogDetail = () => {
|
||||
showLogDetail.value = false
|
||||
selectedLog.value = null
|
||||
}
|
||||
|
||||
const clearLogs = () => {
|
||||
logs.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 min-h-screen">
|
||||
<!-- 页面标题 -->
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<i class="fa-solid fa-gear text-orange-500"></i>
|
||||
<span class="font-medium">Settings</span>
|
||||
<i class="fa-solid fa-brain text-orange-500"></i>
|
||||
<span class="font-medium">Models</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-6">
|
||||
<!-- 左侧菜单 -->
|
||||
<nav class="w-48 flex-shrink-0">
|
||||
<ul class="space-y-1">
|
||||
<li
|
||||
v-for="item in menuItems"
|
||||
:key="item.key"
|
||||
@click="activeMenu = item.key"
|
||||
class="px-4 py-3 rounded-lg cursor-pointer transition-colors flex items-center gap-3"
|
||||
:class="activeMenu === item.key
|
||||
? 'bg-orange-500/10 text-orange-400'
|
||||
: 'text-gray-400 hover:bg-dark-600 hover:text-white'"
|
||||
>
|
||||
<i :class="['fa-solid', item.icon]"></i>
|
||||
<span>{{ item.label }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="flex-1 space-y-4">
|
||||
<!-- General 设置 -->
|
||||
<div v-if="activeMenu === 'general'">
|
||||
<!-- 内容区域 -->
|
||||
<div class="space-y-4">
|
||||
<!-- Models 内容 -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">General Settings</h2>
|
||||
<p class="text-sm text-gray-400 mt-1">Manage your personal information and preferences</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-dark-700 rounded-xl p-6">
|
||||
<el-form :model="generalForm" label-position="top">
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<el-form-item label="Name">
|
||||
<el-input v-model="generalForm.name" placeholder="Enter your name" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Email">
|
||||
<el-input v-model="generalForm.email" placeholder="Enter your email" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Password">
|
||||
<div class="flex gap-3">
|
||||
<el-input v-model="generalForm.password" type="password" disabled class="flex-1" />
|
||||
<el-button @click="showChangePassword">Change</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Language">
|
||||
<el-select v-model="generalForm.language" placeholder="Select language" class="w-full">
|
||||
<el-option
|
||||
v-for="lang in languageOptions"
|
||||
:key="lang.value"
|
||||
:label="lang.label"
|
||||
:value="lang.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Timezone">
|
||||
<el-select v-model="generalForm.timezone" placeholder="Select timezone" class="w-full">
|
||||
<el-option
|
||||
v-for="tz in timezoneOptions"
|
||||
:key="tz.value"
|
||||
:label="tz.label"
|
||||
:value="tz.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<el-button type="primary" @click="saveChanges">Save Changes</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Members 设置 -->
|
||||
<div v-if="activeMenu === 'members'">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Members</h2>
|
||||
<p class="text-sm text-gray-400 mt-1">Manage team members</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notifications 设置 -->
|
||||
<div v-if="activeMenu === 'notifications'">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Notifications</h2>
|
||||
<p class="text-sm text-gray-400 mt-1">Configure notification preferences</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Settings 设置 -->
|
||||
<div v-if="activeMenu === 'modelSettings'">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Model Settings</h2>
|
||||
<p class="text-sm text-gray-400 mt-1">Configure AI model settings</p>
|
||||
<p class="text-sm text-gray-400 mt-1">Configure AI models</p>
|
||||
</div>
|
||||
<button class="bg-gradient-to-r from-primary-orange to-red-500 hover:from-orange-500 hover:to-red-600 text-white px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all" @click="showAddModelForm = true">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
@@ -533,142 +307,5 @@ const clearLogs = () => {
|
||||
</template>
|
||||
</FormDialog>
|
||||
</div>
|
||||
|
||||
<!-- Logs 设置 -->
|
||||
<div v-if="activeMenu === 'logs'">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Logs</h2>
|
||||
<p class="text-sm text-gray-400 mt-1">View system logs</p>
|
||||
</div>
|
||||
<button @click="clearLogs" class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors flex items-center gap-2">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
Clear Logs
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="flex gap-4 mb-6">
|
||||
<div class="flex-1 relative">
|
||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
<input
|
||||
v-model="logSearchQuery"
|
||||
type="text"
|
||||
placeholder="Search logs..."
|
||||
class="w-full bg-dark-600 border border-dark-500 rounded-lg py-2 pl-10 pr-4 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
||||
>
|
||||
</div>
|
||||
<el-select v-model="logFilterLevel" placeholder="All Levels" class="w-40" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option v-for="opt in logLevelOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<el-select v-model="logFilterSource" placeholder="All Sources" class="w-40" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option v-for="opt in logSourceOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-dark-600">
|
||||
<tr>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Level</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Source</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Message</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">User</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Timestamp</th>
|
||||
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in filteredLogs" :key="log.id" class="border-t border-dark-600 hover:bg-dark-600/50">
|
||||
<td class="px-5 py-4">
|
||||
<span :class="['px-2 py-1 rounded text-xs font-medium', logLevelClass(log.level)]">
|
||||
{{ log.level.toUpperCase() }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-gray-300">{{ log.source }}</td>
|
||||
<td class="px-5 py-4 text-gray-300 max-w-md">
|
||||
<div class="truncate">{{ log.message }}</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-gray-400">{{ log.user || '-' }}</td>
|
||||
<td class="px-5 py-4 text-gray-400 text-sm">{{ log.timestamp }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-end">
|
||||
<button
|
||||
@click="viewLogDetail(log)"
|
||||
class="p-2 rounded-lg hover:bg-dark-500 transition-colors"
|
||||
title="View Details"
|
||||
>
|
||||
<i class="fa-solid fa-eye text-gray-400 hover:text-white"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredLogs.length === 0" class="py-12 text-center text-gray-500">
|
||||
<i class="fa-solid fa-file-lines text-4xl mb-3"></i>
|
||||
<p>No logs found</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志详情弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showLogDetail" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click="closeLogDetail">
|
||||
<div class="bg-dark-700 rounded-2xl w-full max-w-2xl border border-dark-500 shadow-2xl" @click.stop>
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">Log Details</h3>
|
||||
<button @click="closeLogDetail" class="text-gray-400 hover:text-white transition-colors">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedLog" class="p-5 space-y-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<span :class="['px-3 py-1 rounded text-sm font-medium', logLevelClass(selectedLog.level)]">
|
||||
{{ selectedLog.level.toUpperCase() }}
|
||||
</span>
|
||||
<span class="text-gray-400">{{ selectedLog.timestamp }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Source</label>
|
||||
<div class="text-white">{{ selectedLog.source }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">User</label>
|
||||
<div class="text-white">{{ selectedLog.user || 'System' }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Message</label>
|
||||
<div class="bg-dark-800 rounded-lg p-4 text-gray-300 font-mono text-sm whitespace-pre-wrap">
|
||||
{{ selectedLog.message }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Log ID</label>
|
||||
<div class="text-gray-500">#{{ selectedLog.id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
|
||||
<button
|
||||
@click="closeLogDetail"
|
||||
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
const router = useRouter()
|
||||
const { register } = useAuth()
|
||||
|
||||
// 表单数据
|
||||
const username = ref('')
|
||||
@@ -14,6 +16,7 @@ const isLoading = ref(false)
|
||||
const showPassword = ref(false)
|
||||
const showConfirmPassword = ref(false)
|
||||
const errorMsg = ref('')
|
||||
const successMsg = ref('')
|
||||
|
||||
// 表单验证
|
||||
const validateForm = () => {
|
||||
@@ -24,11 +27,6 @@ const validateForm = () => {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!email.value || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)) {
|
||||
errorMsg.value = 'Please enter a valid email address'
|
||||
return false
|
||||
}
|
||||
|
||||
if (!password.value || password.value.length < 6) {
|
||||
errorMsg.value = 'Password must be at least 6 characters'
|
||||
return false
|
||||
@@ -48,21 +46,34 @@ const validateForm = () => {
|
||||
}
|
||||
|
||||
// 注册处理
|
||||
const handleSignup = () => {
|
||||
const handleSignup = async () => {
|
||||
if (!validateForm()) return
|
||||
|
||||
isLoading.value = true
|
||||
// 模拟注册
|
||||
errorMsg.value = ''
|
||||
successMsg.value = ''
|
||||
|
||||
try {
|
||||
await register({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
email: email.value || undefined,
|
||||
})
|
||||
// 注册成功
|
||||
successMsg.value = 'Account created successfully! Redirecting to login...'
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
// 注册成功,跳转到登录页
|
||||
router.push('/')
|
||||
router.push('/?registered=true')
|
||||
}, 1500)
|
||||
} catch (error: any) {
|
||||
errorMsg.value = error.message || 'Registration failed, please try again'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到登录
|
||||
const goToLogin = () => {
|
||||
router.push('/')
|
||||
router.push('/?registered=true')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -87,6 +98,12 @@ const goToLogin = () => {
|
||||
|
||||
<!-- 注册表单 -->
|
||||
<div class="bg-dark-700 rounded-2xl p-8 shadow-xl border border-dark-500/50">
|
||||
<!-- 成功提示 -->
|
||||
<div v-if="successMsg" class="mb-4 p-3 bg-green-500/10 border border-green-500/30 rounded-lg text-green-400 text-sm flex items-center gap-2">
|
||||
<i class="fa-solid fa-check-circle"></i>
|
||||
{{ successMsg }}
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="errorMsg" class="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm flex items-center gap-2">
|
||||
<i class="fa-solid fa-circle-exclamation"></i>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useSkills } from './skill/useSkills'
|
||||
import { Play, Pause, Edit, Trash2 } from 'lucide-vue-next'
|
||||
import '@/views/database/database.css'
|
||||
|
||||
const {
|
||||
@@ -98,13 +99,13 @@ const {
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<button @click="toggleStatus(skill)" class="btn-icon" :title="skill.status === 'running' ? 'Stop' : 'Start'">
|
||||
<i :class="['fa-solid', skill.status === 'running' ? 'fa-stop' : 'fa-play', 'text-gray-400 hover:text-white']"></i>
|
||||
<component :is="skill.status === 'running' ? Pause : Play" class="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
</button>
|
||||
<button @click="openEdit(skill)" class="btn-icon" title="Edit">
|
||||
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
|
||||
<Edit class="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
</button>
|
||||
<button @click="deleteSkill(skill.id)" class="btn-icon" title="Delete">
|
||||
<i class="fa-solid fa-trash text-gray-400 hover:text-red-400"></i>
|
||||
<Trash2 class="w-4 h-4 text-gray-400 hover:text-red-400" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -192,41 +193,26 @@ const {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-5 space-y-4">
|
||||
<div class="p-5 space-y-4 max-h-[60vh] overflow-y-auto">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Skill Name</label>
|
||||
<input v-model="newSkillForm.name" type="text" placeholder="Enter skill name..." class="input-field">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Type</label>
|
||||
<el-select v-model="newSkillForm.type" placeholder="Select" class="w-full" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option v-for="type in types" :key="type" :label="type" :value="type" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Category</label>
|
||||
<el-select v-model="newSkillForm.category" placeholder="Select" class="w-full" size="large" popper-class="dark-select-dropdown">
|
||||
<el-option v-for="cat in categories" :key="cat.value" :label="cat.label" :value="cat.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Port</label>
|
||||
<input v-model="newSkillForm.port" type="number" placeholder="3000" class="input-field">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||
<textarea v-model="newSkillForm.description" rows="2" placeholder="Describe this skill..." class="input-field resize-none"></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-2">Markdown Content</label>
|
||||
<textarea v-model="newSkillForm.markdown" rows="10" placeholder="# Skill Documentation Describe your skill here using Markdown..." class="input-field resize-none font-mono text-sm"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-600 bg-dark-700/50">
|
||||
<button @click="closeCreate" class="btn-secondary">Cancel</button>
|
||||
<button @click="saveNewSkill" class="btn-primary">Create Skill</button>
|
||||
<button @click="saveNewSkill" :disabled="!newSkillForm.name || !newSkillForm.description || !newSkillForm.markdown" class="btn-primary disabled:opacity-50 disabled:cursor-not-allowed">Create Skill</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,42 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
FileText,
|
||||
Globe,
|
||||
Calculator,
|
||||
Code,
|
||||
Braces,
|
||||
Github,
|
||||
MessageSquare,
|
||||
Mail,
|
||||
Database,
|
||||
Folder,
|
||||
GitBranch,
|
||||
Box,
|
||||
Wrench,
|
||||
Server,
|
||||
Terminal,
|
||||
Search,
|
||||
Plus,
|
||||
Pause,
|
||||
Play,
|
||||
Edit,
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-vue-next'
|
||||
import { useTools } from './tools/useTools'
|
||||
import '@/views/database/database.css'
|
||||
|
||||
interface Tool {
|
||||
id: number
|
||||
name: string
|
||||
type: 'built-in' | 'custom' | 'mcp'
|
||||
description: string
|
||||
status: 'active' | 'inactive'
|
||||
icon?: string
|
||||
provider?: string
|
||||
config?: object
|
||||
createdAt: string
|
||||
// 使用工具 composable
|
||||
const { tools, toolsLoading, fetchTools, syncTools, deleteTool: deleteToolApi } = useTools()
|
||||
|
||||
// 图标组件映射
|
||||
const iconComponents: Record<string, any> = {
|
||||
FileText,
|
||||
Globe,
|
||||
Calculator,
|
||||
Code,
|
||||
Braces,
|
||||
Github,
|
||||
MessageSquare,
|
||||
Mail,
|
||||
Database,
|
||||
Folder,
|
||||
GitBranch,
|
||||
Box,
|
||||
Wrench,
|
||||
Server,
|
||||
Terminal,
|
||||
}
|
||||
|
||||
// Mock data for tools
|
||||
const builtInTools = ref<Tool[]>([
|
||||
{ id: 1, name: 'File Reader', type: 'built-in', description: 'Read files from the filesystem', status: 'active', icon: 'fa-file-lines', createdAt: '2025-01-15' },
|
||||
{ id: 2, name: 'Web Search', type: 'built-in', description: 'Search the web for information', status: 'active', icon: 'fa-globe', createdAt: '2025-01-15' },
|
||||
{ id: 3, name: 'Calculator', type: 'built-in', description: 'Perform mathematical calculations', status: 'active', icon: 'fa-calculator', createdAt: '2025-01-15' },
|
||||
{ id: 4, name: 'Code Executor', type: 'built-in', description: 'Execute code in a sandbox', status: 'active', icon: 'fa-code', createdAt: '2025-01-15' },
|
||||
{ id: 5, name: 'JSON Parser', type: 'built-in', description: 'Parse and validate JSON data', status: 'active', icon: 'fa-brackets-curly', createdAt: '2025-01-15' },
|
||||
])
|
||||
const getIconComponent = (iconName?: string) => {
|
||||
if (!iconName) return Box
|
||||
return iconComponents[iconName] || Box
|
||||
}
|
||||
|
||||
const customTools = ref<Tool[]>([
|
||||
{ id: 101, name: 'GitHub API', type: 'custom', description: 'Interact with GitHub REST API', status: 'active', icon: 'fa-github', provider: 'Custom', createdAt: '2025-03-01' },
|
||||
{ id: 102, name: 'Slack Notifier', type: 'custom', description: 'Send notifications to Slack', status: 'inactive', icon: 'fa-slack', provider: 'Custom', createdAt: '2025-03-05' },
|
||||
{ id: 103, name: 'Email Sender', type: 'custom', description: 'Send emails via SMTP', status: 'active', icon: 'fa-envelope', provider: 'Custom', createdAt: '2025-03-08' },
|
||||
])
|
||||
interface Tool {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
provider: string
|
||||
security_level: string
|
||||
require_approval: boolean
|
||||
parameters: string
|
||||
status: string
|
||||
type: 'built-in' | 'mcp'
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
const mcpTools = ref<Tool[]>([
|
||||
{ id: 201, name: 'Puppeteer', type: 'mcp', description: 'Browser automation via Puppeteer', status: 'active', icon: 'fa-browser', provider: 'MCP Server', createdAt: '2025-02-10' },
|
||||
{ id: 202, name: 'SQL Database', type: 'mcp', description: 'Execute SQL queries on databases', status: 'active', icon: 'fa-database', provider: 'MCP Server', createdAt: '2025-02-12' },
|
||||
{ id: 203, name: 'Filesystem', type: 'mcp', description: 'File operations via MCP', status: 'active', icon: 'fa-folder', provider: 'MCP Server', createdAt: '2025-02-15' },
|
||||
{ id: 204, name: 'Git Operations', type: 'mcp', description: 'Git repository operations', status: 'inactive', icon: 'fa-git-alt', provider: 'MCP Server', createdAt: '2025-02-18' },
|
||||
])
|
||||
// 页面加载时获取工具列表
|
||||
onMounted(async () => {
|
||||
await fetchTools()
|
||||
})
|
||||
|
||||
const activeTab = ref<'built-in' | 'custom' | 'mcp'>('built-in')
|
||||
// 按类型分类工具
|
||||
const builtInTools = computed(() => {
|
||||
return tools.value.filter(t => t.provider === 'system')
|
||||
})
|
||||
|
||||
const mcpTools = computed(() => {
|
||||
return tools.value.filter(t => t.provider !== 'system')
|
||||
})
|
||||
|
||||
const activeTab = ref<'built-in' | 'mcp'>('built-in')
|
||||
const searchQuery = ref('')
|
||||
const filterStatus = ref('all')
|
||||
const editingTool = ref<Tool | null>(null)
|
||||
@@ -50,21 +96,19 @@ const editForm = ref({
|
||||
|
||||
// Statistics
|
||||
const stats = computed(() => ({
|
||||
total: builtInTools.value.length + customTools.value.length + mcpTools.value.length,
|
||||
active: [...builtInTools.value, ...customTools.value, ...mcpTools.value].filter(t => t.status === 'active').length,
|
||||
total: tools.value.length,
|
||||
active: tools.value.filter(t => t.status === 'active').length,
|
||||
builtIn: builtInTools.value.length,
|
||||
custom: customTools.value.length,
|
||||
mcp: mcpTools.value.length
|
||||
}))
|
||||
|
||||
const currentTools = computed(() => {
|
||||
let tools: Tool[] = []
|
||||
let toolsList: Tool[] = []
|
||||
switch (activeTab.value) {
|
||||
case 'built-in': tools = builtInTools.value; break
|
||||
case 'custom': tools = customTools.value; break
|
||||
case 'mcp': tools = mcpTools.value; break
|
||||
case 'built-in': toolsList = builtInTools.value as Tool[]; break
|
||||
case 'mcp': toolsList = mcpTools.value as Tool[]; break
|
||||
}
|
||||
return tools.filter(tool => {
|
||||
return toolsList.filter(tool => {
|
||||
const matchSearch = tool.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
tool.description.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
const matchStatus = filterStatus.value === 'all' || tool.status === filterStatus.value
|
||||
@@ -74,64 +118,64 @@ const currentTools = computed(() => {
|
||||
|
||||
const tabCounts = computed(() => ({
|
||||
'built-in': builtInTools.value.length,
|
||||
'custom': customTools.value.length,
|
||||
'mcp': mcpTools.value.length,
|
||||
}))
|
||||
|
||||
const openEdit = (tool: Tool) => {
|
||||
const openEdit = (tool: any) => {
|
||||
editingTool.value = tool
|
||||
editForm.value = { name: tool.name, description: tool.description, provider: tool.provider || '' }
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
const saveEdit = () => {
|
||||
if (editingTool.value) {
|
||||
const targetArray = editingTool.value.type === 'built-in' ? builtInTools.value :
|
||||
editingTool.value.type === 'custom' ? customTools.value : mcpTools.value
|
||||
const index = targetArray.findIndex(t => t.id === editingTool.value!.id)
|
||||
if (index !== -1) targetArray[index] = { ...targetArray[index], ...editForm.value }
|
||||
}
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const cancelEdit = () => { isEditing.value = false; editingTool.value = null }
|
||||
|
||||
const toggleStatus = (tool: Tool) => {
|
||||
const targetArray = tool.type === 'built-in' ? builtInTools.value :
|
||||
tool.type === 'custom' ? customTools.value : mcpTools.value
|
||||
const found = targetArray.find(t => t.id === tool.id)
|
||||
if (found) found.status = found.status === 'active' ? 'inactive' : 'active'
|
||||
const toggleStatus = async (tool: any) => {
|
||||
const newStatus = tool.status === 'active' ? 'inactive' : 'active'
|
||||
// TODO: 调用 API 更新状态
|
||||
}
|
||||
|
||||
const deleteTool = (id: number) => {
|
||||
switch (activeTab.value) {
|
||||
case 'built-in': builtInTools.value = builtInTools.value.filter(t => t.id !== id); break
|
||||
case 'custom': customTools.value = customTools.value.filter(t => t.id !== id); break
|
||||
case 'mcp': mcpTools.value = mcpTools.value.filter(t => t.id !== id); break
|
||||
const handleDeleteTool = async (id: string) => {
|
||||
if (confirm('Are you sure you want to delete this tool?')) {
|
||||
await deleteToolApi(id)
|
||||
}
|
||||
}
|
||||
|
||||
// 同步工具
|
||||
const handleSyncTools = async () => {
|
||||
await syncTools()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 min-h-screen">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex justify-between items-center mb-6 h-10">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-tools text-orange-500"></i>
|
||||
<Wrench class="w-5 h-5 text-orange-500" />
|
||||
<span class="font-medium">Tools</span>
|
||||
</div>
|
||||
<button class="btn-primary">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
New Tool
|
||||
<div class="h-10 flex gap-2">
|
||||
<button v-if="activeTab === 'built-in'" @click="handleSyncTools" class="btn-secondary">
|
||||
<i class="fa-solid fa-sync mr-1"></i>
|
||||
Sync Tools
|
||||
</button>
|
||||
<button v-if="activeTab === 'mcp'" class="btn-primary">
|
||||
<Plus class="w-4 h-4 mr-1" />
|
||||
Add MCP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 导航 -->
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<button
|
||||
v-for="(label, tab) in { 'built-in': 'Built-in', 'custom': 'Custom', 'mcp': 'MCP Servers' }"
|
||||
v-for="(label, tab) in { 'built-in': 'Built-in', 'mcp': 'MCP Servers' }"
|
||||
:key="tab"
|
||||
@click="activeTab = tab as 'built-in' | 'custom' | 'mcp'"
|
||||
@click="activeTab = tab as 'built-in' | 'mcp'"
|
||||
class="px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all"
|
||||
:class="activeTab === tab
|
||||
? 'bg-orange-500 text-white'
|
||||
@@ -147,12 +191,12 @@ const deleteTool = (id: number) => {
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="flex gap-4 mb-6">
|
||||
<div class="flex-1 relative">
|
||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search tools by name or description..."
|
||||
class="search-input w-full"
|
||||
class="search-input w-full pl-10"
|
||||
>
|
||||
</div>
|
||||
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
|
||||
@@ -164,7 +208,11 @@ const deleteTool = (id: number) => {
|
||||
|
||||
<!-- Tools 列表 -->
|
||||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||||
<table v-if="currentTools.length > 0" class="w-full">
|
||||
<!-- Loading -->
|
||||
<div v-if="toolsLoading" class="py-12 text-center text-gray-500">
|
||||
<i class="fa-solid fa-spinner fa-spin text-2xl"></i>
|
||||
</div>
|
||||
<table v-else-if="currentTools.length > 0" class="w-full">
|
||||
<thead class="bg-dark-600">
|
||||
<tr>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Tool Name</th>
|
||||
@@ -178,8 +226,8 @@ const deleteTool = (id: number) => {
|
||||
<tr v-for="tool in currentTools" :key="tool.id" class="table-row">
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg flex items-center justify-center" :class="tool.type === 'built-in' ? 'bg-orange-500/20' : tool.type === 'custom' ? 'bg-blue-500/20' : 'bg-emerald-500/20'">
|
||||
<i :class="['fa-solid', tool.icon || 'fa-cube', 'text-lg', tool.type === 'built-in' ? 'text-orange-400' : tool.type === 'custom' ? 'text-blue-400' : 'text-emerald-400']"></i>
|
||||
<div class="w-10 h-10 rounded-lg flex items-center justify-center" :class="tool.type === 'built-in' ? 'bg-orange-500/20' : 'bg-emerald-500/20'">
|
||||
<component :is="getIconComponent(tool.icon)" class="w-5 h-5" :class="tool.type === 'built-in' ? 'text-orange-400' : 'text-emerald-400'" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium">{{ tool.name }}</div>
|
||||
@@ -188,7 +236,7 @@ const deleteTool = (id: number) => {
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-center">
|
||||
<span class="text-gray-400 text-sm">{{ tool.provider || (tool.type === 'built-in' ? 'System' : 'Custom') }}</span>
|
||||
<span class="text-gray-400 text-sm">{{ tool.provider || (tool.type === 'built-in' ? 'System' : 'MCP Server') }}</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-center">
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs" :class="tool.status === 'active' ? 'bg-green-500/20 text-green-400' : 'bg-gray-500/20 text-gray-400'">
|
||||
@@ -196,17 +244,17 @@ const deleteTool = (id: number) => {
|
||||
<span class="capitalize">{{ tool.status }}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ tool.createdAt }}</td>
|
||||
<td class="px-5 py-4 text-center text-gray-400 text-sm">{{ tool.created_at || '-' }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<button @click="toggleStatus(tool)" class="btn-icon" :title="tool.status === 'active' ? 'Deactivate' : 'Activate'">
|
||||
<i :class="['fa-solid', tool.status === 'active' ? 'fa-pause' : 'fa-play', 'text-gray-400 hover:text-white']"></i>
|
||||
<component :is="tool.status === 'active' ? Pause : Play" class="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
</button>
|
||||
<button @click="openEdit(tool)" class="btn-icon" title="Edit">
|
||||
<i class="fa-solid fa-pen text-gray-400 hover:text-white"></i>
|
||||
<Edit class="w-4 h-4 text-gray-400 hover:text-white" />
|
||||
</button>
|
||||
<button @click="deleteTool(tool.id)" class="btn-icon" title="Delete">
|
||||
<i class="fa-solid fa-trash text-gray-400 hover:text-red-400"></i>
|
||||
<button @click="handleDeleteTool(tool.id)" class="btn-icon" title="Delete">
|
||||
<Trash2 class="w-4 h-4 text-gray-400 hover:text-red-400" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -217,10 +265,10 @@ const deleteTool = (id: number) => {
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="empty-box">
|
||||
<div class="empty-icon">
|
||||
<i class="fa-solid fa-tools"></i>
|
||||
<Wrench class="w-12 h-12" />
|
||||
</div>
|
||||
<p class="empty-text">No tools found</p>
|
||||
<p class="empty-tip">Click "New Tool" to add a tool</p>
|
||||
<p class="empty-tip">Click "Add MCP" to add a new MCP server</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -231,7 +279,7 @@ const deleteTool = (id: number) => {
|
||||
<div class="flex items-center justify-between p-5 border-b border-dark-500">
|
||||
<h3 class="text-lg font-semibold">Edit Tool</h3>
|
||||
<button @click="cancelEdit" class="btn-icon">
|
||||
<i class="fa-solid fa-xmark text-xl"></i>
|
||||
<X class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -46,10 +46,8 @@ export function useSkills() {
|
||||
|
||||
const newSkillForm = ref({
|
||||
name: '',
|
||||
type: 'API',
|
||||
category: 'api',
|
||||
port: 3000,
|
||||
description: '',
|
||||
markdown: '',
|
||||
})
|
||||
|
||||
// 分类选项
|
||||
@@ -85,7 +83,7 @@ export function useSkills() {
|
||||
|
||||
// 打开创建弹窗
|
||||
const openCreate = () => {
|
||||
newSkillForm.value = { name: '', type: 'API', category: 'api', port: 3000, description: '' }
|
||||
newSkillForm.value = { name: '', description: '', markdown: '' }
|
||||
isCreating.value = true
|
||||
}
|
||||
|
||||
@@ -101,10 +99,10 @@ export function useSkills() {
|
||||
id: newId,
|
||||
name: newSkillForm.value.name,
|
||||
description: newSkillForm.value.description,
|
||||
type: newSkillForm.value.type,
|
||||
category: newSkillForm.value.category,
|
||||
type: 'Custom',
|
||||
category: 'custom',
|
||||
status: 'stopped',
|
||||
port: newSkillForm.value.port,
|
||||
port: 3000,
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
tools: 0,
|
||||
})
|
||||
|
||||
148
web/src/views/tools/useTools.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const API_BASE = 'http://localhost:8082'
|
||||
|
||||
// Tool 接口
|
||||
export interface Tool {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
provider: string
|
||||
security_level: string
|
||||
require_approval: boolean
|
||||
parameters: string
|
||||
status: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export function useTools() {
|
||||
const tools = ref<Tool[]>([])
|
||||
const toolsLoading = ref(false)
|
||||
|
||||
// 获取工具列表
|
||||
const fetchTools = async (category?: string, status?: string) => {
|
||||
toolsLoading.value = true
|
||||
try {
|
||||
let url = `${API_BASE}/tool/list?`
|
||||
if (category) url += `category=${category}&`
|
||||
if (status) url += `status=${status}&`
|
||||
|
||||
const response = await fetch(url)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.list) {
|
||||
tools.value = data.list
|
||||
}
|
||||
return data.list || []
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch tools:', error)
|
||||
return []
|
||||
} finally {
|
||||
toolsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 同步工具到数据库
|
||||
const syncTools = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/tool/sync`)
|
||||
const data = await response.json()
|
||||
await fetchTools() // 重新获取列表
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Failed to sync tools:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 获取工具详情
|
||||
const fetchToolById = async (id: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/tool/${id}`)
|
||||
const data = await response.json()
|
||||
return data.tool
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch tool:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 创建工具
|
||||
const createTool = async (tool: Partial<Tool>) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/tool/add`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(tool),
|
||||
})
|
||||
const data = await response.json()
|
||||
await fetchTools() // 重新获取列表
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Failed to create tool:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 更新工具
|
||||
const updateTool = async (id: string, tool: Partial<Tool>) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/tool/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(tool),
|
||||
})
|
||||
const data = await response.json()
|
||||
await fetchTools() // 重新获取列表
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Failed to update tool:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 删除工具
|
||||
const deleteTool = async (id: string) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/tool/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
const data = await response.json()
|
||||
await fetchTools() // 重新获取列表
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Failed to delete tool:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 按分类获取工具
|
||||
const getToolsByCategory = (category: string) => {
|
||||
return tools.value.filter(tool => tool.category === category)
|
||||
}
|
||||
|
||||
// 获取所有分类
|
||||
const categories = computed(() => {
|
||||
const cats = new Set(tools.value.map(tool => tool.category))
|
||||
return Array.from(cats)
|
||||
})
|
||||
|
||||
return {
|
||||
tools,
|
||||
toolsLoading,
|
||||
categories,
|
||||
fetchTools,
|
||||
syncTools,
|
||||
fetchToolById,
|
||||
createTool,
|
||||
updateTool,
|
||||
deleteTool,
|
||||
getToolsByCategory,
|
||||
}
|
||||
}
|
||||