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,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User-facing messages for API modules
|
|
3
|
+
Supports internationalization - Chinese and English messages
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Language detection helper
|
|
7
|
+
def get_message(messages_dict, lang='zh'):
|
|
8
|
+
"""Get message in specified language, fallback to Chinese"""
|
|
9
|
+
return messages_dict.get(lang, messages_dict.get('zh'))
|
|
10
|
+
|
|
11
|
+
# Error messages
|
|
12
|
+
ERROR_MESSAGES = {
|
|
13
|
+
'access_denied': {
|
|
14
|
+
'zh': '访问被拒绝',
|
|
15
|
+
'en': 'Access denied'
|
|
16
|
+
},
|
|
17
|
+
'invalid_file_path': {
|
|
18
|
+
'zh': '无效的文件路径',
|
|
19
|
+
'en': 'Invalid file path'
|
|
20
|
+
},
|
|
21
|
+
'file_not_found': {
|
|
22
|
+
'zh': '文件未找到',
|
|
23
|
+
'en': 'File not found'
|
|
24
|
+
},
|
|
25
|
+
'folder_not_found': {
|
|
26
|
+
'zh': '文件夹未找到',
|
|
27
|
+
'en': 'Folder not found'
|
|
28
|
+
},
|
|
29
|
+
'file_or_folder_not_found': {
|
|
30
|
+
'zh': '文件或文件夹不存在',
|
|
31
|
+
'en': 'File or folder does not exist'
|
|
32
|
+
},
|
|
33
|
+
'decode_error': {
|
|
34
|
+
'zh': '无法解码文件内容',
|
|
35
|
+
'en': 'Unable to decode file content'
|
|
36
|
+
},
|
|
37
|
+
'session_not_exist': {
|
|
38
|
+
'zh': '会话不存在',
|
|
39
|
+
'en': 'Session does not exist'
|
|
40
|
+
},
|
|
41
|
+
'delete_session_failed': {
|
|
42
|
+
'zh': '删除会话失败',
|
|
43
|
+
'en': 'Failed to delete session'
|
|
44
|
+
},
|
|
45
|
+
'invalid_project_id': {
|
|
46
|
+
'zh': '无效的 Project ID: {project_id},必须是整数',
|
|
47
|
+
'en': 'Invalid Project ID: {project_id}, must be an integer'
|
|
48
|
+
},
|
|
49
|
+
'project_id_required': {
|
|
50
|
+
'zh': '请先设置项目 ID 后再上传文件。',
|
|
51
|
+
'en': 'Please set project ID before uploading files.'
|
|
52
|
+
},
|
|
53
|
+
'unsupported_file_type': {
|
|
54
|
+
'zh': '不支持的文件类型: {file_ext}',
|
|
55
|
+
'en': 'Unsupported file type: {file_ext}'
|
|
56
|
+
},
|
|
57
|
+
'file_too_large': {
|
|
58
|
+
'zh': '文件 {filename} 超过大小限制 (10MB)',
|
|
59
|
+
'en': 'File {filename} exceeds size limit (10MB)'
|
|
60
|
+
},
|
|
61
|
+
'no_permission_project': {
|
|
62
|
+
'zh': '您没有权限使用项目 ID: {project_id}。请从项目列表中选择您有权限的项目。',
|
|
63
|
+
'en': 'You do not have permission to use project ID: {project_id}. Please select a project you have permission for.'
|
|
64
|
+
},
|
|
65
|
+
'accesskey_not_found': {
|
|
66
|
+
'zh': '未找到 AccessKey',
|
|
67
|
+
'en': 'AccessKey not found'
|
|
68
|
+
},
|
|
69
|
+
'appkey_not_found': {
|
|
70
|
+
'zh': '未找到 AppKey',
|
|
71
|
+
'en': 'AppKey not found'
|
|
72
|
+
},
|
|
73
|
+
'accesskey_or_appkey_not_found': {
|
|
74
|
+
'zh': '未找到 AccessKey 或 AppKey',
|
|
75
|
+
'en': 'AccessKey or AppKey not found'
|
|
76
|
+
},
|
|
77
|
+
'get_project_list_failed': {
|
|
78
|
+
'zh': '获取项目列表失败',
|
|
79
|
+
'en': 'Failed to get project list'
|
|
80
|
+
},
|
|
81
|
+
'project_not_belong_to_user': {
|
|
82
|
+
'zh': '该项目不属于当前用户',
|
|
83
|
+
'en': 'This project does not belong to current user'
|
|
84
|
+
},
|
|
85
|
+
'temp_user_no_session': {
|
|
86
|
+
'zh': '临时用户没有历史会话',
|
|
87
|
+
'en': 'Temporary users have no session history'
|
|
88
|
+
},
|
|
89
|
+
'temp_user_cannot_export': {
|
|
90
|
+
'zh': '临时用户没有会话可导出',
|
|
91
|
+
'en': 'Temporary users have no sessions to export'
|
|
92
|
+
},
|
|
93
|
+
'no_session_found': {
|
|
94
|
+
'zh': '没有找到会话',
|
|
95
|
+
'en': 'No sessions found'
|
|
96
|
+
},
|
|
97
|
+
'clear_failed': {
|
|
98
|
+
'zh': '清除失败: {error}',
|
|
99
|
+
'en': 'Clear failed: {error}'
|
|
100
|
+
},
|
|
101
|
+
'export_failed': {
|
|
102
|
+
'zh': '导出失败: {error}',
|
|
103
|
+
'en': 'Export failed: {error}'
|
|
104
|
+
},
|
|
105
|
+
'no_active_session': {
|
|
106
|
+
'zh': '没有活动的会话',
|
|
107
|
+
'en': 'No active session'
|
|
108
|
+
},
|
|
109
|
+
'session_service_not_initialized': {
|
|
110
|
+
'zh': '会话服务未初始化',
|
|
111
|
+
'en': 'Session service not initialized'
|
|
112
|
+
},
|
|
113
|
+
'get_session_list_failed': {
|
|
114
|
+
'zh': '获取会话列表失败',
|
|
115
|
+
'en': 'Failed to get session list'
|
|
116
|
+
},
|
|
117
|
+
'get_session_messages_failed': {
|
|
118
|
+
'zh': '获取会话消息失败',
|
|
119
|
+
'en': 'Failed to get session messages'
|
|
120
|
+
},
|
|
121
|
+
'please_set_project_id': {
|
|
122
|
+
'zh': '🔒 请先设置项目 ID',
|
|
123
|
+
'en': '🔒 Please set project ID first'
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Success messages
|
|
128
|
+
SUCCESS_MESSAGES = {
|
|
129
|
+
'project_id_set': {
|
|
130
|
+
'zh': 'Project ID 已设置为: {project_id}',
|
|
131
|
+
'en': 'Project ID set to: {project_id}'
|
|
132
|
+
},
|
|
133
|
+
'project_id_set_from_env': {
|
|
134
|
+
'zh': 'Project ID 已从环境变量设置为: {project_id} (开发模式)',
|
|
135
|
+
'en': 'Project ID set from environment variable: {project_id} (development mode)'
|
|
136
|
+
},
|
|
137
|
+
'delete_success': {
|
|
138
|
+
'zh': '成功删除: {filename}',
|
|
139
|
+
'en': 'Successfully deleted: {filename}'
|
|
140
|
+
},
|
|
141
|
+
'session_cleared': {
|
|
142
|
+
'zh': '历史会话已清除',
|
|
143
|
+
'en': 'Session history cleared'
|
|
144
|
+
},
|
|
145
|
+
'no_session_to_clear': {
|
|
146
|
+
'zh': '没有找到历史会话',
|
|
147
|
+
'en': 'No session history found'
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# UI labels
|
|
152
|
+
UI_LABELS = {
|
|
153
|
+
'workspace': {
|
|
154
|
+
'zh': '工作空间',
|
|
155
|
+
'en': 'Workspace'
|
|
156
|
+
},
|
|
157
|
+
'websocket_server_running': {
|
|
158
|
+
'zh': '{agent_name} WebSocket 服务器正在运行',
|
|
159
|
+
'en': '{agent_name} WebSocket server is running'
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Projects API using bohrium-open-sdk
|
|
2
|
+
from typing import Dict, Any, List, Optional
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, Request
|
|
5
|
+
from bohrium_open_sdk import OpenSDK
|
|
6
|
+
|
|
7
|
+
from server.utils import get_ak_info_from_request
|
|
8
|
+
|
|
9
|
+
router = APIRouter()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def make_client_from_request(request: Request) -> OpenSDK:
|
|
13
|
+
"""Parse AccessKey/AppKey from request headers and create SDK client"""
|
|
14
|
+
access_key, app_key = get_ak_info_from_request(request.headers)
|
|
15
|
+
if not access_key:
|
|
16
|
+
raise ValueError("未找到 AccessKey")
|
|
17
|
+
if not app_key:
|
|
18
|
+
raise ValueError("未找到 AppKey")
|
|
19
|
+
return OpenSDK(access_key=access_key, app_key=app_key)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def normalize_project(item: Dict[str, Any]) -> Dict[str, Any]:
|
|
23
|
+
"""
|
|
24
|
+
Normalize project structure for frontend:
|
|
25
|
+
- id: item['project_id'] or item['id']
|
|
26
|
+
- name: item['project_name'] or item['name']
|
|
27
|
+
Set default values for missing fields
|
|
28
|
+
"""
|
|
29
|
+
pid = item.get("project_id", item.get("id"))
|
|
30
|
+
name = item.get("project_name", item.get("name"))
|
|
31
|
+
return {
|
|
32
|
+
"id": pid,
|
|
33
|
+
"name": name,
|
|
34
|
+
"creatorName": item.get("creatorName", ""),
|
|
35
|
+
"createTime": item.get("createTime", ""),
|
|
36
|
+
"projectRole": item.get("projectRole", 0),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def verify_user_project(access_key: str, app_key: str, project_id: int) -> bool:
|
|
41
|
+
"""
|
|
42
|
+
Use bohrium-open-sdk to verify if project_id belongs to current user
|
|
43
|
+
"""
|
|
44
|
+
if not access_key or not app_key or not project_id:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
client = OpenSDK(access_key=access_key, app_key=app_key)
|
|
49
|
+
res = client.user.list_project()
|
|
50
|
+
if res.get("code") != 0:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
items: List[Dict[str, Any]] = res.get("data", {}).get("items", []) or []
|
|
54
|
+
user_project_ids = [it.get("project_id", it.get("id")) for it in items]
|
|
55
|
+
|
|
56
|
+
# Compatible with string/integer ID
|
|
57
|
+
user_project_ids_int: List[int] = []
|
|
58
|
+
for pid in user_project_ids:
|
|
59
|
+
try:
|
|
60
|
+
user_project_ids_int.append(int(pid))
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
return int(project_id) in user_project_ids_int
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@router.get("/api/projects")
|
|
71
|
+
async def get_projects(request: Request) -> Dict[str, Any]:
|
|
72
|
+
"""Get user's project list using SDK"""
|
|
73
|
+
try:
|
|
74
|
+
client = make_client_from_request(request)
|
|
75
|
+
user_info = client.user.get_info()
|
|
76
|
+
# Response data contains: user_id, name, org_id
|
|
77
|
+
response = f"Get userid success,the user_id is: {user_info['data']['user_id']}"
|
|
78
|
+
print(response)
|
|
79
|
+
res = client.user.list_project()
|
|
80
|
+
if res.get("code") != 0:
|
|
81
|
+
return {
|
|
82
|
+
"success": False,
|
|
83
|
+
"error": res.get("error", "获取项目列表失败"),
|
|
84
|
+
"projects": [],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
items: List[Dict[str, Any]] = res.get("data", {}).get("items", []) or []
|
|
88
|
+
projects = [normalize_project(it) for it in items]
|
|
89
|
+
|
|
90
|
+
return {"success": True, "projects": projects}
|
|
91
|
+
|
|
92
|
+
except ValueError as ve:
|
|
93
|
+
return {"success": False, "error": str(ve), "projects": []}
|
|
94
|
+
except Exception as e:
|
|
95
|
+
return {"success": False, "error": str(e), "projects": []}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@router.get("/api/projects/{project_id}/verify")
|
|
99
|
+
async def verify_project(project_id: int, request: Request) -> Dict[str, Any]:
|
|
100
|
+
"""Verify if specific project_id belongs to current user"""
|
|
101
|
+
try:
|
|
102
|
+
access_key, app_key = get_ak_info_from_request(request.headers)
|
|
103
|
+
|
|
104
|
+
if not access_key or not app_key:
|
|
105
|
+
return {
|
|
106
|
+
"success": False,
|
|
107
|
+
"error": "未找到 AccessKey 或 AppKey",
|
|
108
|
+
"valid": False,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
is_valid = await verify_user_project(access_key, app_key, project_id)
|
|
112
|
+
|
|
113
|
+
if not is_valid:
|
|
114
|
+
return {
|
|
115
|
+
"success": True,
|
|
116
|
+
"valid": False,
|
|
117
|
+
"error": "该项目不属于当前用户",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Find specific project info and return
|
|
121
|
+
client = OpenSDK(access_key=access_key, app_key=app_key)
|
|
122
|
+
res = client.user.list_project()
|
|
123
|
+
if res.get("code") != 0:
|
|
124
|
+
return {
|
|
125
|
+
"success": False,
|
|
126
|
+
"error": res.get("error", "获取项目列表失败"),
|
|
127
|
+
"valid": False,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
items: List[Dict[str, Any]] = res.get("data", {}).get("items", []) or []
|
|
131
|
+
project_info: Optional[Dict[str, Any]] = None
|
|
132
|
+
for it in items:
|
|
133
|
+
raw_id = it.get("project_id", it.get("id"))
|
|
134
|
+
try:
|
|
135
|
+
if int(raw_id) == int(project_id):
|
|
136
|
+
project_info = normalize_project(it)
|
|
137
|
+
break
|
|
138
|
+
except Exception:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
return {"success": True, "valid": True, "project": project_info or {"id": project_id}}
|
|
142
|
+
|
|
143
|
+
except ValueError as ve:
|
|
144
|
+
return {"success": False, "error": str(ve), "valid": False}
|
|
145
|
+
except Exception as e:
|
|
146
|
+
return {"success": False, "error": str(e), "valid": False}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Session management API
|
|
2
|
+
import json
|
|
3
|
+
import shutil
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from fastapi import Request
|
|
6
|
+
from fastapi.responses import JSONResponse, Response
|
|
7
|
+
|
|
8
|
+
from server.utils import get_ak_info_from_request
|
|
9
|
+
from api.websocket import manager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def clear_user_sessions(request: Request):
|
|
13
|
+
"""Clear all historical sessions for current user"""
|
|
14
|
+
access_key, _ = get_ak_info_from_request(request.headers)
|
|
15
|
+
|
|
16
|
+
if not access_key:
|
|
17
|
+
return JSONResponse(
|
|
18
|
+
content={"error": "临时用户没有历史会话"},
|
|
19
|
+
status_code=400
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
# Get user's session directory
|
|
24
|
+
ak_hash = manager.persistent_manager._get_ak_hash(access_key)
|
|
25
|
+
user_sessions_dir = manager.persistent_manager.ak_sessions_dir / ak_hash / "sessions"
|
|
26
|
+
|
|
27
|
+
if user_sessions_dir.exists():
|
|
28
|
+
# Delete all session files
|
|
29
|
+
shutil.rmtree(user_sessions_dir)
|
|
30
|
+
user_sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
|
|
32
|
+
return JSONResponse(content={
|
|
33
|
+
"message": "历史会话已清除",
|
|
34
|
+
"status": "success"
|
|
35
|
+
})
|
|
36
|
+
else:
|
|
37
|
+
return JSONResponse(content={
|
|
38
|
+
"message": "没有找到历史会话",
|
|
39
|
+
"status": "success"
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
except Exception as e:
|
|
43
|
+
return JSONResponse(
|
|
44
|
+
content={"error": f"清除失败: {str(e)}"},
|
|
45
|
+
status_code=500
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def export_user_sessions(request: Request):
|
|
50
|
+
"""Export all sessions for current user"""
|
|
51
|
+
access_key, _ = get_ak_info_from_request(request.headers)
|
|
52
|
+
|
|
53
|
+
if not access_key:
|
|
54
|
+
return JSONResponse(
|
|
55
|
+
content={"error": "临时用户没有会话可导出"},
|
|
56
|
+
status_code=400
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# Load all user sessions
|
|
61
|
+
sessions = await manager.persistent_manager.load_user_sessions(access_key)
|
|
62
|
+
|
|
63
|
+
if not sessions:
|
|
64
|
+
return JSONResponse(
|
|
65
|
+
content={"error": "没有找到会话"},
|
|
66
|
+
status_code=404
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Build export data
|
|
70
|
+
export_data = {
|
|
71
|
+
"export_time": datetime.now().isoformat(),
|
|
72
|
+
"user_type": "registered",
|
|
73
|
+
"sessions": []
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for session in sessions.values():
|
|
77
|
+
session_data = manager.persistent_manager._serialize_session(session)
|
|
78
|
+
export_data["sessions"].append(session_data)
|
|
79
|
+
|
|
80
|
+
# Return JSON file
|
|
81
|
+
return Response(
|
|
82
|
+
content=json.dumps(export_data, indent=2, ensure_ascii=False),
|
|
83
|
+
media_type="application/json",
|
|
84
|
+
headers={
|
|
85
|
+
"Content-Disposition": f"attachment; filename=sessions_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
return JSONResponse(
|
|
91
|
+
content={"error": f"导出失败: {str(e)}"},
|
|
92
|
+
status_code=500
|
|
93
|
+
)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common utilities for API modules
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Tuple
|
|
7
|
+
from fastapi import Request
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
|
|
10
|
+
from server.utils import get_ak_info_from_request
|
|
11
|
+
from server.user_files import UserFileManager
|
|
12
|
+
from api.messages import ERROR_MESSAGES, get_message
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_user_identifier(access_key: Optional[str], app_key: Optional[str], session_id: Optional[str]) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Get unique user identifier based on access_key or session_id
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
access_key: User's access key
|
|
21
|
+
app_key: User's app key
|
|
22
|
+
session_id: Session ID for temporary users
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Unique user identifier string
|
|
26
|
+
"""
|
|
27
|
+
if access_key:
|
|
28
|
+
return access_key
|
|
29
|
+
return session_id or "anonymous"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def extract_session_id_from_request(request: Request) -> Optional[str]:
|
|
33
|
+
"""
|
|
34
|
+
Extract session_id from request cookies
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
request: FastAPI request object
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Session ID if found, None otherwise
|
|
41
|
+
"""
|
|
42
|
+
cookie_header = request.headers.get("cookie", "")
|
|
43
|
+
if not cookie_header:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
from http.cookies import SimpleCookie
|
|
47
|
+
simple_cookie = SimpleCookie()
|
|
48
|
+
simple_cookie.load(cookie_header)
|
|
49
|
+
|
|
50
|
+
if "session_id" in simple_cookie:
|
|
51
|
+
return simple_cookie["session_id"].value
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_user_context_from_request(request: Request) -> Tuple[str, Optional[str], Optional[str]]:
|
|
56
|
+
"""
|
|
57
|
+
Extract user context from request
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Tuple of (user_identifier, access_key, app_key)
|
|
61
|
+
"""
|
|
62
|
+
access_key, app_key = get_ak_info_from_request(request.headers)
|
|
63
|
+
session_id = extract_session_id_from_request(request)
|
|
64
|
+
user_identifier = get_user_identifier(access_key, app_key, session_id)
|
|
65
|
+
|
|
66
|
+
return user_identifier, access_key, app_key
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def validate_file_access(file_path: Path, user_files_dir: Path, lang='zh') -> Optional[JSONResponse]:
|
|
70
|
+
"""
|
|
71
|
+
Validate if file path is within user directory and accessible
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
file_path: Path to validate
|
|
75
|
+
user_files_dir: User's files directory
|
|
76
|
+
lang: Language for error messages
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
JSONResponse with error if validation fails, None if valid
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
# Resolve paths to absolute
|
|
83
|
+
file_resolved = file_path.resolve()
|
|
84
|
+
user_files_dir_resolved = user_files_dir.resolve()
|
|
85
|
+
|
|
86
|
+
# Check if file is within user directory
|
|
87
|
+
if not str(file_resolved).startswith(str(user_files_dir_resolved)):
|
|
88
|
+
return JSONResponse(
|
|
89
|
+
content={"error": get_message(ERROR_MESSAGES['access_denied'], lang)},
|
|
90
|
+
status_code=403
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Check if file exists
|
|
94
|
+
if not file_resolved.exists():
|
|
95
|
+
error_key = 'folder_not_found' if file_path.is_dir() else 'file_not_found'
|
|
96
|
+
return JSONResponse(
|
|
97
|
+
content={"error": get_message(ERROR_MESSAGES[error_key], lang)},
|
|
98
|
+
status_code=404
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
except Exception:
|
|
104
|
+
return JSONResponse(
|
|
105
|
+
content={"error": get_message(ERROR_MESSAGES['invalid_file_path'], lang)},
|
|
106
|
+
status_code=400
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def process_file_path(file_path_str: str, user_files_dir: Path) -> Path:
|
|
111
|
+
"""
|
|
112
|
+
Process file path string and convert to Path object
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
file_path_str: File path as string
|
|
116
|
+
user_files_dir: User's files directory
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Processed Path object
|
|
120
|
+
"""
|
|
121
|
+
if file_path_str.startswith('/'):
|
|
122
|
+
return Path(file_path_str)
|
|
123
|
+
else:
|
|
124
|
+
# Relative path, based on user directory
|
|
125
|
+
return user_files_dir / file_path_str
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def safe_filename(filename: str) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Generate safe filename by replacing dangerous characters
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
filename: Original filename
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Safe filename
|
|
137
|
+
"""
|
|
138
|
+
return filename.replace('/', '_').replace('\\', '_').replace('..', '_')
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def check_project_id_required(context_manager, user_identifier: str) -> bool:
|
|
142
|
+
"""
|
|
143
|
+
Check if project_id is required and set
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
context_manager: WebSocket manager instance
|
|
147
|
+
user_identifier: User identifier
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
True if project_id is set, False otherwise
|
|
151
|
+
"""
|
|
152
|
+
# First check environment variable
|
|
153
|
+
if os.environ.get('BOHR_PROJECT_ID'):
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
# Check user's connection context
|
|
157
|
+
for context in context_manager.active_connections.values():
|
|
158
|
+
if context.get_user_identifier() == user_identifier:
|
|
159
|
+
return bool(context.project_id)
|
|
160
|
+
|
|
161
|
+
return False
|