- 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>
303 lines
11 KiB
Python
303 lines
11 KiB
Python
@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})
|
||
|
||
|