"""当前用户的 Agent Run 与模型服务商配置页面。""" from django.contrib import messages from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.shortcuts import get_object_or_404, redirect, render from common.exceptions import AgentConfigurationError, ProviderConnectionError from .forms import ProviderConfigurationForm from .providers import CATEGORY_LABELS, PROVIDERS, get_provider from .services import ( disconnect_provider, provider_configs_for_user, runs_for_user, save_verified_provider_config, verify_provider_api_key, ) @login_required def run_list(request): """分页显示当前用户的运行记录。""" page = Paginator(runs_for_user(request.user), 20).get_page(request.GET.get("page")) return render(request, "agent_runtime/run_list.html", {"page": page}) @login_required def run_detail(request, run_id): """越权与不存在统一返回 404,避免泄露资源存在性。""" run = get_object_or_404( runs_for_user(request.user).prefetch_related("events", "tool_calls", "approvals"), pk=run_id ) return render(request, "agent_runtime/run_detail.html", {"run": run}) def _provider_page_context(request, form=None): """构造纯展示视图模型,模板不承担查询、权限或协议判断。""" configs = {item.provider_code: item for item in provider_configs_for_user(request.user)} category = request.GET.get("category", "all") query = request.GET.get("q", "").strip().lower() selected = get_provider(request.GET.get("provider", "")) cards = [] for provider in PROVIDERS: config = configs.get(provider.code) if category == "connected" and config is None: continue if category not in {"all", "connected"} and provider.category != category: continue searchable = f"{provider.name} {provider.vendor} {provider.code}".lower() if query and query not in searchable: continue cards.append({"provider": provider, "config": config}) selected_config = configs.get(selected.code) if selected else None return { "cards": cards, "provider_count": len(PROVIDERS), "connected_count": len(configs), "categories": CATEGORY_LABELS, "active_category": category if category in CATEGORY_LABELS else "all", "query": request.GET.get("q", "").strip(), "selected_provider": selected, "selected_config": selected_config, "provider_form": form or ProviderConfigurationForm( initial={ "model_id": selected_config.default_model_id if selected_config else "", "use_as_default": selected_config.is_default if selected_config else False, } ), } @login_required def provider_list(request): """展示当前用户的服务商目录和已连接状态。""" return render(request, "agent_runtime/provider_list.html", _provider_page_context(request)) @login_required def provider_configure(request, provider_code): """验证并保存当前用户的 API Key;失败时不覆盖已有配置。""" provider = get_provider(provider_code) if provider is None: return redirect("agent_runtime:provider-list") if request.method != "POST": return redirect(f"{redirect('agent_runtime:provider-list').url}?provider={provider.code}") form = ProviderConfigurationForm(request.POST) if not provider.key_only: messages.info(request, provider.note or "该服务商需要额外配置,当前版本尚未开放。") return redirect(f"{redirect('agent_runtime:provider-list').url}?provider={provider.code}") if form.is_valid(): try: result = verify_provider_api_key(provider, form.cleaned_data["api_key"]) save_verified_provider_config( request.user, provider, form.cleaned_data["api_key"], result, model_id=form.cleaned_data["model_id"], use_as_default=form.cleaned_data["use_as_default"], ) except (ProviderConnectionError, AgentConfigurationError) as exc: form.add_error(None, str(exc)) else: messages.success(request, f"{provider.name} 已验证并启用。") return redirect("agent_runtime:provider-list") # POST 出错时保持配置抽屉打开,且表单只回显非敏感错误,不回显已有密钥。 query = request.GET.copy() query["provider"] = provider.code request.GET = query return render( request, "agent_runtime/provider_list.html", _provider_page_context(request, form), status=400, ) @login_required def provider_disconnect(request, provider_code): """仅接受 POST 断开当前用户自己的服务商配置。""" if request.method == "POST" and disconnect_provider(request.user, provider_code): messages.success(request, "服务商配置已移除。") return redirect("agent_runtime:provider-list")