refactor(ui): remove web_search config section

Remove ToolsWebSection from config page and clean up i18n keys for web
search provider configuration (duckduckgo, exa, tavily, brave). Web search
now configured entirely via builtin_tool_tenant_configs REST API.

Delete tools-web-section.tsx component and update locale catalogs (en/vi/zh).
This commit is contained in:
viettranx committed 2026-04-18 17:47:33 +07:00
1 parent ff1ab33fbc
commit 96b75f0413
5 files changed
-276

No files matched your search

-15
View File
@@ -208,21 +208,6 @@
"tools.execAskMode": "Ask Mode",
"tools.execAskModeTip": "When to prompt the user for approval before executing a command.",
"tools.execAllowlistLabel": "Allowed Commands (one pattern per line)",
"tools.webSearch": "Web Search",
"tools.webFetch": "Web fetch and browser automation",
"tools.maxResults": "Max Results",
"tools.braveApiKey": "API key",
"tools.braveApiKeyTip": "Required to use Brave Search. Create a key at https://brave.com/search/api/",
"tools.braveApiKeyPlaceholder": "BSA…",
"tools.braveApiKeyManaged": "A key is already stored on the server and cannot be shown here.",
"tools.webFetchPolicy": "Web Fetch Policy",
"tools.webFetchPolicyTip": "Controls which URLs the agent can fetch. 'Allowlist' restricts to approved domains only.",
"tools.allowedDomains": "Allowed Domains",
"tools.blockedDomains": "Blocked Domains",
"tools.blockedDomainsTip": "Domains always blocked regardless of policy. One entry per line, supports wildcards (*.example.com).",
"tools.browser": "Browser Automation",
"tools.browserEnabled": "Enabled",
"tools.browserHeadless": "Headless",
"tools.scrubCredentials": "Scrub Credentials",
"tts.title": "Text-to-Speech",
-15
View File
@@ -208,21 +208,6 @@
"tools.execAskMode": "Chế độ hỏi",
"tools.execAskModeTip": "Khi nào nhắc người dùng phê duyệt trước khi thực thi lệnh.",
"tools.execAllowlistLabel": "Lệnh được phép (mỗi dòng một mẫu)",
"tools.webSearch": "Tìm kiếm web",
"tools.webFetch": "Tải web và tự động hóa trình duyệt",
"tools.maxResults": "Kết quả tối đa",
"tools.braveApiKey": "API Key",
"tools.braveApiKeyTip": "Cần thiết để dùng Brave Search. Tạo khóa tại https://brave.com/search/api/",
"tools.braveApiKeyPlaceholder": "BSA…",
"tools.braveApiKeyManaged": "Khóa đã được lưu trên máy chủ và không thể hiển thị tại đây.",
"tools.webFetchPolicy": "Chính sách tải web",
"tools.webFetchPolicyTip": "Kiểm soát URL nào agent có thể tải. 'Danh sách cho phép' giới hạn chỉ các domain được duyệt.",
"tools.allowedDomains": "Domain được phép",
"tools.blockedDomains": "Domain bị chặn",
"tools.blockedDomainsTip": "Các domain luôn bị chặn bất kể chính sách. Mỗi dòng một mục, hỗ trợ ký tự đại diện (*.example.com).",
"tools.browser": "Tự động hóa trình duyệt",
"tools.browserEnabled": "Đã bật",
"tools.browserHeadless": "Không giao diện",
"tools.scrubCredentials": "Ẩn thông tin xác thực",
"tts.title": "Chuyển văn bản thành giọng nói",
-15
View File
@@ -208,21 +208,6 @@
"tools.execAskMode": "询问模式",
"tools.execAskModeTip": "在执行命令之前何时提示用户审批。",
"tools.execAllowlistLabel": "允许的命令(每行一个模式)",
"tools.webSearch": "网页搜索",
"tools.webFetch": "网页抓取和浏览器自动化",
"tools.maxResults": "最大结果数",
"tools.braveApiKey": "API 密钥",
"tools.braveApiKeyTip": "使用 Brave 搜索时需要。可在 https://brave.com/search/api/ 创建密钥",
"tools.braveApiKeyPlaceholder": "BSA…",
"tools.braveApiKeyManaged": "密钥已保存在服务器上,无法在此显示。",
"tools.webFetchPolicy": "网页抓取策略",
"tools.webFetchPolicyTip": "控制 Agent 可以抓取哪些 URL。\"允许列表\"仅限已批准的域名。",
"tools.allowedDomains": "允许的域名",
"tools.blockedDomains": "屏蔽的域名",
"tools.blockedDomainsTip": "无论策略如何都始终屏蔽的域名,每行一条,支持通配符(*.example.com)。",
"tools.browser": "浏览器自动化",
"tools.browserEnabled": "已启用",
"tools.browserHeadless": "无界面模式",
"tools.scrubCredentials": "擦除凭证",
"tts.title": "文字转语音",
-6
View File
@@ -16,7 +16,6 @@ import { AiDefaultsSection } from "./sections/ai-defaults-section";
import { QuotaSection } from "./sections/quota-section";
import { ToolsProfileSection } from "./sections/tools-profile-section";
import { ToolsExecSection } from "./sections/tools-exec-section";
import { ToolsWebSection } from "./sections/tools-web-section";
import { ShellSecuritySection } from "./sections/shell-security-section";
import { TtsSection } from "./sections/tts-section";
import { CronSection } from "./sections/cron-section";
@@ -144,11 +143,6 @@ export function ConfigPage() {
onSave={(v) => patch({ tools: v })}
saving={saving}
/>
<ToolsWebSection
data={config.tools as any}
onSave={(v) => patch({ tools: v })}
saving={saving}
/>
<ShellSecuritySection
data={config.tools as any}
onSave={(v) => patch({ tools: v })}
@@ -1,225 +0,0 @@
import { useState, useEffect } from "react";
import { Save } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { InfoLabel } from "@/components/shared/info-label";
import { isSecret } from "@/lib/secret";
type ToolsData = Record<string, any>;
interface Props {
data: ToolsData | undefined;
onSave: (value: ToolsData) => Promise<void>;
saving: boolean;
}
export function ToolsWebSection({ data, onSave, saving }: Props) {
const { t } = useTranslation("config");
const [draft, setDraft] = useState<ToolsData>(data ?? {});
const [dirty, setDirty] = useState(false);
useEffect(() => {
setDraft(data ?? {});
setDirty(false);
}, [data]);
const updateNested = (section: string, patch: Record<string, any>) => {
setDraft((prev) => ({
...prev,
[section]: { ...(prev[section] ?? {}), ...patch },
}));
setDirty(true);
};
const handleSave = () => {
const toSave: ToolsData = { ...draft };
const web = { ...(toSave.web ?? {}) };
const brave = { ...(web.brave ?? {}) };
if (isSecret(brave.api_key)) {
delete brave.api_key;
}
web.brave = brave;
toSave.web = web;
onSave(toSave);
};
if (!data) return null;
const web = draft.web ?? {};
const ddg = web.duckduckgo ?? {};
const brave = web.brave ?? {};
const webFetch = draft.web_fetch ?? {};
const browser = draft.browser ?? {};
return (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">{t("tools.webSearch")}</CardTitle>
<CardDescription>{t("tools.webFetch")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Web Search */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>DuckDuckGo</Label>
<Switch
checked={ddg.enabled !== false}
onCheckedChange={(v) => updateNested("web", { duckduckgo: { ...ddg, enabled: v } })}
/>
</div>
<div className="grid gap-1.5">
<Label className="text-xs text-muted-foreground">{t("tools.maxResults")}</Label>
<Input
type="number"
className="text-base md:text-sm"
value={ddg.max_results ?? ""}
onChange={(e) => updateNested("web", { duckduckgo: { ...ddg, max_results: Number(e.target.value) } })}
placeholder="5"
min={1}
/>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Brave Search</Label>
<Switch
checked={brave.enabled ?? false}
onCheckedChange={(v) => updateNested("web", { brave: { ...brave, enabled: v } })}
/>
</div>
<div className="grid gap-1.5">
<Label className="text-xs text-muted-foreground">{t("tools.maxResults")}</Label>
<Input
type="number"
className="text-base md:text-sm"
value={brave.max_results ?? ""}
onChange={(e) => updateNested("web", { brave: { ...brave, max_results: Number(e.target.value) } })}
placeholder="5"
min={1}
/>
</div>
{brave.enabled && (
<div className="grid gap-1.5">
<InfoLabel tip={t("tools.braveApiKeyTip")}>{t("tools.braveApiKey")}</InfoLabel>
<Input
type="password"
className="text-base md:text-sm"
value={isSecret(brave.api_key) ? "" : (brave.api_key ?? "")}
onChange={(e) =>
updateNested("web", { brave: { ...brave, api_key: e.target.value } })
}
placeholder={t("tools.braveApiKeyPlaceholder")}
autoComplete="off"
/>
{isSecret(brave.api_key) && (
<p className="text-xs text-muted-foreground">{t("tools.braveApiKeyManaged")}</p>
)}
</div>
)}
</div>
</div>
<Separator />
{/* Web Fetch */}
<div className="grid gap-3">
<div className="grid gap-1.5 max-w-xs">
<InfoLabel tip={t("tools.webFetchPolicyTip")}>{t("tools.webFetchPolicy")}</InfoLabel>
<Select
value={webFetch.policy ?? "allow_all"}
onValueChange={(v) => updateNested("web_fetch", { ...webFetch, policy: v })}
>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="allow_all">Allow All</SelectItem>
<SelectItem value="allowlist">Allowlist</SelectItem>
</SelectContent>
</Select>
</div>
{webFetch.policy === "allowlist" && (
<div className="grid gap-1.5">
<Label>{t("tools.allowedDomains")}</Label>
<Textarea
value={(webFetch.allowed_domains ?? []).join("\n")}
onChange={(e) =>
updateNested("web_fetch", {
...webFetch,
allowed_domains: e.target.value.split("\n").filter(Boolean),
})
}
className="min-h-[80px] font-mono text-xs"
placeholder={"github.com\n*.wikipedia.org\ndocs.google.com"}
/>
</div>
)}
<div className="grid gap-1.5">
<InfoLabel tip={t("tools.blockedDomainsTip")}>{t("tools.blockedDomains")}</InfoLabel>
<Textarea
value={(webFetch.blocked_domains ?? []).join("\n")}
onChange={(e) =>
updateNested("web_fetch", {
...webFetch,
blocked_domains: e.target.value.split("\n").filter(Boolean),
})
}
className="min-h-[80px] font-mono text-xs"
placeholder={"ifconfig.co\nipinfo.io\n*.whatismyip.com"}
/>
</div>
</div>
<Separator />
{/* Browser */}
<div>
<h4 className="mb-3 text-sm font-medium">{t("tools.browser")}</h4>
<div className="flex gap-6">
<div className="flex items-center gap-2">
<Label>{t("tools.browserEnabled")}</Label>
<Switch
checked={browser.enabled !== false}
onCheckedChange={(v) => updateNested("browser", { enabled: v })}
/>
</div>
<div className="flex items-center gap-2">
<Label>{t("tools.browserHeadless")}</Label>
<Switch
checked={browser.headless !== false}
onCheckedChange={(v) => updateNested("browser", { headless: v })}
/>
</div>
</div>
</div>
{dirty && (
<div className="flex justify-end pt-2">
<Button size="sm" onClick={handleSave} disabled={saving} className="gap-1.5">
<Save className="h-3.5 w-3.5" /> {saving ? t("saving") : t("save")}
</Button>
</div>
)}
</CardContent>
</Card>
);
}