diff --git a/backend/api/server.py b/backend/api/server.py index 00f2fb7..dd0daf8 100644 --- a/backend/api/server.py +++ b/backend/api/server.py @@ -83,6 +83,9 @@ FEISHU_SPREADSHEET_MAX_CELL_CHARS = 5000 FEISHU_SPREADSHEET_WRITE_BATCH_ROWS = 500 FEISHU_ONLINE_DOCS_FILENAME = 'online-docs.json' JUPYTER_WORKSPACE_FILENAME = 'jupyter_workspace.json' +JUPYTER_STREAM_CHUNK_BYTES = 1024 * 1024 +FEISHU_REMOTE_DOC_MAX_BYTES = 50 * 1024 * 1024 +FEISHU_REMOTE_SHEET_MAX_BYTES = 100 * 1024 * 1024 @dataclass @@ -1096,11 +1099,20 @@ def create_app(state: AgentState) -> FastAPI: if runtime is None: raise HTTPException(status_code=404, detail='Jupyter runtime is not connected') try: - data, filename = runtime.read_file_bytes(path) + response, filename = runtime.open_file_stream(path) except JupyterRuntimeError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc - return Response( - data, + + def stream_chunks() -> Any: + try: + for chunk in response.iter_content(chunk_size=JUPYTER_STREAM_CHUNK_BYTES): + if chunk: + yield chunk + finally: + response.close() + + return StreamingResponse( + stream_chunks(), media_type=_content_type_for_filename(filename), headers={ 'content-disposition': ( @@ -3050,9 +3062,27 @@ def _materialize_jupyter_file_for_feishu( if runtime is None: raise HTTPException(status_code=404, detail='Jupyter runtime is not connected') try: - data, filename = runtime.read_file_bytes(remote_path) + info = runtime.file_info(remote_path) + filename = _safe_uploaded_filename(str(info.get('name') or 'remote-file')) + suffix = Path(filename).suffix.lower() + max_bytes = ( + FEISHU_REMOTE_SHEET_MAX_BYTES + if suffix in FEISHU_SPREADSHEET_SUFFIXES + else FEISHU_REMOTE_DOC_MAX_BYTES + ) + size = info.get('size') + if isinstance(size, int) and size > max_bytes: + raise HTTPException( + status_code=413, + detail=( + f'远端文件过大,当前在线文档转换上限为 ' + f'{_format_bytes(max_bytes)},请先在 Jupyter 侧裁剪或抽样后再转换。' + ), + ) + response, stream_filename = runtime.open_file_stream(remote_path) except JupyterRuntimeError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc + filename = filename or _safe_uploaded_filename(stream_filename) target_dir = ( state.account_paths(account_id)['base'] / 'integrations' @@ -3061,8 +3091,27 @@ def _materialize_jupyter_file_for_feishu( / session_id ) target_dir.mkdir(parents=True, exist_ok=True) - target = target_dir / _safe_uploaded_filename(filename) - target.write_bytes(data) + target = target_dir / filename + written = 0 + try: + with target.open('wb') as handle: + for chunk in response.iter_content(chunk_size=JUPYTER_STREAM_CHUNK_BYTES): + if not chunk: + continue + written += len(chunk) + if written > max_bytes: + handle.close() + target.unlink(missing_ok=True) + raise HTTPException( + status_code=413, + detail=( + f'远端文件过大,当前在线文档转换上限为 ' + f'{_format_bytes(max_bytes)},请先在 Jupyter 侧裁剪或抽样后再转换。' + ), + ) + handle.write(chunk) + finally: + response.close() return target @@ -3080,6 +3129,16 @@ def _safe_uploaded_filename(filename: str) -> str: return cleaned[:180] or 'remote-file' +def _format_bytes(value: int) -> str: + if value >= 1024 * 1024 * 1024: + return f'{value / (1024 * 1024 * 1024):.1f}GB' + if value >= 1024 * 1024: + return f'{value / (1024 * 1024):.0f}MB' + if value >= 1024: + return f'{value / 1024:.0f}KB' + return f'{value}B' + + def _resolve_account_file(state: AgentState, account_id: str | None, raw_path: str) -> Path: path = Path(raw_path).expanduser().resolve() account_base = state.account_paths(account_id)['base'].resolve() diff --git a/frontend/app/components/assistant-ui/activity-panel.tsx b/frontend/app/components/assistant-ui/activity-panel.tsx index 4c37ca0..1271ede 100644 --- a/frontend/app/components/assistant-ui/activity-panel.tsx +++ b/frontend/app/components/assistant-ui/activity-panel.tsx @@ -282,6 +282,7 @@ type SessionFile = { name: string; path: string; kind: "input" | "output"; + source?: "local" | "jupyter"; size: number; modified_at: string; download_url: string; @@ -593,6 +594,7 @@ function SessionFileRow({ file }: { file: SessionFile }) { )}