"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Copy, Loader2, Pencil, Plus, Trash2 } from "lucide-react"; import { ListPageLayout, ListPageSection } from "@/components/layout/list-page-layout"; import { BackendField, OpenCodeFields, ReasonerFields } from "@/components/task-backends/backend-fields"; import { backendPayload, draftFromBackend, emptyBackendDraft } from "@/components/task-backends/backend-form"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { DataList } from "@/components/ui/data-list"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { FilterPills } from "@/components/ui/filter-pills"; import { Input } from "@/components/ui/input"; import { ListToolbar } from "@/components/ui/list-toolbar"; import { SearchInput } from "@/components/ui/search-input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { modelResourcesApi, taskBackendsApi, toolsApi, type ModelResource, type TaskBackendResource, type TaskBackendType, type Tool } from "@/lib/api"; const typeLabels: Record = { reasoner: "内置 Reasoner", opencode: "外部 OpenCode" }; const filters = ["全部", "内置 Reasoner", "外部 OpenCode"] as const; type Filter = (typeof filters)[number]; export function ComponentsTaskBackendsPage() { const [resources, setResources] = useState([]); const [models, setModels] = useState([]); const [tools, setTools] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [search, setSearch] = useState(""); const [filter, setFilter] = useState("全部"); const [dialogOpen, setDialogOpen] = useState(false); const [editing, setEditing] = useState(null); const [draft, setDraft] = useState(emptyBackendDraft); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(""); const [busyId, setBusyId] = useState(null); const load = useCallback(async () => { setLoading(true); setError(""); try { const [nextResources, nextModels, nextTools] = await Promise.all([ taskBackendsApi.list(), modelResourcesApi.list(), toolsApi.list(), ]); setResources(nextResources); setModels(nextModels); setTools(nextTools); } catch (error) { setError(error instanceof Error ? error.message : "任务后端加载失败"); } finally { setLoading(false); } }, []); useEffect(() => { // Load external resources once; retry and CRUD actions use the same loader. // eslint-disable-next-line react-hooks/set-state-in-effect void load(); }, [load]); const filtered = useMemo(() => { const query = search.trim().toLowerCase(); return resources.filter(resource => (filter === "全部" || typeLabels[resource.type] === filter) && (!query || `${resource.name} ${resource.description}`.toLowerCase().includes(query))); }, [resources, search, filter]); function openEditor(resource?: TaskBackendResource) { setEditing(resource ?? null); setDraft(resource ? draftFromBackend(resource) : emptyBackendDraft()); setSaveError(""); setDialogOpen(true); } async function save() { setSaving(true); setSaveError(""); try { const payload = backendPayload(draft); if (payload.type === "reasoner" && payload.config.reasoningEffort !== "provider_default" && models.find(model => model.id === payload.config.modelResourceId)?.values.supportsReasoningEffort !== true) { throw new Error("该模型未声明支持推理强度,请选择模型默认"); } if (editing) await taskBackendsApi.update(editing.id, payload); else await taskBackendsApi.create(payload); setDialogOpen(false); await load(); } catch (error) { setSaveError(error instanceof Error ? error.message : "保存失败"); } finally { setSaving(false); } } async function act(resource: TaskBackendResource, action: "duplicate" | "remove") { if (action === "remove" && !window.confirm(`确认删除任务后端“${resource.name}”?`)) return; setBusyId(resource.id); try { await taskBackendsApi[action](resource.id); await load(); } catch (error) { setError(error instanceof Error ? error.message : "操作失败"); } finally { setBusyId(null); } } return <> openEditor()}>添加任务后端}> } search={} /> resource.id} loading={loading} error={error} onRetry={() => void load()} onRowClick={resource => openEditor(resource)} empty={{ title: "暂无任务后端", description: "创建内置 Reasoner 或外部 OpenCode,再到提示词助手中绑定。" }} columns={[ { key: "name", header: "名称", cell: resource =>
{resource.name}
{resource.description || "未填写说明"}
}, { key: "type", header: "后端", width: "md:w-[164px]", cell: resource => {typeLabels[resource.type]} }, { key: "status", header: "状态", width: "md:w-[84px]", cell: resource => {resource.enabled ? "已启用" : "已停用"} }, { key: "action", header: "操作", width: "md:w-[144px]", align: "right", cell: resource =>
event.stopPropagation()}>
}, ]} />
{ if (!saving) setDialogOpen(open); }}> {editing ? "编辑任务后端" : "添加任务后端"} 连接、模型、执行策略与技能在此复用。助手侧配置任务目标与委派要求。
setDraft(previous => ({ ...previous, name: event.target.value }))} />