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.
Files changed (49) hide show
  1. bohr_agent_sdk-0.1.103.dist-info/METADATA +292 -0
  2. bohr_agent_sdk-0.1.103.dist-info/RECORD +80 -0
  3. dp/agent/cli/cli.py +126 -25
  4. dp/agent/cli/templates/__init__.py +1 -0
  5. dp/agent/cli/templates/calculation/simple.py.template +15 -0
  6. dp/agent/cli/templates/device/tescan_device.py.template +158 -0
  7. dp/agent/cli/templates/main.py.template +67 -0
  8. dp/agent/cli/templates/ui/__init__.py +1 -0
  9. dp/agent/cli/templates/ui/api/__init__.py +1 -0
  10. dp/agent/cli/templates/ui/api/config.py +32 -0
  11. dp/agent/cli/templates/ui/api/constants.py +61 -0
  12. dp/agent/cli/templates/ui/api/debug.py +257 -0
  13. dp/agent/cli/templates/ui/api/files.py +469 -0
  14. dp/agent/cli/templates/ui/api/files_upload.py +115 -0
  15. dp/agent/cli/templates/ui/api/files_user.py +50 -0
  16. dp/agent/cli/templates/ui/api/messages.py +161 -0
  17. dp/agent/cli/templates/ui/api/projects.py +146 -0
  18. dp/agent/cli/templates/ui/api/sessions.py +93 -0
  19. dp/agent/cli/templates/ui/api/utils.py +161 -0
  20. dp/agent/cli/templates/ui/api/websocket.py +184 -0
  21. dp/agent/cli/templates/ui/config/__init__.py +1 -0
  22. dp/agent/cli/templates/ui/config/agent_config.py +257 -0
  23. dp/agent/cli/templates/ui/frontend/index.html +13 -0
  24. dp/agent/cli/templates/ui/frontend/package.json +46 -0
  25. dp/agent/cli/templates/ui/frontend/tsconfig.json +26 -0
  26. dp/agent/cli/templates/ui/frontend/tsconfig.node.json +10 -0
  27. dp/agent/cli/templates/ui/frontend/ui-static/assets/index-DdAmKhul.js +105 -0
  28. dp/agent/cli/templates/ui/frontend/ui-static/assets/index-DfN2raU9.css +1 -0
  29. dp/agent/cli/templates/ui/frontend/ui-static/index.html +14 -0
  30. dp/agent/cli/templates/ui/frontend/vite.config.ts +37 -0
  31. dp/agent/cli/templates/ui/scripts/build_ui.py +56 -0
  32. dp/agent/cli/templates/ui/server/__init__.py +0 -0
  33. dp/agent/cli/templates/ui/server/app.py +98 -0
  34. dp/agent/cli/templates/ui/server/connection.py +210 -0
  35. dp/agent/cli/templates/ui/server/file_watcher.py +85 -0
  36. dp/agent/cli/templates/ui/server/middleware.py +43 -0
  37. dp/agent/cli/templates/ui/server/models.py +53 -0
  38. dp/agent/cli/templates/ui/server/session_manager.py +1158 -0
  39. dp/agent/cli/templates/ui/server/user_files.py +85 -0
  40. dp/agent/cli/templates/ui/server/utils.py +50 -0
  41. dp/agent/cli/templates/ui/test_download.py +98 -0
  42. dp/agent/cli/templates/ui/ui_utils.py +260 -0
  43. dp/agent/cli/templates/ui/websocket-server.py +87 -0
  44. dp/agent/server/storage/http_storage.py +1 -1
  45. bohr_agent_sdk-0.1.101.dist-info/METADATA +0 -224
  46. bohr_agent_sdk-0.1.101.dist-info/RECORD +0 -40
  47. {bohr_agent_sdk-0.1.101.dist-info → bohr_agent_sdk-0.1.103.dist-info}/WHEEL +0 -0
  48. {bohr_agent_sdk-0.1.101.dist-info → bohr_agent_sdk-0.1.103.dist-info}/entry_points.txt +0 -0
  49. {bohr_agent_sdk-0.1.101.dist-info → bohr_agent_sdk-0.1.103.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,469 @@
1
+ # File API
2
+ import os
3
+ import json
4
+ import uuid
5
+ import zipfile
6
+ import tempfile
7
+ import shutil
8
+ from pathlib import Path
9
+ from typing import List
10
+ from fastapi import Request, Response, File, UploadFile
11
+ from fastapi.responses import JSONResponse, PlainTextResponse, FileResponse, StreamingResponse
12
+ from bohrium_open_sdk import OpenSDK
13
+
14
+ from config.agent_config import agentconfig
15
+ from server.utils import get_ak_info_from_request
16
+ from server.user_files import UserFileManager
17
+
18
+
19
+
20
+
21
+ # Get sessions directory path from config
22
+ files_config = agentconfig.get_files_config()
23
+ user_working_dir = os.environ.get('USER_WORKING_DIR', os.getcwd())
24
+ sessions_dir = files_config.get('sessionsDir', '.agent_sessions')
25
+ user_file_manager = UserFileManager(user_working_dir, sessions_dir)
26
+
27
+ # Import SessionManager instance
28
+ from api.websocket import manager
29
+
30
+
31
+ def get_user_identifier(access_key: str = None, app_key: str = None, session_id: str = None) -> str:
32
+ """Get user unique identifier"""
33
+ # First try to get from connected context
34
+ if access_key:
35
+ cached_user_id = manager.get_user_identifier_from_request(access_key, app_key)
36
+ if cached_user_id:
37
+ return cached_user_id
38
+
39
+ # If no cache, call OpenSDK
40
+ if access_key and app_key:
41
+ try:
42
+ # Use OpenSDK to get user info
43
+ client = OpenSDK(
44
+ access_key=access_key,
45
+ app_key=app_key
46
+ )
47
+ user_info = client.user.get_info()
48
+ if user_info and user_info.get('code') == 0:
49
+ data = user_info.get('data', {})
50
+ bohrium_user_id = data.get('user_id')
51
+ if bohrium_user_id:
52
+ return bohrium_user_id
53
+ except Exception as e:
54
+ pass
55
+
56
+ # If has session_id, use it
57
+ if session_id:
58
+ return session_id
59
+
60
+ # Generate temporary ID if none available
61
+ return f"user_{uuid.uuid4().hex[:8]}"
62
+
63
+
64
+ async def get_file_tree(request: Request, path: str = None):
65
+ """Get file tree structure"""
66
+ try:
67
+ # Get user identity
68
+ access_key, app_key = get_ak_info_from_request(request.headers)
69
+
70
+ # Get session_id (from cookie)
71
+ session_id = None
72
+ cookie_header = request.headers.get("cookie", "")
73
+ if cookie_header:
74
+ from http.cookies import SimpleCookie
75
+ simple_cookie = SimpleCookie()
76
+ simple_cookie.load(cookie_header)
77
+ if "session_id" in simple_cookie:
78
+ session_id = simple_cookie["session_id"].value
79
+
80
+ # Get user unique identifier
81
+ user_identifier = get_user_identifier(access_key, app_key, session_id)
82
+
83
+ # Get user-specific file directory
84
+ user_files_dir = user_file_manager.get_user_files_dir(user_identifier)
85
+
86
+ def build_tree(directory: Path):
87
+ items = []
88
+ try:
89
+ for item in sorted(directory.iterdir()):
90
+ if item.name.startswith('.'):
91
+ continue
92
+
93
+ node = {
94
+ "name": item.name,
95
+ "path": str(item),
96
+ "type": "directory" if item.is_dir() else "file"
97
+ }
98
+
99
+ if item.is_dir():
100
+ node["children"] = build_tree(item)
101
+ else:
102
+ node["size"] = item.stat().st_size
103
+
104
+ items.append(node)
105
+ except PermissionError:
106
+ pass
107
+ return items
108
+
109
+ if path is None:
110
+ # Return file tree structure for user directory
111
+ tree = []
112
+
113
+ # User file root directory
114
+ if user_files_dir.exists():
115
+ # Build root directory node
116
+ root_node = {
117
+ "name": "工作空间",
118
+ "path": str(user_files_dir),
119
+ "type": "directory",
120
+ "isExpanded": True,
121
+ "children": build_tree(user_files_dir)
122
+ }
123
+ tree.append(root_node)
124
+
125
+ return JSONResponse(content=tree)
126
+ else:
127
+ # Handle specific path request
128
+ # Ensure path is within user directory
129
+ if os.path.isabs(path):
130
+ request_path = Path(path)
131
+ else:
132
+ request_path = user_files_dir / path
133
+
134
+ # Security check: ensure requested path is within user directory
135
+ try:
136
+ request_path = request_path.resolve()
137
+ user_files_dir_resolved = user_files_dir.resolve()
138
+ if not str(request_path).startswith(str(user_files_dir_resolved)):
139
+ return JSONResponse(content=[], status_code=403)
140
+ except:
141
+ return JSONResponse(content=[], status_code=400)
142
+
143
+ if not request_path.exists():
144
+ request_path.mkdir(parents=True, exist_ok=True)
145
+
146
+ return JSONResponse(content=build_tree(request_path))
147
+
148
+ except Exception as e:
149
+ return JSONResponse(content=[], status_code=500)
150
+
151
+
152
+ async def get_file_content(request: Request, file_path: str):
153
+ """Get file content"""
154
+ try:
155
+ # Get user identity
156
+ access_key, app_key = get_ak_info_from_request(request.headers)
157
+
158
+ # Get session_id (from cookie)
159
+ session_id = None
160
+ cookie_header = request.headers.get("cookie", "")
161
+ if cookie_header:
162
+ from http.cookies import SimpleCookie
163
+ simple_cookie = SimpleCookie()
164
+ simple_cookie.load(cookie_header)
165
+ if "session_id" in simple_cookie:
166
+ session_id = simple_cookie["session_id"].value
167
+
168
+ # Get user unique identifier
169
+ user_identifier = get_user_identifier(access_key, app_key, session_id)
170
+
171
+ # Get user-specific file directory
172
+ user_files_dir = user_file_manager.get_user_files_dir(user_identifier)
173
+
174
+ # Handle file path
175
+ if file_path.startswith('/'):
176
+ file = Path(file_path)
177
+ else:
178
+ # Relative path, based on user directory
179
+ file = user_files_dir / file_path
180
+
181
+ # Security check: ensure file is within user directory
182
+ try:
183
+ file_resolved = file.resolve()
184
+ user_files_dir_resolved = user_files_dir.resolve()
185
+ if not str(file_resolved).startswith(str(user_files_dir_resolved)):
186
+ return JSONResponse(
187
+ content={"error": "访问被拒绝"},
188
+ status_code=403
189
+ )
190
+ except:
191
+ return JSONResponse(
192
+ content={"error": "无效的文件路径"},
193
+ status_code=400
194
+ )
195
+
196
+ if not file.exists() or not file.is_file():
197
+ return JSONResponse(
198
+ content={"error": "文件未找到"},
199
+ status_code=404
200
+ )
201
+
202
+ # Determine file type
203
+ suffix = file.suffix.lower()
204
+
205
+ # Text files
206
+ if suffix in ['.json', '.md', '.txt', '.csv', '.py', '.js', '.ts', '.log', '.xml', '.yaml', '.yml']:
207
+ try:
208
+ content = file.read_text(encoding='utf-8')
209
+ return PlainTextResponse(content)
210
+ except UnicodeDecodeError:
211
+ return JSONResponse(
212
+ content={"error": "无法解码文件内容"},
213
+ status_code=400
214
+ )
215
+ else:
216
+ # Binary files
217
+ return FileResponse(file)
218
+
219
+ except Exception as e:
220
+ return JSONResponse(
221
+ content={"error": str(e)},
222
+ status_code=500
223
+ )
224
+
225
+
226
+ async def download_file(request: Request, file_path: str):
227
+ """Download a single file"""
228
+ try:
229
+ # Get user identity
230
+ access_key, app_key = get_ak_info_from_request(request.headers)
231
+
232
+ # Get session_id (from cookie)
233
+ session_id = None
234
+ cookie_header = request.headers.get("cookie", "")
235
+ if cookie_header:
236
+ from http.cookies import SimpleCookie
237
+ simple_cookie = SimpleCookie()
238
+ simple_cookie.load(cookie_header)
239
+ if "session_id" in simple_cookie:
240
+ session_id = simple_cookie["session_id"].value
241
+
242
+ # Get user unique identifier
243
+ user_identifier = get_user_identifier(access_key, app_key, session_id)
244
+
245
+ # Get user-specific file directory
246
+ user_files_dir = user_file_manager.get_user_files_dir(user_identifier)
247
+
248
+ # Process file path
249
+ if file_path.startswith('/'):
250
+ file = Path(file_path)
251
+ else:
252
+ # Relative path, based on user directory
253
+ file = user_files_dir / file_path
254
+
255
+ # Security check: ensure file is within user directory
256
+ try:
257
+ file_resolved = file.resolve()
258
+ user_files_dir_resolved = user_files_dir.resolve()
259
+ if not str(file_resolved).startswith(str(user_files_dir_resolved)):
260
+ return JSONResponse(
261
+ content={"error": "访问被拒绝"},
262
+ status_code=403
263
+ )
264
+ except:
265
+ return JSONResponse(
266
+ content={"error": "无效的文件路径"},
267
+ status_code=400
268
+ )
269
+
270
+ if not file.exists() or not file.is_file():
271
+ return JSONResponse(
272
+ content={"error": "文件未找到"},
273
+ status_code=404
274
+ )
275
+
276
+ # Get filename
277
+ filename = file.name
278
+
279
+ # Return file response with download headers
280
+ return FileResponse(
281
+ path=file,
282
+ filename=filename,
283
+ media_type='application/octet-stream'
284
+ )
285
+
286
+ except Exception as e:
287
+ return JSONResponse(
288
+ content={"error": str(e)},
289
+ status_code=500
290
+ )
291
+
292
+
293
+ async def download_folder(request: Request, folder_path: str):
294
+ """Download folder (packaged as zip)"""
295
+ try:
296
+ # Get user identity
297
+ access_key, app_key = get_ak_info_from_request(request.headers)
298
+
299
+ # Get session_id (from cookie)
300
+ session_id = None
301
+ cookie_header = request.headers.get("cookie", "")
302
+ if cookie_header:
303
+ from http.cookies import SimpleCookie
304
+ simple_cookie = SimpleCookie()
305
+ simple_cookie.load(cookie_header)
306
+ if "session_id" in simple_cookie:
307
+ session_id = simple_cookie["session_id"].value
308
+
309
+ # Get user unique identifier
310
+ user_identifier = get_user_identifier(access_key, app_key, session_id)
311
+
312
+ # Get user-specific file directory
313
+ user_files_dir = user_file_manager.get_user_files_dir(user_identifier)
314
+
315
+ # Process folder path
316
+ if folder_path.startswith('/'):
317
+ folder = Path(folder_path)
318
+ else:
319
+ # Relative path, based on user directory
320
+ folder = user_files_dir / folder_path
321
+
322
+ # Security check: ensure folder is within user directory
323
+ try:
324
+ folder_resolved = folder.resolve()
325
+ user_files_dir_resolved = user_files_dir.resolve()
326
+ if not str(folder_resolved).startswith(str(user_files_dir_resolved)):
327
+ return JSONResponse(
328
+ content={"error": "访问被拒绝"},
329
+ status_code=403
330
+ )
331
+ except:
332
+ return JSONResponse(
333
+ content={"error": "无效的文件夹路径"},
334
+ status_code=400
335
+ )
336
+
337
+ if not folder.exists() or not folder.is_dir():
338
+ return JSONResponse(
339
+ content={"error": "文件夹未找到"},
340
+ status_code=404
341
+ )
342
+
343
+ # Create temporary zip file
344
+ temp_dir = Path(tempfile.gettempdir())
345
+ zip_filename = f"{folder.name}_{uuid.uuid4().hex[:8]}.zip"
346
+ zip_path = temp_dir / zip_filename
347
+
348
+ try:
349
+ # Create zip file
350
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
351
+ for root, dirs, files in os.walk(folder):
352
+ # Skip hidden files and directories
353
+ dirs[:] = [d for d in dirs if not d.startswith('.')]
354
+
355
+ for file in files:
356
+ if file.startswith('.'):
357
+ continue
358
+
359
+ file_path = Path(root) / file
360
+ # Calculate relative path
361
+ arcname = file_path.relative_to(folder)
362
+ zipf.write(file_path, arcname=str(arcname))
363
+
364
+ # Return zip file
365
+ def cleanup():
366
+ """Clean up temporary file"""
367
+ try:
368
+ if zip_path.exists():
369
+ zip_path.unlink()
370
+ except:
371
+ pass
372
+
373
+ # Read file content
374
+ with open(zip_path, 'rb') as f:
375
+ content = f.read()
376
+
377
+ # Clean up temporary file
378
+ cleanup()
379
+
380
+ # Return response
381
+ return Response(
382
+ content=content,
383
+ media_type='application/zip',
384
+ headers={
385
+ 'Content-Disposition': f'attachment; filename="{folder.name}.zip"'
386
+ }
387
+ )
388
+
389
+ except Exception as e:
390
+ # Ensure temporary file cleanup
391
+ if zip_path.exists():
392
+ zip_path.unlink()
393
+ raise e
394
+
395
+ except Exception as e:
396
+ return JSONResponse(
397
+ content={"error": str(e)},
398
+ status_code=500
399
+ )
400
+
401
+
402
+ async def delete_file(request: Request, file_path: str):
403
+ """Delete file or folder"""
404
+ try:
405
+ # Get user identity
406
+ access_key, app_key = get_ak_info_from_request(request.headers)
407
+
408
+ # Get session_id (from cookie)
409
+ session_id = None
410
+ cookie_header = request.headers.get("cookie", "")
411
+ if cookie_header:
412
+ from http.cookies import SimpleCookie
413
+ simple_cookie = SimpleCookie()
414
+ simple_cookie.load(cookie_header)
415
+ if "session_id" in simple_cookie:
416
+ session_id = simple_cookie["session_id"].value
417
+
418
+ # Get user unique identifier
419
+ user_identifier = get_user_identifier(access_key, app_key, session_id)
420
+
421
+ # Get user-specific file directory
422
+ user_files_dir = user_file_manager.get_user_files_dir(user_identifier)
423
+
424
+ # Process file path
425
+ if file_path.startswith('/'):
426
+ target_path = Path(file_path)
427
+ else:
428
+ # Relative path, based on user directory
429
+ target_path = user_files_dir / file_path
430
+
431
+ # Security check: ensure file is within user directory
432
+ try:
433
+ target_path_resolved = target_path.resolve()
434
+ user_files_dir_resolved = user_files_dir.resolve()
435
+ if not str(target_path_resolved).startswith(str(user_files_dir_resolved)):
436
+ return JSONResponse(
437
+ content={"error": "访问被拒绝"},
438
+ status_code=403
439
+ )
440
+ except:
441
+ return JSONResponse(
442
+ content={"error": "无效的文件路径"},
443
+ status_code=400
444
+ )
445
+
446
+ # Check if file exists
447
+ if not target_path.exists():
448
+ return JSONResponse(
449
+ content={"error": "文件或文件夹不存在"},
450
+ status_code=404
451
+ )
452
+
453
+ # Delete file or folder
454
+ if target_path.is_file():
455
+ target_path.unlink()
456
+ else:
457
+ # Recursively delete folder
458
+ shutil.rmtree(target_path)
459
+
460
+ return JSONResponse({
461
+ "success": True,
462
+ "message": f"成功删除: {target_path.name}"
463
+ })
464
+
465
+ except Exception as e:
466
+ return JSONResponse(
467
+ content={"error": str(e)},
468
+ status_code=500
469
+ )
@@ -0,0 +1,115 @@
1
+ # File Upload API Extension
2
+ from typing import List
3
+ from pathlib import Path
4
+ from fastapi import Request, File, UploadFile
5
+ from fastapi.responses import JSONResponse
6
+ import uuid
7
+
8
+ from server.utils import get_ak_info_from_request
9
+ from server.user_files import UserFileManager
10
+ from config.agent_config import agentconfig
11
+
12
+ # Import required functions and variables from files.py
13
+ from .files import user_file_manager
14
+ from .utils import get_user_identifier, extract_session_id_from_request, check_project_id_required
15
+ from .constants import MAX_FILE_SIZE, ALLOWED_EXTENSIONS
16
+ from .messages import ERROR_MESSAGES, get_message
17
+
18
+
19
+ async def upload_files(request: Request, files: List[UploadFile] = File(...)):
20
+ """Upload files to user directory"""
21
+ try:
22
+ # Get user identity
23
+ access_key, app_key = get_ak_info_from_request(request.headers)
24
+
25
+ # Get session_id (from cookie)
26
+ session_id = extract_session_id_from_request(request)
27
+
28
+ # Get user unique identifier
29
+ user_identifier = get_user_identifier(access_key, app_key, session_id)
30
+
31
+ # Check if user has set project_id
32
+ from api.websocket import manager
33
+
34
+ has_project_id = await check_project_id_required(manager, user_identifier)
35
+
36
+ # If project_id not set, reject upload
37
+ if not has_project_id:
38
+ return JSONResponse(
39
+ content={
40
+ "error": get_message(ERROR_MESSAGES['project_id_required']),
41
+ "code": "PROJECT_ID_REQUIRED"
42
+ },
43
+ status_code=403
44
+ )
45
+
46
+ # Get user-specific file directory
47
+ user_files_dir = user_file_manager.get_user_files_dir(user_identifier)
48
+ output_dir = user_files_dir / "output"
49
+ output_dir.mkdir(exist_ok=True)
50
+
51
+ # File limits are now imported from constants
52
+
53
+ uploaded_files = []
54
+
55
+ for file in files:
56
+ # Verify file extension
57
+ file_ext = file.filename.split('.')[-1].lower() if '.' in file.filename else ''
58
+ if file_ext not in ALLOWED_EXTENSIONS:
59
+ return JSONResponse(
60
+ content={"error": get_message(ERROR_MESSAGES['unsupported_file_type']).format(file_ext=file_ext)},
61
+ status_code=400
62
+ )
63
+
64
+ # Read file content
65
+ content = await file.read()
66
+
67
+ # Verify file size
68
+ if len(content) > MAX_FILE_SIZE:
69
+ return JSONResponse(
70
+ content={"error": get_message(ERROR_MESSAGES['file_too_large']).format(filename=file.filename)},
71
+ status_code=400
72
+ )
73
+
74
+ # Generate safe filename
75
+ safe_filename = file.filename.replace('/', '_').replace('\\', '_')
76
+
77
+ # Handle filename conflicts
78
+ file_path = output_dir / safe_filename
79
+ if file_path.exists():
80
+ # Add timestamp to avoid overwriting
81
+ name_parts = safe_filename.rsplit('.', 1)
82
+ if len(name_parts) == 2:
83
+ safe_filename = f"{name_parts[0]}_{uuid.uuid4().hex[:8]}.{name_parts[1]}"
84
+ else:
85
+ safe_filename = f"{safe_filename}_{uuid.uuid4().hex[:8]}"
86
+ file_path = output_dir / safe_filename
87
+
88
+ # Save file
89
+ file_path.write_bytes(content)
90
+
91
+ # Generate relative path (relative to user_files_dir)
92
+ relative_path = file_path.relative_to(user_files_dir)
93
+
94
+ # Add to return list
95
+ uploaded_files.append({
96
+ "name": file.filename,
97
+ "saved_name": safe_filename,
98
+ "path": str(file_path),
99
+ "file_path": str(file_path.resolve()),
100
+ "relative_path": str(relative_path),
101
+ "url": f"/api/files/{user_identifier}/output/{safe_filename}",
102
+ "size": len(content),
103
+ "mime_type": file.content_type or 'application/octet-stream'
104
+ })
105
+
106
+ return JSONResponse({
107
+ "success": True,
108
+ "files": uploaded_files
109
+ })
110
+
111
+ except Exception as e:
112
+ return JSONResponse(
113
+ content={"error": str(e)},
114
+ status_code=500
115
+ )
@@ -0,0 +1,50 @@
1
+ # User-specific file access API
2
+ from pathlib import Path
3
+ from fastapi import Request
4
+ from fastapi.responses import JSONResponse, FileResponse
5
+
6
+ from server.user_files import UserFileManager
7
+ from config.agent_config import agentconfig
8
+
9
+ # Import user_file_manager from files.py
10
+ from .files import user_file_manager
11
+
12
+
13
+ async def get_user_file(request: Request, user_id: str, filename: str):
14
+ """Get file for specific user"""
15
+ try:
16
+ # Use user_id to directly get user file directory
17
+ # UserFileManager will automatically choose the correct path based on user_id type
18
+ user_files_dir = user_file_manager.get_user_files_dir(user_id)
19
+ file_path = user_files_dir / "output" / filename
20
+
21
+ # Security check
22
+ if not file_path.exists() or not file_path.is_file():
23
+ return JSONResponse(
24
+ content={"error": "文件未找到"},
25
+ status_code=404
26
+ )
27
+
28
+ # Verify file is indeed within user directory
29
+ try:
30
+ file_path_resolved = file_path.resolve()
31
+ user_dir_resolved = user_files_dir.resolve()
32
+ if not str(file_path_resolved).startswith(str(user_dir_resolved)):
33
+ return JSONResponse(
34
+ content={"error": "访问被拒绝"},
35
+ status_code=403
36
+ )
37
+ except:
38
+ return JSONResponse(
39
+ content={"error": "无效的文件路径"},
40
+ status_code=400
41
+ )
42
+
43
+ # Return file
44
+ return FileResponse(file_path)
45
+
46
+ except Exception as e:
47
+ return JSONResponse(
48
+ content={"error": str(e)},
49
+ status_code=500
50
+ )