@app.route('/admin/monitor') @login_required @admin_required def admin_monitor_page(): """管理员监控页面入口""" return send_from_directory(str(ADMIN_ASSET_DIR), 'index.html') @app.route('/admin/policy') @login_required @admin_required def admin_policy_page(): """管理员策略配置页面""" return send_from_directory(Path(app.static_folder) / 'admin_policy', 'index.html') @app.route('/admin/custom-tools') @login_required @admin_required def admin_custom_tools_page(): """自定义工具管理页面""" return send_from_directory(str(ADMIN_CUSTOM_TOOLS_DIR), 'index.html') @app.route('/api/admin/balance', methods=['GET']) @login_required @admin_required def admin_balance_api(): """查询第三方账户余额(Kimi/DeepSeek/Qwen)。""" data = balance_client.fetch_all_balances() return jsonify({"success": True, "data": data}) @app.route('/admin/assets/') @login_required @admin_required def admin_asset_file(filename: str): return send_from_directory(str(ADMIN_ASSET_DIR), filename) @app.route('/user_upload/') @login_required def serve_user_upload(filename: str): """ 直接向前端暴露当前登录用户的上传目录文件,用于 等场景。 - 仅登录用户可访问 - 路径穿越校验:目标必须位于用户自己的 uploads_dir 内 """ user = get_current_user_record() if not user: return redirect('/login') workspace = user_manager.ensure_user_workspace(user.username) uploads_dir = workspace.uploads_dir.resolve() target = (uploads_dir / filename).resolve() try: target.relative_to(uploads_dir) except ValueError: abort(403) if not target.exists() or not target.is_file(): abort(404) return send_from_directory(str(uploads_dir), str(target.relative_to(uploads_dir))) @app.route('/workspace/') @login_required def serve_workspace_file(filename: str): """ 暴露当前登录用户项目目录下的文件(主要用于图片展示)。 - 仅登录用户可访问自己的项目文件 - 路径穿越校验:目标必须位于用户自己的 project_path 内 - 非图片直接拒绝,避免误暴露其他文件 """ user = get_current_user_record() if not user: return redirect('/login') workspace = user_manager.ensure_user_workspace(user.username) project_root = workspace.project_path.resolve() target = (project_root / filename).resolve() try: target.relative_to(project_root) except ValueError: abort(403) if not target.exists() or not target.is_file(): abort(404) mime_type, _ = mimetypes.guess_type(str(target)) if not mime_type or not mime_type.startswith("image/"): abort(415) return send_from_directory(str(target.parent), target.name) @app.route('/static/') def static_files(filename): """提供静态文件""" if filename.startswith('admin_dashboard'): abort(404) return send_from_directory('static', filename) @app.route('/api/status') @api_login_required @with_terminal def get_status(terminal: WebTerminal, workspace: UserWorkspace, username: str): """获取系统状态(增强版:包含对话信息)""" status = terminal.get_status() # 添加终端状态信息 if terminal.terminal_manager: terminal_status = terminal.terminal_manager.list_terminals() status['terminals'] = terminal_status # 【新增】添加当前对话的详细信息 try: current_conv = terminal.context_manager.current_conversation_id status['conversation'] = status.get('conversation', {}) status['conversation']['current_id'] = current_conv if current_conv and not current_conv.startswith('temp_'): current_conv_data = terminal.context_manager.conversation_manager.load_conversation(current_conv) if current_conv_data: status['conversation']['title'] = current_conv_data.get('title', '未知对话') status['conversation']['created_at'] = current_conv_data.get('created_at') status['conversation']['updated_at'] = current_conv_data.get('updated_at') except Exception as e: print(f"[Status] 获取当前对话信息失败: {e}") status['project_path'] = str(workspace.project_path) try: status['container'] = container_manager.get_container_status(username) except Exception as exc: status['container'] = {"success": False, "error": str(exc)} status['version'] = AGENT_VERSION try: policy = resolve_admin_policy(user_manager.get_user(username)) status['admin_policy'] = { "ui_blocks": policy.get("ui_blocks") or {}, "disabled_models": policy.get("disabled_models") or [], "forced_category_states": policy.get("forced_category_states") or {}, "version": policy.get("updated_at"), } except Exception as exc: debug_log(f"[status] 附加管理员策略失败: {exc}") return jsonify(status) @app.route('/api/container-status') @api_login_required @with_terminal def get_container_status_api(terminal: WebTerminal, workspace: UserWorkspace, username: str): """轮询容器状态(供前端用量面板定时刷新)。""" try: status = container_manager.get_container_status(username) return jsonify({"success": True, "data": status}) except Exception as exc: return jsonify({"success": False, "error": str(exc)}), 500 @app.route('/api/project-storage') @api_login_required @with_terminal def get_project_storage(terminal: WebTerminal, workspace: UserWorkspace, username: str): """获取项目目录占用情况,供前端轮询。""" now = time.time() cache_entry = PROJECT_STORAGE_CACHE.get(username) if cache_entry and (now - cache_entry.get("ts", 0)) < PROJECT_STORAGE_CACHE_TTL_SECONDS: return jsonify({"success": True, "data": cache_entry["data"]}) try: file_manager = getattr(terminal, 'file_manager', None) if not file_manager: return jsonify({"success": False, "error": "文件管理器未初始化"}), 500 used_bytes = file_manager._get_project_size() limit_bytes = PROJECT_MAX_STORAGE_MB * 1024 * 1024 if PROJECT_MAX_STORAGE_MB else None usage_percent = (used_bytes / limit_bytes * 100) if limit_bytes else None data = { "used_bytes": used_bytes, "limit_bytes": limit_bytes, "limit_label": f"{PROJECT_MAX_STORAGE_MB}MB" if PROJECT_MAX_STORAGE_MB else "未限制", "usage_percent": usage_percent } PROJECT_STORAGE_CACHE[username] = {"ts": now, "data": data} return jsonify({"success": True, "data": data}) except Exception as exc: stale = PROJECT_STORAGE_CACHE.get(username) if stale: return jsonify({"success": True, "data": stale.get("data"), "stale": True}), 200 return jsonify({"success": False, "error": str(exc)}), 500 @app.route('/api/admin/dashboard') @api_login_required @admin_api_required def admin_dashboard_snapshot_api(): try: snapshot = build_admin_dashboard_snapshot() return jsonify({"success": True, "data": snapshot}) except Exception as exc: logging.exception("Failed to build admin dashboard") return jsonify({"success": False, "error": str(exc)}), 500 @app.route('/api/admin/policy', methods=['GET', 'POST']) @api_login_required @admin_api_required def admin_policy_api(): if request.method == 'GET': try: data = admin_policy_manager.load_policy() defaults = admin_policy_manager.describe_defaults() return jsonify({"success": True, "data": data, "defaults": defaults}) except Exception as exc: return jsonify({"success": False, "error": str(exc)}), 500 # POST 更新 payload = request.get_json() or {} target_type = payload.get("target_type") target_value = payload.get("target_value") or "" config = payload.get("config") or {} try: saved = admin_policy_manager.save_scope_policy(target_type, target_value, config) return jsonify({"success": True, "data": saved}) except ValueError as exc: return jsonify({"success": False, "error": str(exc)}), 400 except Exception as exc: return jsonify({"success": False, "error": str(exc)}), 500 @app.route('/api/admin/custom-tools', methods=['GET', 'POST', 'DELETE']) @api_login_required @admin_api_required def admin_custom_tools_api(): """自定义工具管理(仅全局管理员)。""" try: if request.method == 'GET': return jsonify({"success": True, "data": custom_tool_registry.list_tools()}) if request.method == 'POST': payload = request.get_json() or {} saved = custom_tool_registry.upsert_tool(payload) return jsonify({"success": True, "data": saved}) # DELETE tool_id = request.args.get("id") or (request.get_json() or {}).get("id") if not tool_id: return jsonify({"success": False, "error": "缺少 id"}), 400 removed = custom_tool_registry.delete_tool(tool_id) if removed: return jsonify({"success": True, "data": {"deleted": tool_id}}) return jsonify({"success": False, "error": "未找到该工具"}), 404 except ValueError as exc: return jsonify({"success": False, "error": str(exc)}), 400 except Exception as exc: logging.exception("custom-tools API error") return jsonify({"success": False, "error": str(exc)}), 500 @app.route('/api/admin/custom-tools/file', methods=['GET', 'POST']) @api_login_required @admin_api_required def admin_custom_tools_file_api(): tool_id = request.args.get("id") or (request.get_json() or {}).get("id") name = request.args.get("name") or (request.get_json() or {}).get("name") if not tool_id or not name: return jsonify({"success": False, "error": "缺少 id 或 name"}), 400 tool_dir = Path(custom_tool_registry.root) / tool_id if not tool_dir.exists(): return jsonify({"success": False, "error": "工具不存在"}), 404 target = tool_dir / name if request.method == 'GET': if not target.exists(): return jsonify({"success": False, "error": "文件不存在"}), 404 try: return target.read_text(encoding="utf-8") except Exception as exc: return jsonify({"success": False, "error": str(exc)}), 500 # POST 保存文件 payload = request.get_json() or {} content = payload.get("content") try: target.write_text(content or "", encoding="utf-8") return jsonify({"success": True}) except Exception as exc: return jsonify({"success": False, "error": str(exc)}), 500 @app.route('/api/admin/custom-tools/reload', methods=['POST']) @api_login_required @admin_api_required def admin_custom_tools_reload_api(): try: custom_tool_registry.reload() return jsonify({"success": True}) except Exception as exc: return jsonify({"success": False, "error": str(exc)}), 500 @app.route('/api/effective-policy', methods=['GET']) @api_login_required def effective_policy_api(): record = get_current_user_record() policy = resolve_admin_policy(record) return jsonify({"success": True, "data": policy})