Compare commits

...

9 Commits

Author SHA1 Message Date
14d656eea3 chore: 更新 package-lock.json 和 useTools
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:27:05 +08:00
765a968e63 feat: 新增 agent/app/core 目录
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:26:53 +08:00
8249f67351 feat: 新增 teams 目录
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:26:47 +08:00
85b4c51fd7 feat: 前端配置和组件更新
- App.vue, Sidebar.vue
- main.ts, router/index.ts
- package.json, vite.config.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:26:33 +08:00
03540fb9e9 feat: 更新前端页面
- Agents, Chat, Settings, Skill, Tools
- Account, Plan, Script
- useSkills composable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:26:25 +08:00
7791d198f1 feat: 前端认证和工具模块
- 新增 useAuth.ts composable
- 新增 tools/useTools.ts
- 更新 Login.vue, Signup.vue

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:26:18 +08:00
fdd6b2c17d fix: 优化后端各模块 handler
- database_handler, knowledge_handler, model_handler
- neo4j_handler, sub_table_handler
- system_handler, upload_handler
- knowledge_service, upload_service

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:26:04 +08:00
ecb885ee5e feat: 后端认证和工具模块更新
- main.go: 添加 Swagger 文档、初始化默认管理员
- 认证模块: 完善用户角色管理
- 新增工具模块: tool_handler, tool_repo, tool_service, tool model
- 更新 go.mod 依赖

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:25:55 +08:00
e34d4bcd37 chore: 删除过时的文档和截图
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:25:44 +08:00
101 changed files with 13469 additions and 4050 deletions

View File

@@ -0,0 +1,8 @@
"""
Core 模块 - AI 核心能力
"""
from . import tools
__all__ = [
"tools",
]

View 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",
]

View 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",
]

View 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"]
}
}

View 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"]
}
}

View 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"]
}
}

View 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"]
}
}

View 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"
}
}
}
}

View 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"]
}
}

View 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()

View File

@@ -0,0 +1,16 @@
"""
沙盒模块
"""
from .sandbox import (
Sandbox,
SandboxConfig,
SafeEval,
sandbox,
)
__all__ = [
"Sandbox",
"SandboxConfig",
"SafeEval",
"sandbox",
]

View 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()

View 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"
}
}
}
}
]
}

View File

@@ -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] 全链路审计日志
---
*本文档将随项目开发持续更新*

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json"
"io" "io"
"log" "log"
"os" "os"
@@ -15,6 +16,8 @@ import (
"x-agents/server/internal/service" "x-agents/server/internal/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
) )
// Logger 日志记录器 // Logger 日志记录器
@@ -56,6 +59,83 @@ func (l *Logger) LogRequest(method, path, body string, status int, duration time
var logger *Logger 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() { func main() {
// 初始化日志 // 初始化日志
logger = NewLogger() logger = NewLogger()
@@ -71,7 +151,66 @@ func main() {
} }
// 3. 自动迁移表 // 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 // 4. 初始化 Repository
dbRepo := repository.NewDatabaseRepository(db) dbRepo := repository.NewDatabaseRepository(db)
@@ -79,6 +218,10 @@ func main() {
modelRepo := repository.NewModelRepository(db) modelRepo := repository.NewModelRepository(db)
knowledgeRepo := repository.NewKnowledgeRepository(db) knowledgeRepo := repository.NewKnowledgeRepository(db)
userRepo := repository.NewUserRepository(db) userRepo := repository.NewUserRepository(db)
toolRepo := repository.NewToolRepository(db)
// 4.1 初始化默认管理员用户
initDefaultAdmin(userRepo)
// 5. 初始化 Service // 5. 初始化 Service
dbService := service.NewDatabaseService(dbRepo, subTableRepo) dbService := service.NewDatabaseService(dbRepo, subTableRepo)
@@ -91,6 +234,14 @@ func main() {
} }
knowledgeService := service.NewKnowledgeService(knowledgeRepo, modelRepo, uploadService, cfg.PythonServiceURL, cfg.AICoreServiceAddr, cfg.MarkdownLocalPath) knowledgeService := service.NewKnowledgeService(knowledgeRepo, modelRepo, uploadService, cfg.PythonServiceURL, cfg.AICoreServiceAddr, cfg.MarkdownLocalPath)
authService := service.NewAuthService(cfg.JWTSecret, userRepo) 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 // 6. 初始化 Handler
dbHandler := handler.NewDatabaseHandler(dbService) dbHandler := handler.NewDatabaseHandler(dbService)
@@ -100,6 +251,7 @@ func main() {
systemHandler := handler.NewSystemHandler() systemHandler := handler.NewSystemHandler()
knowledgeHandler := handler.NewKnowledgeHandler(knowledgeService) knowledgeHandler := handler.NewKnowledgeHandler(knowledgeService)
authHandler := handler.NewAuthHandler(authService) authHandler := handler.NewAuthHandler(authService)
toolHandler := handler.NewToolHandler(toolService)
var uploadHandler *handler.UploadHandler var uploadHandler *handler.UploadHandler
if uploadService != nil { if uploadService != nil {
uploadHandler = handler.NewUploadHandler(uploadService, knowledgeRepo) uploadHandler = handler.NewUploadHandler(uploadService, knowledgeRepo)
@@ -137,8 +289,8 @@ func main() {
// CORS 中间件 // CORS 中间件
r.Use(func(c *gin.Context) { r.Use(func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 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-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
if c.Request.Method == "OPTIONS" { if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204) c.AbortWithStatus(204)
@@ -162,6 +314,14 @@ func main() {
authProtectedGroup.GET("/me", authHandler.GetCurrentUser) 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") databaseGroup := r.Group("/database")
{ {
@@ -225,6 +385,20 @@ func main() {
// 系统信息模块 // 系统信息模块
r.GET("/system/info", systemHandler.GetSystemInfo) 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 { if uploadHandler != nil {
// 本地文件静态服务 // 本地文件静态服务

2936
server/docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

2907
server/docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

1960
server/docs/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,78 +1,115 @@
module x-agents/server module x-agents/server
go 1.25 go 1.25.0
require ( 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/go-sql-driver/mysql v1.7.0
github.com/golang-jwt/jwt/v5 v5.2.0 github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/spf13/viper v1.18.2 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/driver/mysql v1.5.2
gorm.io/gorm v1.25.5 gorm.io/gorm v1.25.5
) )
require ( require (
github.com/bytedance/sonic v1.9.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // 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/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect github.com/go-ini/ini v1.67.0 // indirect
github.com/go-ole/go-ole v1.2.6 // 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/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.2 // 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/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // 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/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.2 // 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/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/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // 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/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.99 // indirect github.com/minio/minio-go/v7 v7.0.99 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // 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/philhofer/fwd v1.2.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // 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/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/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // 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/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // 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/tinylib/msgp v1.6.1 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.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 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/atomic v1.9.0 // indirect
go.uber.org/multierr 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 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/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.48.0 // indirect golang.org/x/mod v0.33.0 // indirect
golang.org/x/sys v0.39.0 // indirect golang.org/x/net v0.51.0 // indirect
golang.org/x/text v0.32.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/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.79.2 // 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/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
) )

View File

@@ -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.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 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 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-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 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-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.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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 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/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 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 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 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 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 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 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 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 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 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 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 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 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= 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/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 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.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 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 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 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 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 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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= 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/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 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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= 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.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 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 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 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= 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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 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.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 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 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 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 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 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.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 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= 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/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 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.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 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= 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= 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/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 h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 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 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 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 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 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 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 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 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= 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 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 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.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.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.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.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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/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 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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.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 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 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 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 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/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 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 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 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 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 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 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 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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.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 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 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 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 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 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 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 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 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 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 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-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-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-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.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= 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.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 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= 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 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= 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.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 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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 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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.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 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 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= 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=

View File

@@ -2,12 +2,57 @@ package handler
import ( import (
"net/http" "net/http"
"strings"
"x-agents/server/internal/service" "x-agents/server/internal/service"
"github.com/gin-gonic/gin" "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 { type AuthHandler struct {
authService *service.AuthService authService *service.AuthService
} }
@@ -26,7 +71,23 @@ type LoginResponse struct {
User interface{} `json:"user"` 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) { func (h *AuthHandler) Login(c *gin.Context) {
var req LoginRequest var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil { 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) { func (h *AuthHandler) Register(c *gin.Context) {
var req struct { var req RegisterRequest
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Email string `json:"email"`
}
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
@@ -75,11 +140,19 @@ func (h *AuthHandler) Register(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{ c.JSON(http.StatusCreated, gin.H{
"id": user.ID, "id": user.ID,
"username": user.Username, "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) { func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
userID, exists := c.Get("user_id") userID, exists := c.Get("user_id")
if !exists { if !exists {
@@ -96,9 +169,73 @@ func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"id": user.ID, "id": user.ID,
"username": user.Username, "username": user.Username,
"email": user.Email, "email": maskEmail(user.Email),
"role_id": user.RoleID, "role_id": user.RoleID,
"is_active": user.IsActive, "is_active": user.IsActive,
"created_at": user.CreatedAt, "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,
})
}

View File

@@ -17,7 +17,16 @@ func NewDatabaseHandler(svc *service.DatabaseService) *DatabaseHandler {
return &DatabaseHandler{service: svc} 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) { func (h *DatabaseHandler) Check(c *gin.Context) {
var req model.CheckRequest var req model.CheckRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -34,7 +43,16 @@ func (h *DatabaseHandler) Check(c *gin.Context) {
c.JSON(http.StatusOK, result) 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) { func (h *DatabaseHandler) Create(c *gin.Context) {
var req model.CreateDatabaseRequest var req model.CreateDatabaseRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -51,7 +69,15 @@ func (h *DatabaseHandler) Create(c *gin.Context) {
c.JSON(http.StatusCreated, info) 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) { func (h *DatabaseHandler) GetByID(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
@@ -64,7 +90,14 @@ func (h *DatabaseHandler) GetByID(c *gin.Context) {
c.JSON(http.StatusOK, info) 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) { func (h *DatabaseHandler) List(c *gin.Context) {
list, err := h.service.List() list, err := h.service.List()
if err != nil { if err != nil {
@@ -79,7 +112,17 @@ func (h *DatabaseHandler) List(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"list": list}) 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) { func (h *DatabaseHandler) Update(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
@@ -98,7 +141,16 @@ func (h *DatabaseHandler) Update(c *gin.Context) {
c.JSON(http.StatusOK, info) 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) { func (h *DatabaseHandler) SaveGraph(c *gin.Context) {
var req model.SaveGraphRequest var req model.SaveGraphRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -115,7 +167,15 @@ func (h *DatabaseHandler) SaveGraph(c *gin.Context) {
c.JSON(http.StatusOK, result) 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) { func (h *DatabaseHandler) Delete(c *gin.Context) {
id := c.Param("id") id := c.Param("id")

View File

@@ -17,7 +17,15 @@ func NewKnowledgeHandler(s *service.KnowledgeService) *KnowledgeHandler {
return &KnowledgeHandler{service: s} 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) { func (h *KnowledgeHandler) Create(c *gin.Context) {
var req model.CreateKnowledgeRequest var req model.CreateKnowledgeRequest
if err := c.ShouldBindJSON(&req); err != nil { 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) { func (h *KnowledgeHandler) List(c *gin.Context) {
list, err := h.service.List() list, err := h.service.List()
if err != nil { if err != nil {
@@ -49,7 +64,16 @@ func (h *KnowledgeHandler) List(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": list}) 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) { func (h *KnowledgeHandler) GetByID(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
if id == "" { if id == "" {
@@ -66,7 +90,17 @@ func (h *KnowledgeHandler) GetByID(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": kb}) 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) { func (h *KnowledgeHandler) Update(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
if 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"}) 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) { func (h *KnowledgeHandler) Delete(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
if 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"}) 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) { func (h *KnowledgeHandler) ListDocuments(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
if id == "" { if id == "" {
@@ -122,7 +175,17 @@ func (h *KnowledgeHandler) ListDocuments(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": list}) 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) { func (h *KnowledgeHandler) UploadDocument(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
if 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) { func (h *KnowledgeHandler) DeleteDocument(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
docID := c.Param("doc_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"}) 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) { func (h *KnowledgeHandler) ReparseDocument(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
docID := c.Param("doc_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"}) 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) { func (h *KnowledgeHandler) GetDocumentPreview(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
docID := c.Param("doc_id") docID := c.Param("doc_id")

View File

@@ -17,7 +17,14 @@ func NewModelHandler(svc *service.ModelService) *ModelHandler {
return &ModelHandler{service: svc} 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) { func (h *ModelHandler) List(c *gin.Context) {
list, err := h.service.List() list, err := h.service.List()
if err != nil { if err != nil {
@@ -32,7 +39,15 @@ func (h *ModelHandler) List(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"list": list}) 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) { func (h *ModelHandler) GetByID(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
model, err := h.service.GetByID(id) model, err := h.service.GetByID(id)
@@ -43,7 +58,16 @@ func (h *ModelHandler) GetByID(c *gin.Context) {
c.JSON(http.StatusOK, model) 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) { func (h *ModelHandler) Create(c *gin.Context) {
var req model.CreateModelRequest var req model.CreateModelRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -60,7 +84,17 @@ func (h *ModelHandler) Create(c *gin.Context) {
c.JSON(http.StatusOK, result) 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) { func (h *ModelHandler) Update(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
var req model.UpdateModelRequest var req model.UpdateModelRequest
@@ -78,7 +112,15 @@ func (h *ModelHandler) Update(c *gin.Context) {
c.JSON(http.StatusOK, result) 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) { func (h *ModelHandler) Delete(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
err := h.service.Delete(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}) 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) { func (h *ModelHandler) Test(c *gin.Context) {
var req model.TestModelRequest var req model.TestModelRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {

View File

@@ -17,7 +17,16 @@ func NewNeo4jHandler(svc *service.Neo4jService) *Neo4jHandler {
return &Neo4jHandler{service: svc} 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) { func (h *Neo4jHandler) Check(c *gin.Context) {
var req model.Neo4jCheckRequest var req model.Neo4jCheckRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -34,7 +43,16 @@ func (h *Neo4jHandler) Check(c *gin.Context) {
c.JSON(http.StatusOK, result) 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) { func (h *Neo4jHandler) GetGraphs(c *gin.Context) {
var req model.Neo4jGraphRequest var req model.Neo4jGraphRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -51,7 +69,16 @@ func (h *Neo4jHandler) GetGraphs(c *gin.Context) {
c.JSON(http.StatusOK, result) 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) { func (h *Neo4jHandler) GetNodes(c *gin.Context) {
var req model.Neo4jNodeRequest var req model.Neo4jNodeRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -68,7 +95,16 @@ func (h *Neo4jHandler) GetNodes(c *gin.Context) {
c.JSON(http.StatusOK, result) 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) { func (h *Neo4jHandler) GetRelationships(c *gin.Context) {
var req model.Neo4jRelRequest var req model.Neo4jRelRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {

View File

@@ -17,7 +17,16 @@ func NewSubTableHandler(svc *service.SubTableService) *SubTableHandler {
return &SubTableHandler{service: svc} 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) { func (h *SubTableHandler) Create(c *gin.Context) {
var req model.CreateSubTableRequest var req model.CreateSubTableRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -34,7 +43,15 @@ func (h *SubTableHandler) Create(c *gin.Context) {
c.JSON(http.StatusCreated, info) 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) { func (h *SubTableHandler) GetByID(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
@@ -47,7 +64,15 @@ func (h *SubTableHandler) GetByID(c *gin.Context) {
c.JSON(http.StatusOK, info) 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) { func (h *SubTableHandler) ListByDatabase(c *gin.Context) {
databaseID := c.Param("database_id") databaseID := c.Param("database_id")
@@ -64,7 +89,15 @@ func (h *SubTableHandler) ListByDatabase(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"list": list}) 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) { func (h *SubTableHandler) GetMappingFromFile(c *gin.Context) {
databaseID := c.Param("database_id") databaseID := c.Param("database_id")
@@ -82,7 +115,17 @@ func (h *SubTableHandler) GetMappingFromFile(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"mapping": mapping}) 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) { func (h *SubTableHandler) Update(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
@@ -101,7 +144,15 @@ func (h *SubTableHandler) Update(c *gin.Context) {
c.JSON(http.StatusOK, info) 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) { func (h *SubTableHandler) Delete(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
@@ -114,7 +165,15 @@ func (h *SubTableHandler) Delete(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "deleted"}) 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) { func (h *SubTableHandler) GetTablesDDL(c *gin.Context) {
databaseID := c.Param("database_id") databaseID := c.Param("database_id")

View File

@@ -14,7 +14,14 @@ func NewSystemHandler() *SystemHandler {
return &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) { func (h *SystemHandler) GetSystemInfo(c *gin.Context) {
info, err := getSystemInfo() info, err := getSystemInfo()
if err != nil { if err != nil {

View 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"})
}

View File

@@ -20,7 +20,16 @@ func NewUploadHandler(uploadService *service.UploadService, knowledgeRepo *repos
return &UploadHandler{uploadService: uploadService, knowledgeRepo: knowledgeRepo} 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) { func (h *UploadHandler) Upload(c *gin.Context) {
file, err := c.FormFile("file") file, err := c.FormFile("file")
if err != nil { if err != nil {
@@ -48,7 +57,16 @@ func (h *UploadHandler) Upload(c *gin.Context) {
c.JSON(http.StatusOK, result) 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) { func (h *UploadHandler) Delete(c *gin.Context) {
filename := c.Param("filename") filename := c.Param("filename")
if 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"}) 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) { func (h *UploadHandler) ProxyFile(c *gin.Context) {
fileKey := c.Query("key") fileKey := c.Query("key")
kbID := c.Query("kb_id") kbID := c.Query("kb_id")

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

View File

@@ -1,6 +1,7 @@
package model package model
import ( import (
"encoding/json"
"time" "time"
) )
@@ -16,11 +17,11 @@ const (
// Role 角色 // Role 角色
type Role struct { type Role struct {
ID string `json:"id" gorm:"primaryKey"` ID string `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"uniqueIndex"` 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"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }
// User 用户 // User 用户
@@ -41,8 +42,13 @@ func (u *User) HasPermission(level PermissionLevel) bool {
if u.Role == nil { if u.Role == nil {
return false return false
} }
for _, p := range u.Role.Permissions { // 解析 JSON 格式的权限
if p >= level { 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 return true
} }
} }

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

View File

@@ -1,7 +1,12 @@
package service package service
import ( import (
"encoding/json"
"errors" "errors"
"log"
"os"
"path/filepath"
"strings"
"time" "time"
"x-agents/server/internal/model" "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) role, err := s.userRepo.FindRoleByID(user.RoleID)
if err != nil { if err != nil {
// 创建默认角色 // 创建默认角色
perms, _ := json.Marshal([]int{int(model.PermissionRead), int(model.PermissionWrite)})
role = &model.Role{ role = &model.Role{
ID: "user", ID: "user",
Name: "user", Name: "user",
Permissions: []model.PermissionLevel{model.PermissionRead, model.PermissionWrite}, Permissions: string(perms),
} }
s.userRepo.CreateRole(role) s.userRepo.CreateRole(role)
user.Role = role user.Role = role
@@ -136,10 +142,74 @@ func (s *AuthService) Register(username, password, email string) (*model.User, e
return nil, err return nil, err
} }
// 创建用户工作空间目录
if err := s.createUserWorkspace(username); err != nil {
// 工作空间创建失败不影响注册成功,仅记录日志
println("Warning: failed to create user workspace:", err.Error())
}
return user, nil 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获取用户 // GetUserByID 根据ID获取用户
func (s *AuthService) GetUserByID(id string) (*model.User, error) { func (s *AuthService) GetUserByID(id string) (*model.User, error) {
return s.userRepo.FindByID(id) return s.userRepo.FindByID(id)
} }
// GetAllUsers 获取所有用户
func (s *AuthService) GetAllUsers() ([]model.User, error) {
return s.userRepo.FindAll()
}

View File

@@ -155,9 +155,9 @@ func (s *KnowledgeService) Delete(id string) error {
// 删除每个文档的 MinIO 文件和本地 Markdown 文件 // 删除每个文档的 MinIO 文件和本地 Markdown 文件
for _, doc := range docs { for _, doc := range docs {
// 删除 MinIO 文件 // 删除存储文件(MinIO 或本地)
if doc.FileKey != "" && kb.StorageConfig.Type == "minio" { if doc.FileKey != "" {
s.uploadService.DeleteFileWithConfig(doc.FileKey, kb.StorageConfig) s.uploadService.DeleteFileWithConfig(doc.FileKey, kb.StorageConfig, kb.Name)
} }
// 删除本地 Markdown 文件 // 删除本地 Markdown 文件
if s.markdownLocalPath != "" { if s.markdownLocalPath != "" {
@@ -215,15 +215,15 @@ func (s *KnowledgeService) UploadDocument(kbID string, file *multipart.FileHeade
// 上传文件(根据知识库的 storage_config 选择存储方式) // 上传文件(根据知识库的 storage_config 选择存储方式)
var result *UploadResponse 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", knowledgeDebugLog.Printf("[Knowledge Upload] 使用知识库存储配置: type=%s, endpoint=%s, bucket=%s",
kb.StorageConfig.Type, kb.StorageConfig.Endpoint, kb.StorageConfig.Bucket) kb.StorageConfig.Type, kb.StorageConfig.Endpoint, kb.StorageConfig.Bucket)
result, err = s.uploadService.UploadWithConfig(file, kb.StorageConfig) result, err = s.uploadService.UploadWithConfig(file, kb.StorageConfig)
} else { } else {
// 使用全局配置 // 本地存储,使用知识库名称作为子目录
knowledgeDebugLog.Printf("[Knowledge Upload] 使用全局存储配置") knowledgeDebugLog.Printf("[Knowledge Upload] 使用本地存储,路径: resources/%s/", kb.Name)
result, err = s.uploadService.Upload(file) result, err = s.uploadService.UploadToKnowledgeBase(file, kb.StorageConfig, kb.Name)
} }
if err != nil { if err != nil {
knowledgeDebugLog.Printf("[Knowledge Upload] 错误: 上传失败, err=%v", err) knowledgeDebugLog.Printf("[Knowledge Upload] 错误: 上传失败, err=%v", err)
@@ -419,13 +419,8 @@ func (s *KnowledgeService) DeleteDocument(kbID, docID string) error {
if doc.FileKey != "" { if doc.FileKey != "" {
knowledgeDebugLog.Printf("[Knowledge DeleteDocument] 删除文件: kbID=%s, docID=%s, fileKey=%s, storageType=%s", knowledgeDebugLog.Printf("[Knowledge DeleteDocument] 删除文件: kbID=%s, docID=%s, fileKey=%s, storageType=%s",
kbID, docID, doc.FileKey, kb.StorageConfig.Type) kbID, docID, doc.FileKey, kb.StorageConfig.Type)
if kb.StorageConfig.Type != "" { // 使用知识库的存储配置删除(传入知识库名称)
// 使用知识库的存储配置删除 s.uploadService.DeleteFileWithConfig(doc.FileKey, kb.StorageConfig, kb.Name)
s.uploadService.DeleteFileWithConfig(doc.FileKey, kb.StorageConfig)
} else {
// 使用全局配置删除
s.uploadService.DeleteFile(doc.FileKey)
}
} }
// 删除文档记录 // 删除文档记录

View 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)"}}}`,
},
}
}

View File

@@ -94,7 +94,7 @@ func (s *UploadService) Upload(file *multipart.FileHeader) (*UploadResponse, err
if s.cfg.UploadMode == "minio" { if s.cfg.UploadMode == "minio" {
return s.uploadToMinIO(f, filename, fileKey, file.Size) return s.uploadToMinIO(f, filename, fileKey, file.Size)
} }
return s.uploadToLocal(f, filename, fileKey) return s.uploadToLocal(f, filename, fileKey, "")
} }
// UploadWithConfig 上传文件(使用指定配置) // UploadWithConfig 上传文件(使用指定配置)
@@ -116,7 +116,29 @@ func (s *UploadService) UploadWithConfig(file *multipart.FileHeader, storageConf
return s.uploadToMinIOWithConfig(f, filename, fileKey, file.Size, storageConfig) 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 // uploadToMinIOWithConfig 使用指定配置上传到 MinIO
@@ -217,9 +239,18 @@ func (s *UploadService) uploadToMinIO(f multipart.File, filename, fileKey string
} }
// uploadToLocal 上传到本地 // uploadToLocal 上传到本地
func (s *UploadService) uploadToLocal(f multipart.File, filename, fileKey string) (*UploadResponse, error) { // knowledgeBaseName: 知识库名称,如果为空则使用全局配置目录,否则使用 resources/{知识库名称}/
// 确保目录存在 func (s *UploadService) uploadToLocal(f multipart.File, filename, fileKey string, knowledgeBaseName string) (*UploadResponse, error) {
uploadPath := s.cfg.UploadLocalPath // 确定存储路径
var uploadPath string
if knowledgeBaseName != "" {
// 使用 resources/{知识库名称}/ 路径
uploadPath = filepath.Join("resources", knowledgeBaseName)
} else {
// 使用全局配置路径
uploadPath = s.cfg.UploadLocalPath
}
if err := os.MkdirAll(uploadPath, 0755); err != nil { if err := os.MkdirAll(uploadPath, 0755); err != nil {
return &UploadResponse{Success: false, Message: fmt.Sprintf("failed to create directory: %v", 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 使用指定配置删除文件 // 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" { if storageConfig.Type == "minio" {
// 创建 MinIO 客户端 // 创建 MinIO 客户端
client, err := minio.New(storageConfig.Endpoint, &minio.Options{ 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) return os.Remove(path)
} }

76
web/package-lock.json generated
View File

@@ -12,7 +12,9 @@
"@vue-office/excel": "^1.7.14", "@vue-office/excel": "^1.7.14",
"echarts": "^6.0.0", "echarts": "^6.0.0",
"element-plus": "^2.13.3", "element-plus": "^2.13.3",
"lucide-vue-next": "^0.577.0",
"marked": "^17.0.4", "marked": "^17.0.4",
"monaco-editor": "^0.55.1",
"papaparse": "^5.5.3", "papaparse": "^5.5.3",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.21", "vue": "^3.4.21",
@@ -27,6 +29,7 @@
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"typescript": "^5.4.3", "typescript": "^5.4.3",
"vite": "^5.2.8", "vite": "^5.2.8",
"vite-plugin-monaco-editor": "^1.1.0",
"vue-tsc": "^2.0.7" "vue-tsc": "^2.0.7"
} }
}, },
@@ -978,7 +981,6 @@
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/lodash": "*" "@types/lodash": "*"
} }
@@ -989,7 +991,6 @@
"integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.18.0" "undici-types": "~7.18.0"
} }
@@ -1004,6 +1005,13 @@
"@types/node": "*" "@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": { "node_modules/@types/web-bluetooth": {
"version": "0.0.20", "version": "0.0.20",
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
@@ -1419,7 +1427,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -1559,6 +1566,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/echarts": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.0.0.tgz", "resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.0.0.tgz",
@@ -1864,7 +1880,6 @@
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
@@ -1893,15 +1908,13 @@
"version": "4.17.23", "version": "4.17.23",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz", "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/lodash-es": { "node_modules/lodash-es": {
"version": "4.17.23", "version": "4.17.23",
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz", "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz",
"integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/lodash-unified": { "node_modules/lodash-unified": {
"version": "1.0.3", "version": "1.0.3",
@@ -1914,6 +1927,15 @@
"lodash-es": "*" "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": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
@@ -1981,6 +2003,28 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/muggle-string": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz",
@@ -2161,7 +2205,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -2597,7 +2640,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -2637,7 +2679,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -2697,7 +2738,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "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": { "node_modules/vscode-uri": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", "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", "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.29.tgz",
"integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.29", "@vue/compiler-dom": "3.5.29",
"@vue/compiler-sfc": "3.5.29", "@vue/compiler-sfc": "3.5.29",
@@ -2787,7 +2836,6 @@
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js" "vue-demi-switch": "bin/vue-demi-switch.js"

View File

@@ -13,7 +13,9 @@
"@vue-office/excel": "^1.7.14", "@vue-office/excel": "^1.7.14",
"echarts": "^6.0.0", "echarts": "^6.0.0",
"element-plus": "^2.13.3", "element-plus": "^2.13.3",
"lucide-vue-next": "^0.577.0",
"marked": "^17.0.4", "marked": "^17.0.4",
"monaco-editor": "^0.55.1",
"papaparse": "^5.5.3", "papaparse": "^5.5.3",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.21", "vue": "^3.4.21",
@@ -28,6 +30,7 @@
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"typescript": "^5.4.3", "typescript": "^5.4.3",
"vite": "^5.2.8", "vite": "^5.2.8",
"vite-plugin-monaco-editor": "^1.1.0",
"vue-tsc": "^2.0.7" "vue-tsc": "^2.0.7"
} }
} }

View File

@@ -5,7 +5,7 @@ import { ElConfigProvider } from 'element-plus'
import Sidebar from '@/components/Sidebar.vue' import Sidebar from '@/components/Sidebar.vue'
const route = useRoute() const route = useRoute()
const showSidebar = computed(() => route.path !== '/') const showSidebar = computed(() => route.path !== '/login' && route.path !== '/' && route.path !== '/signup')
</script> </script>
<template> <template>

View File

@@ -1,16 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, onMounted } from 'vue' import { computed, ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import { fetchKnowledgeBases } from '@/views/knowledge/useKnowledge' import { fetchKnowledgeBases } from '@/views/knowledge/useKnowledge'
import { useDatabase } from '@/views/database/useDatabase' import { useDatabase } from '@/views/database/useDatabase'
import { useAuth } from '@/composables/useAuth'
// 下拉菜单展开状态 // 下拉菜单展开状态
const userDropdownVisible = ref(false) const userDropdownVisible = ref(false)
// 退出确认弹窗状态
const showLogoutConfirm = ref(false)
const router = useRouter() const router = useRouter()
const route = useRoute() 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 数量 // 获取 Knowledge 数量
const knowledgeCount = ref(0) const knowledgeCount = ref(0)
const fetchKnowledgeCount = async () => { const fetchKnowledgeCount = async () => {
@@ -60,11 +77,11 @@ const group3 = computed(() => [
{ name: 'Memory', icon: 'fa-brain', path: '/memory' }, { name: 'Memory', icon: 'fa-brain', path: '/memory' },
]) ])
// 第4组: Dashboard, Account, Settings // 第4组: Dashboard, Models, Logs
const group4 = computed(() => [ const group4 = computed(() => [
{ name: 'Dashboard', icon: 'fa-gauge', path: '/dashboard' }, { name: 'Dashboard', icon: 'fa-gauge', path: '/dashboard' },
{ name: 'Account', icon: 'fa-user', path: '/account' }, { name: 'Models', icon: 'fa-brain', path: '/settings' },
{ name: 'Settings', icon: 'fa-gear', path: '/settings' }, { name: 'Logs', icon: 'fa-file-lines', path: '/logs' },
]) ])
const activeMenu = computed(() => { const activeMenu = computed(() => {
@@ -83,31 +100,43 @@ const navigateTo = (item: MenuItem) => {
} }
} }
// 通知设置弹窗状态
const showNotificationSettings = ref(false)
// 用户菜单操作 // 用户菜单操作
const handleUserCommand = (command: string) => { const handleUserCommand = (command: string) => {
switch (command) { switch (command) {
case 'settings': case 'notifications':
// 全局设置 // 通知设置
router.push('/settings') showNotificationSettings.value = true
break
case 'account':
// 账户设置
router.push('/account')
break break
case 'userManagement': case 'userManagement':
// 用户管理 // 用户管理
router.push('/user-management') router.push('/user-management')
break break
case 'logout': case 'logout':
// 退出登录 // 退出登录 - 显示自定义确认弹窗
ElMessageBox.confirm('确定要退出登录吗?', '提示', { showLogoutConfirm.value = true
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
// 清除登录状态
localStorage.removeItem('token')
router.push('/login')
}).catch(() => {})
break break
} }
} }
// 确认退出登录
const confirmLogout = () => {
showLogoutConfirm.value = false
userDropdownVisible.value = false
authLogout()
router.push('/login')
}
// 取消退出
const cancelLogout = () => {
showLogoutConfirm.value = false
}
</script> </script>
<template> <template>
@@ -186,7 +215,7 @@ const handleUserCommand = (command: string) => {
<!-- 分隔线3 --> <!-- 分隔线3 -->
<li class="my-4 border-t border-dark-500"></li> <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"> <li v-for="item in group4" :key="item.name">
<a <a
href="javascript:void(0)" href="javascript:void(0)"
@@ -198,11 +227,6 @@ const handleUserCommand = (command: string) => {
<i :class="['fa-solid', item.icon, 'w-5', 'text-center']"></i> <i :class="['fa-solid', item.icon, 'w-5', 'text-center']"></i>
<span>{{ item.name }}</span> <span>{{ item.name }}</span>
</div> </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> </a>
</li> </li>
</ul> </ul>
@@ -210,65 +234,218 @@ const handleUserCommand = (command: string) => {
<!-- 底部用户信息 --> <!-- 底部用户信息 -->
<div class="border-t border-dark-500"> <div class="border-t border-dark-500">
<el-dropdown trigger="click" @command="handleUserCommand" class="user-dropdown" @visible-change="(v: boolean) => userDropdownVisible = v"> <div class="relative">
<div class="w-full flex items-center justify-between cursor-pointer hover:bg-dark-600 px-4 py-3 transition-colors"> <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"> <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="min-w-0">
<div class="font-medium text-sm text-gray-300 truncate">Alex Smith</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">alex@gmail.com</div> <div class="text-xs text-gray-500 truncate">{{ currentUser?.email || 'user@example.com' }}</div>
</div> </div>
</div> </div>
<i :class="['fa-solid', userDropdownVisible ? 'fa-chevron-up' : 'fa-chevron-down', 'text-xs', 'text-gray-500']"></i> <i :class="['fa-solid', userDropdownVisible ? 'fa-chevron-up' : 'fa-chevron-down', 'text-xs', 'text-gray-500']"></i>
</div> </div>
<template #dropdown>
<el-dropdown-menu> <!-- 简洁下拉弹窗 -->
<el-dropdown-item command="settings"> <Transition name="dropdown-fade">
<i class="fa-solid fa-gear w-4 text-center"></i> <div v-if="userDropdownVisible" class="user-dropdown-panel" @click.stop>
Settings <div class="dropdown-menu">
</el-dropdown-item> <div class="menu-item" @click="handleUserCommand('notifications')">
<el-dropdown-item command="userManagement"> <i class="fa-solid fa-bell text-gray-400"></i>
<i class="fa-solid fa-user w-4 text-center"></i> <span class="text-gray-300">Notifications</span>
Users </div>
</el-dropdown-item>
<el-dropdown-item divided command="logout"> <div class="menu-item" @click="handleUserCommand('account')">
<i class="fa-solid fa-arrow-right-from-bracket w-4 text-center"></i> <i class="fa-solid fa-user text-gray-400"></i>
Sign Out <span class="text-gray-300">Account</span>
</el-dropdown-item> </div>
</el-dropdown-menu>
</template> <div class="menu-item">
</el-dropdown> <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> </div>
</aside> </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> </template>
<style> <style>
.user-dropdown { /* 用户下拉弹窗 */
width: 100%; .user-dropdown-panel {
} position: absolute;
.user-dropdown .el-dropdown-menu { bottom: 100%;
background-color: #262626; left: 0;
border: none; right: 0;
padding: 6px; margin-bottom: 8px;
background: #1f1f1f;
border: 1px solid #404040;
border-radius: 10px; 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; .dropdown-menu {
padding: 8px 14px; padding: 6px;
border-radius: 6px; }
font-size: 13px;
.menu-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; 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; .menu-item:hover {
color: white; background: #333;
} }
.user-dropdown .el-dropdown-menu__item--divided {
border-top: 1px solid #404040; .dropdown-divider {
margin-top: 4px; height: 1px;
padding-top: 8px; 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> </style>

View 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,
}
}

View File

@@ -1,4 +1,4 @@
import { createApp } from 'vue' import { createApp, type Component } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import ElementPlus from 'element-plus' import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
@@ -6,8 +6,18 @@ import router from './router'
import './assets/styles/index.css' import './assets/styles/index.css'
import App from './App.vue' import App from './App.vue'
// 全局注册 Lucide 图标
import * as iconModule from 'lucide-vue-next'
const app = createApp(App) 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(createPinia())
app.use(router) app.use(router)
app.use(ElementPlus) app.use(ElementPlus)

View File

@@ -16,11 +16,23 @@ import Settings from '@/views/Settings.vue'
import Account from '@/views/Account.vue' import Account from '@/views/Account.vue'
import Logs from '@/views/Logs.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({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [ routes: [
{ {
path: '/', path: '/',
redirect: () => isAuthenticated() ? '/dashboard' : '/login'
},
{
path: '/login',
name: 'login', name: 'login',
component: 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 export default router

View File

@@ -1,15 +1,47 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useAuth } from '@/composables/useAuth'
import './database/database.css' 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 = [ const menuItems = [
{ key: 'profile', label: 'Profile', icon: 'fa-user' },
{ key: 'users', label: 'Users', icon: 'fa-users' }, { key: 'users', label: 'Users', icon: 'fa-users' },
{ key: 'roles', label: 'Roles', icon: 'fa-user-shield' }, { key: 'roles', label: 'Roles', icon: 'fa-user-shield' },
{ key: 'permissions', label: 'Permissions', icon: 'fa-lock' }, { key: 'permissions', label: 'Permissions', icon: 'fa-lock' },
@@ -193,6 +225,61 @@ const statusClass = (status: string) => {
<!-- 右侧内容 --> <!-- 右侧内容 -->
<div class="flex-1"> <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 --> <!-- Users -->
<div v-if="activeMenu === 'users'" class="space-y-4"> <div v-if="activeMenu === 'users'" class="space-y-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">

View File

@@ -1,6 +1,70 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' 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 { interface Agent {
id: number id: number
name: string name: string
@@ -73,7 +137,7 @@ const deleteAgent = (id: number) => {
<i class="fa-solid fa-robot text-orange-500"></i> <i class="fa-solid fa-robot text-orange-500"></i>
<span class="font-medium">Agents</span> <span class="font-medium">Agents</span>
</div> </div>
<button class="btn-primary"> <button @click="openCreateModal" class="btn-primary">
<i class="fa-solid fa-plus"></i> <i class="fa-solid fa-plus"></i>
New Agent New Agent
</button> </button>
@@ -224,4 +288,81 @@ const deleteAgent = (id: number) => {
</div> </div>
</div> </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> </template>

View File

@@ -53,6 +53,76 @@ const chatSessions = ref<ChatSession[]>([
{ id: 3, title: '数据分析咨询', agentId: 4, lastMessage: 'DeepSeek: 好的', timestamp: new Date(Date.now() - 86400000) }, { 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) const sidebarCollapsed = ref(false)
@@ -398,15 +468,26 @@ const toggleSidebar = () => {
<!-- 新建对话按钮 --> <!-- 新建对话按钮 -->
<div class="p-3"> <div class="p-3">
<button <div class="flex gap-2">
@click="newChat" <button
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 class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</svg> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
<span>新建对话</span> </svg>
</button> <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> </div>
<!-- AI 助手选择 --> <!-- AI 助手选择 -->
@@ -432,27 +513,115 @@ const toggleSidebar = () => {
</div> </div>
</div> </div>
<!-- 历史对话列表 --> <!-- 群聊列表 -->
<div class="flex-1 overflow-y-auto px-3 pb-3"> <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"> <div class="space-y-1">
<button <button
v-for="session in chatSessions" v-for="group in groupChats"
:key="session.id" :key="group.id"
@click="selectSession(session)"
class="w-full text-left px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all duration-200 group" 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"> <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"> <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> </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>
<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> </button>
</div> </div>
</div> </div>
</div> </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> </template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: all 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -1,8 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useAuth } from '@/composables/useAuth'
const router = useRouter() const router = useRouter()
const route = useRoute()
const { login } = useAuth()
// 表单数据 // 表单数据
const username = ref('') const username = ref('')
@@ -11,23 +14,38 @@ const rememberMe = ref(false)
const isLoading = ref(false) const isLoading = ref(false)
const showPassword = ref(false) const showPassword = ref(false)
const errorMsg = ref('') 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 = '' errorMsg.value = ''
if (username.value !== 'admin' || password.value !== 'admin') { if (!username.value || !password.value) {
errorMsg.value = 'Invalid username or password' errorMsg.value = 'Please enter username and password'
return return
} }
isLoading.value = true isLoading.value = true
// 模拟登录
setTimeout(() => { try {
isLoading.value = false await login({
username: username.value,
password: password.value,
})
// 登录成功,跳转到 Dashboard // 登录成功,跳转到 Dashboard
router.push('/dashboard') router.push('/chat')
}, 1500) } catch (error: any) {
errorMsg.value = error.message || 'Login failed, please check your credentials'
} finally {
isLoading.value = false
}
} }
</script> </script>
@@ -52,6 +70,12 @@ const handleLogin = () => {
<!-- 登录表单 --> <!-- 登录表单 -->
<div class="bg-dark-700 rounded-2xl p-8 shadow-xl border border-dark-500/50"> <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"> <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> <i class="fa-solid fa-circle-exclamation"></i>

View File

@@ -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 searchQuery = ref('')
const filteredTasks = computed(() => { const filteredTasks = computed(() => {
let result = tasks.value let result = tasks.value
if (activeTab.value === 'running') { if (filterStatus.value !== 'all') {
result = result.filter(t => t.status === 'running') result = result.filter(t => t.status === filterStatus.value)
} else if (activeTab.value === 'completed') {
result = result.filter(t => t.status === 'stopped')
} }
if (searchQuery.value) { if (searchQuery.value) {
const query = searchQuery.value.toLowerCase() const query = searchQuery.value.toLowerCase()
@@ -63,7 +61,7 @@ const filteredTasks = computed(() => {
const getTaskCount = (status: string) => { const getTaskCount = (status: string) => {
if (status === 'running') return tasks.value.filter(t => t.status === 'running').length 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 return tasks.value.length
} }
@@ -101,28 +99,11 @@ const getStatusClass = (status: string) => {
class="search-input w-full" class="search-input w-full"
> >
</div> </div>
</div> <el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
<el-option label="All Status" value="all" />
<!-- Tab Navigation --> <el-option label="Running" value="running" />
<div class="flex gap-6 mb-4"> <el-option label="Stopped" value="stopped" />
<button </el-select>
: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>
</div> </div>
<!-- Task List Table --> <!-- Task List Table -->

View File

@@ -1,26 +1,29 @@
<script setup lang="ts"> <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 { interface Script {
id: number id: number
name: string name: string
type: string type: string
description: string description: string
code: string
status: 'running' | 'stopped' status: 'running' | 'stopped'
createdAt: string createdAt: string
} }
// 模拟脚本数据 // 模拟脚本数据
const scripts = ref<Script[]>([ const scripts = ref<Script[]>([
{ id: 1, name: 'Data Processing', type: 'Python', description: 'Process and transform data', status: 'running', createdAt: '2025-04-10' }, { 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', status: 'stopped', createdAt: '2025-04-08' }, { 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', status: 'running', createdAt: '2025-04-05' }, { 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', status: 'stopped', createdAt: '2025-04-12' }, { id: 4, name: 'Data Sync', type: 'Python', description: 'Sync data between systems', code: '', status: 'stopped', createdAt: '2025-04-12' },
]) ])
const searchQuery = ref('') const searchQuery = ref('')
const filterStatus = ref('all') const filterStatus = ref('all')
const isCreating = ref(false) const isCreating = ref(false)
const isCreatingCode = ref(false)
const isEditing = ref(false) const isEditing = ref(false)
const editingScript = ref<Script | null>(null) const editingScript = ref<Script | null>(null)
@@ -28,6 +31,90 @@ const newScriptForm = ref({
name: '', name: '',
type: 'Python', type: 'Python',
description: '', 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: '', name: '',
type: 'Python', type: 'Python',
description: '', description: '',
code: '',
} }
isCreating.value = true isCreating.value = true
} }
@@ -94,6 +182,19 @@ const closeCreate = () => {
isCreating.value = false 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 saveNewScript = () => {
const newId = Math.max(...scripts.value.map(s => s.id), 0) + 1 const newId = Math.max(...scripts.value.map(s => s.id), 0) + 1
scripts.value.push({ scripts.value.push({
@@ -101,10 +202,11 @@ const saveNewScript = () => {
name: newScriptForm.value.name || 'Untitled Script', name: newScriptForm.value.name || 'Untitled Script',
type: newScriptForm.value.type, type: newScriptForm.value.type,
description: newScriptForm.value.description, description: newScriptForm.value.description,
code: newScriptForm.value.code,
status: 'stopped', status: 'stopped',
createdAt: new Date().toISOString().split('T')[0], createdAt: new Date().toISOString().split('T')[0],
}) })
isCreating.value = false isCreatingCode.value = false
} }
</script> </script>
@@ -254,6 +356,43 @@ const saveNewScript = () => {
> >
Cancel Cancel
</button> </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 <button
@click="saveNewScript" @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" 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"

View File

@@ -1,12 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, computed } from 'vue' import { onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useModelSettings } from './settings/useModelSettings' import { useModelSettings } from './settings/useModelSettings'
import FormDialog from '@/components/FormDialog.vue' import FormDialog from '@/components/FormDialog.vue'
// 当前选中的设置菜单
const activeMenu = ref('general')
// 导入 Model Settings 逻辑 // 导入 Model Settings 逻辑
const { const {
models, models,
@@ -32,265 +28,43 @@ const {
testConnectionEdit, testConnectionEdit,
} = useModelSettings() } = useModelSettings()
// 监听菜单切换,获取模型列表 // 页面加载时获取模型列表
watch(activeMenu, (newVal) => { onMounted(() => {
if (newVal === 'modelSettings') { fetchModels()
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> </script>
<template> <template>
<div class="p-6 min-h-screen"> <div class="p-6 min-h-screen">
<!-- 页面标题 --> <!-- 页面标题 -->
<div class="flex items-center gap-2 mb-6"> <div class="flex items-center gap-2 mb-6">
<i class="fa-solid fa-gear text-orange-500"></i> <i class="fa-solid fa-brain text-orange-500"></i>
<span class="font-medium">Settings</span> <span class="font-medium">Models</span>
</div> </div>
<div class="flex gap-6"> <!-- 内容区域 -->
<!-- 左侧菜单 --> <div class="space-y-4">
<nav class="w-48 flex-shrink-0"> <!-- Models 内容 -->
<ul class="space-y-1"> <div class="flex items-center justify-between mb-6">
<li <div>
v-for="item in menuItems" <p class="text-sm text-gray-400 mt-1">Configure AI models</p>
: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="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> </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>
Add New Model
</button>
</div>
<!-- 模型列表 -->
<!-- Members 设置 --> <div v-if="modelsLoading" class="py-12 text-center text-gray-500">
<div v-if="activeMenu === 'members'"> <i class="fa-solid fa-spinner fa-spin text-2xl"></i>
<div class="flex items-center justify-between mb-6"> </div>
<div> <div v-else class="bg-dark-700 rounded-xl overflow-hidden">
<h2 class="text-xl font-semibold">Members</h2> <table class="w-full">
<p class="text-sm text-gray-400 mt-1">Manage team members</p> <thead class="bg-dark-600">
</div> <tr>
</div> <th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model Name</th>
</div> <th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Provider</th>
<!-- 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>
</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>
Add New Model
</button>
</div>
<!-- 模型列表 -->
<div v-if="modelsLoading" class="py-12 text-center text-gray-500">
<i class="fa-solid fa-spinner fa-spin text-2xl"></i>
</div>
<div v-else 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">Model Name</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Provider</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model</th> <th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model Type</th> <th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model Type</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Base URL</th> <th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Base URL</th>
@@ -532,143 +306,6 @@ const clearLogs = () => {
</button> </button>
</template> </template>
</FormDialog> </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>
</div> </div>
</template> </template>

View File

@@ -1,8 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useAuth } from '@/composables/useAuth'
const router = useRouter() const router = useRouter()
const { register } = useAuth()
// 表单数据 // 表单数据
const username = ref('') const username = ref('')
@@ -14,6 +16,7 @@ const isLoading = ref(false)
const showPassword = ref(false) const showPassword = ref(false)
const showConfirmPassword = ref(false) const showConfirmPassword = ref(false)
const errorMsg = ref('') const errorMsg = ref('')
const successMsg = ref('')
// 表单验证 // 表单验证
const validateForm = () => { const validateForm = () => {
@@ -24,11 +27,6 @@ const validateForm = () => {
return false 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) { if (!password.value || password.value.length < 6) {
errorMsg.value = 'Password must be at least 6 characters' errorMsg.value = 'Password must be at least 6 characters'
return false return false
@@ -48,21 +46,34 @@ const validateForm = () => {
} }
// 注册处理 // 注册处理
const handleSignup = () => { const handleSignup = async () => {
if (!validateForm()) return if (!validateForm()) return
isLoading.value = true isLoading.value = true
// 模拟注册 errorMsg.value = ''
setTimeout(() => { successMsg.value = ''
try {
await register({
username: username.value,
password: password.value,
email: email.value || undefined,
})
// 注册成功
successMsg.value = 'Account created successfully! Redirecting to login...'
setTimeout(() => {
router.push('/?registered=true')
}, 1500)
} catch (error: any) {
errorMsg.value = error.message || 'Registration failed, please try again'
} finally {
isLoading.value = false isLoading.value = false
// 注册成功,跳转到登录页 }
router.push('/')
}, 1500)
} }
// 跳转到登录 // 跳转到登录
const goToLogin = () => { const goToLogin = () => {
router.push('/') router.push('/?registered=true')
} }
</script> </script>
@@ -87,6 +98,12 @@ const goToLogin = () => {
<!-- 注册表单 --> <!-- 注册表单 -->
<div class="bg-dark-700 rounded-2xl p-8 shadow-xl border border-dark-500/50"> <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"> <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> <i class="fa-solid fa-circle-exclamation"></i>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { useSkills } from './skill/useSkills' import { useSkills } from './skill/useSkills'
import { Play, Pause, Edit, Trash2 } from 'lucide-vue-next'
import '@/views/database/database.css' import '@/views/database/database.css'
const { const {
@@ -98,13 +99,13 @@ const {
<td class="px-5 py-4"> <td class="px-5 py-4">
<div class="flex items-center justify-center gap-2"> <div class="flex items-center justify-center gap-2">
<button @click="toggleStatus(skill)" class="btn-icon" :title="skill.status === 'running' ? 'Stop' : 'Start'"> <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>
<button @click="openEdit(skill)" class="btn-icon" title="Edit"> <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>
<button @click="deleteSkill(skill.id)" class="btn-icon" title="Delete"> <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> </button>
</div> </div>
</td> </td>
@@ -192,41 +193,26 @@ const {
</button> </button>
</div> </div>
<div class="p-5 space-y-4"> <div class="p-5 space-y-4 max-h-[60vh] overflow-y-auto">
<div> <div>
<label class="block text-sm font-medium text-gray-300 mb-2">Skill Name</label> <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"> <input v-model="newSkillForm.name" type="text" placeholder="Enter skill name..." class="input-field">
</div> </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> <div>
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label> <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> <textarea v-model="newSkillForm.description" rows="2" placeholder="Describe this skill..." class="input-field resize-none"></textarea>
</div> </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&#10;&#10;Describe your skill here using Markdown..." class="input-field resize-none font-mono text-sm"></textarea>
</div>
</div> </div>
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-600 bg-dark-700/50"> <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="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> </div>
</div> </div>

View File

@@ -1,42 +1,88 @@
<script setup lang="ts"> <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' import '@/views/database/database.css'
interface Tool { // 使用工具 composable
id: number const { tools, toolsLoading, fetchTools, syncTools, deleteTool: deleteToolApi } = useTools()
name: string
type: 'built-in' | 'custom' | 'mcp' // 图标组件映射
description: string const iconComponents: Record<string, any> = {
status: 'active' | 'inactive' FileText,
icon?: string Globe,
provider?: string Calculator,
config?: object Code,
createdAt: string Braces,
Github,
MessageSquare,
Mail,
Database,
Folder,
GitBranch,
Box,
Wrench,
Server,
Terminal,
} }
// Mock data for tools const getIconComponent = (iconName?: string) => {
const builtInTools = ref<Tool[]>([ if (!iconName) return Box
{ id: 1, name: 'File Reader', type: 'built-in', description: 'Read files from the filesystem', status: 'active', icon: 'fa-file-lines', createdAt: '2025-01-15' }, return iconComponents[iconName] || Box
{ 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 customTools = ref<Tool[]>([ interface 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: string
{ id: 102, name: 'Slack Notifier', type: 'custom', description: 'Send notifications to Slack', status: 'inactive', icon: 'fa-slack', provider: 'Custom', createdAt: '2025-03-05' }, name: string
{ id: 103, name: 'Email Sender', type: 'custom', description: 'Send emails via SMTP', status: 'active', icon: 'fa-envelope', provider: 'Custom', createdAt: '2025-03-08' }, 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' }, onMounted(async () => {
{ 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' }, await fetchTools()
{ 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' },
])
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 searchQuery = ref('')
const filterStatus = ref('all') const filterStatus = ref('all')
const editingTool = ref<Tool | null>(null) const editingTool = ref<Tool | null>(null)
@@ -50,21 +96,19 @@ const editForm = ref({
// Statistics // Statistics
const stats = computed(() => ({ const stats = computed(() => ({
total: builtInTools.value.length + customTools.value.length + mcpTools.value.length, total: tools.value.length,
active: [...builtInTools.value, ...customTools.value, ...mcpTools.value].filter(t => t.status === 'active').length, active: tools.value.filter(t => t.status === 'active').length,
builtIn: builtInTools.value.length, builtIn: builtInTools.value.length,
custom: customTools.value.length,
mcp: mcpTools.value.length mcp: mcpTools.value.length
})) }))
const currentTools = computed(() => { const currentTools = computed(() => {
let tools: Tool[] = [] let toolsList: Tool[] = []
switch (activeTab.value) { switch (activeTab.value) {
case 'built-in': tools = builtInTools.value; break case 'built-in': toolsList = builtInTools.value as Tool[]; break
case 'custom': tools = customTools.value; break case 'mcp': toolsList = mcpTools.value as Tool[]; break
case 'mcp': tools = mcpTools.value; break
} }
return tools.filter(tool => { return toolsList.filter(tool => {
const matchSearch = tool.name.toLowerCase().includes(searchQuery.value.toLowerCase()) || const matchSearch = tool.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
tool.description.toLowerCase().includes(searchQuery.value.toLowerCase()) tool.description.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchStatus = filterStatus.value === 'all' || tool.status === filterStatus.value const matchStatus = filterStatus.value === 'all' || tool.status === filterStatus.value
@@ -74,64 +118,64 @@ const currentTools = computed(() => {
const tabCounts = computed(() => ({ const tabCounts = computed(() => ({
'built-in': builtInTools.value.length, 'built-in': builtInTools.value.length,
'custom': customTools.value.length,
'mcp': mcpTools.value.length, 'mcp': mcpTools.value.length,
})) }))
const openEdit = (tool: Tool) => { const openEdit = (tool: any) => {
editingTool.value = tool editingTool.value = tool
editForm.value = { name: tool.name, description: tool.description, provider: tool.provider || '' } editForm.value = { name: tool.name, description: tool.description, provider: tool.provider || '' }
isEditing.value = true isEditing.value = true
} }
const saveEdit = () => { 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 isEditing.value = false
} }
const cancelEdit = () => { isEditing.value = false; editingTool.value = null } const cancelEdit = () => { isEditing.value = false; editingTool.value = null }
const toggleStatus = (tool: Tool) => { const toggleStatus = async (tool: any) => {
const targetArray = tool.type === 'built-in' ? builtInTools.value : const newStatus = tool.status === 'active' ? 'inactive' : 'active'
tool.type === 'custom' ? customTools.value : mcpTools.value // TODO: 调用 API 更新状态
const found = targetArray.find(t => t.id === tool.id)
if (found) found.status = found.status === 'active' ? 'inactive' : 'active'
} }
const deleteTool = (id: number) => { const handleDeleteTool = async (id: string) => {
switch (activeTab.value) { if (confirm('Are you sure you want to delete this tool?')) {
case 'built-in': builtInTools.value = builtInTools.value.filter(t => t.id !== id); break await deleteToolApi(id)
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 handleSyncTools = async () => {
await syncTools()
}
</script> </script>
<template> <template>
<div class="p-6 min-h-screen"> <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"> <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> <span class="font-medium">Tools</span>
</div> </div>
<button class="btn-primary"> <div class="h-10 flex gap-2">
<i class="fa-solid fa-plus"></i> <button v-if="activeTab === 'built-in'" @click="handleSyncTools" class="btn-secondary">
New Tool <i class="fa-solid fa-sync mr-1"></i>
</button> Sync Tools
</button>
<button v-if="activeTab === 'mcp'" class="btn-primary">
<Plus class="w-4 h-4 mr-1" />
Add MCP
</button>
</div>
</div> </div>
<!-- Tab 导航 --> <!-- Tab 导航 -->
<div class="flex items-center gap-2 mb-6"> <div class="flex items-center gap-2 mb-6">
<button <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" :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="px-4 py-2 rounded-lg font-medium flex items-center gap-2 transition-all"
:class="activeTab === tab :class="activeTab === tab
? 'bg-orange-500 text-white' ? 'bg-orange-500 text-white'
@@ -147,12 +191,12 @@ const deleteTool = (id: number) => {
<!-- 搜索和筛选 --> <!-- 搜索和筛选 -->
<div class="flex gap-4 mb-6"> <div class="flex gap-4 mb-6">
<div class="flex-1 relative"> <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 <input
v-model="searchQuery" v-model="searchQuery"
type="text" type="text"
placeholder="Search tools by name or description..." placeholder="Search tools by name or description..."
class="search-input w-full" class="search-input w-full pl-10"
> >
</div> </div>
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large"> <el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
@@ -164,7 +208,11 @@ const deleteTool = (id: number) => {
<!-- Tools 列表 --> <!-- Tools 列表 -->
<div class="bg-dark-700 rounded-xl overflow-hidden"> <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"> <thead class="bg-dark-600">
<tr> <tr>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Tool Name</th> <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"> <tr v-for="tool in currentTools" :key="tool.id" class="table-row">
<td class="px-5 py-4"> <td class="px-5 py-4">
<div class="flex items-center gap-3"> <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'"> <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'">
<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> <component :is="getIconComponent(tool.icon)" class="w-5 h-5" :class="tool.type === 'built-in' ? 'text-orange-400' : 'text-emerald-400'" />
</div> </div>
<div> <div>
<div class="font-medium">{{ tool.name }}</div> <div class="font-medium">{{ tool.name }}</div>
@@ -188,7 +236,7 @@ const deleteTool = (id: number) => {
</div> </div>
</td> </td>
<td class="px-5 py-4 text-center"> <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>
<td class="px-5 py-4 text-center"> <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'"> <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 class="capitalize">{{ tool.status }}</span>
</span> </span>
</td> </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"> <td class="px-5 py-4">
<div class="flex items-center justify-center gap-2"> <div class="flex items-center justify-center gap-2">
<button @click="toggleStatus(tool)" class="btn-icon" :title="tool.status === 'active' ? 'Deactivate' : 'Activate'"> <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>
<button @click="openEdit(tool)" class="btn-icon" title="Edit"> <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>
<button @click="deleteTool(tool.id)" class="btn-icon" title="Delete"> <button @click="handleDeleteTool(tool.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> </button>
</div> </div>
</td> </td>
@@ -217,10 +265,10 @@ const deleteTool = (id: number) => {
<!-- 空状态 --> <!-- 空状态 -->
<div v-else class="empty-box"> <div v-else class="empty-box">
<div class="empty-icon"> <div class="empty-icon">
<i class="fa-solid fa-tools"></i> <Wrench class="w-12 h-12" />
</div> </div>
<p class="empty-text">No tools found</p> <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>
</div> </div>
@@ -231,7 +279,7 @@ const deleteTool = (id: number) => {
<div class="flex items-center justify-between p-5 border-b border-dark-500"> <div class="flex items-center justify-between p-5 border-b border-dark-500">
<h3 class="text-lg font-semibold">Edit Tool</h3> <h3 class="text-lg font-semibold">Edit Tool</h3>
<button @click="cancelEdit" class="btn-icon"> <button @click="cancelEdit" class="btn-icon">
<i class="fa-solid fa-xmark text-xl"></i> <X class="w-5 h-5" />
</button> </button>
</div> </div>

View File

@@ -46,10 +46,8 @@ export function useSkills() {
const newSkillForm = ref({ const newSkillForm = ref({
name: '', name: '',
type: 'API',
category: 'api',
port: 3000,
description: '', description: '',
markdown: '',
}) })
// 分类选项 // 分类选项
@@ -85,7 +83,7 @@ export function useSkills() {
// 打开创建弹窗 // 打开创建弹窗
const openCreate = () => { const openCreate = () => {
newSkillForm.value = { name: '', type: 'API', category: 'api', port: 3000, description: '' } newSkillForm.value = { name: '', description: '', markdown: '' }
isCreating.value = true isCreating.value = true
} }
@@ -101,10 +99,10 @@ export function useSkills() {
id: newId, id: newId,
name: newSkillForm.value.name, name: newSkillForm.value.name,
description: newSkillForm.value.description, description: newSkillForm.value.description,
type: newSkillForm.value.type, type: 'Custom',
category: newSkillForm.value.category, category: 'custom',
status: 'stopped', status: 'stopped',
port: newSkillForm.value.port, port: 3000,
createdAt: new Date().toISOString().split('T')[0], createdAt: new Date().toISOString().split('T')[0],
tools: 0, tools: 0,
}) })

View 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,
}
}

Some files were not shown because too many files have changed in this diff Show More