From ff927d6739fb44303f4465e73b3f8afb998f83a4 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Wed, 12 Aug 2026 19:14:01 +0800 Subject: [PATCH] =?UTF-8?q?refactor(files):=20=E4=B8=8B=E7=BA=BF=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=20/file-manager=20=E9=A1=B5=E9=9D=A2=E5=8F=8A?= =?UTF-8?q?=E5=85=B6=E5=86=99=E6=93=8D=E4=BD=9C=E7=AB=AF=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除页面路由:/file-manager、/file-manager/editor、/file-preview/ - 删除写端点:/api/gui/files/{create,delete,rename,copy,move,upload,download/batch} - 保留主 SPA 共享端点:/api/files、entries、text、download、/api/project/files/search - 拆除前端入口链:QuickMenu → InputComposer → App.vue → openGuiFileManager - 删除 static/file_manager/ 目录(git 历史可恢复) - sanitize_filename_preserve_unicode 迁至 server/utils_common.py, 清理 5 个拆分文件的死 import,修复 api_v1/chat 上传的引用来源 - 安全收益:create/rename 路径穿越与 preview 存储型 XSS 随页面物理消解 --- server/api_v1.py | 2 +- server/auth.py | 32 - server/chat/approval.py | 1 - server/chat/files.py | 2 +- server/chat/misc.py | 1 - server/chat/permission.py | 1 - server/chat/settings.py | 1 - server/chat/terminal.py | 1 - server/files.py | 206 +--- server/utils_common.py | 17 + static/file_manager/app.js | 899 ------------------ static/file_manager/editor.css | 60 -- static/file_manager/editor.html | 31 - static/file_manager/editor.js | 135 --- static/file_manager/index.html | 65 -- static/file_manager/style.css | 357 ------- static/src/App.vue | 1 - static/src/app/methods/ui/system.ts | 6 - static/src/components/input/InputComposer.vue | 2 - static/src/components/input/QuickMenu.vue | 10 - 20 files changed, 25 insertions(+), 1805 deletions(-) delete mode 100644 static/file_manager/app.js delete mode 100644 static/file_manager/editor.css delete mode 100644 static/file_manager/editor.html delete mode 100644 static/file_manager/editor.js delete mode 100644 static/file_manager/index.html delete mode 100644 static/file_manager/style.css diff --git a/server/api_v1.py b/server/api_v1.py index fa4dab1d..46aa8d78 100644 --- a/server/api_v1.py +++ b/server/api_v1.py @@ -12,7 +12,7 @@ from flask import Blueprint, request, jsonify, send_file, session from .api_auth import api_token_required from .tasks import task_manager from .context import get_user_resources, ensure_conversation_loaded, get_upload_guard, apply_conversation_overrides -from .files import sanitize_filename_preserve_unicode +from .utils_common import sanitize_filename_preserve_unicode from .utils_common import debug_log from config.model_profiles import get_registered_model_profiles from core.tool_config import TOOL_CATEGORIES diff --git a/server/auth.py b/server/auth.py index 2ecef362..2b81024a 100644 --- a/server/auth.py +++ b/server/auth.py @@ -7,7 +7,6 @@ from flask import Blueprint, request, jsonify, session, redirect, send_from_dire from modules.personalization_manager import load_personalization_config from modules.host_workspace_manager import resolve_host_workspace -from modules.user_manager import UserWorkspace from config import ( TERMINAL_SANDBOX_MODE, DATA_DIR, @@ -25,7 +24,6 @@ from .security import ( is_action_blocked, clear_failures, ) -from .context import with_terminal, get_gui_manager from . import state from .utils_common import debug_log @@ -442,36 +440,6 @@ def terminal_page(): return current_app.send_static_file('terminal.html') -@auth_bp.route('/file-manager') -@login_required -def gui_file_manager_page(): - from .auth_helpers import resolve_admin_policy - policy = resolve_admin_policy(get_current_user_record()) - if policy.get("ui_blocks", {}).get("block_file_manager"): - return "文件管理器已被管理员禁用", 403 - return send_from_directory(Path(current_app.static_folder) / 'file_manager', 'index.html') - - -@auth_bp.route('/file-manager/editor') -@login_required -def gui_file_editor_page(): - return send_from_directory(Path(current_app.static_folder) / 'file_manager', 'editor.html') - - -@auth_bp.route('/file-preview/') -@login_required -@with_terminal -def gui_file_preview(relative_path: str, terminal, workspace: UserWorkspace, username: str): - manager = get_gui_manager(workspace) - try: - target = manager.prepare_download(relative_path) - if not target.is_file(): - return "预览仅支持文件", 400 - return send_from_directory(directory=target.parent, path=target.name, mimetype='text/html') - except Exception as exc: - return f"无法预览文件: {exc}", 400 - - @auth_bp.route('/user_upload/') @login_required def serve_user_upload(filename: str): diff --git a/server/chat/approval.py b/server/chat/approval.py index 6abbb75c..df7e3ed2 100644 --- a/server/chat/approval.py +++ b/server/chat/approval.py @@ -41,7 +41,6 @@ from server.state import PROJECT_MAX_STORAGE_MB, pending_socket_tokens, SOCKET_T from server.state import tool_approval_manager, user_question_manager from server.extensions import socketio from server.monitor import get_cached_monitor_snapshot -from server.files import sanitize_filename_preserve_unicode UPLOAD_FOLDER_NAME = ".astrion/user_upload" @chat_bp.route('/api/user-questions/pending', methods=['GET']) diff --git a/server/chat/files.py b/server/chat/files.py index 41f58990..00241841 100644 --- a/server/chat/files.py +++ b/server/chat/files.py @@ -42,7 +42,7 @@ from server.state import PROJECT_MAX_STORAGE_MB, pending_socket_tokens, SOCKET_T from server.state import tool_approval_manager, user_question_manager from server.extensions import socketio from server.monitor import get_cached_monitor_snapshot -from server.files import sanitize_filename_preserve_unicode +from server.utils_common import sanitize_filename_preserve_unicode UPLOAD_FOLDER_NAME = ".astrion/user_upload" diff --git a/server/chat/misc.py b/server/chat/misc.py index 0b139d3f..0beacd0c 100644 --- a/server/chat/misc.py +++ b/server/chat/misc.py @@ -41,7 +41,6 @@ from server.state import PROJECT_MAX_STORAGE_MB, pending_socket_tokens, SOCKET_T from server.state import tool_approval_manager, user_question_manager from server.extensions import socketio from server.monitor import get_cached_monitor_snapshot -from server.files import sanitize_filename_preserve_unicode UPLOAD_FOLDER_NAME = ".astrion/user_upload" @chat_bp.route('/api/memory', methods=['GET']) diff --git a/server/chat/permission.py b/server/chat/permission.py index e1fc2504..26dbd98b 100644 --- a/server/chat/permission.py +++ b/server/chat/permission.py @@ -51,7 +51,6 @@ from server.state import PROJECT_MAX_STORAGE_MB, pending_socket_tokens, SOCKET_T from server.state import tool_approval_manager, user_question_manager from server.extensions import socketio from server.monitor import get_cached_monitor_snapshot -from server.files import sanitize_filename_preserve_unicode import os import re diff --git a/server/chat/settings.py b/server/chat/settings.py index 48630b35..6a4977a2 100644 --- a/server/chat/settings.py +++ b/server/chat/settings.py @@ -43,7 +43,6 @@ from server.state import PROJECT_MAX_STORAGE_MB, pending_socket_tokens, SOCKET_T from server.state import tool_approval_manager, user_question_manager from server.extensions import socketio from server.monitor import get_cached_monitor_snapshot -from server.files import sanitize_filename_preserve_unicode UPLOAD_FOLDER_NAME = ".astrion/user_upload" diff --git a/server/chat/terminal.py b/server/chat/terminal.py index 0e788322..9d3fb768 100644 --- a/server/chat/terminal.py +++ b/server/chat/terminal.py @@ -41,7 +41,6 @@ from server.state import PROJECT_MAX_STORAGE_MB, pending_socket_tokens, SOCKET_T from server.state import tool_approval_manager, user_question_manager from server.extensions import socketio from server.monitor import get_cached_monitor_snapshot -from server.files import sanitize_filename_preserve_unicode UPLOAD_FOLDER_NAME = ".astrion/user_upload" @chat_bp.route('/api/terminals') diff --git a/server/files.py b/server/files.py index 414b17bf..1871bdc4 100644 --- a/server/files.py +++ b/server/files.py @@ -1,39 +1,23 @@ -"""文件与GUI文件管理相关路由。""" +"""文件相关共享路由(主 SPA 使用):项目结构、目录列举、下载、文本读写、@文件搜索。 + +原 /file-manager 独立页面的写操作端点(create/delete/rename/copy/move/upload/batch) +已随该页面下线移除;页面路由见 git 历史。 +""" from __future__ import annotations import os -import re import zipfile from io import BytesIO from pathlib import Path from typing import Dict, Any, List, Optional, Tuple from flask import Blueprint, jsonify, request, send_file -from werkzeug.utils import secure_filename -from modules.upload_security import UploadSecurityError from .auth_helpers import api_login_required, resolve_admin_policy, get_current_user_record -from .security import rate_limited -from .context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response +from .context import with_terminal, get_gui_manager from .utils_common import debug_log files_bp = Blueprint("files", __name__) - -def sanitize_filename_preserve_unicode(filename: str) -> str: - """在保留中文等字符的同时,移除危险字符和路径成分""" - import re - if not filename: - return "" - cleaned = filename.strip().replace("\x00", "") - if not cleaned: - return "" - cleaned = cleaned.replace("\\", "/").split("/")[-1] - cleaned = re.sub(r'[<>:"\\|?*\n\r\t]', "_", cleaned) - cleaned = cleaned.strip(". ") - if not cleaned: - return "" - return cleaned[:255] - @files_bp.route('/api/files') @api_login_required @with_terminal @@ -81,153 +65,6 @@ def gui_list_entries(terminal, workspace, username): return jsonify({"success": False, "error": str(exc)}), 400 -@files_bp.route('/api/gui/files/create', methods=['POST']) -@api_login_required -@with_terminal -@rate_limited("gui_file_create", 30, 60, scope="user") -def gui_create_entry(terminal, workspace, username): - payload = request.get_json() or {} - parent = payload.get('path') or "" - name = payload.get('name') or "" - entry_type = payload.get('type') or "file" - policy = resolve_admin_policy(get_current_user_record()) - if policy.get("ui_blocks", {}).get("block_file_manager"): - return jsonify({"success": False, "error": "文件管理已被管理员禁用"}), 403 - manager = get_gui_manager(workspace) - try: - new_path = manager.create_entry(parent, name, entry_type) - return jsonify({"success": True, "path": new_path}) - except Exception as exc: - return jsonify({"success": False, "error": str(exc)}), 400 - - -@files_bp.route('/api/gui/files/delete', methods=['POST']) -@api_login_required -@with_terminal -@rate_limited("gui_file_delete", 30, 60, scope="user") -def gui_delete_entries(terminal, workspace, username): - payload = request.get_json() or {} - paths = payload.get('paths') or [] - policy = resolve_admin_policy(get_current_user_record()) - if policy.get("ui_blocks", {}).get("block_file_manager"): - return jsonify({"success": False, "error": "文件管理已被管理员禁用"}), 403 - manager = get_gui_manager(workspace) - try: - result = manager.delete_entries(paths) - return jsonify({"success": True, "result": result}) - except Exception as exc: - return jsonify({"success": False, "error": str(exc)}), 400 - - -@files_bp.route('/api/gui/files/rename', methods=['POST']) -@api_login_required -@with_terminal -@rate_limited("gui_file_rename", 30, 60, scope="user") -def gui_rename_entry(terminal, workspace, username): - payload = request.get_json() or {} - path = payload.get('path') - new_name = payload.get('new_name') - if not path or not new_name: - return jsonify({"success": False, "error": "缺少 path 或 new_name"}), 400 - policy = resolve_admin_policy(get_current_user_record()) - if policy.get("ui_blocks", {}).get("block_file_manager"): - return jsonify({"success": False, "error": "文件管理已被管理员禁用"}), 403 - manager = get_gui_manager(workspace) - try: - new_path = manager.rename_entry(path, new_name) - return jsonify({"success": True, "path": new_path}) - except Exception as exc: - return jsonify({"success": False, "error": str(exc)}), 400 - - -@files_bp.route('/api/gui/files/copy', methods=['POST']) -@api_login_required -@with_terminal -@rate_limited("gui_file_copy", 40, 120, scope="user") -def gui_copy_entries(terminal, workspace, username): - payload = request.get_json() or {} - paths = payload.get('paths') or [] - target_dir = payload.get('target_dir') or "" - policy = resolve_admin_policy(get_current_user_record()) - if policy.get("ui_blocks", {}).get("block_file_manager"): - return jsonify({"success": False, "error": "文件管理已被管理员禁用"}), 403 - manager = get_gui_manager(workspace) - try: - result = manager.copy_entries(paths, target_dir) - return jsonify({"success": True, "result": result}) - except Exception as exc: - return jsonify({"success": False, "error": str(exc)}), 400 - - -@files_bp.route('/api/gui/files/move', methods=['POST']) -@api_login_required -@with_terminal -@rate_limited("gui_file_move", 40, 120, scope="user") -def gui_move_entries(terminal, workspace, username): - payload = request.get_json() or {} - paths = payload.get('paths') or [] - target_dir = payload.get('target_dir') or "" - manager = get_gui_manager(workspace) - try: - result = manager.move_entries(paths, target_dir) - return jsonify({"success": True, "result": result}) - except Exception as exc: - return jsonify({"success": False, "error": str(exc)}), 400 - - -@files_bp.route('/api/gui/files/upload', methods=['POST']) -@api_login_required -@with_terminal -@rate_limited("gui_file_upload", 10, 300, scope="user") -def gui_upload_entry(terminal, workspace, username): - policy = resolve_admin_policy(get_current_user_record()) - if policy.get("ui_blocks", {}).get("block_upload"): - return jsonify({"success": False, "error": "文件上传已被管理员禁用"}), 403 - if 'file' not in request.files: - return jsonify({"success": False, "error": "未找到文件"}), 400 - file_obj = request.files['file'] - if not file_obj or not file_obj.filename: - return jsonify({"success": False, "error": "文件名为空"}), 400 - current_dir = request.form.get('path') or "" - raw_name = request.form.get('filename') or file_obj.filename - filename = sanitize_filename_preserve_unicode(raw_name) or secure_filename(raw_name) - if not filename: - return jsonify({"success": False, "error": "非法文件名"}), 400 - manager = get_gui_manager(workspace) - try: - target_path = manager.prepare_upload(current_dir, filename) - except Exception as exc: - return jsonify({"success": False, "error": str(exc)}), 400 - try: - relative_path = manager._to_relative(target_path) - except Exception as exc: - return jsonify({"success": False, "error": str(exc)}), 400 - guard = get_upload_guard(workspace) - try: - result = guard.process_upload( - file_obj, - target_path, - username=username, - source="web_gui", - original_name=raw_name, - relative_path=relative_path, - ) - except UploadSecurityError as exc: - return build_upload_error_response(exc) - except Exception as exc: - return jsonify({"success": False, "error": f"保存文件失败: {exc}"}), 500 - - metadata = result.get("metadata", {}) - return jsonify({ - "success": True, - "path": relative_path, - "filename": target_path.name, - "scan": metadata.get("scan"), - "sha256": metadata.get("sha256"), - "size": metadata.get("size"), - }) - - @files_bp.route('/api/gui/files/download', methods=['GET']) @api_login_required @with_terminal @@ -254,37 +91,6 @@ def gui_download_entry(terminal, workspace, username): return jsonify({"success": False, "error": str(exc)}), 400 -@files_bp.route('/api/gui/files/download/batch', methods=['POST']) -@api_login_required -@with_terminal -def gui_download_batch(terminal, workspace, username): - payload = request.get_json() or {} - paths = payload.get('paths') or [] - if not paths: - return jsonify({"success": False, "error": "缺少待下载的路径"}), 400 - manager = get_gui_manager(workspace) - try: - memory_file = BytesIO() - with zipfile.ZipFile(memory_file, mode='w', compression=zipfile.ZIP_DEFLATED) as zf: - for rel in paths: - target = manager.prepare_download(rel) - arc_base = rel.strip('/') or target.name - if target.is_dir(): - for root, _, files in os.walk(target): - for file in files: - full_path = Path(root) / file - relative_sub = full_path.relative_to(target) - arcname = Path(arc_base) / relative_sub - zf.write(full_path, arcname=str(arcname)) - else: - zf.write(target, arcname=arc_base) - memory_file.seek(0) - download_name = f"selected_{len(paths)}.zip" - return send_file(memory_file, as_attachment=True, download_name=download_name, mimetype='application/zip') - except Exception as exc: - return jsonify({"success": False, "error": str(exc)}), 400 - - @files_bp.route('/api/gui/files/text', methods=['GET', 'POST']) @api_login_required @with_terminal diff --git a/server/utils_common.py b/server/utils_common.py index 2202a0c1..4a17a914 100644 --- a/server/utils_common.py +++ b/server/utils_common.py @@ -17,6 +17,22 @@ def _sanitize_filename_component(text: str) -> str: return safe or "untitled" +def sanitize_filename_preserve_unicode(filename: str) -> str: + """在保留中文等字符的同时,移除危险字符和路径成分(原 server/files.py,随 /file-manager 下线迁入)。""" + import re + if not filename: + return "" + cleaned = filename.strip().replace("\x00", "") + if not cleaned: + return "" + cleaned = cleaned.replace("\\", "/").split("/")[-1] + cleaned = re.sub(r'[<>:"\\|?*\n\r\t]', "_", cleaned) + cleaned = cleaned.strip(". ") + if not cleaned: + return "" + return cleaned[:255] + + def build_review_lines(messages, limit=None): """ 将对话消息序列拍平成简化文本。 @@ -187,6 +203,7 @@ def log_streaming_debug_entry(data: Dict[str, Any]): __all__ = [ "_sanitize_filename_component", + "sanitize_filename_preserve_unicode", "build_review_lines", "brief_log", "debug_log", diff --git a/static/file_manager/app.js b/static/file_manager/app.js deleted file mode 100644 index 567ace5c..00000000 --- a/static/file_manager/app.js +++ /dev/null @@ -1,899 +0,0 @@ -(() => { - const API_BASE = '/api/gui/files'; - const EDITOR_PAGE = '/file-manager/editor'; - - const state = { - currentPath: '', - items: [], - selected: new Set(), - lastSelectedIndex: null, - clipboard: null, // {mode: 'copy'|'cut', items: []} - treeCache: new Map(), - treeExpanded: new Set(['']), - isDraggingSelection: false, - dragStart: null, - selectionRect: null, - selectionJustFinished: false, - selectionDisabled: false, - }; - - const icons = { - directory: '📁', - default: '📄', - editable: '📝', - code: '💻', - markdown: '🧾', - image: '🖼️', - archive: '🗃️', - }; - - const fileGrid = document.getElementById('fileGrid'); - const directoryTree = document.getElementById('directoryTree'); - const breadcrumbEl = document.getElementById('breadcrumb'); - const selectionInfo = document.getElementById('selectionInfo'); - const statusBar = document.getElementById('statusBar'); - const contextMenu = document.getElementById('contextMenu'); - const dialogBackdrop = document.getElementById('dialogBackdrop'); - const dialogTitle = document.getElementById('dialogTitle'); - const dialogContent = document.getElementById('dialogContent'); - const dialogCancel = document.getElementById('dialogCancel'); - const dialogConfirm = document.getElementById('dialogConfirm'); - const hiddenUploader = document.getElementById('hiddenUploader'); - const pasteBtn = document.getElementById('btnPaste'); - - const newFolderBtn = document.getElementById('btnNewFolder'); - const newFileBtn = document.getElementById('btnNewFile'); - const refreshBtn = document.getElementById('btnRefresh'); - const uploadBtn = document.getElementById('btnUpload'); - const backBtn = document.getElementById('btnBack'); - const returnChatBtn = document.getElementById('btnReturnChat'); - const downloadBtn = document.getElementById('btnDownload'); - const renameBtn = document.getElementById('btnRename'); - const copyBtn = document.getElementById('btnCopy'); - const cutBtn = document.getElementById('btnCut'); - const deleteBtn = document.getElementById('btnDelete'); - const toggleSelectionBtn = document.getElementById('btnToggleSelection'); - - const clamp = (value, min, max) => Math.max(min, Math.min(value, max)); - const urlParams = new URLSearchParams(window.location.search); - const initialPathParam = (urlParams.get('path') || '').replace(/^\//, '').replace(/\/$/, ''); - - dialogBackdrop.hidden = true; - - let dialogHandlers = { confirm: null, cancel: null }; - - function clearDialogHandlers() { - dialogHandlers.confirm = null; - dialogHandlers.cancel = null; - } - - function registerDialogHandlers(confirmHandler, cancelHandler) { - dialogHandlers.confirm = confirmHandler || null; - dialogHandlers.cancel = cancelHandler || null; - } - - function closeDialog() { - dialogBackdrop.hidden = true; - clearDialogHandlers(); - } - - dialogCancel.addEventListener('click', () => { - if (dialogHandlers.cancel) { - const handler = dialogHandlers.cancel; - clearDialogHandlers(); - handler(); - } else { - closeDialog(); - } - }); - - dialogConfirm.addEventListener('click', () => { - if (dialogHandlers.confirm) { - const handler = dialogHandlers.confirm; - clearDialogHandlers(); - handler(); - } else { - closeDialog(); - } - }); - - dialogBackdrop.addEventListener('click', (event) => { - if (event.target === dialogBackdrop) { - if (dialogHandlers.cancel) { - const handler = dialogHandlers.cancel; - clearDialogHandlers(); - handler(); - } else { - closeDialog(); - } - } - }); - - document.addEventListener('keydown', (event) => { - if (event.key === 'Escape' && !dialogBackdrop.hidden) { - if (dialogHandlers.cancel) { - const handler = dialogHandlers.cancel; - clearDialogHandlers(); - handler(); - } else { - closeDialog(); - } - } - }); - closeDialog(); - - function showStatus(message) { - statusBar.textContent = message; - } - - function formatSize(size) { - if (size < 1024) return `${size} B`; - if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`; - if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(1)} MB`; - return `${(size / 1024 / 1024 / 1024).toFixed(1)} GB`; - } - - function formatTime(ts) { - const d = new Date(ts * 1000); - return d.toLocaleString(); - } - - function joinPath(base, name) { - if (!base) return name; - return `${base.replace(/\/$/, '')}/${name}`; - } - - function getIcon(entry) { - if (entry.type === 'directory') return icons.directory; - if (entry.is_editable) return icons.editable; - const ext = entry.extension || ''; - if (['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp'].includes(ext)) { - return icons.image; - } - if (['.zip', '.rar', '.7z', '.tar', '.gz'].includes(ext)) { - return icons.archive; - } - if (['.js', '.ts', '.py', '.rb', '.php', '.java', '.kt', '.go', '.rs', '.c', '.cpp', '.h', '.hpp'].includes(ext)) { - return icons.code; - } - if (['.md', '.markdown'].includes(ext)) { - return icons.markdown; - } - return icons.default; - } - - async function request(url, options = {}) { - const response = await fetch(url, options); - const data = await response.json().catch(() => ({})); - if (!response.ok || data.success === false) { - const message = data.error || data.message || `请求失败 (${response.status})`; - throw new Error(message); - } - return data; - } - - function updateUrl(path) { - const url = new URL(window.location.href); - if (path) { - url.searchParams.set('path', path); - } else { - url.searchParams.delete('path'); - } - window.history.replaceState({}, '', url.pathname + url.search); - } - - async function ensureAncestors(path) { - await ensureTreeNode('', false); - state.treeExpanded.add(''); - if (!path) { - renderTree(); - return; - } - const segments = path.split('/').filter(Boolean); - let current = ''; - for (let index = 0; index < segments.length; index += 1) { - const segment = segments[index]; - current = current ? `${current}/${segment}` : segment; - if (index < segments.length - 1) { - state.treeExpanded.add(current); - } - await ensureTreeNode(current, false); - } - renderTree(); - } - - async function loadDirectory(path = '', { updateHistory = true } = {}) { - hideContextMenu(); - showStatus('加载中...'); - try { - const result = await request(`${API_BASE}/entries?path=${encodeURIComponent(path)}`); - const resolvedPath = result.data.path || ''; - state.currentPath = resolvedPath; - state.items = result.data.items || []; - const directoryEntries = state.items.filter((item) => item.type === 'directory'); - state.treeCache.set(resolvedPath, directoryEntries); - if (updateHistory) { - updateUrl(resolvedPath); - } - await ensureAncestors(resolvedPath); - renderBreadcrumb(result.data.breadcrumb || []); - renderGrid(); - updateSelection([]); - state.lastSelectedIndex = null; - showStatus(`已加载 ${state.items.length} 项`); - } catch (err) { - showStatus(err.message); - } - } - - function renderBreadcrumb(crumbs) { - breadcrumbEl.innerHTML = ''; - crumbs.forEach((crumb, index) => { - const span = document.createElement('span'); - span.textContent = crumb.name; - span.dataset.path = crumb.path; - span.addEventListener('click', () => { - loadDirectory(crumb.path); - }); - breadcrumbEl.appendChild(span); - if (index < crumbs.length - 1) { - const sep = document.createElement('span'); - sep.textContent = '›'; - sep.classList.add('fm-breadcrumb-sep'); - breadcrumbEl.appendChild(sep); - } - }); - } - - function renderGrid() { - fileGrid.innerHTML = ''; - state.items.forEach((entry, index) => { - const card = document.createElement('div'); - card.className = 'fm-card'; - card.tabIndex = 0; - card.dataset.path = entry.path; - card.dataset.index = index; - if (state.selected.has(entry.path)) { - card.classList.add('selected'); - } - - const icon = document.createElement('div'); - icon.className = 'fm-card-icon'; - icon.textContent = getIcon(entry); - - const name = document.createElement('div'); - name.className = 'fm-card-name'; - name.textContent = entry.name; - - const meta = document.createElement('div'); - meta.className = 'fm-card-meta'; - const lines = []; - if (entry.type === 'file') { - lines.push(formatSize(entry.size)); - } else { - lines.push('目录'); - } - lines.push(formatTime(entry.modified_at)); - meta.innerHTML = lines.join('
'); - - card.appendChild(icon); - card.appendChild(name); - card.appendChild(meta); - - card.addEventListener('click', (event) => handleItemClick(event, entry, index)); - card.addEventListener('dblclick', () => handleItemDoubleClick(entry)); - card.addEventListener('contextmenu', (event) => handleItemContextMenu(event, entry)); - - fileGrid.appendChild(card); - }); - - const overlay = document.createElement('div'); - overlay.className = 'fm-drop-overlay'; - overlay.textContent = '释放即可上传到此目录'; - fileGrid.appendChild(overlay); - } - - function updateSelection(paths, options = { append: false, range: false }) { - if (!options.append && !options.range) { - state.selected.clear(); - if (paths.length === 0) { - state.lastSelectedIndex = null; - } - } - paths.forEach((path) => { - if (state.selected.has(path) && options.append) { - state.selected.delete(path); - } else { - state.selected.add(path); - } - }); - syncSelectionUI(); - } - - function syncSelectionUI() { - const cards = fileGrid.querySelectorAll('.fm-card'); - cards.forEach((card) => { - if (state.selected.has(card.dataset.path)) { - card.classList.add('selected'); - } else { - card.classList.remove('selected'); - } - }); - selectionInfo.textContent = `已选中 ${state.selected.size} 项`; - pasteBtn.disabled = !state.clipboard || !state.clipboard.items.length; - } - - function handleItemClick(event, entry, index) { - const isMetaKey = event.metaKey || event.ctrlKey; - const isShiftKey = event.shiftKey; - if (isShiftKey && state.lastSelectedIndex !== null) { - const start = Math.min(state.lastSelectedIndex, index); - const end = Math.max(state.lastSelectedIndex, index); - const paths = state.items.slice(start, end + 1).map((item) => item.path); - updateSelection(paths, { range: true }); - } else if (isMetaKey) { - updateSelection([entry.path], { append: true }); - state.lastSelectedIndex = index; - } else { - updateSelection([entry.path], { append: false }); - state.lastSelectedIndex = index; - } - } - - function handleItemDoubleClick(entry) { - if (entry.type === 'directory') { - loadDirectory(entry.path); - return; - } - if (entry.is_editable) { - window.location.href = `${EDITOR_PAGE}?path=${encodeURIComponent(entry.path)}`; - return; - } - window.open(`${API_BASE}/download?path=${encodeURIComponent(entry.path)}`, '_blank'); - } - - function handleItemContextMenu(event, entry) { - event.preventDefault(); - if (!state.selected.has(entry.path)) { - updateSelection([entry.path], { append: false }); - } - showContextMenu(event.clientX, event.clientY); - } - - function showContextMenu(x, y) { - const single = state.selected.size === 1; - const singleEntry = single ? getSingleSelected() : null; - contextMenu.innerHTML = ''; - - const entries = []; - - if (singleEntry) { - if (singleEntry.type === 'directory') { - entries.push({ label: '打开', action: openSelected, disabled: false }); - } else if (singleEntry.is_editable) { - entries.push({ label: '在编辑器中打开', action: openEditor, disabled: false }); - if (singleEntry.extension === '.html' || singleEntry.extension === '.htm') { - entries.push({ label: '预览', action: previewSelected, disabled: false }); - } - entries.push({ label: '下载', action: downloadSelected, disabled: false }); - } else { - const isHtml = singleEntry.extension === '.html' || singleEntry.extension === '.htm'; - if (isHtml) { - entries.push({ label: '预览', action: previewSelected, disabled: false }); - } - entries.push({ label: '下载', action: downloadSelected, disabled: false }); - } - } else if (state.selected.size > 0) { - entries.push({ label: '下载', action: downloadSelected, disabled: false }); - } - - entries.push( - { label: '重命名', action: renameSelected, disabled: !single }, - { label: '复制', action: copySelected, disabled: state.selected.size === 0 }, - { label: '剪切', action: cutSelected, disabled: state.selected.size === 0 }, - { label: '粘贴', action: pasteClipboard, disabled: !state.clipboard || !state.clipboard.items.length }, - { label: '删除', action: deleteSelected, disabled: state.selected.size === 0 }, - ); - - entries.forEach((item) => { - const btn = document.createElement('button'); - btn.textContent = item.label; - btn.disabled = item.disabled; - btn.addEventListener('click', () => { - hideContextMenu(); - item.action(); - }); - contextMenu.appendChild(btn); - }); - contextMenu.style.display = 'block'; - const { innerWidth, innerHeight } = window; - const menuRect = contextMenu.getBoundingClientRect(); - const left = clamp(x, 0, innerWidth - menuRect.width); - const top = clamp(y, 0, innerHeight - menuRect.height); - contextMenu.style.left = `${left}px`; - contextMenu.style.top = `${top}px`; - } - - function hideContextMenu() { - contextMenu.style.display = 'none'; - } - - function getSingleSelected() { - if (state.selected.size !== 1) return null; - const path = Array.from(state.selected)[0]; - return state.items.find((item) => item.path === path) || null; - } - - function openSelected() { - const entry = getSingleSelected(); - if (!entry) return; - handleItemDoubleClick(entry); - } - - function previewSelected() { - const entry = getSingleSelected(); - if (!entry) return; - if (entry.extension !== '.html' && entry.extension !== '.htm') { - showStatus('仅支持预览 HTML 文件'); - return; - } - window.open(`/file-preview/${encodeURIComponent(entry.path)}`, '_blank'); - } - - function openEditor() { - const entry = getSingleSelected(); - if (!entry || !entry.is_editable) return; - window.location.href = `${EDITOR_PAGE}?path=${encodeURIComponent(entry.path)}`; - } - - function triggerBlobDownload(filename, blob) { - const url = URL.createObjectURL(blob); - const anchor = document.createElement('a'); - anchor.href = url; - anchor.download = filename; - document.body.appendChild(anchor); - anchor.click(); - anchor.remove(); - URL.revokeObjectURL(url); - } - - async function downloadSelected() { - if (!state.selected.size) return; - if (state.selected.size === 1) { - const path = Array.from(state.selected)[0]; - window.open(`${API_BASE}/download?path=${encodeURIComponent(path)}`, '_blank'); - return; - } - try { - const resp = await fetch(`${API_BASE}/download/batch`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ paths: Array.from(state.selected) }) - }); - if (!resp.ok) { - let message = '批量下载失败'; - try { - const data = await resp.json(); - message = data.error || data.message || message; - } catch (_) { - // ignore - } - throw new Error(message); - } - const blob = await resp.blob(); - triggerBlobDownload(`selected_${Date.now()}.zip`, blob); - showStatus(`已开始下载 ${state.selected.size} 个项目`); - } catch (err) { - showStatus(err.message); - } - } - - async function renameSelected() { - const entry = getSingleSelected(); - if (!entry) return; - const newName = await promptDialog('重命名', entry.name); - if (!newName) return; - try { - await request(`${API_BASE}/rename`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path: entry.path, new_name: newName }), - }); - await loadDirectory(state.currentPath); - } catch (err) { - showStatus(err.message); - } - } - - function copySelected() { - if (!state.selected.size) return; - state.clipboard = { mode: 'copy', items: Array.from(state.selected) }; - showStatus(`已复制 ${state.clipboard.items.length} 项`); - syncSelectionUI(); - } - - function cutSelected() { - if (!state.selected.size) return; - state.clipboard = { mode: 'cut', items: Array.from(state.selected) }; - showStatus(`已剪切 ${state.clipboard.items.length} 项`); - syncSelectionUI(); - } - - async function pasteClipboard() { - if (!state.clipboard || !state.clipboard.items.length) return; - const endpoint = state.clipboard.mode === 'copy' ? 'copy' : 'move'; - try { - await request(`${API_BASE}/${endpoint}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - paths: state.clipboard.items, - target_dir: state.currentPath, - }), - }); - if (state.clipboard.mode === 'cut') { - state.clipboard = null; - } - await loadDirectory(state.currentPath); - } catch (err) { - showStatus(err.message); - } - } - - async function deleteSelected() { - if (!state.selected.size) return; - const confirm = await confirmDialog(`确认删除选中的 ${state.selected.size} 项吗?该操作不可撤销。`); - if (!confirm) return; - try { - await request(`${API_BASE}/delete`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ paths: Array.from(state.selected) }), - }); - await loadDirectory(state.currentPath); - } catch (err) { - showStatus(err.message); - } - } - - function promptDialog(title, defaultValue = '') { - return new Promise((resolve) => { - dialogTitle.textContent = title; - dialogContent.innerHTML = ''; - const input = document.createElement('input'); - input.value = defaultValue; - input.autofocus = true; - dialogContent.appendChild(input); - const finish = (value) => { - closeDialog(); - resolve(value); - }; - registerDialogHandlers(() => finish(input.value.trim()), () => finish(null)); - dialogBackdrop.hidden = false; - input.addEventListener('keydown', (evt) => { - if (evt.key === 'Enter') { - evt.preventDefault(); - finish(input.value.trim()); - } else if (evt.key === 'Escape') { - evt.preventDefault(); - finish(null); - } - }); - setTimeout(() => input.select(), 50); - }); - } - - function confirmDialog(message) { - return new Promise((resolve) => { - dialogTitle.textContent = '确认操作'; - dialogContent.innerHTML = `

${message}

`; - const finish = (value) => { - closeDialog(); - resolve(value); - }; - registerDialogHandlers(() => finish(true), () => finish(false)); - dialogBackdrop.hidden = false; - }); - } - - function handleGlobalClick(event) { - if (!contextMenu.contains(event.target)) { - hideContextMenu(); - } - } - - function handleGridBackgroundClick(event) { - if (event.target === fileGrid) { - if (state.selectionJustFinished) { - return; - } - updateSelection([]); - } - } - - function handleDragEnter(event) { - event.preventDefault(); - fileGrid.classList.add('drop-target'); - } - - function handleDragOver(event) { - event.preventDefault(); - } - - function handleDragLeave(event) { - if (event.target === fileGrid) { - fileGrid.classList.remove('drop-target'); - } - } - - async function handleDrop(event) { - event.preventDefault(); - fileGrid.classList.remove('drop-target'); - if (!event.dataTransfer || !event.dataTransfer.files.length) return; - const files = event.dataTransfer.files; - await uploadFiles(files, state.currentPath); - } - - async function uploadFiles(fileList, targetPath) { - for (const file of fileList) { - const form = new FormData(); - form.append('file', file, file.name); - form.append('filename', file.name); - form.append('path', targetPath); - try { - await request(`${API_BASE}/upload`, { - method: 'POST', - body: form, - }); - showStatus(`已上传 ${file.name}`); - } catch (err) { - showStatus(`上传失败:${err.message}`); - } - } - await loadDirectory(state.currentPath); - } - - function initSelectionRectangle() { - fileGrid.addEventListener('pointerdown', (event) => { - if (state.selectionDisabled) return; - if (event.target !== fileGrid) return; - state.isDraggingSelection = true; - state.dragStart = { x: event.clientX, y: event.clientY }; - state.selectionRect = document.createElement('div'); - state.selectionRect.className = 'fm-selection-rect'; - fileGrid.appendChild(state.selectionRect); - updateSelection([]); - state.selectionJustFinished = false; - fileGrid.setPointerCapture(event.pointerId); - }); - - fileGrid.addEventListener('pointermove', (event) => { - if (!state.isDraggingSelection || !state.selectionRect) return; - const rect = fileGrid.getBoundingClientRect(); - const current = { x: event.clientX, y: event.clientY }; - const x = Math.min(state.dragStart.x, current.x) - rect.left + fileGrid.scrollLeft; - const y = Math.min(state.dragStart.y, current.y) - rect.top + fileGrid.scrollTop; - const width = Math.abs(state.dragStart.x - current.x); - const height = Math.abs(state.dragStart.y - current.y); - Object.assign(state.selectionRect.style, { - left: `${x}px`, - top: `${y}px`, - width: `${width}px`, - height: `${height}px`, - }); - - const selectionBox = { - left: Math.min(state.dragStart.x, current.x), - right: Math.max(state.dragStart.x, current.x), - top: Math.min(state.dragStart.y, current.y), - bottom: Math.max(state.dragStart.y, current.y), - }; - - const selected = []; - const cards = fileGrid.querySelectorAll('.fm-card'); - cards.forEach((card) => { - const bounds = card.getBoundingClientRect(); - const intersects = !(selectionBox.right < bounds.left || - selectionBox.left > bounds.right || - selectionBox.bottom < bounds.top || - selectionBox.top > bounds.bottom); - if (intersects) { - selected.push(card.dataset.path); - } - }); - updateSelection(selected); - }); - - fileGrid.addEventListener('pointerup', (event) => { - if (!state.isDraggingSelection) return; - state.isDraggingSelection = false; - if (state.selectionRect) { - state.selectionRect.remove(); - state.selectionRect = null; - } - state.selectionJustFinished = true; - requestAnimationFrame(() => { - state.selectionJustFinished = false; - }); - fileGrid.releasePointerCapture(event.pointerId); - }); - } - - async function ensureTreeNode(path, shouldRender = true) { - let changed = false; - if (!state.treeCache.has(path)) { - try { - const result = await request(`${API_BASE}/entries?path=${encodeURIComponent(path)}`); - const directories = result.data.items.filter((item) => item.type === 'directory'); - state.treeCache.set(path, directories); - changed = true; - } catch (err) { - showStatus(err.message); - } - } - if (shouldRender && changed) { - renderTree(); - } - return changed; - } - - function renderTree() { - directoryTree.innerHTML = ''; - const rootNode = createTreeNode('', '根目录'); - directoryTree.appendChild(rootNode); - } - - function createTreeNode(path, name) { - const li = document.createElement('li'); - const header = document.createElement('div'); - header.className = 'fm-tree-item'; - if (path === state.currentPath) { - header.classList.add('active'); - } - const toggle = document.createElement('span'); - toggle.className = 'fm-tree-toggle'; - toggle.textContent = state.treeExpanded.has(path) ? '▾' : '▸'; - toggle.addEventListener('click', async (event) => { - event.stopPropagation(); - if (state.treeExpanded.has(path)) { - state.treeExpanded.delete(path); - } else { - state.treeExpanded.add(path); - await ensureTreeNode(path, false); - } - renderTree(); - }); - - const label = document.createElement('span'); - label.textContent = name; - label.addEventListener('click', () => loadDirectory(path)); - - header.addEventListener('click', () => loadDirectory(path)); - - header.appendChild(toggle); - header.appendChild(label); - li.appendChild(header); - - if (state.treeExpanded.has(path)) { - const children = document.createElement('ul'); - children.className = 'fm-tree-children'; - const dirs = state.treeCache.get(path) || []; - dirs.forEach((dir) => { - const child = createTreeNode(dir.path, dir.name); - children.appendChild(child); - }); - li.appendChild(children); - } - return li; - } - - function bindToolbar() { - newFolderBtn.addEventListener('click', async () => { - const name = await promptDialog('新建文件夹', '新建文件夹'); - if (!name) return; - try { - await request(`${API_BASE}/create`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - path: state.currentPath, - name, - type: 'directory', - }), - }); - await loadDirectory(state.currentPath); - } catch (err) { - showStatus(err.message); - } - }); - - newFileBtn.addEventListener('click', async () => { - const name = await promptDialog('新建文件', '新建文件.txt'); - if (!name) return; - try { - await request(`${API_BASE}/create`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - path: state.currentPath, - name, - type: 'file', - }), - }); - await loadDirectory(state.currentPath); - } catch (err) { - showStatus(err.message); - } - }); - - refreshBtn.addEventListener('click', () => loadDirectory(state.currentPath)); - uploadBtn.addEventListener('click', () => hiddenUploader.click()); - backBtn.addEventListener('click', () => { - if (!state.currentPath) { - loadDirectory(''); - return; - } - const segments = state.currentPath.split('/').filter(Boolean); - if (segments.length === 0) { - loadDirectory(''); - return; - } - segments.pop(); - const parentPath = segments.join('/'); - loadDirectory(parentPath); - }); - - returnChatBtn.addEventListener('click', () => { - window.location.href = '/new'; - }); - downloadBtn.addEventListener('click', downloadSelected); - renameBtn.addEventListener('click', renameSelected); - copyBtn.addEventListener('click', copySelected); - cutBtn.addEventListener('click', cutSelected); - pasteBtn.addEventListener('click', pasteClipboard); - deleteBtn.addEventListener('click', deleteSelected); - toggleSelectionBtn.addEventListener('click', () => { - state.selectionDisabled = !state.selectionDisabled; - toggleSelectionBtn.textContent = state.selectionDisabled ? '启用框选' : '禁用框选'; - const msg = state.selectionDisabled ? '已禁用框选' : '已启用框选'; - showStatus(msg); - }); - - hiddenUploader.addEventListener('change', async (event) => { - const files = event.target.files; - if (files && files.length) { - await uploadFiles(files, state.currentPath); - } - hiddenUploader.value = ''; - }); - } - - function bindGlobalEvents() { - document.addEventListener('click', handleGlobalClick); - fileGrid.addEventListener('click', handleGridBackgroundClick); - fileGrid.addEventListener('contextmenu', (event) => { - if (event.target === fileGrid) { - event.preventDefault(); - if (state.selected.size) { - showContextMenu(event.clientX, event.clientY); - } - } - }); - fileGrid.addEventListener('dragenter', handleDragEnter); - fileGrid.addEventListener('dragover', handleDragOver); - fileGrid.addEventListener('dragleave', handleDragLeave); - fileGrid.addEventListener('drop', handleDrop); - initSelectionRectangle(); - } - - async function bootstrap() { - bindToolbar(); - bindGlobalEvents(); - await loadDirectory(initialPathParam, { updateHistory: false }); - } - - bootstrap().catch((err) => { - console.error(err); - showStatus(err.message); - }); -})(); diff --git a/static/file_manager/editor.css b/static/file_manager/editor.css deleted file mode 100644 index 174171b5..00000000 --- a/static/file_manager/editor.css +++ /dev/null @@ -1,60 +0,0 @@ -.fe-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 20px; - border-bottom: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(17, 18, 26, 0.9); - backdrop-filter: blur(8px); - position: sticky; - top: 0; - z-index: 20; -} - -.fe-left { - display: flex; - align-items: center; - gap: 12px; -} - -.fe-right { - display: flex; - align-items: center; - gap: 12px; -} - -.fe-path { - font-size: 14px; - color: rgba(255, 255, 255, 0.75); - max-width: 52vw; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.fe-status { - font-size: 13px; - color: rgba(255, 255, 255, 0.6); -} - -.fe-main { - height: calc(100vh - 64px); -} - -#editorArea { - width: 100%; - height: 100%; - border: none; - outline: none; - background: #0f1119; - color: #f1f3f5; - font-size: 14px; - line-height: 1.6; - padding: 20px; - font-family: "Fira Code", "JetBrains Mono", "SFMono-Regular", Consolas, monospace; - resize: none; -} - -#editorArea:focus { - outline: none; -} diff --git a/static/file_manager/editor.html b/static/file_manager/editor.html deleted file mode 100644 index c29dd7f6..00000000 --- a/static/file_manager/editor.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - 文件编辑器 - - - - -
-
-
- -
-
-
- 未保存修改 - - - - -
-
-
- -
-
- - - - diff --git a/static/file_manager/editor.js b/static/file_manager/editor.js deleted file mode 100644 index 84075156..00000000 --- a/static/file_manager/editor.js +++ /dev/null @@ -1,135 +0,0 @@ -(() => { - const API_BASE = '/api/gui/files'; - const params = new URLSearchParams(window.location.search); - const path = params.get('path'); - - const editorArea = document.getElementById('editorArea'); - const filePathEl = document.getElementById('filePath'); - const statusInfo = document.getElementById('statusInfo'); - const saveBtn = document.getElementById('btnSave'); - const downloadBtn = document.getElementById('btnDownload'); - const backBtn = document.getElementById('btnBack'); - const fontIncreaseBtn = document.getElementById('btnIncreaseFont'); - const fontDecreaseBtn = document.getElementById('btnDecreaseFont'); - - let originalContent = ''; - let dirty = false; - let fontSize = 14; - - if (!path) { - editorArea.value = '缺少 path 参数,无法加载文件。'; - editorArea.disabled = true; - saveBtn.disabled = true; - downloadBtn.disabled = true; - statusInfo.textContent = '缺少路径'; - return; - } - - filePathEl.textContent = path; - - function setDirty(value) { - dirty = value; - statusInfo.textContent = dirty ? '有未保存的更改' : '已保存'; - saveBtn.disabled = !dirty; - } - - async function request(url, options = {}) { - const response = await fetch(url, options); - const data = await response.json().catch(() => ({})); - if (!response.ok || data.success === false) { - const message = data.error || data.message || `请求失败 (${response.status})`; - throw new Error(message); - } - return data; - } - - async function loadFile() { - statusInfo.textContent = '加载中...'; - try { - const result = await request(`${API_BASE}/text?path=${encodeURIComponent(path)}`); - originalContent = result.content || ''; - editorArea.value = originalContent; - setDirty(false); - statusInfo.textContent = `最后修改时间:${result.modified_at}`; - } catch (err) { - editorArea.value = `文件加载失败:${err.message}`; - editorArea.disabled = true; - saveBtn.disabled = true; - statusInfo.textContent = '无法加载文件'; - } - } - - async function saveFile() { - statusInfo.textContent = '保存中...'; - try { - await request(`${API_BASE}/text`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path, content: editorArea.value }), - }); - originalContent = editorArea.value; - setDirty(false); - statusInfo.textContent = '已保存'; - } catch (err) { - statusInfo.textContent = `保存失败:${err.message}`; - } - } - - editorArea.addEventListener('input', () => { - if (editorArea.disabled) return; - setDirty(editorArea.value !== originalContent); - }); - - saveBtn.addEventListener('click', () => { - if (!dirty) return; - saveFile(); - }); - - downloadBtn.addEventListener('click', () => { - window.open(`${API_BASE}/download?path=${encodeURIComponent(path)}`, '_blank'); - }); - - const getParentDirectory = () => { - const segments = path.split('/').filter(Boolean); - if (segments.length <= 1) { - return ''; - } - segments.pop(); - return segments.join('/'); - }; - - const navigateToManager = () => { - const parentDir = getParentDirectory(); - const target = parentDir ? `/file-manager?path=${encodeURIComponent(parentDir)}` : '/file-manager'; - window.location.href = target; - }; - - backBtn.addEventListener('click', () => { - if (dirty) { - const confirmLeave = window.confirm('有未保存的更改,确认要离开吗?'); - if (!confirmLeave) return; - } - navigateToManager(); - }); - - fontIncreaseBtn.addEventListener('click', () => { - fontSize = Math.min(fontSize + 1, 28); - editorArea.style.fontSize = `${fontSize}px`; - }); - - fontDecreaseBtn.addEventListener('click', () => { - fontSize = Math.max(fontSize - 1, 10); - editorArea.style.fontSize = `${fontSize}px`; - }); - - window.addEventListener('beforeunload', (event) => { - if (!dirty) return; - event.preventDefault(); - event.returnValue = ''; - }); - - loadFile().catch((err) => { - console.error(err); - statusInfo.textContent = err.message; - }); -})(); diff --git a/static/file_manager/index.html b/static/file_manager/index.html deleted file mode 100644 index 2c2ee824..00000000 --- a/static/file_manager/index.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - 文件管理器 - - - - - - - -
-
-
- - - - -
-
- - - - - -
-
-
- -
-
-
已选中 0 项
-
- - - - - - -
-
-
-
拖拽文件到此处可上传到当前目录
-
-
-
- -
- - - - diff --git a/static/file_manager/style.css b/static/file_manager/style.css deleted file mode 100644 index 3f2747a5..00000000 --- a/static/file_manager/style.css +++ /dev/null @@ -1,357 +0,0 @@ -* { - box-sizing: border-box; -} - -html, body { - height: 100%; - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", sans-serif; - background: #1d1f27; - color: #f1f3f5; -} - -a { - color: inherit; - text-decoration: none; -} - -#app { - display: flex; - flex-direction: column; - height: 100%; -} - -.fm-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 20px; - border-bottom: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(17, 18, 26, 0.9); - backdrop-filter: blur(8px); - position: sticky; - top: 0; - z-index: 20; -} - -.fm-header-left { - display: flex; - align-items: center; - gap: 16px; -} - -.fm-header-right { - display: flex; - align-items: center; - gap: 12px; -} - -.fm-breadcrumb { - display: flex; - align-items: center; - gap: 6px; - font-size: 14px; -} - -.fm-breadcrumb span { - padding: 4px 8px; - border-radius: 6px; - cursor: pointer; - transition: background 0.2s; -} - -.fm-breadcrumb span:hover { - background: rgba(255, 255, 255, 0.08); -} - -.fm-btn { - border: none; - border-radius: 6px; - padding: 6px 14px; - font-size: 14px; - cursor: pointer; - background: rgba(255, 255, 255, 0.08); - color: inherit; - transition: background 0.2s, transform 0.2s; -} - -.fm-btn:hover { - background: rgba(255, 255, 255, 0.16); -} - -.fm-btn:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.fm-btn.primary { - background: #3d8bfd; - color: #fff; -} - -.fm-btn.primary:hover { - background: #377df5; -} - -.fm-btn.danger { - background: #f06595; - color: #fff; -} - -.fm-btn.danger:hover { - background: #e64980; -} - -.fm-main { - flex: 1; - display: grid; - grid-template-columns: 260px 1fr; - overflow: hidden; -} - -.fm-sidebar { - border-right: 1px solid rgba(255, 255, 255, 0.06); - padding: 16px 12px; - overflow-y: auto; - background: rgba(17, 18, 26, 0.92); -} - -.fm-sidebar-title { - font-size: 13px; - font-weight: 600; - letter-spacing: 0.05em; - text-transform: uppercase; - color: rgba(255, 255, 255, 0.6); - margin-bottom: 12px; -} - -.fm-tree { - list-style: none; - padding-left: 0; - margin: 0; - font-size: 14px; -} - -.fm-tree li { - margin: 4px 0; -} - -.fm-tree-item { - display: flex; - align-items: center; - gap: 6px; - padding: 4px 8px; - border-radius: 6px; - cursor: pointer; - transition: background 0.2s; -} - -.fm-tree-item:hover, -.fm-tree-item.active { - background: rgba(255, 255, 255, 0.1); -} - -.fm-tree-toggle { - width: 16px; - text-align: center; - cursor: pointer; - color: rgba(255, 255, 255, 0.6); -} - -.fm-tree-children { - list-style: none; - padding-left: 16px; - margin: 6px 0 0; - border-left: 1px dashed rgba(255, 255, 255, 0.1); -} - -.fm-content { - display: flex; - flex-direction: column; - overflow: hidden; - position: relative; -} - -.fm-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 16px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - background: rgba(17, 18, 26, 0.8); - backdrop-filter: blur(8px); - position: sticky; - top: 0; - z-index: 10; -} - -.fm-toolbar-right { - display: flex; - gap: 10px; -} - -.fm-selection-info { - font-size: 13px; - color: rgba(255, 255, 255, 0.65); -} - -.fm-grid { - flex: 1; - padding: 18px; - position: relative; - overflow: auto; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); - gap: 14px; -} - -.fm-card { - background: rgba(255, 255, 255, 0.05); - border: 1px solid transparent; - border-radius: 12px; - padding: 14px; - display: flex; - flex-direction: column; - gap: 12px; - justify-content: space-between; - cursor: pointer; - transition: border 0.2s, background 0.2s, transform 0.2s; - user-select: none; - aspect-ratio: 1; -} - -.fm-card:hover { - border-color: rgba(255, 255, 255, 0.16); -} - -.fm-card.selected { - border-color: #3d8bfd; - background: rgba(61, 139, 253, 0.2); -} - -.fm-card-icon { - font-size: 32px; -} - -.fm-card-name { - font-size: 14px; - font-weight: 600; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.fm-card-meta { - font-size: 12px; - color: rgba(255, 255, 255, 0.6); - line-height: 1.5; -} - -.fm-status-bar { - border-top: 1px solid rgba(255, 255, 255, 0.06); - padding: 8px 16px; - font-size: 12px; - color: rgba(255, 255, 255, 0.55); - background: rgba(17, 18, 26, 0.8); -} - -.fm-context-menu { - position: fixed; - z-index: 1000; - background: rgba(17, 18, 26, 0.95); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 8px; - padding: 6px 0; - width: 180px; - display: none; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); -} - -.fm-context-menu button { - width: 100%; - border: none; - background: transparent; - color: inherit; - padding: 8px 16px; - text-align: left; - font-size: 14px; - cursor: pointer; -} - -.fm-context-menu button:hover { - background: rgba(255, 255, 255, 0.1); -} - -.fm-dialog-backdrop { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.55); - display: flex; - align-items: center; - justify-content: center; - z-index: 1100; -} - -.fm-dialog-backdrop[hidden] { - display: none !important; -} - -.fm-dialog { - background: #1f212b; - border-radius: 12px; - padding: 24px; - width: 360px; - max-width: 92vw; -} - -.fm-dialog h3 { - margin: 0 0 16px; -} - -.fm-dialog input, -.fm-dialog textarea { - width: 100%; - padding: 10px 12px; - border-radius: 8px; - border: 1px solid rgba(255, 255, 255, 0.12); - background: rgba(255, 255, 255, 0.05); - color: inherit; - font-size: 14px; -} - -.fm-dialog textarea { - min-height: 120px; - resize: vertical; -} - -.fm-dialog-actions { - display: flex; - justify-content: flex-end; - gap: 12px; - margin-top: 20px; -} - -.fm-selection-rect { - position: absolute; - border: 1px solid rgba(61, 139, 253, 0.8); - background: rgba(61, 139, 253, 0.25); - pointer-events: none; - z-index: 50; -} - -.fm-drop-overlay { - position: absolute; - inset: 0; - border: 2px dashed rgba(61, 139, 253, 0.8); - background: rgba(61, 139, 253, 0.12); - display: none; - justify-content: center; - align-items: center; - font-size: 16px; - font-weight: 600; - color: rgba(61, 139, 253, 0.9); -} - -.fm-grid.drop-target .fm-drop-overlay { - display: flex; -} diff --git a/static/src/App.vue b/static/src/App.vue index 18d48f68..84382dea 100644 --- a/static/src/App.vue +++ b/static/src/App.vue @@ -320,7 +320,6 @@ @send-message="sendMessage" @send-or-stop="handleSendOrStop" @quick-upload="handleQuickUpload" - @open-file-manager="openGuiFileManager" @toggle-tool-menu="toggleToolMenu" @toggle-mode-menu="toggleModeMenu" @toggle-model-menu="toggleModelMenu" diff --git a/static/src/app/methods/ui/system.ts b/static/src/app/methods/ui/system.ts index f7bbe469..e644f1ed 100644 --- a/static/src/app/methods/ui/system.ts +++ b/static/src/app/methods/ui/system.ts @@ -138,12 +138,6 @@ export const systemMethods = { } return style; }, - openGuiFileManager() { - if (this.isPolicyBlocked('block_file_manager', '文件管理器已被管理员禁用')) { - return; - } - window.open('/file-manager', '_blank'); - }, renderMarkdown(content, isStreaming = false) { return renderMarkdownHelper(content, isStreaming); }, diff --git a/static/src/components/input/InputComposer.vue b/static/src/components/input/InputComposer.vue index d6d06882..d9e6e397 100644 --- a/static/src/components/input/InputComposer.vue +++ b/static/src/components/input/InputComposer.vue @@ -435,7 +435,6 @@ :goal-mode-armed="goalModeArmed" :goal-running="goalRunning" @quick-upload="triggerQuickUpload" - @open-file-manager="$emit('open-file-manager')" @pick-images="$emit('pick-images')" @pick-video="$emit('pick-video')" @toggle-tool-menu="$emit('toggle-tool-menu')" @@ -605,7 +604,6 @@ const emit = defineEmits([ 'send-message', 'send-or-stop', 'quick-upload', - 'open-file-manager', 'pick-images', 'pick-video', 'toggle-tool-menu', diff --git a/static/src/components/input/QuickMenu.vue b/static/src/components/input/QuickMenu.vue index b29c7507..41853c53 100644 --- a/static/src/components/input/QuickMenu.vue +++ b/static/src/components/input/QuickMenu.vue @@ -11,15 +11,6 @@ > {{ uploading ? '上传中...' : '上传文件' }} -