"use client"; import * as React from "react"; import { PlayIcon, PlusIcon, RotateCcwIcon, SaveIcon, SearchIcon, Trash2Icon } from "lucide-react"; import { useTranslations } from "next-intl"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { api } from "@/lib/api"; import type { Agent, Tool } from "@/lib/types"; // Traffic tools are host tools gated by the global 流量捕获 switch: bindable, but // only usable when capture is on. Keep in sync with traffic.SeedToolMetas. const TRAFFIC_TOOL_KEYS = new Set(["traffic_search", "traffic_get"]); // paramRows flattens schema.properties into an ordered row list for editing. // name/type/required are read-only (welded to the Go handler); description/default // are editable. Non-scalar params (array/object) can't carry an editable default. // parentKey is set for rows that live inside an array param's items.properties. type ParamRow = { name: string; type: string; required: boolean; description: string; defaultStr: string; // "" = no default declared scalar: boolean; parentKey?: string; // set when this is a sub-field of an array param's items }; // eslint-disable-next-line @typescript-eslint/no-explicit-any function toRows(schema: Record): ParamRow[] { const props = (schema?.properties ?? {}) as Record>; const required = new Set((schema?.required as string[]) ?? []); const rows: ParamRow[] = []; for (const [name, p] of Object.entries(props)) { const type = String(p?.type ?? ""); const hasDefault = p != null && "default" in p && (p as Record).default != null; rows.push({ name, type, required: required.has(name), description: String(p?.description ?? ""), defaultStr: hasDefault ? String((p as Record).default) : "", scalar: ["string", "integer", "number", "boolean"].includes(type), }); // Expand items.properties for array params so sub-fields are editable. if (type === "array") { // eslint-disable-next-line @typescript-eslint/no-explicit-any const itemProps = (p as any)?.items?.properties as Record> | undefined; // eslint-disable-next-line @typescript-eslint/no-explicit-any const itemRequired = new Set(((p as any)?.items?.required as string[]) ?? []); if (itemProps) { for (const [subName, subP] of Object.entries(itemProps)) { const subType = String((subP as Record)?.type ?? ""); const subHasDefault = subP != null && "default" in subP && subP.default != null; rows.push({ name: subName, type: subType, required: itemRequired.has(subName), description: String(subP?.description ?? ""), defaultStr: subHasDefault ? String(subP.default) : "", scalar: ["string", "integer", "number", "boolean"].includes(subType), parentKey: name, }); } } } } return rows; } // coerceDefault turns the edited default string back into a typed JSON value, or // undefined to drop the "default" key entirely. function coerceDefault(type: string, raw: string): unknown { const s = raw.trim(); if (s === "") return undefined; if (type === "integer" || type === "number") { const n = Number(s); return Number.isNaN(n) ? undefined : n; } if (type === "boolean") return s === "true"; return raw; } // applyRows writes edited rows back into a deep-copied schema (structure untouched). // eslint-disable-next-line @typescript-eslint/no-explicit-any function applyRows(schema: Record, rows: ParamRow[]): Record { const next = structuredClone(schema ?? {}); const props = (next.properties ?? {}) as Record>; for (const r of rows) { if (r.parentKey) { // Sub-field of an array param's items.properties // eslint-disable-next-line @typescript-eslint/no-explicit-any const parent = props[r.parentKey] as any; const subP = parent?.items?.properties?.[r.name] as Record | undefined; if (!subP) continue; subP.description = r.description; if (r.scalar) { const dv = coerceDefault(r.type, r.defaultStr); if (dv === undefined) delete subP.default; else subP.default = dv; } } else { const p = props[r.name]; if (!p) continue; p.description = r.description; if (r.scalar) { const dv = coerceDefault(r.type, r.defaultStr); if (dv === undefined) delete p.default; else p.default = dv; } } } return next; } // ToolEditor is the full edit form for one tool, rendered inside the drawer. function ToolEditor({ tool, agents, captureOn, onSaved, onClose, }: { tool: Tool; agents: Agent[]; captureOn: boolean; onSaved: () => void; onClose: () => void; }) { const t = useTranslations("toolsPage"); // traffic tools can't be bound/enabled until the global 流量捕获 switch is on. const trafficGated = TRAFFIC_TOOL_KEYS.has(tool.key) && !captureOn; const [description, setDescription] = React.useState(tool.description); const [bound, setBound] = React.useState(tool.agents); const [enabled, setEnabled] = React.useState(tool.enabled); const [rows, setRows] = React.useState(() => toRows(tool.schema)); const [saving, setSaving] = React.useState(false); const setRow = (i: number, patch: Partial) => setRows((rs) => rs.map((r, j) => (j === i ? { ...r, ...patch } : r))); const toggleAgent = (k: string) => setBound((b) => (b.includes(k) ? b.filter((x) => x !== k) : [...b, k])); async function save() { setSaving(true); try { await api.saveTool(tool.key, { description, schema: applyRows(tool.schema, rows), agents: bound, enabled, }); toast.success(t("savedTool", { key: tool.key })); onSaved(); onClose(); } catch (e) { toast.error(t("saveFailed", { msg: (e as Error).message })); } finally { setSaving(false); } } async function reset() { try { await api.resetTool(tool.key); toast.success(t("resetDone", { key: tool.key })); onSaved(); onClose(); } catch (e) { toast.error(t("resetFailed", { msg: (e as Error).message })); } } return (
{trafficGated && (
{t.rich("trafficGatedNote", { b: (c) => {c} })}
)} {/* binding + switch */}
{agents.map((ag) => ( ))} {agents.length === 0 && {t("noAgents")}}
{/* description */}