bohr-agent-sdk 0.1.101__py3-none-any.whl → 0.1.103__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bohr_agent_sdk-0.1.103.dist-info/METADATA +292 -0
- bohr_agent_sdk-0.1.103.dist-info/RECORD +80 -0
- dp/agent/cli/cli.py +126 -25
- dp/agent/cli/templates/__init__.py +1 -0
- dp/agent/cli/templates/calculation/simple.py.template +15 -0
- dp/agent/cli/templates/device/tescan_device.py.template +158 -0
- dp/agent/cli/templates/main.py.template +67 -0
- dp/agent/cli/templates/ui/__init__.py +1 -0
- dp/agent/cli/templates/ui/api/__init__.py +1 -0
- dp/agent/cli/templates/ui/api/config.py +32 -0
- dp/agent/cli/templates/ui/api/constants.py +61 -0
- dp/agent/cli/templates/ui/api/debug.py +257 -0
- dp/agent/cli/templates/ui/api/files.py +469 -0
- dp/agent/cli/templates/ui/api/files_upload.py +115 -0
- dp/agent/cli/templates/ui/api/files_user.py +50 -0
- dp/agent/cli/templates/ui/api/messages.py +161 -0
- dp/agent/cli/templates/ui/api/projects.py +146 -0
- dp/agent/cli/templates/ui/api/sessions.py +93 -0
- dp/agent/cli/templates/ui/api/utils.py +161 -0
- dp/agent/cli/templates/ui/api/websocket.py +184 -0
- dp/agent/cli/templates/ui/config/__init__.py +1 -0
- dp/agent/cli/templates/ui/config/agent_config.py +257 -0
- dp/agent/cli/templates/ui/frontend/index.html +13 -0
- dp/agent/cli/templates/ui/frontend/package.json +46 -0
- dp/agent/cli/templates/ui/frontend/tsconfig.json +26 -0
- dp/agent/cli/templates/ui/frontend/tsconfig.node.json +10 -0
- dp/agent/cli/templates/ui/frontend/ui-static/assets/index-DdAmKhul.js +105 -0
- dp/agent/cli/templates/ui/frontend/ui-static/assets/index-DfN2raU9.css +1 -0
- dp/agent/cli/templates/ui/frontend/ui-static/index.html +14 -0
- dp/agent/cli/templates/ui/frontend/vite.config.ts +37 -0
- dp/agent/cli/templates/ui/scripts/build_ui.py +56 -0
- dp/agent/cli/templates/ui/server/__init__.py +0 -0
- dp/agent/cli/templates/ui/server/app.py +98 -0
- dp/agent/cli/templates/ui/server/connection.py +210 -0
- dp/agent/cli/templates/ui/server/file_watcher.py +85 -0
- dp/agent/cli/templates/ui/server/middleware.py +43 -0
- dp/agent/cli/templates/ui/server/models.py +53 -0
- dp/agent/cli/templates/ui/server/session_manager.py +1158 -0
- dp/agent/cli/templates/ui/server/user_files.py +85 -0
- dp/agent/cli/templates/ui/server/utils.py +50 -0
- dp/agent/cli/templates/ui/test_download.py +98 -0
- dp/agent/cli/templates/ui/ui_utils.py +260 -0
- dp/agent/cli/templates/ui/websocket-server.py +87 -0
- dp/agent/server/storage/http_storage.py +1 -1
- bohr_agent_sdk-0.1.101.dist-info/METADATA +0 -224
- bohr_agent_sdk-0.1.101.dist-info/RECORD +0 -40
- {bohr_agent_sdk-0.1.101.dist-info → bohr_agent_sdk-0.1.103.dist-info}/WHEEL +0 -0
- {bohr_agent_sdk-0.1.101.dist-info → bohr_agent_sdk-0.1.103.dist-info}/entry_points.txt +0 -0
- {bohr_agent_sdk-0.1.101.dist-info → bohr_agent_sdk-0.1.103.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User file manager
|
|
3
|
+
"""
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UserFileManager:
|
|
8
|
+
"""Manage user-specific file directories"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, base_dir: str, sessions_dir: str = ".agent_sessions"):
|
|
11
|
+
self.base_dir = Path(base_dir)
|
|
12
|
+
# Support custom sessions directory, can be absolute or relative path
|
|
13
|
+
sessions_path = Path(sessions_dir)
|
|
14
|
+
if sessions_path.is_absolute():
|
|
15
|
+
self.sessions_dir = sessions_path
|
|
16
|
+
else:
|
|
17
|
+
self.sessions_dir = self.base_dir / sessions_dir
|
|
18
|
+
self.user_sessions_dir = self.sessions_dir / "user_sessions"
|
|
19
|
+
self.temp_sessions_dir = self.sessions_dir / "temp_sessions"
|
|
20
|
+
|
|
21
|
+
# Ensure directories exist
|
|
22
|
+
self.user_sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
self.temp_sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
|
|
25
|
+
def _get_user_dir(self, user_id: str) -> str:
|
|
26
|
+
"""Get user directory name"""
|
|
27
|
+
# Use user_id directly as directory name
|
|
28
|
+
return user_id
|
|
29
|
+
|
|
30
|
+
def get_user_files_dir(self, user_id: str) -> Path:
|
|
31
|
+
"""Get user's file directory
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
user_id: User's unique identifier (Bohrium user_id or temporary user ID)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
User's file directory path
|
|
38
|
+
"""
|
|
39
|
+
if not user_id:
|
|
40
|
+
# If no user_id, use default directory
|
|
41
|
+
user_files_dir = self.temp_sessions_dir / "default" / "files"
|
|
42
|
+
elif user_id.startswith("user_"):
|
|
43
|
+
# Temporary user (generated ID starts with user_)
|
|
44
|
+
user_files_dir = self.temp_sessions_dir / user_id / "files"
|
|
45
|
+
else:
|
|
46
|
+
# Registered user (Bohrium user_id)
|
|
47
|
+
user_dir_name = self._get_user_dir(user_id)
|
|
48
|
+
user_files_dir = self.user_sessions_dir / user_dir_name / "files"
|
|
49
|
+
|
|
50
|
+
# Ensure directory exists
|
|
51
|
+
user_files_dir.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
|
|
53
|
+
# Create default output subdirectory
|
|
54
|
+
output_dir = user_files_dir / "output"
|
|
55
|
+
output_dir.mkdir(exist_ok=True)
|
|
56
|
+
|
|
57
|
+
return user_files_dir
|
|
58
|
+
|
|
59
|
+
def cleanup_temp_files(self, max_age_days: int = 7):
|
|
60
|
+
"""Clean up expired temporary user files
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
max_age_days: Maximum days to keep files
|
|
64
|
+
"""
|
|
65
|
+
import time
|
|
66
|
+
import shutil
|
|
67
|
+
|
|
68
|
+
current_time = time.time()
|
|
69
|
+
max_age_seconds = max_age_days * 24 * 60 * 60
|
|
70
|
+
|
|
71
|
+
# Only clean temporary sessions directory
|
|
72
|
+
if not self.temp_sessions_dir.exists():
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
for session_dir in self.temp_sessions_dir.iterdir():
|
|
76
|
+
if not session_dir.is_dir():
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
# Check directory modification time
|
|
80
|
+
dir_mtime = session_dir.stat().st_mtime
|
|
81
|
+
if current_time - dir_mtime > max_age_seconds:
|
|
82
|
+
try:
|
|
83
|
+
shutil.rmtree(session_dir)
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
from typing import Tuple
|
|
6
|
+
from http.cookies import SimpleCookie
|
|
7
|
+
import socket
|
|
8
|
+
|
|
9
|
+
def get_ak_info_from_request(headers) -> Tuple[str, str]:
|
|
10
|
+
"""Extract AK info from request headers
|
|
11
|
+
|
|
12
|
+
Priority:
|
|
13
|
+
1. Get from Cookie (production environment)
|
|
14
|
+
2. Get from environment variables (development debugging)
|
|
15
|
+
3. Return empty string to allow user custom input (commented out restriction)
|
|
16
|
+
"""
|
|
17
|
+
# First try to get from cookie
|
|
18
|
+
cookie_header = headers.get("cookie", "")
|
|
19
|
+
if cookie_header:
|
|
20
|
+
simple_cookie = SimpleCookie()
|
|
21
|
+
simple_cookie.load(cookie_header)
|
|
22
|
+
|
|
23
|
+
access_key = ""
|
|
24
|
+
app_key = ""
|
|
25
|
+
|
|
26
|
+
if "appAccessKey" in simple_cookie:
|
|
27
|
+
access_key = simple_cookie["appAccessKey"].value
|
|
28
|
+
if "clientName" in simple_cookie:
|
|
29
|
+
app_key = simple_cookie["clientName"].value
|
|
30
|
+
|
|
31
|
+
# If got valid values from cookie, return directly
|
|
32
|
+
if access_key or app_key:
|
|
33
|
+
return access_key, app_key
|
|
34
|
+
|
|
35
|
+
# If not in cookie, try to get from environment variables (for development debugging)
|
|
36
|
+
access_key = os.environ.get("BOHR_ACCESS_KEY", "")
|
|
37
|
+
app_key = os.environ.get("BOHR_APP_KEY", "")
|
|
38
|
+
|
|
39
|
+
return access_key, app_key
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def check_port_available(port: int) -> bool:
|
|
43
|
+
"""Check if port is available"""
|
|
44
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
45
|
+
try:
|
|
46
|
+
sock.bind(('', port))
|
|
47
|
+
sock.close()
|
|
48
|
+
return True
|
|
49
|
+
except OSError:
|
|
50
|
+
return False
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""测试文件下载功能"""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
import tempfile
|
|
8
|
+
import requests
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
def test_download_api():
|
|
12
|
+
"""测试下载 API"""
|
|
13
|
+
|
|
14
|
+
# API 基础地址
|
|
15
|
+
base_url = "http://localhost:8001"
|
|
16
|
+
|
|
17
|
+
print("=" * 60)
|
|
18
|
+
print("文件下载功能测试")
|
|
19
|
+
print("=" * 60)
|
|
20
|
+
|
|
21
|
+
# 1. 创建测试文件
|
|
22
|
+
test_dir = Path(tempfile.gettempdir()) / "download_test"
|
|
23
|
+
test_dir.mkdir(exist_ok=True)
|
|
24
|
+
|
|
25
|
+
# 创建测试文件
|
|
26
|
+
test_file = test_dir / "test.txt"
|
|
27
|
+
test_file.write_text("This is a test file for download functionality.\n测试文件下载功能。")
|
|
28
|
+
|
|
29
|
+
test_json = test_dir / "data.json"
|
|
30
|
+
test_json.write_text('{"name": "test", "value": 123}')
|
|
31
|
+
|
|
32
|
+
# 创建子目录和文件
|
|
33
|
+
sub_dir = test_dir / "subdir"
|
|
34
|
+
sub_dir.mkdir(exist_ok=True)
|
|
35
|
+
(sub_dir / "file1.txt").write_text("File 1 content")
|
|
36
|
+
(sub_dir / "file2.txt").write_text("File 2 content")
|
|
37
|
+
|
|
38
|
+
print(f"✅ 测试文件已创建在: {test_dir}")
|
|
39
|
+
print()
|
|
40
|
+
|
|
41
|
+
# 2. 测试单文件下载
|
|
42
|
+
print("测试单文件下载...")
|
|
43
|
+
try:
|
|
44
|
+
# 模拟文件下载请求
|
|
45
|
+
file_path = str(test_file)
|
|
46
|
+
download_url = f"{base_url}/api/download/file{file_path}"
|
|
47
|
+
|
|
48
|
+
print(f" 下载 URL: {download_url}")
|
|
49
|
+
print(f" 预期结果: 文件应该可以正常下载")
|
|
50
|
+
print()
|
|
51
|
+
|
|
52
|
+
# 注意:实际测试需要运行服务器并通过浏览器或 curl 测试
|
|
53
|
+
print(" 💡 请在浏览器中打开以下链接测试下载:")
|
|
54
|
+
print(f" {download_url}")
|
|
55
|
+
print()
|
|
56
|
+
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print(f" ❌ 错误: {e}")
|
|
59
|
+
print()
|
|
60
|
+
|
|
61
|
+
# 3. 测试文件夹下载
|
|
62
|
+
print("测试文件夹下载...")
|
|
63
|
+
try:
|
|
64
|
+
# 模拟文件夹下载请求
|
|
65
|
+
folder_path = str(sub_dir)
|
|
66
|
+
download_url = f"{base_url}/api/download/folder{folder_path}"
|
|
67
|
+
|
|
68
|
+
print(f" 下载 URL: {download_url}")
|
|
69
|
+
print(f" 预期结果: 文件夹应打包为 zip 下载")
|
|
70
|
+
print()
|
|
71
|
+
|
|
72
|
+
print(" 💡 请在浏览器中打开以下链接测试下载:")
|
|
73
|
+
print(f" {download_url}")
|
|
74
|
+
print()
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
print(f" ❌ 错误: {e}")
|
|
78
|
+
print()
|
|
79
|
+
|
|
80
|
+
# 4. 测试说明
|
|
81
|
+
print("=" * 60)
|
|
82
|
+
print("测试说明:")
|
|
83
|
+
print("1. 确保 Agent UI 服务正在运行 (端口 8001)")
|
|
84
|
+
print("2. 在浏览器中打开文件浏览器")
|
|
85
|
+
print("3. 测试以下功能:")
|
|
86
|
+
print(" - 点击文件旁的下载图标下载单个文件")
|
|
87
|
+
print(" - 点击文件夹旁的下载图标下载整个文件夹(zip格式)")
|
|
88
|
+
print(" - 在文件预览界面点击下载按钮")
|
|
89
|
+
print("4. 验证下载的文件内容是否正确")
|
|
90
|
+
print("=" * 60)
|
|
91
|
+
|
|
92
|
+
# 清理测试文件(可选)
|
|
93
|
+
# import shutil
|
|
94
|
+
# shutil.rmtree(test_dir)
|
|
95
|
+
# print(f"\n✅ 测试文件已清理")
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
test_download_api()
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import subprocess
|
|
4
|
+
import signal
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, Any, Optional, List
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UIConfigManager:
|
|
13
|
+
"""管理 UI 配置的工具类"""
|
|
14
|
+
|
|
15
|
+
DEFAULT_CONFIG = {
|
|
16
|
+
"agent": {
|
|
17
|
+
"module": "agent", # 用户必须提供具体的模块路径
|
|
18
|
+
"rootAgent": "root_agent",
|
|
19
|
+
"name": "DP Agent Assistant",
|
|
20
|
+
"description": "AI Assistant powered by DP Agent SDK",
|
|
21
|
+
"welcomeMessage": "欢迎使用 DP Agent Assistant!我可以帮助您进行科学计算、数据分析等任务。",
|
|
22
|
+
},
|
|
23
|
+
"ui": {
|
|
24
|
+
"title": "DP Agent Assistant"
|
|
25
|
+
},
|
|
26
|
+
"server": {
|
|
27
|
+
"port": int(os.environ.get('AGENT_SERVER_PORT', '50002')),
|
|
28
|
+
"host": ["*"] # 默认允许所有主机访问
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def __init__(self, config_path: Optional[str] = None):
|
|
33
|
+
self.config_path = Path(config_path) if config_path else Path.cwd() / "agent-config.json"
|
|
34
|
+
self.config = self._load_config()
|
|
35
|
+
|
|
36
|
+
def _load_config(self) -> Dict[str, Any]:
|
|
37
|
+
"""加载配置文件,如果不存在则使用默认配置"""
|
|
38
|
+
if self.config_path.exists():
|
|
39
|
+
with open(self.config_path, 'r') as f:
|
|
40
|
+
user_config = json.load(f)
|
|
41
|
+
# 深度合并用户配置和默认配置
|
|
42
|
+
return self._deep_merge(self.DEFAULT_CONFIG.copy(), user_config)
|
|
43
|
+
return self.DEFAULT_CONFIG.copy()
|
|
44
|
+
|
|
45
|
+
def _deep_merge(self, base: Dict, update: Dict) -> Dict:
|
|
46
|
+
"""深度合并两个字典"""
|
|
47
|
+
for key, value in update.items():
|
|
48
|
+
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
|
|
49
|
+
base[key] = self._deep_merge(base[key], value)
|
|
50
|
+
else:
|
|
51
|
+
base[key] = value
|
|
52
|
+
return base
|
|
53
|
+
|
|
54
|
+
def save_config(self, config_path: Optional[Path] = None):
|
|
55
|
+
"""保存配置到文件"""
|
|
56
|
+
save_path = config_path or self.config_path
|
|
57
|
+
with open(save_path, 'w') as f:
|
|
58
|
+
json.dump(self.config, f, indent=2)
|
|
59
|
+
|
|
60
|
+
def update_from_cli(self, **kwargs):
|
|
61
|
+
"""从命令行参数更新配置"""
|
|
62
|
+
if kwargs.get('agent'):
|
|
63
|
+
module, _, variable = kwargs['agent'].partition(':')
|
|
64
|
+
self.config['agent']['module'] = module
|
|
65
|
+
if variable:
|
|
66
|
+
self.config['agent']['rootAgent'] = variable
|
|
67
|
+
|
|
68
|
+
if kwargs.get('port'):
|
|
69
|
+
self.config['server']['port'] = kwargs['port']
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class UIProcessManager:
|
|
75
|
+
"""管理 UI 相关进程的工具类"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, ui_dir: Path, config: Dict[str, Any]):
|
|
78
|
+
self.ui_dir = ui_dir
|
|
79
|
+
self.config = config
|
|
80
|
+
self.processes: List[subprocess.Popen] = []
|
|
81
|
+
self._setup_signal_handlers()
|
|
82
|
+
|
|
83
|
+
def _setup_signal_handlers(self):
|
|
84
|
+
"""设置信号处理器以优雅关闭进程"""
|
|
85
|
+
signal.signal(signal.SIGINT, self._signal_handler)
|
|
86
|
+
signal.signal(signal.SIGTERM, self._signal_handler)
|
|
87
|
+
|
|
88
|
+
def _signal_handler(self, signum, frame):
|
|
89
|
+
"""处理终止信号"""
|
|
90
|
+
# Prevent multiple signal handling
|
|
91
|
+
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
92
|
+
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
|
93
|
+
self.cleanup()
|
|
94
|
+
# Don't exit here, let the main process handle it
|
|
95
|
+
|
|
96
|
+
def start_websocket_server(self):
|
|
97
|
+
"""启动 WebSocket 服务器"""
|
|
98
|
+
# 统一使用 server.port
|
|
99
|
+
server_port = self.config.get('server', {}).get('port', int(os.environ.get('AGENT_SERVER_PORT', '50002')))
|
|
100
|
+
|
|
101
|
+
websocket_script = self.ui_dir / "websocket-server.py"
|
|
102
|
+
if not websocket_script.exists():
|
|
103
|
+
raise FileNotFoundError(f"找不到 websocket-server.py: {websocket_script}")
|
|
104
|
+
|
|
105
|
+
# 设置环境变量
|
|
106
|
+
env = os.environ.copy()
|
|
107
|
+
env['AGENT_CONFIG_PATH'] = str(self.ui_dir / "config" / "agent-config.temp.json")
|
|
108
|
+
env['USER_WORKING_DIR'] = str(Path.cwd()) # 传递用户工作目录
|
|
109
|
+
env['UI_TEMPLATE_DIR'] = str(self.ui_dir) # 传递UI模板目录
|
|
110
|
+
# Ensure PYTHONPATH includes the user's working directory
|
|
111
|
+
if 'PYTHONPATH' in env:
|
|
112
|
+
env['PYTHONPATH'] = f"{str(Path.cwd())}:{env['PYTHONPATH']}"
|
|
113
|
+
else:
|
|
114
|
+
env['PYTHONPATH'] = str(Path.cwd())
|
|
115
|
+
|
|
116
|
+
# 静默启动,将输出重定向到日志文件
|
|
117
|
+
# 使用 'w' 模式清空旧日志
|
|
118
|
+
log_file = open(Path.cwd() / "websocket.log", "w")
|
|
119
|
+
process = subprocess.Popen(
|
|
120
|
+
[sys.executable, str(websocket_script)],
|
|
121
|
+
cwd=str(Path.cwd()), # 在用户工作目录运行
|
|
122
|
+
env=env,
|
|
123
|
+
stdout=log_file,
|
|
124
|
+
stderr=subprocess.STDOUT
|
|
125
|
+
)
|
|
126
|
+
self.processes.append(process)
|
|
127
|
+
|
|
128
|
+
# 等待服务器启动
|
|
129
|
+
time.sleep(2)
|
|
130
|
+
|
|
131
|
+
if process.poll() is not None:
|
|
132
|
+
raise RuntimeError("WebSocket 服务器启动失败")
|
|
133
|
+
|
|
134
|
+
click.echo(f"🚀 WebSocket 服务器已启动(端口 {server_port})")
|
|
135
|
+
click.echo("📝 查看日志: websocket.log")
|
|
136
|
+
|
|
137
|
+
return process
|
|
138
|
+
|
|
139
|
+
def start_frontend_server(self, dev_mode: bool = True):
|
|
140
|
+
"""启动前端服务器"""
|
|
141
|
+
server_port = self.config.get('server', {}).get('port', int(os.environ.get('AGENT_SERVER_PORT', '50002')))
|
|
142
|
+
|
|
143
|
+
ui_path = self.ui_dir / "frontend"
|
|
144
|
+
if not ui_path.exists():
|
|
145
|
+
raise FileNotFoundError(f"找不到 UI 目录: {ui_path}")
|
|
146
|
+
|
|
147
|
+
# 检查是否有构建好的静态文件
|
|
148
|
+
dist_path = ui_path / "ui-static"
|
|
149
|
+
if dist_path.exists() and not dev_mode:
|
|
150
|
+
# 生产模式:静态文件由 WebSocket 服务器提供
|
|
151
|
+
click.echo(f"✨ Agent UI 已启动: http://{os.environ.get('AGENT_HOST', 'localhost')}:{server_port}")
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
if not dev_mode and not dist_path.exists():
|
|
155
|
+
click.echo("警告: 未找到构建的静态文件,将使用开发模式")
|
|
156
|
+
dev_mode = True
|
|
157
|
+
|
|
158
|
+
# 检查是否已安装依赖
|
|
159
|
+
node_modules = ui_path / "node_modules"
|
|
160
|
+
if not node_modules.exists():
|
|
161
|
+
click.echo("检测到未安装前端依赖,正在安装...")
|
|
162
|
+
subprocess.run(["npm", "install"], cwd=str(ui_path), check=True)
|
|
163
|
+
|
|
164
|
+
# 设置环境变量
|
|
165
|
+
env = os.environ.copy()
|
|
166
|
+
# 开发模式下,前端开发服务器端口
|
|
167
|
+
frontend_dev_port = int(os.environ.get('FRONTEND_DEV_PORT', '3000'))
|
|
168
|
+
env['FRONTEND_PORT'] = str(frontend_dev_port)
|
|
169
|
+
# 告诉前端后端服务器在哪个端口
|
|
170
|
+
env['VITE_WS_PORT'] = str(server_port)
|
|
171
|
+
|
|
172
|
+
# 启动命令
|
|
173
|
+
if dev_mode:
|
|
174
|
+
cmd = ["npm", "run", "dev"]
|
|
175
|
+
click.echo(f"启动前端开发服务器...")
|
|
176
|
+
else:
|
|
177
|
+
cmd = ["npm", "run", "build"]
|
|
178
|
+
click.echo("构建前端生产版本...")
|
|
179
|
+
|
|
180
|
+
# 启动前端
|
|
181
|
+
log_file_path = Path.cwd() / "frontend.log"
|
|
182
|
+
with open(log_file_path, "a") as log_file:
|
|
183
|
+
process = subprocess.Popen(
|
|
184
|
+
cmd,
|
|
185
|
+
cwd=str(ui_path),
|
|
186
|
+
env=env,
|
|
187
|
+
stdout=log_file,
|
|
188
|
+
stderr=subprocess.STDOUT
|
|
189
|
+
)
|
|
190
|
+
self.processes.append(process)
|
|
191
|
+
|
|
192
|
+
# 等待服务器启动
|
|
193
|
+
time.sleep(3)
|
|
194
|
+
|
|
195
|
+
# 检查进程状态
|
|
196
|
+
if process.poll() is not None:
|
|
197
|
+
# 读取错误日志
|
|
198
|
+
with open(log_file_path, "r") as f:
|
|
199
|
+
error_log = f.read()
|
|
200
|
+
if "EADDRINUSE" in error_log:
|
|
201
|
+
raise RuntimeError(f"端口 {frontend_dev_port} 已被占用")
|
|
202
|
+
else:
|
|
203
|
+
raise RuntimeError(f"前端服务器启动失败,请查看 frontend.log 了解详情")
|
|
204
|
+
|
|
205
|
+
if dev_mode:
|
|
206
|
+
click.echo(f"\n✨ 前端开发服务器: http://localhost:{frontend_dev_port}")
|
|
207
|
+
click.echo(f"📡 后端服务器: http://localhost:{server_port}\n")
|
|
208
|
+
|
|
209
|
+
return process
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def wait_for_processes(self):
|
|
213
|
+
"""等待所有进程结束"""
|
|
214
|
+
try:
|
|
215
|
+
for process in self.processes:
|
|
216
|
+
if process: # 处理可能的 None
|
|
217
|
+
process.wait()
|
|
218
|
+
except KeyboardInterrupt:
|
|
219
|
+
# Don't handle here, let it bubble up to main
|
|
220
|
+
raise
|
|
221
|
+
|
|
222
|
+
def cleanup(self):
|
|
223
|
+
"""清理所有进程"""
|
|
224
|
+
if not self.processes:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
click.echo("\n🛑 正在停止所有进程...")
|
|
228
|
+
|
|
229
|
+
# First attempt to terminate all processes gracefully
|
|
230
|
+
for process in self.processes:
|
|
231
|
+
if process and process.poll() is None:
|
|
232
|
+
try:
|
|
233
|
+
process.terminate()
|
|
234
|
+
except:
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
# Give processes time to terminate gracefully
|
|
238
|
+
time.sleep(1)
|
|
239
|
+
|
|
240
|
+
# Force kill any remaining processes
|
|
241
|
+
for process in self.processes:
|
|
242
|
+
if process and process.poll() is None:
|
|
243
|
+
try:
|
|
244
|
+
if sys.platform == "win32":
|
|
245
|
+
# Windows specific kill
|
|
246
|
+
subprocess.run(["taskkill", "/F", "/PID", str(process.pid)], capture_output=True)
|
|
247
|
+
else:
|
|
248
|
+
# Unix-like systems
|
|
249
|
+
process.kill()
|
|
250
|
+
process.wait(timeout=1)
|
|
251
|
+
except:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
self.processes.clear()
|
|
255
|
+
|
|
256
|
+
# 等待端口释放
|
|
257
|
+
time.sleep(0.5)
|
|
258
|
+
click.echo("✅ 所有进程已停止")
|
|
259
|
+
|
|
260
|
+
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Agent WebSocket 服务器 - 主入口文件
|
|
4
|
+
使用 Session 运行 rootagent,并通过 WebSocket 与前端通信
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
# 忽略 paramiko 的加密算法弃用警告
|
|
12
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning, module="paramiko")
|
|
13
|
+
|
|
14
|
+
# Add user working directory to Python path first
|
|
15
|
+
user_working_dir = os.environ.get('USER_WORKING_DIR')
|
|
16
|
+
if user_working_dir and user_working_dir not in sys.path:
|
|
17
|
+
sys.path.insert(0, user_working_dir)
|
|
18
|
+
|
|
19
|
+
# Add UI template directory to Python path for config imports
|
|
20
|
+
ui_template_dir = os.environ.get('UI_TEMPLATE_DIR')
|
|
21
|
+
if ui_template_dir and ui_template_dir not in sys.path:
|
|
22
|
+
sys.path.insert(0, ui_template_dir)
|
|
23
|
+
|
|
24
|
+
import uvicorn
|
|
25
|
+
from server.app import create_app
|
|
26
|
+
from server.utils import check_port_available
|
|
27
|
+
from config.agent_config import agentconfig
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
print("🚀 启动 Agent WebSocket 服务器...")
|
|
32
|
+
|
|
33
|
+
# 统一使用 server 配置
|
|
34
|
+
server_config = agentconfig.config.get('server', {})
|
|
35
|
+
port = server_config.get('port', 8000)
|
|
36
|
+
# host 数组中的第一个作为显示用
|
|
37
|
+
hosts = server_config.get('host', ['localhost'])
|
|
38
|
+
display_host = hosts[0] if isinstance(hosts, list) else hosts
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# 创建应用
|
|
42
|
+
app = create_app()
|
|
43
|
+
|
|
44
|
+
print("📡 使用 Session 模式运行 rootagent")
|
|
45
|
+
print(f"🌐 服务器地址: http://{display_host}:{port}")
|
|
46
|
+
print(f"🔌 WebSocket 端点: ws://{display_host}:{port}/ws")
|
|
47
|
+
print("🛑 使用 Ctrl+C 优雅关闭服务器")
|
|
48
|
+
|
|
49
|
+
# uvicorn 始终监听 0.0.0.0 以支持所有配置的主机
|
|
50
|
+
uvicorn.run(
|
|
51
|
+
app,
|
|
52
|
+
host="0.0.0.0",
|
|
53
|
+
port=port,
|
|
54
|
+
log_level="info", # 使用 info 级别,过滤掉 warning
|
|
55
|
+
access_log=False, # 禁用访问日志,减少噪音
|
|
56
|
+
# 添加自定义的日志配置
|
|
57
|
+
log_config={
|
|
58
|
+
"version": 1,
|
|
59
|
+
"disable_existing_loggers": False,
|
|
60
|
+
"formatters": {
|
|
61
|
+
"default": {
|
|
62
|
+
"fmt": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
63
|
+
"datefmt": "%Y-%m-%d %H:%M:%S"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"handlers": {
|
|
67
|
+
"default": {
|
|
68
|
+
"formatter": "default",
|
|
69
|
+
"class": "logging.StreamHandler",
|
|
70
|
+
"stream": "ext://sys.stdout"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"root": {
|
|
74
|
+
"level": "INFO",
|
|
75
|
+
"handlers": ["default"]
|
|
76
|
+
},
|
|
77
|
+
"loggers": {
|
|
78
|
+
"uvicorn.error": {
|
|
79
|
+
"level": "ERROR"
|
|
80
|
+
},
|
|
81
|
+
"uvicorn.access": {
|
|
82
|
+
"handlers": [],
|
|
83
|
+
"propagate": False
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
)
|