Files

132 lines
5.0 KiB
Python

"""当前用户的 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")