agent-Specialization/server/_admin_segment.py
JOJO d6fb59e1d8 refactor: split web_server into modular architecture
- Refactor 6000+ line web_server.py into server/ module
- Create separate modules: auth, chat, conversation, files, admin, etc.
- Keep web_server.py as backward-compatible entry point
- Add container running status field in user_container_manager
- Improve admin dashboard API with credentials and debug support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-22 09:21:53 +08:00

303 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@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/<path:filename>')
@login_required
@admin_required
def admin_asset_file(filename: str):
return send_from_directory(str(ADMIN_ASSET_DIR), filename)
@app.route('/user_upload/<path:filename>')
@login_required
def serve_user_upload(filename: str):
"""
直接向前端暴露当前登录用户的上传目录文件,用于 <show_image src="/user_upload/..."> 等场景。
- 仅登录用户可访问
- 路径穿越校验:目标必须位于用户自己的 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/<path:filename>')
@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/<path:filename>')
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})