from __future__ import annotations import json, time from datetime import datetime from typing import Dict, Any, Optional from pathlib import Path from io import BytesIO import zipfile import os from flask import Blueprint, jsonify, request, session, send_file # 审批路由独立蓝图:审批(工具/计划/提问)属 Runtime 人机交互契约, # 不属 web chat 域;headless 形态只挂本蓝图而不挂 chat_bp。 # 注意:URL 路径与原 chat_bp 时期完全一致,前端与客户端零改动。 approval_bp = Blueprint("approval", __name__) from werkzeug.utils import secure_filename from werkzeug.exceptions import RequestEntityTooLarge import secrets from config import MAX_UPLOAD_SIZE, OUTPUT_FORMATS from modules.personalization_manager import ( load_personalization_config, resolve_context_compression_settings, save_personalization_config, RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN, RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX, ) from modules.skills_manager import ( get_skills_catalog, infer_private_skills_dir, merge_enabled_skills, sync_workspace_skills, ) from modules.upload_security import UploadSecurityError from modules.host_sandbox_policy import load_policy, save_policy from modules.user_manager import UserWorkspace from core.web_terminal import WebTerminal from config.model_profiles import get_model_context_window from server.auth_helpers import resolve_admin_policy, get_current_user_record, get_current_username from server.gateway_auth import api_login_or_host_token_required from server.context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, get_or_create_usage_tracker from server.security import rate_limited from server.utils_common import debug_log from server.state import PROJECT_MAX_STORAGE_MB from server.runtime import runtime_service from server.monitor import get_cached_monitor_snapshot from modules.i18n import tr UPLOAD_FOLDER_NAME = ".astrion/user_upload" @approval_bp.route('/api/user-questions/pending', methods=['GET']) @api_login_or_host_token_required @with_terminal def list_pending_user_questions(terminal: WebTerminal, workspace: UserWorkspace, username: str): """获取当前用户待回答的问题列表。""" requested_conv_id = (request.args.get("conversation_id") or "").strip() or None if requested_conv_id is None: requested_conv_id = getattr(terminal.context_manager, "current_conversation_id", None) items = runtime_service.list_pending_approvals(username, requested_conv_id, kind="question")["question"] return jsonify({ "success": True, "items": items, "conversation_id": requested_conv_id, }) @approval_bp.route('/api/user-questions//answer', methods=['POST']) @api_login_or_host_token_required @with_terminal @rate_limited("user_question_answer", 120, 60, scope="user") def answer_user_question(terminal: WebTerminal, workspace: UserWorkspace, username: str, question_id: str): """提交 ask_user 工具问题的回答。""" data = request.get_json() or {} try: item = runtime_service.resolve_approval( "question", username=username, item_id=question_id, selected_option_id=data.get("selected_option_id"), text=data.get("text"), dismissed=bool(data.get("dismissed")), ) except ValueError as exc: return jsonify({"success": False, "error": str(exc)}), 400 except KeyError: return jsonify({"success": False, "error": tr("chat_approval.question_not_found")}), 404 except PermissionError as exc: return jsonify({"success": False, "error": str(exc)}), 403 except Exception as exc: return jsonify({"success": False, "error": str(exc)}), 500 return jsonify({ "success": True, "item": item, }) @approval_bp.route('/api/plan-approvals/pending', methods=['GET']) @api_login_or_host_token_required @with_terminal def list_pending_plan_approvals(terminal: WebTerminal, workspace: UserWorkspace, username: str): """获取当前用户待批准的计划列表(work_mode=plan 的 submit_plan 工具)。""" requested_conv_id = (request.args.get("conversation_id") or "").strip() or None if requested_conv_id is None: requested_conv_id = getattr(terminal.context_manager, "current_conversation_id", None) items = runtime_service.list_pending_approvals(username, requested_conv_id, kind="plan")["plan"] return jsonify({ "success": True, "items": items, "conversation_id": requested_conv_id, }) @approval_bp.route('/api/plan-approvals//answer', methods=['POST']) @api_login_or_host_token_required @with_terminal @rate_limited("plan_approval_answer", 120, 60, scope="user") def answer_plan_approval(terminal: WebTerminal, workspace: UserWorkspace, username: str, approval_id: str): """提交计划批准/拒绝决策。approved=true 时工具循环侧会自动切换到 execute 模式。""" data = request.get_json() or {} try: item = runtime_service.resolve_approval( "plan", username=username, item_id=approval_id, approved=bool(data.get("approved")), comment=data.get("comment"), ) except ValueError as exc: return jsonify({"success": False, "error": str(exc)}), 400 except KeyError: return jsonify({"success": False, "error": tr("chat_approval.plan_approval_not_found")}), 404 except PermissionError as exc: return jsonify({"success": False, "error": str(exc)}), 403 except Exception as exc: return jsonify({"success": False, "error": str(exc)}), 500 return jsonify({ "success": True, "item": item, }) @approval_bp.route('/api/tool-approvals/pending', methods=['GET']) @api_login_or_host_token_required @with_terminal def list_pending_tool_approvals(terminal: WebTerminal, workspace: UserWorkspace, username: str): """获取当前用户待审批工具列表。""" requested_conv_id = (request.args.get("conversation_id") or "").strip() or None if requested_conv_id is None: requested_conv_id = getattr(terminal.context_manager, "current_conversation_id", None) items = runtime_service.list_pending_approvals(username, requested_conv_id, kind="tool")["tool"] return jsonify({ "success": True, "items": items, "conversation_id": requested_conv_id, }) @approval_bp.route('/api/tool-approvals//decision', methods=['POST']) @api_login_or_host_token_required @with_terminal @rate_limited("tool_approval_decision", 60, 60, scope="user") def decide_tool_approval(terminal: WebTerminal, workspace: UserWorkspace, username: str, approval_id: str): """提交工具审批决策。""" data = request.get_json() or {} decision = str(data.get("decision") or "").strip().lower() try: item = runtime_service.resolve_approval( "tool", username=username, item_id=approval_id, decision=decision ) except ValueError as exc: return jsonify({"success": False, "error": str(exc)}), 400 except KeyError: return jsonify({"success": False, "error": tr("chat_approval.approval_not_found")}), 404 except PermissionError as exc: return jsonify({"success": False, "error": str(exc)}), 403 except Exception as exc: return jsonify({"success": False, "error": str(exc)}), 500 return jsonify({ "success": True, "item": item, })