537yaha 3 месяцев назад
Родитель
Сommit
2cf7316b00
33 измененных файлов с 3517 добавлено и 1190 удалено
  1. 94 43
      frontend/app/agent/analytics/page.tsx
  2. 47 2
      frontend/app/agent/chat/[conversationId]/page.tsx
  3. 39 11
      frontend/app/agent/conversations/page.tsx
  4. 2 1
      frontend/app/agent/dashboard/page.tsx
  5. 77 68
      frontend/app/agent/faqs/page.tsx
  6. 504 459
      frontend/app/agent/knowledge/page.tsx
  7. 11 9
      frontend/app/agent/login/page.tsx
  8. 83 61
      frontend/app/agent/logs/page.tsx
  9. 66 74
      frontend/app/agent/prompts/page.tsx
  10. 102 73
      frontend/app/agent/settings/page.tsx
  11. 136 96
      frontend/app/agent/users/page.tsx
  12. 10 0
      frontend/app/globals.css
  13. 14 2
      frontend/app/layout.tsx
  14. 22 10
      frontend/components/dashboard/ChatHeader.tsx
  15. 13 4
      frontend/components/dashboard/ConversationHeader.tsx
  16. 11 5
      frontend/components/dashboard/ConversationListItem.tsx
  17. 4 2
      frontend/components/dashboard/ConversationSidebar.tsx
  18. 14 7
      frontend/components/dashboard/DashboardShell.tsx
  19. 12 0
      frontend/components/dashboard/DashboardSuspenseFallback.tsx
  20. 20 7
      frontend/components/dashboard/MessageInput.tsx
  21. 86 6
      frontend/components/dashboard/MessageList.tsx
  22. 13 7
      frontend/components/dashboard/NavigationSidebar.tsx
  23. 36 0
      frontend/components/i18n/LanguageSwitcher.tsx
  24. 46 44
      frontend/components/layout/Footer.tsx
  25. 74 7
      frontend/components/layout/Header.tsx
  26. 42 42
      frontend/components/layout/ResponsiveLayout.tsx
  27. 116 141
      frontend/components/marketing/HomePageClient.tsx
  28. 11 3
      frontend/components/visitor/ChatWidget.tsx
  29. 50 0
      frontend/features/agent/hooks/useMessages.ts
  30. 11 0
      frontend/lib/constants/agent-pages.tsx
  31. 1672 0
      frontend/lib/i18n/dict.ts
  32. 71 0
      frontend/lib/i18n/provider.tsx
  33. 8 6
      frontend/lib/stats-config.ts

+ 94 - 43
frontend/app/agent/analytics/page.tsx

@@ -8,6 +8,8 @@ import {
 } from "@/features/agent/services/analyticsApi";
 import { Button } from "@/components/ui/button";
 import { toast } from "@/hooks/useToast";
+import type { I18nKey } from "@/lib/i18n/dict";
+import { useI18n } from "@/lib/i18n/provider";
 
 function formatPercent(n: number) {
   if (Number.isNaN(n)) return "—";
@@ -24,7 +26,7 @@ function StatCard({
   sub?: string;
 }) {
   return (
-    <div className="rounded-xl border border-border/60 bg-card p-4 shadow-sm">
+    <div className="rounded-xl border border-border/60 bg-card p-3 shadow-sm sm:p-4">
       <div className="text-xs font-medium text-muted-foreground">{title}</div>
       <div className="mt-1 text-2xl font-semibold tabular-nums">{value}</div>
       {sub ? <div className="mt-1 text-xs text-muted-foreground">{sub}</div> : null}
@@ -37,6 +39,7 @@ function DailyBars({
   field,
   label,
   color,
+  emptyLabel,
 }: {
   daily: AnalyticsDailyRow[];
   field: keyof Pick<
@@ -45,6 +48,7 @@ function DailyBars({
   >;
   label: string;
   color: string;
+  emptyLabel: string;
 }) {
   const max = useMemo(() => {
     let m = 1;
@@ -56,13 +60,14 @@ function DailyBars({
   }, [daily, field]);
 
   if (daily.length === 0) {
-    return <p className="text-sm text-muted-foreground">暂无数据</p>;
+    return <p className="text-sm text-muted-foreground">{emptyLabel}</p>;
   }
 
   return (
-    <div>
+    <div className="min-w-0">
       <div className="mb-2 text-sm font-medium text-foreground">{label}</div>
-      <div className="flex h-36 items-end gap-1 border-b border-border/40 pb-1">
+      <div className="-mx-1 overflow-x-auto px-1 pb-1">
+        <div className="flex h-36 min-w-max items-end gap-1 border-b border-border/40 pb-1">
         {daily.map((row) => {
           const v = Number(row[field]) || 0;
           const h = Math.round((v / max) * 100);
@@ -86,12 +91,24 @@ function DailyBars({
             </div>
           );
         })}
+        </div>
       </div>
     </div>
   );
 }
 
 export default function AnalyticsPage(_props: { embedded?: boolean }) {
+  const { t } = useI18n();
+
+  const tr = (key: I18nKey, vars?: Record<string, string>) => {
+    let s = t(key);
+    if (!vars) return s;
+    for (const k of Object.keys(vars)) {
+      s = s.replaceAll(`{{${k}}}`, vars[k] ?? "");
+    }
+    return s;
+  };
+
   const [from, setFrom] = useState(() => {
     const d = new Date();
     d.setDate(d.getDate() - 6);
@@ -118,40 +135,36 @@ export default function AnalyticsPage(_props: { embedded?: boolean }) {
     void load();
   }, [load]);
 
-  const t = data?.totals;
+  const totals = data?.totals;
 
   return (
-    <div
-      className="flex flex-col min-h-0 overflow-auto p-4 max-w-6xl mx-auto w-full"
-    >
-      <div className="mb-6 flex flex-wrap items-end gap-4">
-        <div>
-          <h1 className="text-xl font-semibold tracking-tight">数据报表</h1>
-          <p className="text-sm text-muted-foreground mt-1">
-            访客小窗与 AI 客服统计(按上海时区自然日,不含「知识库测试」内部会话)
-          </p>
+    <div className="mx-auto flex min-h-0 w-full max-w-6xl flex-col overflow-auto p-3 sm:p-4 md:p-6">
+      <div className="mb-6 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
+        <div className="min-w-0">
+          <h1 className="text-xl font-semibold tracking-tight">{t("agent.analytics.title")}</h1>
+          <p className="mt-1 text-sm text-muted-foreground">{t("agent.analytics.subtitle")}</p>
         </div>
-        <div className="flex flex-wrap items-center gap-2 ml-auto">
-          <label className="text-xs text-muted-foreground flex items-center gap-1">
-            
+        <div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center lg:ml-auto">
+          <label className="flex items-center gap-2 text-xs text-muted-foreground sm:gap-1">
+            <span className="shrink-0">{t("agent.analytics.from")}</span>
             <input
               type="date"
               value={from}
               onChange={(e) => setFrom(e.target.value)}
-              className="rounded-md border border-input bg-background px-2 py-1 text-sm"
+              className="min-w-0 flex-1 rounded-md border border-input bg-background px-2 py-1.5 text-sm sm:flex-initial"
             />
           </label>
-          <label className="text-xs text-muted-foreground flex items-center gap-1">
-            
+          <label className="flex items-center gap-2 text-xs text-muted-foreground sm:gap-1">
+            <span className="shrink-0">{t("agent.analytics.to")}</span>
             <input
               type="date"
               value={to}
               onChange={(e) => setTo(e.target.value)}
-              className="rounded-md border border-input bg-background px-2 py-1 text-sm"
+              className="min-w-0 flex-1 rounded-md border border-input bg-background px-2 py-1.5 text-sm sm:flex-initial"
             />
           </label>
-          <Button size="sm" onClick={() => void load()} disabled={loading}>
-            {loading ? "加载中…" : "查询"}
+          <Button className="w-full sm:w-auto" size="sm" onClick={() => void load()} disabled={loading}>
+            {loading ? t("agent.analytics.loading") : t("agent.analytics.query")}
           </Button>
         </div>
       </div>
@@ -160,54 +173,92 @@ export default function AnalyticsPage(_props: { embedded?: boolean }) {
         <p className="text-xs text-muted-foreground mb-4">{data.note}</p>
       )}
 
-      {t && (
+      {totals && (
         <>
           <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3 mb-6">
-            <StatCard title="小窗打开次数" value={t.widget_opens} sub="需前端埋点,历史数据可能为 0" />
-            <StatCard title="新建会话数" value={t.sessions} />
-            <StatCard title="消息数" value={t.messages} />
-            <StatCard title="AI 回复次数" value={t.ai_replies} />
-            <StatCard title="AI 失败次数" value={t.ai_failed} />
-            <StatCard title="AI 失败率" value={formatPercent(t.ai_failure_rate_percent)} sub="占 AI 回复条数" />
-            <StatCard title="知识库命中次数" value={t.kb_hits} />
-            <StatCard title="知识库命中率" value={formatPercent(t.kb_hit_rate_percent)} sub="占成功 AI 回复" />
-            <StatCard title="最大 AI 对话轮数" value={t.max_ai_rounds} sub="单会话内用户+AI 一轮" />
-            <StatCard title="AI 参与会话" value={t.sessions_with_ai} sub={`占新建会话 ${formatPercent(t.ai_participation_rate_percent)}`} />
-            <StatCard title="AI→人工(会话数)" value={t.ai_to_human_sessions} sub={`占有过 AI 发言的会话 ${formatPercent(t.ai_to_human_rate_percent)}`} />
-            <StatCard title="人工→AI(会话数)" value={t.human_to_ai_sessions} sub={`占有过人工发言的会话 ${formatPercent(t.human_to_ai_rate_percent)}`} />
+            <StatCard
+              title={t("agent.analytics.stat.widgetOpens")}
+              value={totals.widget_opens}
+              sub={t("agent.analytics.stat.widgetOpensSub")}
+            />
+            <StatCard title={t("agent.analytics.stat.sessions")} value={totals.sessions} />
+            <StatCard title={t("agent.analytics.stat.messages")} value={totals.messages} />
+            <StatCard title={t("agent.analytics.stat.aiReplies")} value={totals.ai_replies} />
+            <StatCard title={t("agent.analytics.stat.aiFailed")} value={totals.ai_failed} />
+            <StatCard
+              title={t("agent.analytics.stat.aiFailureRate")}
+              value={formatPercent(totals.ai_failure_rate_percent)}
+              sub={t("agent.analytics.stat.aiFailureRateSub")}
+            />
+            <StatCard title={t("agent.analytics.stat.kbHits")} value={totals.kb_hits} />
+            <StatCard
+              title={t("agent.analytics.stat.kbHitRate")}
+              value={formatPercent(totals.kb_hit_rate_percent)}
+              sub={t("agent.analytics.stat.kbHitRateSub")}
+            />
+            <StatCard
+              title={t("agent.analytics.stat.maxAiRounds")}
+              value={totals.max_ai_rounds}
+              sub={t("agent.analytics.stat.maxAiRoundsSub")}
+            />
+            <StatCard
+              title={t("agent.analytics.stat.sessionsWithAi")}
+              value={totals.sessions_with_ai}
+              sub={tr("agent.analytics.stat.sessionsWithAiSub", {
+                pct: formatPercent(totals.ai_participation_rate_percent),
+              })}
+            />
+            <StatCard
+              title={t("agent.analytics.stat.aiToHuman")}
+              value={totals.ai_to_human_sessions}
+              sub={tr("agent.analytics.stat.aiToHumanSub", {
+                pct: formatPercent(totals.ai_to_human_rate_percent),
+              })}
+            />
+            <StatCard
+              title={t("agent.analytics.stat.humanToAi")}
+              value={totals.human_to_ai_sessions}
+              sub={tr("agent.analytics.stat.humanToAiSub", {
+                pct: formatPercent(totals.human_to_ai_rate_percent),
+              })}
+            />
           </div>
 
-          <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 rounded-xl border border-border/60 bg-card p-4">
+          <div className="grid grid-cols-1 gap-6 rounded-xl border border-border/60 bg-card p-3 sm:p-4 md:gap-8 lg:grid-cols-2">
             <DailyBars
               daily={data!.daily}
               field="widget_opens"
-              label="每日小窗打开"
+              label={t("agent.analytics.chart.widgetOpens")}
               color="rgb(34 197 94)"
+              emptyLabel={t("agent.analytics.empty")}
             />
             <DailyBars
               daily={data!.daily}
               field="sessions"
-              label="每日新建会话"
+              label={t("agent.analytics.chart.sessions")}
               color="rgb(59 130 246)"
+              emptyLabel={t("agent.analytics.empty")}
             />
             <DailyBars
               daily={data!.daily}
               field="messages"
-              label="每日消息数"
+              label={t("agent.analytics.chart.messages")}
               color="rgb(168 85 247)"
+              emptyLabel={t("agent.analytics.empty")}
             />
             <DailyBars
               daily={data!.daily}
               field="ai_replies"
-              label="每日 AI 回复"
+              label={t("agent.analytics.chart.aiReplies")}
               color="rgb(249 115 22)"
+              emptyLabel={t("agent.analytics.empty")}
             />
           </div>
         </>
       )}
 
-      {!loading && !t && (
-        <p className="text-sm text-muted-foreground">暂无数据或加载失败</p>
+      {!loading && !totals && (
+        <p className="text-sm text-muted-foreground">{t("agent.analytics.emptyOrFailed")}</p>
       )}
     </div>
   );

+ 47 - 2
frontend/app/agent/chat/[conversationId]/page.tsx

@@ -25,10 +25,12 @@ import {
 import type { WSMessage } from "@/lib/websocket";
 import { toast } from "@/hooks/useToast";
 import { getAgentWSToken } from "@/utils/storage";
+import { useI18n } from "@/lib/i18n/provider";
 
 export default function AgentChatPage() {
   const params = useParams();
   const router = useRouter();
+  const { t } = useI18n();
 
   const { agent, loading: authLoading } = useAuth();
 
@@ -152,6 +154,49 @@ export default function AgentChatPage() {
     loadMessages();
   }, [conversationId, agent, loadConversationDetail, loadMessages]);
 
+  // 与 `useMessages` 一致:默认不拉取 AI 分段消息时,访客在 AI 模式下的未读不会出现在列表中,
+  // 仅靠滚动无法 mark,会导致未读数长期残留。
+  useEffect(() => {
+    if (!conversationId || !agent) {
+      return;
+    }
+    if (loadingMessages) {
+      return;
+    }
+    if (conversationDetail && conversationDetail.id !== conversationId) {
+      return;
+    }
+
+    const serverUnread = Number(conversationDetail?.unread_count ?? 0);
+    if (serverUnread <= 0) {
+      return;
+    }
+
+    const messagesBelongToConv =
+      messages.length === 0 ||
+      messages.every((m) => m.conversation_id === conversationId);
+    if (!messagesBelongToConv) {
+      return;
+    }
+
+    const visibleVisitorUnread = messages.filter(
+      (msg) => !msg.sender_is_agent && !msg.is_read
+    ).length;
+    if (visibleVisitorUnread > 0) {
+      return;
+    }
+
+    void handleMarkMessagesRead(conversationId, true);
+  }, [
+    conversationId,
+    agent,
+    loadingMessages,
+    messages,
+    conversationDetail?.id,
+    conversationDetail?.unread_count,
+    handleMarkMessagesRead,
+  ]);
+
   const handleNewMessage = useCallback(
     (message: MessageItem) => {
       setMessages((prev) => {
@@ -360,7 +405,7 @@ export default function AgentChatPage() {
   if (authLoading || !agent) {
     return (
       <div className="flex justify-center items-center min-h-screen bg-gray-50 text-gray-600">
-        加载中...
+        {t("common.loading")}
       </div>
     );
   }
@@ -378,7 +423,7 @@ export default function AgentChatPage() {
             variant="outline"
             size="sm"
           >
-            ← 返回
+            ← {t("agent.settings.backDashboard")}
           </Button>
           <div className="flex-1">
             <ChatHeader

+ 39 - 11
frontend/app/agent/conversations/page.tsx

@@ -3,6 +3,8 @@ import { useEffect, useState } from "react";
 import { useRouter } from "next/navigation";
 import { apiUrl } from "@/lib/config";
 import { Button } from "@/components/ui/button";
+import type { I18nKey } from "@/lib/i18n/dict";
+import { useI18n } from "@/lib/i18n/provider";
 
 // 对话类型定义
 interface Conversation {
@@ -15,12 +17,24 @@ interface Conversation {
 }
 
 export default function ConversationsPage() {
+  const { t, lang } = useI18n();
   const [conversations, setConversations] = useState<Conversation[]>([]);
   const [loading, setLoading] = useState(true);
   const [username, setUsername] = useState<string>("");
   const [role, setRole] = useState<string>("");
   const router = useRouter();
 
+  const tr = (key: I18nKey, vars?: Record<string, string>) => {
+    let s = t(key);
+    if (!vars) return s;
+    for (const k of Object.keys(vars)) {
+      s = s.replaceAll(`{{${k}}}`, vars[k] ?? "");
+    }
+    return s;
+  };
+
+  const locale = lang === "en" ? "en-US" : "zh-CN";
+
   // 检查是否已登录
   useEffect(() => {
     const userId = localStorage.getItem("agent_user_id");
@@ -90,13 +104,13 @@ export default function ConversationsPage() {
 
     // 今天:只显示时间
     if (diff < 24 * 3600 * 1000 && date.getDate() === now.getDate()) {
-      return date.toLocaleTimeString("zh-CN", {
+      return date.toLocaleTimeString(locale, {
         hour: "2-digit",
         minute: "2-digit",
       });
     }
     // 更早:显示日期+时间
-    return date.toLocaleString("zh-CN", {
+    return date.toLocaleString(locale, {
       month: "2-digit",
       day: "2-digit",
       hour: "2-digit",
@@ -104,6 +118,12 @@ export default function ConversationsPage() {
     });
   };
 
+  const statusLabel = (status: string) => {
+    if (status === "open") return t("agent.conversations.status.open");
+    if (status === "closed") return t("agent.conversations.status.closed");
+    return status;
+  };
+
   // 点击对话,跳转到聊天页面
   const handleConversationClick = (conversationId: number) => {
     router.push(`/agent/chat/${conversationId}`);
@@ -123,9 +143,11 @@ export default function ConversationsPage() {
       <div className="bg-card border-b p-4 shadow-sm">
         <div className="flex justify-between items-center">
           <div>
-            <h1 className="text-xl font-bold text-foreground">对话列表</h1>
+            <h1 className="text-xl font-bold text-foreground">{t("agent.conversationsPage.title")}</h1>
             <div className="text-sm text-muted-foreground mt-1">
-              {username} ({role === "admin" ? "管理员" : "客服"})
+              {username} (
+              {role === "admin" ? t("agent.users.role.admin") : t("agent.users.role.agent")}
+              )
             </div>
           </div>
           <Button
@@ -133,7 +155,7 @@ export default function ConversationsPage() {
             variant="outline"
             size="sm"
           >
-            退出登录
+            {t("agent.logout")}
           </Button>
         </div>
       </div>
@@ -142,7 +164,7 @@ export default function ConversationsPage() {
       <div className="flex-1 overflow-y-auto p-4">
         {conversations.length === 0 ? (
           <div className="text-center text-gray-400 mt-8">
-            暂无对话
+            {t("agent.conversationsPage.empty")}
           </div>
         ) : (
           <div className="space-y-2">
@@ -156,7 +178,7 @@ export default function ConversationsPage() {
                   <div className="flex-1">
                     <div className="flex items-center gap-2 mb-1">
                       <span className="font-medium text-gray-800">
-                        对话 #{conv.id}
+                        {tr("agent.conversationsPage.convLabel", { id: String(conv.id) })}
                       </span>
                       <span
                         className={`px-2 py-1 rounded text-xs ${
@@ -165,18 +187,24 @@ export default function ConversationsPage() {
                             : "bg-gray-100 text-gray-700"
                         }`}
                       >
-                        {conv.status === "open" ? "进行中" : conv.status}
+                        {statusLabel(conv.status)}
                       </span>
                     </div>
                     <div className="text-sm text-gray-600">
-                      访客ID: {conv.visitor_id}
+                      {tr("agent.conversationsPage.visitorLabel", {
+                        id: String(conv.visitor_id),
+                      })}
                     </div>
                     <div className="text-xs text-gray-400 mt-1">
-                      创建时间: {formatTime(conv.created_at)}
+                      {tr("agent.conversationsPage.createdAt", {
+                        time: formatTime(conv.created_at),
+                      })}
                     </div>
                   </div>
                   <div className="text-xs text-gray-400">
-                    最后更新: {formatTime(conv.updated_at)}
+                    {tr("agent.conversationsPage.updatedAt", {
+                      time: formatTime(conv.updated_at),
+                    })}
                   </div>
                 </div>
               </div>

+ 2 - 1
frontend/app/agent/dashboard/page.tsx

@@ -1,9 +1,10 @@
 import { Suspense } from "react";
 import { DashboardShell } from "@/components/dashboard/DashboardShell";
+import { DashboardSuspenseFallback } from "@/components/dashboard/DashboardSuspenseFallback";
 
 export default function AgentDashboardPage() {
   return (
-    <Suspense fallback={<div className="flex justify-center items-center min-h-screen bg-background"><div className="text-muted-foreground">加载中...</div></div>}>
+    <Suspense fallback={<DashboardSuspenseFallback />}>
       <DashboardShell />
     </Suspense>
   );

+ 77 - 68
frontend/app/agent/faqs/page.tsx

@@ -35,11 +35,23 @@ import {
 } from "lucide-react";
 import { toast } from "@/hooks/useToast";
 import { Textarea } from "@/components/ui/textarea";
+import type { I18nKey } from "@/lib/i18n/dict";
+import { useI18n } from "@/lib/i18n/provider";
 
 export default function FAQsPage(props: any = {}) {
   const { embedded = false } = props;
   const router = useRouter();
   const { agent } = useAuth();
+  const { t, lang } = useI18n();
+
+  const tr = (key: I18nKey, vars?: Record<string, string>) => {
+    let s = t(key);
+    if (!vars) return s;
+    for (const k of Object.keys(vars)) {
+      s = s.replaceAll(`{{${k}}}`, vars[k] ?? "");
+    }
+    return s;
+  };
   const [faqs, setFaqs] = useState<FAQSummary[]>([]);
   const [loading, setLoading] = useState(true);
   const [searchQuery, setSearchQuery] = useState("");
@@ -73,7 +85,7 @@ export default function FAQsPage(props: any = {}) {
       setFaqs(data);
     } catch (error) {
       console.error("加载 FAQ 列表失败:", error);
-      toast.error((error as Error).message || "加载 FAQ 列表失败");
+      toast.error((error as Error).message || t("agent.faqs.toast.loadFailed"));
     } finally {
       setLoading(false);
     }
@@ -102,7 +114,7 @@ export default function FAQsPage(props: any = {}) {
   // 创建 FAQ
   const handleCreate = async () => {
     if (!createForm.question.trim() || !createForm.answer.trim()) {
-      toast.error("问题和答案不能为空");
+      toast.error(t("agent.faqs.toast.emptyRequired"));
       return;
     }
     setSubmitting(true);
@@ -111,9 +123,9 @@ export default function FAQsPage(props: any = {}) {
       setCreateDialogOpen(false);
       setCreateForm({ question: "", answer: "", keywords: "" });
       await loadFAQs();
-      toast.success("创建成功");
+      toast.success(t("agent.faqs.toast.createSuccess"));
     } catch (error) {
-      toast.error((error as Error).message || "创建 FAQ 失败");
+      toast.error((error as Error).message || t("agent.faqs.toast.createFailed"));
     } finally {
       setSubmitting(false);
     }
@@ -136,7 +148,7 @@ export default function FAQsPage(props: any = {}) {
       return;
     }
     if (!editForm.question?.trim() || !editForm.answer?.trim()) {
-      toast.error("问题和答案不能为空");
+      toast.error(t("agent.faqs.toast.emptyRequired"));
       return;
     }
     setSubmitting(true);
@@ -145,9 +157,9 @@ export default function FAQsPage(props: any = {}) {
       setEditDialogOpen(false);
       setSelectedFAQ(null);
       await loadFAQs();
-      toast.success("更新成功");
+      toast.success(t("agent.faqs.toast.updateSuccess"));
     } catch (error) {
-      toast.error((error as Error).message || "更新 FAQ 失败");
+      toast.error((error as Error).message || t("agent.faqs.toast.updateFailed"));
     } finally {
       setSubmitting(false);
     }
@@ -170,9 +182,9 @@ export default function FAQsPage(props: any = {}) {
       setDeleteDialogOpen(false);
       setSelectedFAQ(null);
       await loadFAQs();
-      toast.success("删除成功");
+      toast.success(t("agent.faqs.toast.deleteSuccess"));
     } catch (error) {
-      toast.error((error as Error).message || "删除 FAQ 失败");
+      toast.error((error as Error).message || t("agent.faqs.toast.deleteFailed"));
     } finally {
       setSubmitting(false);
     }
@@ -181,7 +193,7 @@ export default function FAQsPage(props: any = {}) {
   // 格式化时间
   const formatTime = (dateStr: string) => {
     const date = new Date(dateStr);
-    return date.toLocaleString("zh-CN", {
+    return date.toLocaleString(lang === "en" ? "en-US" : "zh-CN", {
       year: "numeric",
       month: "2-digit",
       day: "2-digit",
@@ -192,16 +204,16 @@ export default function FAQsPage(props: any = {}) {
 
   // 构建头部内容
   const headerContent = (
-    <div className="bg-card border-b p-4 shadow-sm">
+    <div className="border-b bg-card p-3 shadow-sm sm:p-4">
       <div className="flex items-center justify-between mb-4">
-        <h1 className="text-xl font-bold text-foreground">事件管理(FAQ)</h1>
+        <h1 className="text-xl font-bold text-foreground">{t("agent.faqs.title")}</h1>
         {!embedded && (
           <Button
             variant="ghost"
             size="sm"
             onClick={() => router.push("/agent/dashboard")}
           >
-            返回
+            {t("agent.common.back")}
           </Button>
         )}
       </div>
@@ -212,7 +224,7 @@ export default function FAQsPage(props: any = {}) {
           <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
           <Input
             type="text"
-            placeholder="关键词搜索(用 % 分隔,例如:openai%api%调用)..."
+            placeholder={t("agent.faqs.search.placeholder")}
             value={searchQuery}
             onChange={(e) => setSearchQuery(e.target.value)}
             className="pl-10"
@@ -223,7 +235,7 @@ export default function FAQsPage(props: any = {}) {
           className="w-full sm:w-auto"
         >
           <Plus className="w-4 h-4 mr-2" />
-          创建事件
+          {t("agent.faqs.createButton")}
         </Button>
       </div>
     </div>
@@ -231,15 +243,15 @@ export default function FAQsPage(props: any = {}) {
 
   // 构建主内容区
   const mainContent = (
-    <div className="flex-1 overflow-y-auto p-4 scrollbar-auto">
+    <div className="scrollbar-auto flex-1 overflow-y-auto p-3 sm:p-4">
       {loading ? (
         <div className="flex items-center justify-center h-full">
-          <span className="text-muted-foreground">加载中...</span>
+          <span className="text-muted-foreground">{t("common.loading")}</span>
         </div>
       ) : faqs.length === 0 ? (
         <div className="flex items-center justify-center h-full">
           <span className="text-muted-foreground">
-            {searchQuery ? "没有找到匹配的事件" : "暂无事件"}
+            {searchQuery ? t("agent.faqs.empty.filtered") : t("agent.faqs.empty")}
           </span>
         </div>
       ) : (
@@ -258,11 +270,11 @@ export default function FAQsPage(props: any = {}) {
                 </div>
                 {faq.keywords && (
                   <div className="text-xs text-muted-foreground mb-2">
-                    关键词: {faq.keywords}
+                    {t("agent.faqs.card.keywords")}: {faq.keywords}
                   </div>
                 )}
                 <div className="text-xs text-muted-foreground">
-                  创建时间: {formatTime(faq.created_at)}
+                  {t("agent.faqs.card.createdAt")}: {formatTime(faq.created_at)}
                 </div>
               </div>
 
@@ -274,7 +286,7 @@ export default function FAQsPage(props: any = {}) {
                   className="flex-1"
                 >
                   <Edit className="w-4 h-4 mr-1" />
-                  编辑
+                  {t("agent.faqs.card.edit")}
                 </Button>
                 <Button
                   variant="destructive"
@@ -291,63 +303,53 @@ export default function FAQsPage(props: any = {}) {
     </div>
   );
 
-  // 如果是嵌入模式,只返回内容,不包含 ResponsiveLayout
-  if (embedded) {
-    return (
-      <>
-        <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
-          {headerContent}
-          {mainContent}
-        </div>
-        {/* 对话框 */}
-        {/* 创建 FAQ 对话框 */}
+  const faqDialogs = (
+    <>
       <Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
         <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
           <DialogHeader>
-            <DialogTitle>创建新事件</DialogTitle>
-            <DialogDescription>
-              填写问题和答案,可以添加关键词以便搜索
-            </DialogDescription>
+            <DialogTitle>{t("agent.faqs.dialog.createTitle2")}</DialogTitle>
+            <DialogDescription>{t("agent.faqs.dialog.createDesc")}</DialogDescription>
           </DialogHeader>
           <div className="space-y-4">
             <div>
-              <Label htmlFor="create-question">问题 *</Label>
+              <Label htmlFor="create-question">{t("agent.faqs.form.question")} *</Label>
               <Textarea
                 id="create-question"
                 value={createForm.question}
                 onChange={(e) =>
                   setCreateForm({ ...createForm, question: e.target.value })
                 }
-                placeholder="请输入问题"
+                placeholder={t("agent.faqs.form.placeholder.question")}
                 rows={2}
                 className="resize-none"
               />
             </div>
             <div>
-              <Label htmlFor="create-answer">答案 *</Label>
+              <Label htmlFor="create-answer">{t("agent.faqs.form.answer")} *</Label>
               <Textarea
                 id="create-answer"
                 value={createForm.answer}
                 onChange={(e) =>
                   setCreateForm({ ...createForm, answer: e.target.value })
                 }
-                placeholder="请输入答案"
+                placeholder={t("agent.faqs.form.placeholder.answer")}
                 rows={6}
                 className="resize-none"
               />
             </div>
             <div>
-              <Label htmlFor="create-keywords">关键词(可选)</Label>
+              <Label htmlFor="create-keywords">{t("agent.faqs.form.keywordsOptional")}</Label>
               <Input
                 id="create-keywords"
                 value={createForm.keywords}
                 onChange={(e) =>
                   setCreateForm({ ...createForm, keywords: e.target.value })
                 }
-                placeholder="例如:API、错误、配置(用逗号或空格分隔)"
+                placeholder={t("agent.faqs.form.placeholder.keywords")}
               />
               <p className="text-xs text-muted-foreground mt-1">
-                提示:即使不填写关键词,系统也会自动搜索问题和答案中的内容。关键词字段用于添加额外的搜索索引,帮助用户更快找到相关内容。
+                {t("agent.faqs.form.keywordsTip")}
               </p>
             </div>
             <div className="flex justify-end gap-2">
@@ -356,65 +358,62 @@ export default function FAQsPage(props: any = {}) {
                 onClick={() => setCreateDialogOpen(false)}
                 disabled={submitting}
               >
-                取消
+                {t("agent.common.cancel")}
               </Button>
               <Button onClick={handleCreate} disabled={submitting}>
-                {submitting ? "创建中..." : "创建"}
+                {submitting ? t("agent.faqs.submit.creating") : t("agent.common.create")}
               </Button>
             </div>
           </div>
         </DialogContent>
       </Dialog>
 
-      {/* 编辑 FAQ 对话框 */}
       <Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
         <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
           <DialogHeader>
-            <DialogTitle>编辑事件</DialogTitle>
-            <DialogDescription>
-              修改问题和答案,可以更新关键词以便搜索
-            </DialogDescription>
+            <DialogTitle>{t("agent.faqs.dialog.editTitle")}</DialogTitle>
+            <DialogDescription>{t("agent.faqs.dialog.editDesc")}</DialogDescription>
           </DialogHeader>
           {selectedFAQ && (
             <div className="space-y-4">
               <div>
-                <Label htmlFor="edit-question">问题 *</Label>
+                <Label htmlFor="edit-question">{t("agent.faqs.form.question")} *</Label>
                 <Textarea
                   id="edit-question"
                   value={editForm.question || ""}
                   onChange={(e) =>
                     setEditForm({ ...editForm, question: e.target.value })
                   }
-                  placeholder="请输入问题"
+                  placeholder={t("agent.faqs.form.placeholder.question")}
                   rows={2}
                   className="resize-none"
                 />
               </div>
               <div>
-                <Label htmlFor="edit-answer">答案 *</Label>
+                <Label htmlFor="edit-answer">{t("agent.faqs.form.answer")} *</Label>
                 <Textarea
                   id="edit-answer"
                   value={editForm.answer || ""}
                   onChange={(e) =>
                     setEditForm({ ...editForm, answer: e.target.value })
                   }
-                  placeholder="请输入答案"
+                  placeholder={t("agent.faqs.form.placeholder.answer")}
                   rows={6}
                   className="resize-none"
                 />
               </div>
               <div>
-                <Label htmlFor="edit-keywords">关键词(可选)</Label>
+                <Label htmlFor="edit-keywords">{t("agent.faqs.form.keywordsOptional")}</Label>
                 <Input
                   id="edit-keywords"
                   value={editForm.keywords || ""}
                   onChange={(e) =>
                     setEditForm({ ...editForm, keywords: e.target.value })
                   }
-                  placeholder="例如:API、错误、配置(用逗号或空格分隔)"
+                  placeholder={t("agent.faqs.form.placeholder.keywords")}
                 />
                 <p className="text-xs text-muted-foreground mt-1">
-                  提示:即使不填写关键词,系统也会自动搜索问题和答案中的内容。关键词字段用于添加额外的搜索索引,帮助用户更快找到相关内容。
+                  {t("agent.faqs.form.keywordsTip")}
                 </p>
               </div>
               <div className="flex justify-end gap-2">
@@ -423,10 +422,10 @@ export default function FAQsPage(props: any = {}) {
                   onClick={() => setEditDialogOpen(false)}
                   disabled={submitting}
                 >
-                  取消
+                  {t("agent.common.cancel")}
                 </Button>
                 <Button onClick={handleUpdate} disabled={submitting}>
-                  {submitting ? "更新中..." : "更新"}
+                  {submitting ? t("common.saving") : t("agent.common.update")}
                 </Button>
               </div>
             </div>
@@ -434,19 +433,18 @@ export default function FAQsPage(props: any = {}) {
         </DialogContent>
       </Dialog>
 
-      {/* 删除确认对话框 */}
       <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
         <DialogContent>
           <DialogHeader>
-            <DialogTitle>删除事件</DialogTitle>
+            <DialogTitle>{t("agent.faqs.dialog.deleteTitle")}</DialogTitle>
           </DialogHeader>
           {selectedFAQ && (
             <div className="space-y-4">
               <p className="text-foreground">
-                确定要删除事件 <strong>&quot;{selectedFAQ.question}&quot;</strong> 吗?
+                {tr("agent.faqs.dialog.deleteConfirm", { name: selectedFAQ.question })}
               </p>
               <p className="text-sm text-muted-foreground">
-                此操作不可恢复,请谨慎操作。
+                {t("common.irreversibleHint")}
               </p>
               <div className="flex justify-end gap-2">
                 <Button
@@ -454,14 +452,14 @@ export default function FAQsPage(props: any = {}) {
                   onClick={() => setDeleteDialogOpen(false)}
                   disabled={submitting}
                 >
-                  取消
+                  {t("agent.common.cancel")}
                 </Button>
                 <Button
                   variant="destructive"
                   onClick={handleDelete}
                   disabled={submitting}
                 >
-                  {submitting ? "删除中..." : "删除"}
+                  {submitting ? t("agent.faqs.submit.deleting") : t("agent.common.delete")}
                 </Button>
               </div>
             </div>
@@ -470,13 +468,24 @@ export default function FAQsPage(props: any = {}) {
       </Dialog>
     </>
   );
+
+  if (embedded) {
+    return (
+      <>
+        <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
+          {headerContent}
+          {mainContent}
+        </div>
+        {faqDialogs}
+      </>
+    );
   }
 
   return (
-    <ResponsiveLayout
-      main={mainContent}
-      header={headerContent}
-    />
+    <>
+      <ResponsiveLayout main={mainContent} header={headerContent} />
+      {faqDialogs}
+    </>
   );
 }
 

+ 504 - 459
frontend/app/agent/knowledge/page.tsx

@@ -4,6 +4,8 @@ import { useCallback, useEffect, useState } from "react";
 import { useRouter } from "next/navigation";
 import { useAuth } from "@/features/agent/hooks/useAuth";
 import { ResponsiveLayout } from "@/components/layout";
+import type { I18nKey } from "@/lib/i18n/dict";
+import { useI18n } from "@/lib/i18n/provider";
 import {
   fetchKnowledgeBases,
   createKnowledgeBase,
@@ -66,6 +68,16 @@ export default function KnowledgePage(props: any = {}) {
   const { embedded = false } = props;
   const router = useRouter();
   const { agent } = useAuth();
+  const { t, lang } = useI18n();
+
+  const tr = (key: I18nKey, vars?: Record<string, string>) => {
+    let s = t(key);
+    if (!vars) return s;
+    for (const k of Object.keys(vars)) {
+      s = s.replaceAll(`{{${k}}}`, vars[k] ?? "");
+    }
+    return s;
+  };
 
   // 知识库状态
   const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
@@ -119,11 +131,11 @@ export default function KnowledgePage(props: any = {}) {
       setKnowledgeBases(data);
     } catch (error) {
       console.error("加载知识库列表失败:", error);
-      toast.error((error as Error).message || "加载知识库列表失败");
+      toast.error((error as Error).message || t("agent.knowledge.toast.loadKbFailed"));
     } finally {
       setLoadingKBs(false);
     }
-  }, []);
+  }, [t]);
 
   // 加载文档列表(silent:后台轮询向量化状态时不全屏“加载中”、不弹 Toast,避免刷屏)
   const loadDocuments = useCallback(
@@ -152,7 +164,7 @@ export default function KnowledgePage(props: any = {}) {
       } catch (error) {
         console.error("加载文档列表失败:", error);
         if (!silent) {
-          toast.error((error as Error).message || "加载文档列表失败");
+          toast.error((error as Error).message || t("agent.knowledge.toast.loadDocFailed"));
           setDocuments([]);
           setDocumentResult(null);
         }
@@ -162,7 +174,7 @@ export default function KnowledgePage(props: any = {}) {
         }
       }
     },
-    [selectedKnowledgeBase, currentPage, searchKeyword, statusFilter]
+    [selectedKnowledgeBase, currentPage, searchKeyword, statusFilter, t]
   );
 
   // 初始加载
@@ -203,7 +215,7 @@ export default function KnowledgePage(props: any = {}) {
   // 创建知识库
   const handleCreateKB = async () => {
     if (!createKBForm.name.trim()) {
-      toast.error("知识库名称不能为空");
+      toast.error(t("agent.knowledge.toast.kbNameRequired"));
       return;
     }
     setSubmitting(true);
@@ -212,9 +224,9 @@ export default function KnowledgePage(props: any = {}) {
       setCreateKBDialogOpen(false);
       setCreateKBForm({ name: "", description: "" });
       await loadKnowledgeBases();
-      toast.success("创建成功");
+      toast.success(t("agent.knowledge.toast.createSuccess"));
     } catch (error) {
-      toast.error((error as Error).message || "创建知识库失败");
+      toast.error((error as Error).message || t("agent.knowledge.toast.createKbFailed"));
     } finally {
       setSubmitting(false);
     }
@@ -238,9 +250,9 @@ export default function KnowledgePage(props: any = {}) {
       await updateKnowledgeBase(selectedKnowledgeBase.id, editKBForm);
       setEditKBDialogOpen(false);
       await loadKnowledgeBases();
-      toast.success("更新成功");
+      toast.success(t("agent.knowledge.toast.updateSuccess"));
     } catch (error) {
-      toast.error((error as Error).message || "更新知识库失败");
+      toast.error((error as Error).message || t("agent.knowledge.toast.updateKbFailed"));
     } finally {
       setSubmitting(false);
     }
@@ -261,9 +273,9 @@ export default function KnowledgePage(props: any = {}) {
       setDeleteKBDialogOpen(false);
       setSelectedKnowledgeBase(null);
       await loadKnowledgeBases();
-      toast.success("删除成功");
+      toast.success(t("agent.knowledge.toast.deleteSuccess"));
     } catch (error) {
-      toast.error((error as Error).message || "删除知识库失败");
+      toast.error((error as Error).message || t("agent.knowledge.toast.deleteKbFailed"));
     } finally {
       setSubmitting(false);
     }
@@ -272,7 +284,7 @@ export default function KnowledgePage(props: any = {}) {
   // 打开创建文档对话框
   const handleOpenCreateDoc = () => {
     if (!selectedKnowledgeBase) {
-      toast.error("请先选择知识库");
+      toast.error(t("agent.knowledge.toast.selectKbFirst"));
       return;
     }
     setCreateDocForm({
@@ -289,7 +301,7 @@ export default function KnowledgePage(props: any = {}) {
   // 创建文档
   const handleCreateDoc = async () => {
     if (!createDocForm.title.trim() || !createDocForm.content.trim()) {
-      toast.error("标题和内容不能为空");
+      toast.error(t("agent.knowledge.toast.docTitleContentRequired"));
       return;
     }
     setSubmitting(true);
@@ -305,9 +317,9 @@ export default function KnowledgePage(props: any = {}) {
         status: "draft",
       });
       await loadDocuments();
-      toast.success("创建成功");
+      toast.success(t("agent.knowledge.toast.createSuccess"));
     } catch (error) {
-      toast.error((error as Error).message || "创建文档失败");
+      toast.error((error as Error).message || t("agent.knowledge.toast.createDocFailed"));
     } finally {
       setSubmitting(false);
     }
@@ -333,9 +345,9 @@ export default function KnowledgePage(props: any = {}) {
       await updateDocument(docId, editDocForm);
       setEditDocDialogOpen(false);
       await loadDocuments();
-      toast.success("更新成功");
+      toast.success(t("agent.knowledge.toast.updateSuccess"));
     } catch (error) {
-      toast.error((error as Error).message || "更新文档失败");
+      toast.error((error as Error).message || t("agent.knowledge.toast.updateDocFailed"));
     } finally {
       setSubmitting(false);
     }
@@ -354,9 +366,9 @@ export default function KnowledgePage(props: any = {}) {
       await deleteDocument(docId);
       setDeleteDocDialogOpen(false);
       await loadDocuments();
-      toast.success("删除成功");
+      toast.success(t("agent.knowledge.toast.deleteSuccess"));
     } catch (error) {
-      toast.error((error as Error).message || "删除文档失败");
+      toast.error((error as Error).message || t("agent.knowledge.toast.deleteDocFailed"));
     } finally {
       setSubmitting(false);
     }
@@ -367,9 +379,9 @@ export default function KnowledgePage(props: any = {}) {
     try {
       await publishDocument(docId);
       await loadDocuments();
-      toast.success("发布成功");
+      toast.success(t("agent.knowledge.toast.publishSuccess"));
     } catch (error) {
-      toast.error((error as Error).message || "发布文档失败");
+      toast.error((error as Error).message || t("agent.knowledge.toast.publishFailed"));
     }
   };
 
@@ -378,20 +390,20 @@ export default function KnowledgePage(props: any = {}) {
     try {
       await unpublishDocument(docId);
       await loadDocuments();
-      toast.success("取消发布成功");
+      toast.success(t("agent.knowledge.toast.unpublishSuccess"));
     } catch (error) {
-      toast.error((error as Error).message || "取消发布文档失败");
+      toast.error((error as Error).message || t("agent.knowledge.toast.unpublishFailed"));
     }
   };
 
   // 导入文件
   const handleImportFiles = async () => {
     if (!selectedKnowledgeBase) {
-      toast.error("请先选择知识库");
+      toast.error(t("agent.knowledge.toast.selectKbFirst"));
       return;
     }
     if (importFiles.length === 0) {
-      toast.error("请选择要导入的文件");
+      toast.error(t("agent.knowledge.toast.selectFiles"));
       return;
     }
     setSubmitting(true);
@@ -399,11 +411,26 @@ export default function KnowledgePage(props: any = {}) {
       const result: ImportResult = await importDocuments(selectedKnowledgeBase.id, importFiles);
       const errMsg = result.errors?.length ? result.errors[0] : "";
       if (result.failed_count > 0 && result.success_count === 0) {
-        toast.error(errMsg || `导入失败:${result.failed_count} 个文件未成功`);
+        toast.error(
+          errMsg ||
+            tr("agent.knowledge.toast.importFailed.files", {
+              count: String(result.failed_count),
+            })
+        );
       } else if (result.failed_count > 0) {
-        toast.success(`导入完成:成功 ${result.success_count},失败 ${result.failed_count}${errMsg ? `(${errMsg})` : ""}`);
+        toast.success(
+          tr("agent.knowledge.toast.importDone.partial", {
+            success: String(result.success_count),
+            failed: String(result.failed_count),
+            err: errMsg ? `(${errMsg})` : "",
+          })
+        );
       } else {
-        toast.success(`导入完成:成功 ${result.success_count} 个文件`);
+        toast.success(
+          tr("agent.knowledge.toast.importDone.files", {
+            success: String(result.success_count),
+          })
+        );
       }
       setImportDialogOpen(false);
       setImportFiles([]);
@@ -411,10 +438,10 @@ export default function KnowledgePage(props: any = {}) {
         await loadDocuments();
         await loadKnowledgeBases();
       } catch {
-        toast.error("导入成功,但刷新列表失败,请手动刷新页面");
+        toast.error(t("agent.knowledge.toast.importRefreshFailed"));
       }
     } catch (error) {
-      toast.error((error as Error).message || "导入文档失败");
+      toast.error((error as Error).message || t("agent.knowledge.toast.importDocFailed"));
     } finally {
       setSubmitting(false);
     }
@@ -423,7 +450,7 @@ export default function KnowledgePage(props: any = {}) {
   // 导入 URL
   const handleImportUrls = async () => {
     if (!selectedKnowledgeBase) {
-      toast.error("请先选择知识库");
+      toast.error(t("agent.knowledge.toast.selectKbFirst"));
       return;
     }
     const urls = importUrls
@@ -431,7 +458,7 @@ export default function KnowledgePage(props: any = {}) {
       .map((url) => url.trim())
       .filter((url) => url.length > 0);
     if (urls.length === 0) {
-      toast.error("请输入至少一个 URL");
+      toast.error(t("agent.knowledge.toast.urlRequired"));
       return;
     }
     setSubmitting(true);
@@ -442,11 +469,26 @@ export default function KnowledgePage(props: any = {}) {
       });
       const errMsg = result.errors?.length ? result.errors[0] : "";
       if (result.failed_count > 0 && result.success_count === 0) {
-        toast.error(errMsg || `导入失败:${result.failed_count} 个 URL 未成功`);
+        toast.error(
+          errMsg ||
+            tr("agent.knowledge.toast.importFailed.urls", {
+              count: String(result.failed_count),
+            })
+        );
       } else if (result.failed_count > 0) {
-        toast.success(`导入完成:成功 ${result.success_count},失败 ${result.failed_count}${errMsg ? `(${errMsg})` : ""}`);
+        toast.success(
+          tr("agent.knowledge.toast.importDone.partial", {
+            success: String(result.success_count),
+            failed: String(result.failed_count),
+            err: errMsg ? `(${errMsg})` : "",
+          })
+        );
       } else {
-        toast.success(`导入完成:成功 ${result.success_count} 个 URL`);
+        toast.success(
+          tr("agent.knowledge.toast.importDone.urls", {
+            success: String(result.success_count),
+          })
+        );
       }
       setImportDialogOpen(false);
       setImportUrls("");
@@ -454,10 +496,10 @@ export default function KnowledgePage(props: any = {}) {
         await loadDocuments();
         await loadKnowledgeBases();
       } catch {
-        toast.error("导入成功,但刷新列表失败,请手动刷新页面");
+        toast.error(t("agent.knowledge.toast.importRefreshFailed"));
       }
     } catch (error) {
-      toast.error((error as Error).message || "导入 URL 失败");
+      toast.error((error as Error).message || t("agent.knowledge.toast.importUrlFailed"));
     } finally {
       setSubmitting(false);
     }
@@ -466,7 +508,7 @@ export default function KnowledgePage(props: any = {}) {
   // 格式化时间
   const formatTime = (dateStr: string) => {
     const date = new Date(dateStr);
-    return date.toLocaleString("zh-CN", {
+    return date.toLocaleString(lang === "en" ? "en-US" : "zh-CN", {
       year: "numeric",
       month: "2-digit",
       day: "2-digit",
@@ -482,13 +524,13 @@ export default function KnowledgePage(props: any = {}) {
         return (
           <span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-green-100 text-green-800">
             <CheckCircle2 className="w-3 h-3 mr-1" />
-            已发布
+            {t("agent.knowledge.status.published")}
           </span>
         );
       case "draft":
         return (
           <span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-gray-100 text-gray-800">
-            草稿
+            {t("agent.knowledge.status.draft")}
           </span>
         );
       default:
@@ -507,28 +549,28 @@ export default function KnowledgePage(props: any = {}) {
         return (
           <span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800">
             <CheckCircle2 className="w-3 h-3 mr-1" />
-            已完成
+            {t("agent.knowledge.embedding.completed")}
           </span>
         );
       case "processing":
         return (
           <span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-yellow-100 text-yellow-800">
             <Loader2 className="w-3 h-3 mr-1 animate-spin" />
-            处理中
+            {t("agent.knowledge.embedding.processing")}
           </span>
         );
       case "failed":
         return (
           <span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-red-100 text-red-800">
             <XCircle className="w-3 h-3 mr-1" />
-            失败
+            {t("agent.knowledge.embedding.failed")}
           </span>
         );
       case "pending":
       default:
         return (
           <span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-gray-100 text-gray-800">
-            待处理
+            {t("agent.knowledge.embedding.pending")}
           </span>
         );
     }
@@ -536,16 +578,16 @@ export default function KnowledgePage(props: any = {}) {
 
   // 构建头部内容
   const headerContent = (
-    <div className="bg-card border-b p-4 shadow-sm">
+    <div className="bg-card border-b p-3 shadow-sm sm:p-4">
       <div className="flex items-center justify-between mb-4">
-        <h1 className="text-xl font-bold text-foreground">知识库管理</h1>
+        <h1 className="text-xl font-bold text-foreground">{t("agent.knowledge.title")}</h1>
         {!embedded && (
           <Button
             variant="ghost"
             size="sm"
             onClick={() => router.push("/agent/dashboard")}
           >
-            返回
+            {t("agent.common.back")}
           </Button>
         )}
       </div>
@@ -554,9 +596,9 @@ export default function KnowledgePage(props: any = {}) {
 
   // 构建主内容区
   const mainContent = (
-    <div className="flex-1 flex overflow-hidden">
-      {/* 左侧:知识库列表 */}
-      <div className="w-64 border-r bg-gray-50 flex flex-col">
+    <div className="flex flex-1 min-h-0 flex-col overflow-hidden md:flex-row">
+      {/* 左侧:知识库列表(小屏置顶且限高,避免挤掉文档区) */}
+      <div className="flex h-[min(40vh,320px)] w-full shrink-0 flex-col border-b border-border bg-gray-50 md:h-auto md:max-h-none md:w-64 md:border-b-0 md:border-r">
         <div className="p-4 border-b">
           <Button
             onClick={() => setCreateKBDialogOpen(true)}
@@ -564,17 +606,17 @@ export default function KnowledgePage(props: any = {}) {
             size="sm"
           >
             <Plus className="w-4 h-4 mr-2" />
-            新建知识库
+            {t("agent.knowledge.kb.create")}
           </Button>
         </div>
         <div className="flex-1 overflow-y-auto p-2">
           {loadingKBs ? (
             <div className="flex items-center justify-center h-full">
-              <span className="text-muted-foreground">加载中...</span>
+              <span className="text-muted-foreground">{t("common.loading")}</span>
             </div>
           ) : knowledgeBases.length === 0 ? (
             <div className="flex items-center justify-center h-full">
-              <span className="text-muted-foreground">暂无知识库</span>
+              <span className="text-muted-foreground">{t("agent.knowledge.kb.empty")}</span>
             </div>
           ) : (
             <div className="space-y-2">
@@ -601,7 +643,9 @@ export default function KnowledgePage(props: any = {}) {
                       )}
                       <div className="flex items-center gap-2 mt-1">
                         <span className="text-xs text-muted-foreground">
-                          {kb.document_count} 篇文档
+                          {tr("agent.knowledge.kb.docCount", {
+                            count: String(kb.document_count),
+                          })}
                         </span>
                       </div>
                     </div>
@@ -638,16 +682,23 @@ export default function KnowledgePage(props: any = {}) {
       </div>
 
       {/* 右侧:文档列表 */}
-      <div className="flex-1 flex flex-col overflow-hidden">
+      <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
         {selectedKnowledgeBase ? (
           <>
             {/* 文档列表头部 */}
-            <div className="p-4 border-b bg-white">
-              <div className="flex items-center justify-between mb-4">
-                <h2 className="text-lg font-semibold">{selectedKnowledgeBase.name}</h2>
-                <div className="flex gap-2 items-center">
-                  <div className="flex items-center gap-2 mr-2">
-                    <Label htmlFor="rag-enabled" className="text-sm text-muted-foreground whitespace-nowrap">参与 RAG</Label>
+            <div className="border-b bg-white p-3 sm:p-4">
+              <div className="mb-4 flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
+                <h2 className="text-lg font-semibold leading-tight break-words">
+                  {selectedKnowledgeBase.name}
+                </h2>
+                <div className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
+                  <div className="flex items-center gap-2">
+                    <Label
+                      htmlFor="rag-enabled"
+                      className="whitespace-nowrap text-sm text-muted-foreground"
+                    >
+                      {t("agent.knowledge.rag")}
+                    </Label>
                     <Switch
                       id="rag-enabled"
                       checked={selectedKnowledgeBase.rag_enabled !== false}
@@ -657,37 +708,41 @@ export default function KnowledgePage(props: any = {}) {
                           setSelectedKnowledgeBase((prev) => (prev?.id === updated.id ? { ...prev, rag_enabled: updated.rag_enabled } : prev));
                           setKnowledgeBases((prev) => prev.map((kb) => (kb.id === updated.id ? { ...kb, rag_enabled: updated.rag_enabled } : kb)));
                         } catch (e) {
-                          toast.error((e as Error).message || "更新失败");
+                          toast.error((e as Error).message || t("agent.knowledge.toast.updateFailed"));
                         }
                       }}
                     />
                   </div>
-                  <Button
-                    variant="outline"
-                    size="sm"
-                    onClick={() => {
-                      setImportTab("url");
-                      setImportDialogOpen(true);
-                    }}
-                  >
-                    <LinkIcon className="w-4 h-4 mr-2" />
-                    导入 URL
-                  </Button>
-                  <Button
-                    variant="outline"
-                    size="sm"
-                    onClick={() => {
-                      setImportTab("file");
-                      setImportDialogOpen(true);
-                    }}
-                  >
-                    <Upload className="w-4 h-4 mr-2" />
-                    导入文件
-                  </Button>
-                  <Button size="sm" onClick={handleOpenCreateDoc}>
-                    <Plus className="w-4 h-4 mr-2" />
-                    新建文档
-                  </Button>
+                  <div className="flex flex-wrap gap-2">
+                    <Button
+                      variant="outline"
+                      size="sm"
+                      className="flex-1 min-w-[8rem] sm:flex-initial"
+                      onClick={() => {
+                        setImportTab("url");
+                        setImportDialogOpen(true);
+                      }}
+                    >
+                      <LinkIcon className="mr-2 h-4 w-4 shrink-0" />
+                      {t("agent.knowledge.import.url")}
+                    </Button>
+                    <Button
+                      variant="outline"
+                      size="sm"
+                      className="flex-1 min-w-[8rem] sm:flex-initial"
+                      onClick={() => {
+                        setImportTab("file");
+                        setImportDialogOpen(true);
+                      }}
+                    >
+                      <Upload className="mr-2 h-4 w-4 shrink-0" />
+                      {t("agent.knowledge.import.file")}
+                    </Button>
+                    <Button size="sm" className="w-full sm:w-auto" onClick={handleOpenCreateDoc}>
+                      <Plus className="mr-2 h-4 w-4 shrink-0" />
+                      {t("agent.knowledge.doc.create")}
+                    </Button>
+                  </div>
                 </div>
               </div>
 
@@ -697,7 +752,7 @@ export default function KnowledgePage(props: any = {}) {
                   <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
                   <Input
                     type="text"
-                    placeholder="搜索文档..."
+                    placeholder={t("agent.knowledge.doc.searchPh")}
                     value={searchKeyword}
                     onChange={(e) => setSearchKeyword(e.target.value)}
                     className="pl-10"
@@ -708,85 +763,95 @@ export default function KnowledgePage(props: any = {}) {
                   onChange={(e) => setStatusFilter(e.target.value)}
                   className="px-3 py-2 border rounded-md text-sm"
                 >
-                  <option value="all">全部状态</option>
-                  <option value="draft">草稿</option>
-                  <option value="published">已发布</option>
+                  <option value="all">{t("agent.knowledge.filter.all")}</option>
+                  <option value="draft">{t("agent.knowledge.status.draft")}</option>
+                  <option value="published">{t("agent.knowledge.status.published")}</option>
                 </select>
               </div>
             </div>
 
             {/* 文档列表 */}
-            <div className="flex-1 overflow-y-auto p-4">
+            <div className="flex-1 overflow-y-auto p-3 sm:p-4">
               {loadingDocs ? (
                 <div className="flex items-center justify-center h-full">
-                  <span className="text-muted-foreground">加载中...</span>
+                  <span className="text-muted-foreground">{t("common.loading")}</span>
                 </div>
               ) : (documents?.length ?? 0) === 0 ? (
                 <div className="flex items-center justify-center h-full">
                   <span className="text-muted-foreground">
                     {searchKeyword || statusFilter !== "all"
-                      ? "没有找到匹配的文档"
-                      : "暂无文档"}
+                      ? t("agent.knowledge.doc.empty.filtered")
+                      : t("agent.knowledge.doc.empty")}
                   </span>
                 </div>
               ) : (
                 <div className="space-y-4">
                   {(documents ?? []).map((doc) => (
-                    <Card key={doc.id} className="p-4">
-                      <div className="flex items-start justify-between">
-                        <div className="flex-1 min-w-0">
-                          <div className="flex items-center gap-2 mb-2">
-                            <FileText className="w-5 h-5 text-blue-600 flex-shrink-0" />
-                            <h3 className="font-medium text-foreground truncate">
+                    <Card key={doc.id} className="p-3 sm:p-4">
+                      <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
+                        <div className="min-w-0 flex-1">
+                          <div className="mb-2 flex flex-wrap items-center gap-2">
+                            <FileText className="h-5 w-5 shrink-0 text-blue-600" />
+                            <h3 className="min-w-0 flex-1 font-medium text-foreground break-words sm:truncate">
                               {doc.title}
                             </h3>
-                            {getStatusBadge(doc.status)}
-                            {getEmbeddingStatusBadge(doc.embedding_status)}
+                            <span className="flex flex-wrap gap-1">
+                              {getStatusBadge(doc.status)}
+                              {getEmbeddingStatusBadge(doc.embedding_status)}
+                            </span>
                           </div>
                           {doc.summary && (
-                            <p className="text-sm text-muted-foreground mb-2 line-clamp-2">
+                            <p className="mb-2 line-clamp-2 text-sm text-muted-foreground">
                               {doc.summary}
                             </p>
                           )}
-                          <div className="flex items-center gap-4 text-xs text-muted-foreground">
-                            <span>类型: {doc.type}</span>
-                            <span>创建时间: {formatTime(doc.created_at)}</span>
+                          <div className="flex flex-col gap-1 text-xs text-muted-foreground sm:flex-row sm:flex-wrap sm:gap-4">
+                            <span>
+                              {t("agent.knowledge.doc.type")}: {doc.type}
+                            </span>
+                            <span className="break-words">
+                              {t("agent.knowledge.doc.createdAt")}: {formatTime(doc.created_at)}
+                            </span>
                           </div>
                         </div>
-                        <div className="flex flex-col gap-2 ml-4">
-                          <div className="flex gap-2">
+                        <div className="flex w-full shrink-0 flex-col gap-2 sm:w-auto sm:ml-4">
+                          <div className="flex flex-wrap gap-2">
                             <Button
                               variant="outline"
                               size="sm"
+                              className="min-w-0 flex-1 sm:flex-initial"
                               onClick={() => handleOpenEditDoc(doc)}
                             >
-                              <Edit className="w-4 h-4 mr-1" />
-                              编辑
+                              <Edit className="mr-1 h-4 w-4 shrink-0" />
+                              {t("agent.common.edit")}
                             </Button>
                             <Button
                               variant="destructive"
                               size="sm"
+                              className="shrink-0"
                               onClick={() => handleOpenDeleteDoc(doc)}
                             >
-                              <Trash2 className="w-4 h-4" />
+                              <Trash2 className="h-4 w-4" />
                             </Button>
                           </div>
-                          <div className="flex gap-2">
+                          <div className="flex flex-wrap gap-2">
                             {doc.status === "published" ? (
                               <Button
                                 variant="outline"
                                 size="sm"
+                                className="w-full sm:w-auto"
                                 onClick={() => handleUnpublishDoc(doc.id)}
                               >
-                                取消发布
+                                {t("agent.knowledge.doc.unpublish")}
                               </Button>
                             ) : (
                               <Button
                                 variant="outline"
                                 size="sm"
+                                className="w-full sm:w-auto"
                                 onClick={() => handlePublishDoc(doc.id)}
                               >
-                                发布
+                                {t("agent.knowledge.doc.publish")}
                               </Button>
                             )}
                           </div>
@@ -809,7 +874,11 @@ export default function KnowledgePage(props: any = {}) {
                     <ChevronLeft className="w-4 h-4" />
                   </Button>
                   <span className="text-sm text-muted-foreground">
-                    第 {currentPage} / {documentResult.total_page} 页,共 {documentResult.total} 条
+                    {tr("agent.knowledge.pagination", {
+                      page: String(currentPage),
+                      totalPage: String(documentResult.total_page),
+                      total: String(documentResult.total),
+                    })}
                   </span>
                   <Button
                     variant="outline"
@@ -825,415 +894,391 @@ export default function KnowledgePage(props: any = {}) {
           </>
         ) : (
           <div className="flex items-center justify-center h-full">
-            <span className="text-muted-foreground">请选择一个知识库</span>
+            <span className="text-muted-foreground">{t("agent.knowledge.kb.selectOne")}</span>
           </div>
         )}
       </div>
     </div>
   );
 
-  // 如果是嵌入模式,只返回内容,不包含 ResponsiveLayout
-  if (embedded) {
-    return (
-      <>
-        <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
-          {headerContent}
-          {mainContent}
-        </div>
+  const dialogs = (
+    <>
+      <Dialog open={createKBDialogOpen} onOpenChange={setCreateKBDialogOpen}>
+        <DialogContent className="max-h-[90dvh] max-w-[min(100vw-2rem,42rem)] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>{t("agent.knowledge.dialog.kbCreateTitle")}</DialogTitle>
+            <DialogDescription>{t("agent.knowledge.dialog.kbCreateDesc")}</DialogDescription>
+          </DialogHeader>
+          <div className="space-y-4">
+            <div>
+              <Label htmlFor="create-kb-name">{t("agent.knowledge.field.name")} *</Label>
+              <Input
+                id="create-kb-name"
+                value={createKBForm.name}
+                onChange={(e) => setCreateKBForm({ ...createKBForm, name: e.target.value })}
+                placeholder={t("agent.knowledge.ph.kbName")}
+              />
+            </div>
+            <div>
+              <Label htmlFor="create-kb-desc">{t("agent.knowledge.field.descOptional")}</Label>
+              <Textarea
+                id="create-kb-desc"
+                value={createKBForm.description || ""}
+                onChange={(e) =>
+                  setCreateKBForm({ ...createKBForm, description: e.target.value })
+                }
+                placeholder={t("agent.knowledge.ph.kbDesc")}
+                rows={3}
+              />
+            </div>
+            <div className="flex justify-end gap-2">
+              <Button
+                variant="outline"
+                onClick={() => setCreateKBDialogOpen(false)}
+                disabled={submitting}
+              >
+                {t("agent.common.cancel")}
+              </Button>
+              <Button onClick={handleCreateKB} disabled={submitting}>
+                {submitting ? t("agent.knowledge.submitting.creating") : t("agent.common.create")}
+              </Button>
+            </div>
+          </div>
+        </DialogContent>
+      </Dialog>
 
-        {/* 对话框 */}
-        {/* 创建知识库对话框 */}
-        <Dialog open={createKBDialogOpen} onOpenChange={setCreateKBDialogOpen}>
-          <DialogContent className="max-w-2xl">
-            <DialogHeader>
-              <DialogTitle>创建知识库</DialogTitle>
-              <DialogDescription>填写知识库名称和描述</DialogDescription>
-            </DialogHeader>
+      <Dialog open={editKBDialogOpen} onOpenChange={setEditKBDialogOpen}>
+        <DialogContent className="max-h-[90dvh] max-w-[min(100vw-2rem,42rem)] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>{t("agent.knowledge.dialog.kbEditTitle")}</DialogTitle>
+            <DialogDescription>{t("agent.knowledge.dialog.kbEditDesc")}</DialogDescription>
+          </DialogHeader>
+          {selectedKnowledgeBase && (
             <div className="space-y-4">
               <div>
-                <Label htmlFor="create-kb-name">名称 *</Label>
+                <Label htmlFor="edit-kb-name">{t("agent.knowledge.field.name")} *</Label>
                 <Input
-                  id="create-kb-name"
-                  value={createKBForm.name}
-                  onChange={(e) =>
-                    setCreateKBForm({ ...createKBForm, name: e.target.value })
-                  }
-                  placeholder="请输入知识库名称"
+                  id="edit-kb-name"
+                  value={editKBForm.name || selectedKnowledgeBase.name}
+                  onChange={(e) => setEditKBForm({ ...editKBForm, name: e.target.value })}
+                  placeholder={t("agent.knowledge.ph.kbName")}
                 />
               </div>
               <div>
-                <Label htmlFor="create-kb-desc">描述(可选)</Label>
+                <Label htmlFor="edit-kb-desc">{t("agent.knowledge.field.descOptional")}</Label>
                 <Textarea
-                  id="create-kb-desc"
-                  value={createKBForm.description || ""}
+                  id="edit-kb-desc"
+                  value={editKBForm.description ?? selectedKnowledgeBase.description ?? ""}
                   onChange={(e) =>
-                    setCreateKBForm({ ...createKBForm, description: e.target.value })
+                    setEditKBForm({ ...editKBForm, description: e.target.value })
                   }
-                  placeholder="请输入知识库描述"
+                  placeholder={t("agent.knowledge.ph.kbDesc")}
                   rows={3}
                 />
               </div>
               <div className="flex justify-end gap-2">
                 <Button
                   variant="outline"
-                  onClick={() => setCreateKBDialogOpen(false)}
+                  onClick={() => setEditKBDialogOpen(false)}
                   disabled={submitting}
                 >
-                  取消
+                  {t("agent.common.cancel")}
                 </Button>
-                <Button onClick={handleCreateKB} disabled={submitting}>
-                  {submitting ? "创建中..." : "创建"}
+                <Button onClick={handleUpdateKB} disabled={submitting}>
+                  {submitting ? t("agent.knowledge.submitting.updating") : t("agent.common.update")}
                 </Button>
               </div>
             </div>
-          </DialogContent>
-        </Dialog>
-
-        {/* 编辑知识库对话框 */}
-        <Dialog open={editKBDialogOpen} onOpenChange={setEditKBDialogOpen}>
-          <DialogContent className="max-w-2xl">
-            <DialogHeader>
-              <DialogTitle>编辑知识库</DialogTitle>
-              <DialogDescription>修改知识库名称和描述</DialogDescription>
-            </DialogHeader>
-            {selectedKnowledgeBase && (
-              <div className="space-y-4">
-                <div>
-                  <Label htmlFor="edit-kb-name">名称 *</Label>
-                  <Input
-                    id="edit-kb-name"
-                    value={editKBForm.name || selectedKnowledgeBase.name}
-                    onChange={(e) =>
-                      setEditKBForm({ ...editKBForm, name: e.target.value })
-                    }
-                    placeholder="请输入知识库名称"
-                  />
-                </div>
-                <div>
-                  <Label htmlFor="edit-kb-desc">描述(可选)</Label>
-                  <Textarea
-                    id="edit-kb-desc"
-                    value={editKBForm.description ?? selectedKnowledgeBase.description ?? ""}
-                    onChange={(e) =>
-                      setEditKBForm({ ...editKBForm, description: e.target.value })
-                    }
-                    placeholder="请输入知识库描述"
-                    rows={3}
-                  />
-                </div>
-                <div className="flex justify-end gap-2">
-                  <Button
-                    variant="outline"
-                    onClick={() => setEditKBDialogOpen(false)}
-                    disabled={submitting}
-                  >
-                    取消
-                  </Button>
-                  <Button onClick={handleUpdateKB} disabled={submitting}>
-                    {submitting ? "更新中..." : "更新"}
-                  </Button>
-                </div>
-              </div>
-            )}
-          </DialogContent>
-        </Dialog>
-
-        {/* 删除知识库对话框 */}
-        <Dialog open={deleteKBDialogOpen} onOpenChange={setDeleteKBDialogOpen}>
-          <DialogContent>
-            <DialogHeader>
-              <DialogTitle>删除知识库</DialogTitle>
-            </DialogHeader>
-            {selectedKnowledgeBase && (
-              <div className="space-y-4">
-                <p className="text-foreground">
-                  确定要删除知识库 <strong>&quot;{selectedKnowledgeBase.name}&quot;</strong> 吗?
-                </p>
-                <p className="text-sm text-muted-foreground">
-                  此操作将同时删除该知识库下的所有文档,此操作不可恢复,请谨慎操作。
-                </p>
-                <div className="flex justify-end gap-2">
-                  <Button
-                    variant="outline"
-                    onClick={() => setDeleteKBDialogOpen(false)}
-                    disabled={submitting}
-                  >
-                    取消
-                  </Button>
-                  <Button
-                    variant="destructive"
-                    onClick={handleDeleteKB}
-                    disabled={submitting}
-                  >
-                    {submitting ? "删除中..." : "删除"}
-                  </Button>
-                </div>
-              </div>
-            )}
-          </DialogContent>
-        </Dialog>
+          )}
+        </DialogContent>
+      </Dialog>
 
-        {/* 创建文档对话框 */}
-        <Dialog open={createDocDialogOpen} onOpenChange={setCreateDocDialogOpen}>
-          <DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
-            <DialogHeader>
-              <DialogTitle>创建文档</DialogTitle>
-              <DialogDescription>填写文档标题和内容</DialogDescription>
-            </DialogHeader>
+      <Dialog open={deleteKBDialogOpen} onOpenChange={setDeleteKBDialogOpen}>
+        <DialogContent className="max-w-[min(100vw-2rem,28rem)]">
+          <DialogHeader>
+            <DialogTitle>{t("agent.knowledge.dialog.kbDeleteTitle")}</DialogTitle>
+          </DialogHeader>
+          {selectedKnowledgeBase && (
             <div className="space-y-4">
-              <div>
-                <Label htmlFor="create-doc-title">标题 *</Label>
-                <Input
-                  id="create-doc-title"
-                  value={createDocForm.title}
-                  onChange={(e) =>
-                    setCreateDocForm({ ...createDocForm, title: e.target.value })
-                  }
-                  placeholder="请输入文档标题"
-                />
-              </div>
-              <div>
-                <Label htmlFor="create-doc-summary">摘要(可选)</Label>
-                <Textarea
-                  id="create-doc-summary"
-                  value={createDocForm.summary || ""}
-                  onChange={(e) =>
-                    setCreateDocForm({ ...createDocForm, summary: e.target.value })
-                  }
-                  placeholder="请输入文档摘要"
-                  rows={2}
-                />
-              </div>
-              <div>
-                <Label htmlFor="create-doc-content">内容 *</Label>
-                <Textarea
-                  id="create-doc-content"
-                  value={createDocForm.content}
-                  onChange={(e) =>
-                    setCreateDocForm({ ...createDocForm, content: e.target.value })
-                  }
-                  placeholder="请输入文档内容"
-                  rows={10}
-                  className="resize-none"
-                />
-              </div>
+              <p className="text-foreground">
+                {tr("agent.knowledge.dialog.kbDeleteConfirm", { name: selectedKnowledgeBase.name })}
+              </p>
+              <p className="text-sm text-muted-foreground">
+                {t("agent.knowledge.dialog.kbDeleteHint")}
+              </p>
               <div className="flex justify-end gap-2">
                 <Button
                   variant="outline"
-                  onClick={() => setCreateDocDialogOpen(false)}
+                  onClick={() => setDeleteKBDialogOpen(false)}
                   disabled={submitting}
                 >
-                  取消
+                  {t("agent.common.cancel")}
                 </Button>
-                <Button onClick={handleCreateDoc} disabled={submitting}>
-                  {submitting ? "创建中..." : "创建"}
+                <Button variant="destructive" onClick={handleDeleteKB} disabled={submitting}>
+                  {submitting ? t("agent.knowledge.submitting.deleting") : t("agent.common.delete")}
                 </Button>
               </div>
             </div>
-          </DialogContent>
-        </Dialog>
+          )}
+        </DialogContent>
+      </Dialog>
+
+      <Dialog open={createDocDialogOpen} onOpenChange={setCreateDocDialogOpen}>
+        <DialogContent className="max-h-[90dvh] max-w-[min(100vw-2rem,48rem)] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>{t("agent.knowledge.dialog.docCreateTitle")}</DialogTitle>
+            <DialogDescription>{t("agent.knowledge.dialog.docCreateDesc")}</DialogDescription>
+          </DialogHeader>
+          <div className="space-y-4">
+            <div>
+              <Label htmlFor="create-doc-title">{t("agent.knowledge.field.title")} *</Label>
+              <Input
+                id="create-doc-title"
+                value={createDocForm.title}
+                onChange={(e) => setCreateDocForm({ ...createDocForm, title: e.target.value })}
+                placeholder={t("agent.knowledge.ph.docTitle")}
+              />
+            </div>
+            <div>
+              <Label htmlFor="create-doc-summary">{t("agent.knowledge.field.summaryOptional")}</Label>
+              <Textarea
+                id="create-doc-summary"
+                value={createDocForm.summary || ""}
+                onChange={(e) => setCreateDocForm({ ...createDocForm, summary: e.target.value })}
+                placeholder={t("agent.knowledge.ph.docSummary")}
+                rows={2}
+              />
+            </div>
+            <div>
+              <Label htmlFor="create-doc-content">{t("agent.knowledge.field.content")} *</Label>
+              <Textarea
+                id="create-doc-content"
+                value={createDocForm.content}
+                onChange={(e) =>
+                  setCreateDocForm({ ...createDocForm, content: e.target.value })
+                }
+                placeholder={t("agent.knowledge.ph.docContent")}
+                rows={10}
+                className="resize-none"
+              />
+            </div>
+            <div className="flex justify-end gap-2">
+              <Button
+                variant="outline"
+                onClick={() => setCreateDocDialogOpen(false)}
+                disabled={submitting}
+              >
+                {t("agent.common.cancel")}
+              </Button>
+              <Button onClick={handleCreateDoc} disabled={submitting}>
+                {submitting ? t("agent.knowledge.submitting.creating") : t("agent.common.create")}
+              </Button>
+            </div>
+          </div>
+        </DialogContent>
+      </Dialog>
+
+      <Dialog open={editDocDialogOpen} onOpenChange={setEditDocDialogOpen}>
+        <DialogContent className="max-h-[90dvh] max-w-[min(100vw-2rem,48rem)] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>{t("agent.knowledge.dialog.docEditTitle")}</DialogTitle>
+            <DialogDescription>{t("agent.knowledge.dialog.docEditDesc")}</DialogDescription>
+          </DialogHeader>
+          <div className="space-y-4">
+            <div>
+              <Label htmlFor="edit-doc-title">{t("agent.knowledge.field.title")} *</Label>
+              <Input
+                id="edit-doc-title"
+                value={editDocForm.title || ""}
+                onChange={(e) => setEditDocForm({ ...editDocForm, title: e.target.value })}
+                placeholder={t("agent.knowledge.ph.docTitle")}
+              />
+            </div>
+            <div>
+              <Label htmlFor="edit-doc-summary">{t("agent.knowledge.field.summaryOptional")}</Label>
+              <Textarea
+                id="edit-doc-summary"
+                value={editDocForm.summary || ""}
+                onChange={(e) => setEditDocForm({ ...editDocForm, summary: e.target.value })}
+                placeholder={t("agent.knowledge.ph.docSummary")}
+                rows={2}
+              />
+            </div>
+            <div>
+              <Label htmlFor="edit-doc-content">{t("agent.knowledge.field.content")} *</Label>
+              <Textarea
+                id="edit-doc-content"
+                value={editDocForm.content || ""}
+                onChange={(e) => setEditDocForm({ ...editDocForm, content: e.target.value })}
+                placeholder={t("agent.knowledge.ph.docContent")}
+                rows={10}
+                className="resize-none"
+              />
+            </div>
+            <div className="flex justify-end gap-2">
+              <Button
+                variant="outline"
+                onClick={() => setEditDocDialogOpen(false)}
+                disabled={submitting}
+              >
+                {t("agent.common.cancel")}
+              </Button>
+              <Button
+                onClick={() => {
+                  if (selectedDocument) {
+                    handleUpdateDoc(selectedDocument.id);
+                  }
+                }}
+                disabled={submitting}
+              >
+                {submitting ? t("agent.knowledge.submitting.updating") : t("agent.common.update")}
+              </Button>
+            </div>
+          </div>
+        </DialogContent>
+      </Dialog>
 
-        {/* 编辑文档对话框 */}
-        <Dialog open={editDocDialogOpen} onOpenChange={setEditDocDialogOpen}>
-          <DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
-            <DialogHeader>
-              <DialogTitle>编辑文档</DialogTitle>
-              <DialogDescription>修改文档标题和内容</DialogDescription>
-            </DialogHeader>
+      <Dialog open={deleteDocDialogOpen} onOpenChange={setDeleteDocDialogOpen}>
+        <DialogContent className="max-w-[min(100vw-2rem,28rem)]">
+          <DialogHeader>
+            <DialogTitle>{t("agent.knowledge.dialog.docDeleteTitle")}</DialogTitle>
+          </DialogHeader>
+          {selectedDocument && (
             <div className="space-y-4">
+              <p className="text-foreground">
+                {tr("agent.knowledge.dialog.docDeleteConfirm", { title: selectedDocument.title })}
+              </p>
+              <p className="text-sm text-muted-foreground">{t("common.irreversibleHint")}</p>
+            </div>
+          )}
+          <div className="flex justify-end gap-2">
+            <Button
+              variant="outline"
+              onClick={() => setDeleteDocDialogOpen(false)}
+              disabled={submitting}
+            >
+              {t("agent.common.cancel")}
+            </Button>
+            <Button
+              variant="destructive"
+              onClick={() => {
+                if (selectedDocument) {
+                  handleDeleteDoc(selectedDocument.id);
+                }
+              }}
+              disabled={submitting}
+            >
+              {submitting ? t("agent.knowledge.submitting.deleting") : t("agent.common.delete")}
+            </Button>
+          </div>
+        </DialogContent>
+      </Dialog>
+
+      <Dialog
+        open={importDialogOpen}
+        onOpenChange={(open) => {
+          setImportDialogOpen(open);
+          if (!open) {
+            setImportFiles([]);
+            setImportUrls("");
+            setImportTab("file");
+          }
+        }}
+      >
+        <DialogContent className="max-h-[90dvh] max-w-[min(100vw-2rem,42rem)] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>{t("agent.knowledge.dialog.importTitle")}</DialogTitle>
+            <DialogDescription>{t("agent.knowledge.dialog.importDesc")}</DialogDescription>
+          </DialogHeader>
+          <Tabs
+            value={importTab}
+            onValueChange={(v) => setImportTab(v as "file" | "url")}
+            defaultValue="file"
+          >
+            <TabsList className="grid w-full grid-cols-2">
+              <TabsTrigger value="file">{t("agent.knowledge.import.tabFile")}</TabsTrigger>
+              <TabsTrigger value="url">{t("agent.knowledge.import.tabUrl")}</TabsTrigger>
+            </TabsList>
+            <TabsContent value="file" className="space-y-4 mt-4">
               <div>
-                <Label htmlFor="edit-doc-title">标题 *</Label>
+                <Label htmlFor="import-files">{t("agent.knowledge.import.pickFiles")}</Label>
                 <Input
-                  id="edit-doc-title"
-                  value={editDocForm.title || ""}
-                  onChange={(e) =>
-                    setEditDocForm({ ...editDocForm, title: e.target.value })
-                  }
-                  placeholder="请输入文档标题"
-                />
-              </div>
-              <div>
-                <Label htmlFor="edit-doc-summary">摘要(可选)</Label>
-                <Textarea
-                  id="edit-doc-summary"
-                  value={editDocForm.summary || ""}
-                  onChange={(e) =>
-                    setEditDocForm({ ...editDocForm, summary: e.target.value })
-                  }
-                  placeholder="请输入文档摘要"
-                  rows={2}
-                />
-              </div>
-              <div>
-                <Label htmlFor="edit-doc-content">内容 *</Label>
-                <Textarea
-                  id="edit-doc-content"
-                  value={editDocForm.content || ""}
-                  onChange={(e) =>
-                    setEditDocForm({ ...editDocForm, content: e.target.value })
-                  }
-                  placeholder="请输入文档内容"
-                  rows={10}
-                  className="resize-none"
+                  id="import-files"
+                  type="file"
+                  multiple
+                  onChange={(e) => {
+                    const files = Array.from(e.target.files || []);
+                    setImportFiles(files);
+                  }}
                 />
+                {importFiles.length > 0 && (
+                  <p className="text-sm text-muted-foreground mt-2">
+                    {tr("agent.knowledge.import.filesSelected", {
+                      count: String(importFiles.length),
+                    })}
+                  </p>
+                )}
               </div>
               <div className="flex justify-end gap-2">
                 <Button
                   variant="outline"
-                  onClick={() => setEditDocDialogOpen(false)}
+                  onClick={() => setImportDialogOpen(false)}
                   disabled={submitting}
                 >
-                  取消
+                  {t("agent.common.cancel")}
                 </Button>
-                <Button
-                  onClick={() => {
-                    if (selectedDocument) {
-                      handleUpdateDoc(selectedDocument.id);
-                    }
-                  }}
-                  disabled={submitting}
-                >
-                  {submitting ? "更新中..." : "更新"}
+                <Button onClick={handleImportFiles} disabled={submitting}>
+                  {submitting ? t("agent.knowledge.submitting.importing") : t("agent.knowledge.import.action")}
                 </Button>
               </div>
-            </div>
-          </DialogContent>
-        </Dialog>
-
-        {/* 删除文档对话框 */}
-        <Dialog open={deleteDocDialogOpen} onOpenChange={setDeleteDocDialogOpen}>
-          <DialogContent>
-            <DialogHeader>
-              <DialogTitle>删除文档</DialogTitle>
-            </DialogHeader>
-            {selectedDocument && (
-              <div className="space-y-4">
-                <p className="text-foreground">
-                  确定要删除文档 <strong>&quot;{selectedDocument.title}&quot;</strong> 吗?
-                </p>
-                <p className="text-sm text-muted-foreground">
-                  此操作不可恢复,请谨慎操作。
-                </p>
+            </TabsContent>
+            <TabsContent value="url" className="space-y-4 mt-4">
+              <div>
+                <Label htmlFor="import-urls">{t("agent.knowledge.import.urlListLabel")}</Label>
+                <Textarea
+                  id="import-urls"
+                  value={importUrls}
+                  onChange={(e) => setImportUrls(e.target.value)}
+                  placeholder="https://example.com/page1&#10;https://example.com/page2"
+                  rows={8}
+                  className="resize-none"
+                />
               </div>
-            )}
               <div className="flex justify-end gap-2">
                 <Button
                   variant="outline"
-                  onClick={() => setDeleteDocDialogOpen(false)}
+                  onClick={() => setImportDialogOpen(false)}
                   disabled={submitting}
                 >
-                  取消
+                  {t("agent.common.cancel")}
                 </Button>
-                <Button
-                  variant="destructive"
-                  onClick={() => {
-                    if (selectedDocument) {
-                      handleDeleteDoc(selectedDocument.id);
-                    }
-                  }}
-                  disabled={submitting}
-                >
-                  {submitting ? "删除中..." : "删除"}
+                <Button onClick={handleImportUrls} disabled={submitting}>
+                  {submitting ? t("agent.knowledge.submitting.importing") : t("agent.knowledge.import.action")}
                 </Button>
               </div>
-          </DialogContent>
-        </Dialog>
+            </TabsContent>
+          </Tabs>
+        </DialogContent>
+      </Dialog>
+    </>
+  );
 
-        {/* 导入文档对话框(文件上传 + URL 导入) */}
-        <Dialog
-          open={importDialogOpen}
-          onOpenChange={(open) => {
-            setImportDialogOpen(open);
-            if (!open) {
-              setImportFiles([]);
-              setImportUrls("");
-              setImportTab("file");
-            }
-          }}
-        >
-          <DialogContent className="max-w-2xl">
-            <DialogHeader>
-              <DialogTitle>导入文档</DialogTitle>
-              <DialogDescription>
-                选择文件上传或输入 URL 批量导入。当前支持的文件格式:<strong>Markdown(.md、.markdown)</strong>;PDF、Word 解析功能开发中。
-              </DialogDescription>
-            </DialogHeader>
-            <Tabs
-              value={importTab}
-              onValueChange={(v) => setImportTab(v as "file" | "url")}
-              defaultValue="file"
-            >
-              <TabsList className="grid w-full grid-cols-2">
-                <TabsTrigger value="file">文件上传</TabsTrigger>
-                <TabsTrigger value="url">URL 导入</TabsTrigger>
-              </TabsList>
-              <TabsContent value="file" className="space-y-4 mt-4">
-                <div>
-                  <Label htmlFor="import-files">选择文件</Label>
-                  <Input
-                    id="import-files"
-                    type="file"
-                    multiple
-                    onChange={(e) => {
-                      const files = Array.from(e.target.files || []);
-                      setImportFiles(files);
-                    }}
-                  />
-                  {importFiles.length > 0 && (
-                    <p className="text-sm text-muted-foreground mt-2">
-                      已选择 {importFiles.length} 个文件
-                    </p>
-                  )}
-                </div>
-                <div className="flex justify-end gap-2">
-                  <Button
-                    variant="outline"
-                    onClick={() => setImportDialogOpen(false)}
-                    disabled={submitting}
-                  >
-                    取消
-                  </Button>
-                  <Button onClick={handleImportFiles} disabled={submitting}>
-                    {submitting ? "导入中..." : "导入"}
-                  </Button>
-                </div>
-              </TabsContent>
-              <TabsContent value="url" className="space-y-4 mt-4">
-                <div>
-                  <Label htmlFor="import-urls">URL 列表(每行一个)</Label>
-                  <Textarea
-                    id="import-urls"
-                    value={importUrls}
-                    onChange={(e) => setImportUrls(e.target.value)}
-                    placeholder="https://example.com/page1&#10;https://example.com/page2"
-                    rows={8}
-                    className="resize-none"
-                  />
-                </div>
-                <div className="flex justify-end gap-2">
-                  <Button
-                    variant="outline"
-                    onClick={() => setImportDialogOpen(false)}
-                    disabled={submitting}
-                  >
-                    取消
-                  </Button>
-                  <Button onClick={handleImportUrls} disabled={submitting}>
-                    {submitting ? "导入中..." : "导入"}
-                  </Button>
-                </div>
-              </TabsContent>
-            </Tabs>
-          </DialogContent>
-        </Dialog>
+  if (embedded) {
+    return (
+      <>
+        <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
+          {headerContent}
+          {mainContent}
+        </div>
+        {dialogs}
       </>
     );
   }
 
   return (
-    <ResponsiveLayout
-      main={mainContent}
-      header={headerContent}
-    />
+    <>
+      <ResponsiveLayout main={mainContent} header={headerContent} />
+      {dialogs}
+    </>
   );
 }

+ 11 - 9
frontend/app/agent/login/page.tsx

@@ -5,8 +5,10 @@ import { apiUrl } from "@/lib/config";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { setAgentWSToken } from "@/utils/storage";
+import { useI18n } from "@/lib/i18n/provider";
 
 export default function AgentLoginPage() {
+  const { t } = useI18n();
   const [username, setUsername] = useState("");
   const [password, setPassword] = useState("");
   const [loading, setLoading] = useState(false);
@@ -18,7 +20,7 @@ export default function AgentLoginPage() {
     e.preventDefault(); // 阻止默认行为
 
     if (!username || !password) {
-      setError("用户名和密码不能为空");
+      setError(t("agent.login.error.empty"));
       return;
     }
 
@@ -51,11 +53,11 @@ export default function AgentLoginPage() {
         router.push("/agent/dashboard");
       } else {
         // 登录失败,显示错误信息
-        setError(data.error || data.message || "登录失败");
+        setError(data.error || data.message || t("agent.login.error.failed"));
       }
     } catch (error) {
       console.error("登录失败:", error);
-      setError("登录失败,请检查网络连接");
+      setError(t("agent.login.error.network"));
     } finally {
       setLoading(false);
     }
@@ -65,16 +67,16 @@ export default function AgentLoginPage() {
     <div className="flex justify-center items-center min-h-screen bg-background">
       <div className="bg-card p-8 rounded-lg border shadow-lg w-full sm:w-96">
         <h1 className="text-center text-2xl font-bold mb-2 text-gray-800">
-          客服登录
+          {t("agent.login.title")}
         </h1>
         <p className="text-center text-sm text-gray-500 mb-6">
-          管理员和客服请在此登录
+          {t("agent.login.subtitle")}
         </p>
 
         <form onSubmit={handleLogin}>
           <Input
             type="text"
-            placeholder="用户名"
+            placeholder={t("agent.login.username")}
             value={username}
             onChange={(e) => setUsername(e.target.value)}
             className="w-full mb-4"
@@ -82,7 +84,7 @@ export default function AgentLoginPage() {
           />
           <Input
             type="password"
-            placeholder="密码"
+            placeholder={t("agent.login.password")}
             value={password}
             onChange={(e) => setPassword(e.target.value)}
             className="w-full mb-4"
@@ -102,12 +104,12 @@ export default function AgentLoginPage() {
             size="default"
             className="w-full"
           >
-            {loading ? "登录中..." : "登录"}
+            {loading ? t("agent.login.submitting") : t("agent.login.submit")}
           </Button>
         </form>
 
         <div className="mt-4 text-center text-xs text-gray-400">
-          <p>默认管理员账号:admin / admin123</p>
+          <p>{t("agent.login.demoHint")}</p>
         </div>
       </div>
     </div>

+ 83 - 61
frontend/app/agent/logs/page.tsx

@@ -18,6 +18,8 @@ import {
   DialogTitle,
 } from "@/components/ui/dialog";
 import { Copy } from "lucide-react";
+import type { I18nKey } from "@/lib/i18n/dict";
+import { useI18n } from "@/lib/i18n/provider";
 
 function tryFormatJSON(raw?: string | null): string {
   if (!raw) return "";
@@ -36,6 +38,18 @@ function levelColor(level: string): string {
 }
 
 export default function LogsPage({ embedded = false }: { embedded?: boolean }) {
+  const { t, lang } = useI18n();
+
+  const tr = (key: I18nKey, vars?: Record<string, string>) => {
+    let s = t(key);
+    if (!vars) return s;
+    for (const k of Object.keys(vars)) {
+      s = s.replaceAll(`{{${k}}}`, vars[k] ?? "");
+    }
+    return s;
+  };
+
+  const locale = lang === "en" ? "en-US" : "zh-CN";
   const [from, setFrom] = useState(() => {
     const d = new Date();
     d.setDate(d.getDate() - 6);
@@ -66,12 +80,12 @@ export default function LogsPage({ embedded = false }: { embedded?: boolean }) {
       setPolicy(p);
       setPolicyDraft(p.effective_min_level);
     } catch (e) {
-      toast.error((e as Error).message || "加载落库策略失败");
+      toast.error((e as Error).message || t("agent.logs.toast.loadPolicyFailed"));
       setPolicy(null);
     } finally {
       setPolicyLoading(false);
     }
-  }, []);
+  }, [t]);
 
   useEffect(() => {
     void loadPolicy();
@@ -95,12 +109,12 @@ export default function LogsPage({ embedded = false }: { embedded?: boolean }) {
       });
       setData(res);
     } catch (e) {
-      toast.error((e as Error).message || "加载日志失败");
+      toast.error((e as Error).message || t("agent.logs.toast.loadLogsFailed"));
       setData(null);
     } finally {
       setLoading(false);
     }
-  }, [from, to, level, category, source, event, keyword, conversationId, page]);
+  }, [from, to, level, category, source, event, keyword, conversationId, page, t]);
 
   useEffect(() => {
     void load();
@@ -112,28 +126,26 @@ export default function LogsPage({ embedded = false }: { embedded?: boolean }) {
   }, [data]);
 
   return (
-    <div className={`flex flex-col min-h-0 overflow-auto ${embedded ? "p-4" : "p-6 max-w-6xl mx-auto w-full"}`}>
+    <div
+      className={`flex min-h-0 flex-col overflow-auto ${embedded ? "p-3 sm:p-4" : "w-full max-w-6xl p-4 sm:p-6 mx-auto"}`}
+    >
       <div className="mb-4">
-        <h1 className="text-xl font-semibold">日志中心</h1>
-        <p className="text-sm text-muted-foreground mt-1">按分类查看 AI / RAG / 系统 / 前端日志,用于排障定位。</p>
+        <h1 className="text-xl font-semibold">{t("agent.logs.title")}</h1>
+        <p className="text-sm text-muted-foreground mt-1">{t("agent.logs.subtitle")}</p>
       </div>
 
       <div className="rounded-xl border border-border/60 bg-card p-4 mb-4 space-y-3">
         <div className="flex flex-wrap items-start justify-between gap-3">
           <div>
-            <h2 className="text-sm font-semibold">落库级别(性能)</h2>
-            <p className="text-xs text-muted-foreground mt-1 max-w-xl">
-              仅将不低于所选级别的记录写入数据库。设为 <code className="text-foreground">warn</code> 可大幅减少成功类{" "}
-              <code className="text-foreground">info</code> 写入。也可在根目录{" "}
-              <code className="text-foreground">SYSTEM_LOG_MIN_LEVEL</code> 配置默认值;此处保存后会写入数据库并覆盖环境变量,直至点击「恢复环境变量」。
-            </p>
+            <h2 className="text-sm font-semibold">{t("agent.logs.policy.title")}</h2>
+            <p className="text-xs text-muted-foreground mt-1 max-w-xl">{t("agent.logs.policy.desc")}</p>
             {policy ? (
               <p className="text-xs text-muted-foreground mt-2">
-                当前生效:<span className="font-medium text-foreground">{policy.effective_min_level}</span>
+                {t("agent.logs.policy.current")}<span className="font-medium text-foreground">{policy.effective_min_level}</span>
                 {" · "}
-                环境变量默认:<span className="font-medium text-foreground">{policy.env_min_level}</span>
+                {t("agent.logs.policy.env")}<span className="font-medium text-foreground">{policy.env_min_level}</span>
                 {policy.persisted_in_database ? (
-                  <span className="text-amber-700 dark:text-amber-500">(已由控制台覆盖)</span>
+                  <span className="text-amber-700 dark:text-amber-500">{t("agent.logs.policy.overridden")}</span>
                 ) : null}
               </p>
             ) : null}
@@ -149,7 +161,7 @@ export default function LogsPage({ embedded = false }: { embedded?: boolean }) {
               <option value="info">info</option>
               <option value="warn">warn</option>
               <option value="error">error</option>
-              <option value="none">none(关闭落库)</option>
+              <option value="none">none</option>
             </select>
             <Button
               size="sm"
@@ -158,16 +170,16 @@ export default function LogsPage({ embedded = false }: { embedded?: boolean }) {
                 setPolicyLoading(true);
                 try {
                   await putLogMinLevelPolicy(policyDraft);
-                  toast.success("已保存并生效");
+                  toast.success("OK");
                   await loadPolicy();
                 } catch (e) {
-                  toast.error((e as Error).message || "保存失败");
+                  toast.error((e as Error).message || t("agent.logs.toast.savePolicyFailed"));
                 } finally {
                   setPolicyLoading(false);
                 }
               }}
             >
-              保存到服务器
+              {t("common.save")}
             </Button>
             <Button
               size="sm"
@@ -177,33 +189,33 @@ export default function LogsPage({ embedded = false }: { embedded?: boolean }) {
                 setPolicyLoading(true);
                 try {
                   await deleteLogMinLevelPolicy();
-                  toast.success("已恢复为环境变量默认值");
+                  toast.success(t("agent.logs.toast.policyRestored"));
                   await loadPolicy();
                 } catch (e) {
-                  toast.error((e as Error).message || "恢复失败");
+                  toast.error((e as Error).message || t("agent.logs.toast.restorePolicyFailed"));
                 } finally {
                   setPolicyLoading(false);
                 }
               }}
             >
-              恢复环境变量
+              {t("common.restoreEnv")}
             </Button>
           </div>
         </div>
       </div>
 
-      <div className="rounded-xl border border-border/60 bg-card p-3 mb-4 flex flex-wrap gap-2 items-center">
+      <div className="mb-4 flex flex-col gap-2 rounded-xl border border-border/60 bg-card p-3 sm:flex-row sm:flex-wrap sm:items-center">
         <input type="date" value={from} onChange={(e) => setFrom(e.target.value)} className="rounded-md border px-2 py-1 text-sm" />
-        <span className="text-xs text-muted-foreground"></span>
+        <span className="text-xs text-muted-foreground">{t("common.to")}</span>
         <input type="date" value={to} onChange={(e) => setTo(e.target.value)} className="rounded-md border px-2 py-1 text-sm" />
         <select value={level} onChange={(e) => setLevel(e.target.value)} className="rounded-md border px-2 py-1 text-sm">
-          <option value="">全部级别</option>
+          <option value="">{t("agent.logs.level.all")}</option>
           <option value="info">info</option>
           <option value="warn">warn</option>
           <option value="error">error</option>
         </select>
         <select value={category} onChange={(e) => setCategory(e.target.value)} className="rounded-md border px-2 py-1 text-sm">
-          <option value="">全部分类</option>
+          <option value="">{t("agent.logs.category.all")}</option>
           <option value="ai">ai</option>
           <option value="rag">rag</option>
           <option value="frontend">frontend</option>
@@ -213,48 +225,52 @@ export default function LogsPage({ embedded = false }: { embedded?: boolean }) {
           <option value="vector">vector</option>
         </select>
         <select value={source} onChange={(e) => setSource(e.target.value)} className="rounded-md border px-2 py-1 text-sm">
-          <option value="">全部来源</option>
+          <option value="">{t("agent.logs.source.all")}</option>
           <option value="backend">backend</option>
           <option value="frontend">frontend</option>
         </select>
         <input
-          placeholder="事件名(event)"
+          placeholder={t("agent.logs.event.placeholder")}
           value={event}
           onChange={(e) => setEvent(e.target.value)}
           className="rounded-md border px-2 py-1 text-sm min-w-[180px]"
         />
         <input
-          placeholder="会话ID"
+          placeholder={t("agent.logs.conversationId.placeholder")}
           value={conversationId}
           onChange={(e) => setConversationId(e.target.value)}
           className="rounded-md border px-2 py-1 text-sm w-24"
         />
         <input
-          placeholder="关键词(message/meta)"
+          placeholder={t("agent.logs.keyword.placeholder")}
           value={keyword}
           onChange={(e) => setKeyword(e.target.value)}
           className="rounded-md border px-2 py-1 text-sm min-w-[220px]"
         />
         <Button size="sm" disabled={loading} onClick={() => { setPage(1); void load(); }}>
-          {loading ? "加载中..." : "查询"}
+          {loading ? t("common.loading") : t("common.search")}
         </Button>
       </div>
 
       <div className="rounded-xl border border-border/60 bg-card overflow-hidden">
-        <div className="px-3 py-2 border-b text-xs text-muted-foreground">
-          共 {data?.total ?? 0} 条,当前第 {data?.page ?? page}/{totalPages} 页
+        <div className="border-b px-3 py-2 text-xs text-muted-foreground">
+          {tr("agent.logs.paginationSummary", {
+            total: String(data?.total ?? 0),
+            page: String(data?.page ?? page),
+            pages: String(totalPages),
+          })}
         </div>
-        <div className="overflow-auto">
-          <table className="w-full text-sm">
+        <div className="overflow-x-auto">
+          <table className="w-full min-w-[720px] text-sm">
             <thead className="bg-muted/40 text-xs text-muted-foreground">
               <tr>
-                <th className="text-left px-3 py-2">时间</th>
-                <th className="text-left px-3 py-2">级别</th>
-                <th className="text-left px-3 py-2">分类</th>
-                <th className="text-left px-3 py-2">事件</th>
-                <th className="text-left px-3 py-2">会话</th>
-                <th className="text-left px-3 py-2">来源</th>
-                <th className="text-left px-3 py-2">消息</th>
+                <th className="text-left px-3 py-2">{t("agent.logs.table.time")}</th>
+                <th className="text-left px-3 py-2">{t("agent.logs.table.level")}</th>
+                <th className="text-left px-3 py-2">{t("agent.logs.table.category")}</th>
+                <th className="text-left px-3 py-2">{t("agent.logs.table.event")}</th>
+                <th className="text-left px-3 py-2">{t("agent.logs.table.conversation")}</th>
+                <th className="text-left px-3 py-2">{t("agent.logs.table.source")}</th>
+                <th className="text-left px-3 py-2">{t("agent.logs.table.message")}</th>
               </tr>
             </thead>
             <tbody>
@@ -264,7 +280,9 @@ export default function LogsPage({ embedded = false }: { embedded?: boolean }) {
                   className="border-t cursor-pointer hover:bg-muted/30"
                   onClick={() => setSelected(item)}
                 >
-                  <td className="px-3 py-2 whitespace-nowrap text-xs">{new Date(item.timestamp).toLocaleString()}</td>
+                  <td className="px-3 py-2 whitespace-nowrap text-xs">
+                    {new Date(item.timestamp).toLocaleString(locale)}
+                  </td>
                   <td className={`px-3 py-2 font-medium ${levelColor(item.level)}`}>{item.level}</td>
                   <td className="px-3 py-2">{item.category}</td>
                   <td className="px-3 py-2">{item.event}</td>
@@ -275,7 +293,9 @@ export default function LogsPage({ embedded = false }: { embedded?: boolean }) {
               ))}
               {(data?.items ?? []).length === 0 && !loading && (
                 <tr>
-                  <td className="px-3 py-8 text-center text-muted-foreground" colSpan={7}>暂无日志</td>
+                  <td className="px-3 py-8 text-center text-muted-foreground" colSpan={7}>
+                    {t("agent.logs.empty")}
+                  </td>
                 </tr>
               )}
             </tbody>
@@ -288,7 +308,7 @@ export default function LogsPage({ embedded = false }: { embedded?: boolean }) {
             disabled={loading || page <= 1}
             onClick={() => setPage((p) => Math.max(1, p - 1))}
           >
-            上一页
+            {t("common.prevPage")}
           </Button>
           <Button
             variant="outline"
@@ -296,7 +316,7 @@ export default function LogsPage({ embedded = false }: { embedded?: boolean }) {
             disabled={loading || page >= totalPages}
             onClick={() => setPage((p) => p + 1)}
           >
-            下一页
+            {t("common.nextPage")}
           </Button>
         </div>
       </div>
@@ -310,7 +330,7 @@ export default function LogsPage({ embedded = false }: { embedded?: boolean }) {
         <DialogContent className="max-w-4xl">
           <DialogHeader>
             <DialogTitle className="flex items-center gap-2">
-              <span>日志详情</span>
+              <span>{t("agent.logs.detail.title")}</span>
               {selected ? (
                 <span className={`text-xs px-2 py-0.5 rounded border ${selected.level === "error" ? "border-red-200 text-red-700" : selected.level === "warn" ? "border-amber-200 text-amber-700" : "border-emerald-200 text-emerald-700"}`}>
                   {selected.level}
@@ -323,29 +343,31 @@ export default function LogsPage({ embedded = false }: { embedded?: boolean }) {
             <div className="space-y-3">
               <div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
                 <div className="rounded-lg border p-2">
-                  <div className="text-xs text-muted-foreground">时间</div>
-                  <div className="font-medium">{new Date(selected.timestamp).toLocaleString()}</div>
+                  <div className="text-xs text-muted-foreground">{t("agent.logs.detail.time")}</div>
+                  <div className="font-medium">
+                    {new Date(selected.timestamp).toLocaleString(locale)}
+                  </div>
                 </div>
                 <div className="rounded-lg border p-2">
-                  <div className="text-xs text-muted-foreground">source / event</div>
+                  <div className="text-xs text-muted-foreground">{t("agent.logs.detail.sourceEvent")}</div>
                   <div className="font-medium">
                     {selected.source} / {selected.event}
                   </div>
                 </div>
                 <div className="rounded-lg border p-2">
-                  <div className="text-xs text-muted-foreground">category</div>
+                  <div className="text-xs text-muted-foreground">{t("agent.logs.detail.category")}</div>
                   <div className="font-medium">{selected.category}</div>
                 </div>
                 <div className="rounded-lg border p-2">
-                  <div className="text-xs text-muted-foreground">trace_id</div>
+                  <div className="text-xs text-muted-foreground">{t("agent.logs.detail.traceId")}</div>
                   <div className="font-medium break-all">{selected.trace_id || "-"}</div>
                 </div>
                 <div className="rounded-lg border p-2">
-                  <div className="text-xs text-muted-foreground">conversation_id</div>
+                  <div className="text-xs text-muted-foreground">{t("agent.logs.detail.conversationId")}</div>
                   <div className="font-medium">{selected.conversation_id ?? "-"}</div>
                 </div>
                 <div className="rounded-lg border p-2">
-                  <div className="text-xs text-muted-foreground">user_id / visitor_id</div>
+                  <div className="text-xs text-muted-foreground">{t("agent.logs.detail.userVisitor")}</div>
                   <div className="font-medium">
                     {selected.user_id ?? "-"} / {selected.visitor_id ?? "-"}
                   </div>
@@ -354,30 +376,30 @@ export default function LogsPage({ embedded = false }: { embedded?: boolean }) {
 
               <div className="rounded-lg border p-3">
                 <div className="flex items-center justify-between gap-2 mb-2">
-                  <div className="text-sm font-medium">message</div>
+                  <div className="text-sm font-medium">{t("agent.logs.detail.message")}</div>
                   <Button
                     size="sm"
                     variant="outline"
                     onClick={async () => {
                       try {
                         await navigator.clipboard.writeText(selected.message);
-                        toast.success("已复制 message");
+                        toast.success(t("agent.logs.toast.messageCopied"));
                       } catch {
-                        toast.error("复制失败");
+                        toast.error(t("agent.logs.toast.copyFailed"));
                       }
                     }}
                   >
                     <Copy className="h-4 w-4 mr-1" />
-                    复制
+                    {t("common.copy")}
                   </Button>
                 </div>
                 <pre className="whitespace-pre-wrap text-sm bg-muted/30 rounded p-2 max-h-48 overflow-auto">{selected.message}</pre>
               </div>
 
               <div className="rounded-lg border p-3">
-                <div className="text-sm font-medium mb-2">meta_json</div>
+                <div className="text-sm font-medium mb-2">{t("agent.logs.detail.metaJson")}</div>
                 <pre className="whitespace-pre-wrap text-xs bg-muted/30 rounded p-2 max-h-80 overflow-auto">
-                  {selectedMeta || "(无 meta_json)"}
+                  {selectedMeta || t("agent.logs.detail.noMeta")}
                 </pre>
               </div>
             </div>

+ 66 - 74
frontend/app/agent/prompts/page.tsx

@@ -7,43 +7,26 @@ import { Button } from "@/components/ui/button";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { fetchPrompts, updatePrompt, type PromptItem } from "@/features/agent/services/promptsApi";
 import { toast } from "@/hooks/useToast";
+import type { I18nKey } from "@/lib/i18n/dict";
+import { useI18n } from "@/lib/i18n/provider";
 
-function getPlaceholderHint(key: string): string {
-  switch (key) {
-    case "rag_prompt":
-    case "rag_prompt_with_web_optional":
-      return "占位符:{{rag_context}} 为知识库检索内容,{{user_message}} 为用户问题。";
-    case "no_kb_prompt":
-      return "占位符:{{user_message}} 为用户问题。";
-    case "web_search_result_prompt":
-      return "占位符:{{web_context}} 为联网搜索结果,{{user_message}} 为用户问题。(当前流程未使用此模板)";
-    case "no_source_reply":
-    case "ai_fail_reply":
-      return "无占位符,内容将作为完整回复语直接展示给用户。";
-    default:
-      return "请勿删除占位符,保存后由系统替换为实际内容。";
-  }
-}
+const PROMPT_HINT_KEYS: Partial<Record<string, I18nKey>> = {
+  rag_prompt: "agent.prompts.hint.rag_prompt",
+  rag_prompt_with_web_optional: "agent.prompts.hint.rag_prompt_with_web_optional",
+  no_kb_prompt: "agent.prompts.hint.no_kb_prompt",
+  web_search_result_prompt: "agent.prompts.hint.web_search_result_prompt",
+  no_source_reply: "agent.prompts.hint.no_source_reply",
+  ai_fail_reply: "agent.prompts.hint.ai_fail_reply",
+};
 
-/** 各提示词的使用场景说明(展示在卡片中) */
-function getUsageScenario(key: string): string {
-  switch (key) {
-    case "rag_prompt":
-      return "有知识库检索结果,且本回合未勾选「联网搜索」时,用此模板拼成 prompt 发给模型。";
-    case "rag_prompt_with_web_optional":
-      return "有知识库检索结果且本回合勾选「联网搜索」时,用此模板并传入联网工具,由模型决定是否调用联网。";
-    case "no_kb_prompt":
-      return "没有知识库检索结果且本回合未走联网时,用此模板让模型仅凭自身知识回答。";
-    case "web_search_result_prompt":
-      return "预留:若将来有「先联网搜再拼成一段 prompt」的流程,会使用此模板。当前未使用。";
-    case "no_source_reply":
-      return "既未命中知识库、也未使用大模型或联网时(如用户关闭了所有数据源),直接向用户展示这句话。";
-    case "ai_fail_reply":
-      return "调用 AI 接口失败(超时、报错等)时,向用户展示这句话。";
-    default:
-      return "";
-  }
-}
+const PROMPT_USAGE_KEYS: Partial<Record<string, I18nKey>> = {
+  rag_prompt: "agent.prompts.usage.rag_prompt",
+  rag_prompt_with_web_optional: "agent.prompts.usage.rag_prompt_with_web_optional",
+  no_kb_prompt: "agent.prompts.usage.no_kb_prompt",
+  web_search_result_prompt: "agent.prompts.usage.web_search_result_prompt",
+  no_source_reply: "agent.prompts.usage.no_source_reply",
+  ai_fail_reply: "agent.prompts.usage.ai_fail_reply",
+};
 
 function getTextareaMinHeight(key: string): string {
   return key === "no_source_reply" || key === "ai_fail_reply" ? "min-h-[80px]" : "min-h-[200px]";
@@ -51,6 +34,7 @@ function getTextareaMinHeight(key: string): string {
 
 export default function PromptsPage({ embedded = false }: { embedded?: boolean }) {
   const router = useRouter();
+  const { t } = useI18n();
   const [userId, setUserId] = useState<number | null>(null);
   const [prompts, setPrompts] = useState<PromptItem[]>([]);
   const [loading, setLoading] = useState(true);
@@ -75,7 +59,7 @@ export default function PromptsPage({ embedded = false }: { embedded?: boolean }
       setPrompts(data);
     } catch (e) {
       console.error("加载提示词失败:", e);
-      setError((e as Error).message || "加载提示词失败");
+      setError((e as Error).message || t("agent.prompts.loadFailed"));
     } finally {
       setLoading(false);
     }
@@ -90,10 +74,10 @@ export default function PromptsPage({ embedded = false }: { embedded?: boolean }
     setSavingKey(key);
     try {
       await updatePrompt(userId, key, content);
-      toast.success("保存成功,将立即生效。");
+      toast.success(t("agent.prompts.saveSuccess"));
       await loadPrompts();
     } catch (e) {
-      toast.error((e as Error).message || "保存失败");
+      toast.error((e as Error).message || t("agent.prompts.saveFailed"));
     } finally {
       setSavingKey(null);
     }
@@ -108,12 +92,12 @@ export default function PromptsPage({ embedded = false }: { embedded?: boolean }
   if (!userId) return null;
 
   const headerContent = (
-    <div className="bg-card border-b p-4 shadow-sm">
+    <div className="border-b bg-card p-3 shadow-sm sm:p-4">
       <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
         <div>
-          <h1 className="text-xl font-bold text-foreground">提示词</h1>
+          <h1 className="text-xl font-bold text-foreground">{t("agent.prompts.title")}</h1>
           <div className="text-sm text-muted-foreground mt-1">
-            配置系统中使用的提示词模板,用于 RAG、联网等场景。仅管理员可修改。占位符说明见下方各卡片。
+            {t("agent.prompts.subtitle")}
           </div>
         </div>
         {!embedded && (
@@ -122,7 +106,7 @@ export default function PromptsPage({ embedded = false }: { embedded?: boolean }
             variant="outline"
             size="sm"
           >
-            返回工作台
+            {t("agent.settings.backDashboard")}
           </Button>
         )}
       </div>
@@ -130,7 +114,7 @@ export default function PromptsPage({ embedded = false }: { embedded?: boolean }
   );
 
   const mainContent = (
-    <div className="flex-1 overflow-auto p-4 md:p-6">
+    <div className="flex-1 overflow-auto p-3 sm:p-4 md:p-6">
       <div className="max-w-4xl mx-auto space-y-6">
         {error && (
           <div className="p-3 bg-red-50 border border-red-200 rounded-md text-red-600 text-sm">
@@ -138,38 +122,46 @@ export default function PromptsPage({ embedded = false }: { embedded?: boolean }
           </div>
         )}
         {loading ? (
-          <div className="text-center py-12 text-muted-foreground">加载中...</div>
+          <div className="text-center py-12 text-muted-foreground">{t("common.loading")}</div>
         ) : (
-          prompts.map((item) => (
-            <Card key={item.key}>
-              <CardHeader>
-                <CardTitle className="text-base">{item.name}</CardTitle>
-                {getUsageScenario(item.key) && (
-                  <p className="text-sm text-muted-foreground mt-1">
-                    <span className="font-medium">使用场景:</span>
-                    {getUsageScenario(item.key)}
-                  </p>
-                )}
-                <p className="text-xs text-muted-foreground mt-1">{getPlaceholderHint(item.key)}</p>
-              </CardHeader>
-              <CardContent className="space-y-3">
-                <textarea
-                  className={`w-full ${getTextareaMinHeight(item.key)} px-3 py-2 border border-input rounded-md text-sm bg-background font-mono resize-y`}
-                  value={item.content}
-                  onChange={(e) => handleContentChange(item.key, e.target.value)}
-                  placeholder={item.key === "no_source_reply" || item.key === "ai_fail_reply" ? "请输入一句完整回复语" : "请输入提示词内容,保留占位符"}
-                  spellCheck={false}
-                />
-                <Button
-                  size="sm"
-                  onClick={() => handleSave(item.key, item.content)}
-                  disabled={savingKey === item.key}
-                >
-                  {savingKey === item.key ? "保存中..." : "保存"}
-                </Button>
-              </CardContent>
-            </Card>
-          ))
+          prompts.map((item) => {
+            const usageKey = PROMPT_USAGE_KEYS[item.key];
+            const hintKey = PROMPT_HINT_KEYS[item.key] ?? "agent.prompts.hint.default";
+            return (
+              <Card key={item.key}>
+                <CardHeader>
+                  <CardTitle className="text-base">{item.name}</CardTitle>
+                  {usageKey && (
+                    <p className="text-sm text-muted-foreground mt-1">
+                      <span className="font-medium">{t("agent.prompts.usageLabel")}</span>
+                      {t(usageKey)}
+                    </p>
+                  )}
+                  <p className="text-xs text-muted-foreground mt-1">{t(hintKey)}</p>
+                </CardHeader>
+                <CardContent className="space-y-3">
+                  <textarea
+                    className={`w-full ${getTextareaMinHeight(item.key)} px-3 py-2 border border-input rounded-md text-sm bg-background font-mono resize-y`}
+                    value={item.content}
+                    onChange={(e) => handleContentChange(item.key, e.target.value)}
+                    placeholder={
+                      item.key === "no_source_reply" || item.key === "ai_fail_reply"
+                        ? t("agent.prompts.ph.shortReply")
+                        : t("agent.prompts.ph.withPlaceholders")
+                    }
+                    spellCheck={false}
+                  />
+                  <Button
+                    size="sm"
+                    onClick={() => handleSave(item.key, item.content)}
+                    disabled={savingKey === item.key}
+                  >
+                    {savingKey === item.key ? t("agent.prompts.saving") : t("agent.prompts.save")}
+                  </Button>
+                </CardContent>
+              </Card>
+            );
+          })
         )}
       </div>
     </div>

+ 102 - 73
frontend/app/agent/settings/page.tsx

@@ -26,10 +26,24 @@ import { apiUrl } from "@/lib/config";
 import { Checkbox } from "@/components/ui/checkbox";
 import { Label } from "@/components/ui/label";
 import { toast } from "@/hooks/useToast";
+import type { I18nKey } from "@/lib/i18n/dict";
+import { useI18n } from "@/lib/i18n/provider";
 
 export default function SettingsPage(props: any = {}) {
   const { embedded = false } = props;
   const router = useRouter();
+  const { t } = useI18n();
+
+  const modelTypeLabel = (mt: string) => {
+    const map: Record<string, I18nKey> = {
+      text: "agent.settings.modelType.text",
+      image: "agent.settings.modelType.image",
+      audio: "agent.settings.modelType.audio",
+      video: "agent.settings.modelType.video",
+    };
+    const k = map[mt];
+    return k ? t(k) : mt;
+  };
   const [userId, setUserId] = useState<number | null>(null);
   const [configs, setConfigs] = useState<AIConfig[]>([]);
   const [loading, setLoading] = useState(true);
@@ -91,7 +105,7 @@ export default function SettingsPage(props: any = {}) {
       setConfigs(data);
     } catch (error) {
       console.error("加载配置失败:", error);
-      setError("加载配置失败");
+      setError(t("agent.settings.error.loadConfigs"));
     } finally {
       setLoading(false);
     }
@@ -121,7 +135,7 @@ export default function SettingsPage(props: any = {}) {
       });
     } catch (e) {
       console.error("加载知识库向量配置失败:", e);
-      setEmbeddingError("加载失败");
+      setEmbeddingError(t("agent.settings.error.loadEmbedding"));
     } finally {
       setEmbeddingLoading(false);
     }
@@ -153,7 +167,7 @@ export default function SettingsPage(props: any = {}) {
       }
       await updateEmbeddingConfig(userId, data);
       await loadEmbeddingConfig();
-      toast.success("保存成功,配置已立即生效。");
+      toast.success(t("agent.settings.toast.embeddingSaved"));
     } catch (err) {
       setEmbeddingError((err as Error).message);
     } finally {
@@ -224,7 +238,7 @@ export default function SettingsPage(props: any = {}) {
       resetForm();
       await loadConfigs();
     } catch (error) {
-      setError((error as Error).message || "操作失败");
+      setError((error as Error).message || t("agent.settings.error.operation"));
     } finally {
       setSubmitting(false);
     }
@@ -233,13 +247,13 @@ export default function SettingsPage(props: any = {}) {
   // 删除配置
   const handleDelete = async (id: number) => {
     if (!userId) return;
-    if (!confirm("确定要删除这个配置吗?")) return;
+    if (!confirm(t("agent.settings.confirmDeleteConfig"))) return;
 
     try {
       await deleteAIConfig(userId, id);
       await loadConfigs();
     } catch (error) {
-      setError((error as Error).message || "删除失败");
+      setError((error as Error).message || t("agent.settings.error.delete"));
     }
   };
 
@@ -263,11 +277,11 @@ export default function SettingsPage(props: any = {}) {
 
   // 构建头部内容
   const headerContent = (
-    <div className="bg-card border-b p-4 shadow-sm">
+    <div className="border-b bg-card p-3 shadow-sm sm:p-4">
       <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
         <div>
-          <h1 className="text-xl font-bold text-foreground">AI 配置管理</h1>
-          <div className="text-sm text-muted-foreground mt-1">管理 AI 服务商配置</div>
+          <h1 className="text-xl font-bold text-foreground">{t("agent.settings.title")}</h1>
+          <div className="text-sm text-muted-foreground mt-1">{t("agent.settings.subtitle")}</div>
         </div>
         {!embedded && (
           <div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
@@ -277,7 +291,7 @@ export default function SettingsPage(props: any = {}) {
               size="sm"
               className="w-full sm:w-auto"
             >
-              返回工作台
+              {t("agent.settings.backDashboard")}
             </Button>
             <Button
               onClick={handleLogout}
@@ -285,7 +299,7 @@ export default function SettingsPage(props: any = {}) {
               size="sm"
               className="w-full sm:w-auto"
             >
-              退出登录
+              {t("agent.logout")}
             </Button>
           </div>
         )}
@@ -295,12 +309,12 @@ export default function SettingsPage(props: any = {}) {
 
   // 构建主内容区
   const mainContent = (
-    <div className="flex-1 overflow-auto p-4 md:p-6">
+    <div className="flex-1 overflow-auto p-3 sm:p-4 md:p-6">
         <div className="max-w-6xl mx-auto space-y-6">
           {/* 全局设置 */}
           <Card>
             <CardHeader>
-              <CardTitle>全局设置</CardTitle>
+              <CardTitle>{t("agent.settings.section.global")}</CardTitle>
             </CardHeader>
             <CardContent>
               <div className="flex items-center space-x-2">
@@ -315,7 +329,7 @@ export default function SettingsPage(props: any = {}) {
                         });
                       } catch (error) {
                         console.error("更新设置失败:", error);
-                        toast.error("更新设置失败,请重试");
+                        toast.error(t("agent.settings.toast.profileUpdateFailed"));
                       }
                     }
                   }}
@@ -325,12 +339,11 @@ export default function SettingsPage(props: any = {}) {
                   htmlFor="receive_ai_conversations"
                   className="text-sm font-medium cursor-pointer"
                 >
-                  客服不接收 AI 对话
+                  {t("agent.settings.global.noReceiveAi")}
                 </Label>
               </div>
               <p className="text-xs text-muted-foreground mt-2">
-                开启后,AI 对话将不会显示在对话列表中,也不会收到 AI 消息通知。
-                但您仍可以在会话页面手动开启&quot;显示 AI 消息&quot;来查看 AI 对话历史。
+                {t("agent.settings.global.noReceiveAiHint")}
               </p>
             </CardContent>
           </Card>
@@ -338,14 +351,14 @@ export default function SettingsPage(props: any = {}) {
           {/* 知识库向量模型(平台级,仅管理员可修改;保存后立即生效) */}
           <Card>
             <CardHeader>
-              <CardTitle>知识库向量模型</CardTitle>
+              <CardTitle>{t("agent.settings.embedding.title")}</CardTitle>
               <p className="text-sm text-muted-foreground mt-1">
-                用于知识库文档向量化与 RAG 检索。仅管理员可修改;保存后立即生效,无需重启。
+                {t("agent.settings.embedding.lead")}
               </p>
             </CardHeader>
             <CardContent>
               {embeddingLoading ? (
-                <div className="text-center py-6 text-muted-foreground">加载中...</div>
+                <div className="text-center py-6 text-muted-foreground">{t("common.loading")}</div>
               ) : (
                 <form onSubmit={handleSaveEmbeddingConfig} className="space-y-4">
                   {embeddingError && (
@@ -355,7 +368,7 @@ export default function SettingsPage(props: any = {}) {
                   )}
                   <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                     <div>
-                      <Label className="block text-sm font-medium mb-1">类型</Label>
+                      <Label className="block text-sm font-medium mb-1">{t("agent.settings.embedding.type")}</Label>
                       <select
                         value={embeddingForm.embedding_type}
                         onChange={(e) =>
@@ -363,39 +376,43 @@ export default function SettingsPage(props: any = {}) {
                         }
                         className="w-full px-3 py-2 border border-input rounded-md text-sm bg-background"
                       >
-                        <option value="openai">OpenAI / 兼容 API</option>
-                        <option value="bge">BGE 本地</option>
+                        <option value="openai">{t("agent.settings.embedding.openaiCompatible")}</option>
+                        <option value="bge">{t("agent.settings.embedding.bgeLocal")}</option>
                       </select>
                     </div>
                     <div>
-                      <Label className="block text-sm font-medium mb-1">API 地址</Label>
+                      <Label className="block text-sm font-medium mb-1">{t("agent.settings.embedding.apiUrl")}</Label>
                       <Input
                         value={embeddingForm.api_url}
                         onChange={(e) =>
                           setEmbeddingForm({ ...embeddingForm, api_url: e.target.value })
                         }
-                        placeholder="https://api.openai.com/v1 或兼容地址"
+                        placeholder={t("agent.settings.embedding.apiUrlPh")}
                       />
                     </div>
                     <div>
-                      <Label className="block text-sm font-medium mb-1">API Key</Label>
+                      <Label className="block text-sm font-medium mb-1">{t("agent.settings.embedding.apiKey")}</Label>
                       <Input
                         type="password"
                         value={embeddingForm.api_key}
                         onChange={(e) =>
                           setEmbeddingForm({ ...embeddingForm, api_key: e.target.value })
                         }
-                        placeholder={embeddingConfig?.api_key_masked ? "留空则不更新" : "输入 API Key"}
+                        placeholder={
+                          embeddingConfig?.api_key_masked
+                            ? t("agent.settings.embedding.apiKeyKeepEmpty")
+                            : t("agent.settings.embedding.apiKeyInput")
+                        }
                       />
                     </div>
                     <div>
-                      <Label className="block text-sm font-medium mb-1">模型</Label>
+                      <Label className="block text-sm font-medium mb-1">{t("agent.settings.embedding.model")}</Label>
                       <Input
                         value={embeddingForm.model}
                         onChange={(e) =>
                           setEmbeddingForm({ ...embeddingForm, model: e.target.value })
                         }
-                        placeholder="text-embedding-3-small"
+                        placeholder={t("agent.settings.embedding.modelPh")}
                       />
                     </div>
                   </div>
@@ -411,11 +428,13 @@ export default function SettingsPage(props: any = {}) {
                       }
                     />
                     <Label htmlFor="customer_can_use_kb" className="text-sm cursor-pointer">
-                      开放知识库给客服使用(允许创建知识库、上传文档、对话中引用)
+                      {t("agent.settings.embedding.customerKb")}
                     </Label>
                   </div>
                   <Button type="submit" disabled={embeddingSubmitting}>
-                    {embeddingSubmitting ? "保存中..." : "保存配置"}
+                    {embeddingSubmitting
+                      ? t("common.saving")
+                      : t("agent.settings.embedding.save")}
                   </Button>
                 </form>
               )}
@@ -425,14 +444,14 @@ export default function SettingsPage(props: any = {}) {
           {/* 联网搜索设置(与知识库向量模型独立;实际仍写入同一配置,仅 UI 分离) */}
           <Card>
             <CardHeader>
-              <CardTitle>联网搜索设置</CardTitle>
+              <CardTitle>{t("agent.settings.webSearch.title")}</CardTitle>
               <p className="text-sm text-muted-foreground mt-1">
-                控制对话中的联网搜索方式与访客端是否显示联网选项。与上方「知识库向量模型」无关,仅影响 AI 对话时的联网行为。
+                {t("agent.settings.webSearch.lead")}
               </p>
             </CardHeader>
             <CardContent>
               {embeddingLoading ? (
-                <div className="text-center py-6 text-muted-foreground">加载中...</div>
+                <div className="text-center py-6 text-muted-foreground">{t("common.loading")}</div>
               ) : (
                 <form onSubmit={handleSaveEmbeddingConfig} className="space-y-4">
                   {embeddingError && (
@@ -441,7 +460,7 @@ export default function SettingsPage(props: any = {}) {
                     </div>
                   )}
                   <div>
-                    <Label className="block text-sm font-medium mb-1">联网方式</Label>
+                    <Label className="block text-sm font-medium mb-1">{t("agent.settings.webSearch.mode")}</Label>
                     <select
                       value={embeddingForm.web_search_source}
                       onChange={(e) =>
@@ -452,11 +471,11 @@ export default function SettingsPage(props: any = {}) {
                       }
                       className="w-full max-w-xs px-3 py-2 border border-input rounded-md text-sm bg-background"
                     >
-                      <option value="custom">自建(Serper)</option>
-                      <option value="vendor">厂商内置</option>
+                      <option value="custom">{t("agent.settings.webSearch.modeCustom")}</option>
+                      <option value="vendor">{t("agent.settings.webSearch.modeVendor")}</option>
                     </select>
                     <p className="text-xs text-muted-foreground mt-1">
-                      自建:由后端通过 Serper(MCP 或 HTTP)执行;厂商内置:使用当前对话所用 AI 厂商自带的 web search,不占用 Serper。
+                      {t("agent.settings.webSearch.modeHint")}
                     </p>
                   </div>
                   <div className="flex items-center gap-2">
@@ -471,11 +490,11 @@ export default function SettingsPage(props: any = {}) {
                       }
                     />
                     <Label htmlFor="visitor_web_search_enabled_standalone" className="text-sm cursor-pointer">
-                      访客小窗显示「本回合联网搜索」选项
+                      {t("agent.settings.webSearch.visitorToggle")}
                     </Label>
                   </div>
                   <Button type="submit" disabled={embeddingSubmitting}>
-                    {embeddingSubmitting ? "保存中..." : "保存联网设置"}
+                    {embeddingSubmitting ? t("common.saving") : t("agent.settings.webSearch.save")}
                   </Button>
                 </form>
               )}
@@ -486,7 +505,9 @@ export default function SettingsPage(props: any = {}) {
           <Card>
             <CardHeader>
               <CardTitle>
-                {editingId ? "编辑 AI 配置" : "添加 AI 配置"}
+                {editingId
+                  ? t("agent.settings.aiCard.titleEdit")
+                  : t("agent.settings.aiCard.titleAdd")}
               </CardTitle>
             </CardHeader>
             <CardContent>
@@ -500,35 +521,38 @@ export default function SettingsPage(props: any = {}) {
                 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                   <div>
                     <label className="block text-sm font-medium mb-1">
-                      服务商名称 <span className="text-red-500">*</span>
+                      {t("agent.settings.aiForm.provider")}{" "}
+                      <span className="text-red-500">*</span>
                     </label>
                     <Input
                       value={formData.provider}
                       onChange={(e) =>
                         setFormData({ ...formData, provider: e.target.value })
                       }
-                      placeholder="例如:OpenAI、Claude、自定义"
+                      placeholder={t("agent.settings.aiForm.providerPh")}
                       required
                     />
                   </div>
 
                   <div>
                     <label className="block text-sm font-medium mb-1">
-                      API 地址 <span className="text-red-500">*</span>
+                      {t("agent.settings.aiForm.apiUrl")}{" "}
+                      <span className="text-red-500">*</span>
                     </label>
                     <Input
                       value={formData.api_url}
                       onChange={(e) =>
                         setFormData({ ...formData, api_url: e.target.value })
                       }
-                      placeholder="https://api.openai.com/v1/chat/completions"
+                      placeholder={t("agent.settings.aiForm.apiUrlPh")}
                       required
                     />
                   </div>
 
                   <div>
                     <label className="block text-sm font-medium mb-1">
-                      API Key <span className="text-red-500">*</span>
+                      {t("agent.settings.aiForm.apiKey")}{" "}
+                      <span className="text-red-500">*</span>
                     </label>
                     <Input
                       type="password"
@@ -536,28 +560,33 @@ export default function SettingsPage(props: any = {}) {
                       onChange={(e) =>
                         setFormData({ ...formData, api_key: e.target.value })
                       }
-                      placeholder={editingId ? "留空则不更新" : "输入 API Key"}
+                      placeholder={
+                        editingId
+                          ? t("agent.settings.embedding.apiKeyKeepEmpty")
+                          : t("agent.settings.embedding.apiKeyInput")
+                      }
                       required={!editingId}
                     />
                   </div>
 
                   <div>
                     <label className="block text-sm font-medium mb-1">
-                      模型名称 <span className="text-red-500">*</span>
+                      {t("agent.settings.aiForm.model")}{" "}
+                      <span className="text-red-500">*</span>
                     </label>
                     <Input
                       value={formData.model}
                       onChange={(e) =>
                         setFormData({ ...formData, model: e.target.value })
                       }
-                      placeholder="例如:gpt-3.5-turbo、gpt-4"
+                      placeholder={t("agent.settings.aiForm.modelPh")}
                       required
                     />
                   </div>
 
                   <div>
                     <label className="block text-sm font-medium mb-1">
-                      模型类型
+                      {t("agent.settings.aiForm.modelType")}
                     </label>
                     <select
                       value={formData.model_type}
@@ -566,24 +595,24 @@ export default function SettingsPage(props: any = {}) {
                       }
                       className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-primary"
                     >
-                      <option value="text">文本</option>
-                      <option value="image">图片</option>
-                      <option value="audio">语音</option>
-                      <option value="video">视频</option>
+                      <option value="text">{t("agent.settings.modelType.text")}</option>
+                      <option value="image">{t("agent.settings.modelType.image")}</option>
+                      <option value="audio">{t("agent.settings.modelType.audio")}</option>
+                      <option value="video">{t("agent.settings.modelType.video")}</option>
                     </select>
                   </div>
                 </div>
 
                 <div>
                   <label className="block text-sm font-medium mb-1">
-                    配置描述
+                    {t("agent.settings.aiForm.description")}
                   </label>
                   <Input
                     value={formData.description}
                     onChange={(e) =>
                       setFormData({ ...formData, description: e.target.value })
                     }
-                    placeholder="例如:OpenAI GPT-3.5 Turbo 模型"
+                    placeholder={t("agent.settings.aiForm.descPh")}
                   />
                 </div>
 
@@ -597,7 +626,7 @@ export default function SettingsPage(props: any = {}) {
                       }
                       className="w-4 h-4"
                     />
-                    <span className="text-sm">启用配置</span>
+                    <span className="text-sm">{t("agent.settings.aiForm.active")}</span>
                   </label>
 
                   <label className="flex items-center gap-2">
@@ -609,17 +638,17 @@ export default function SettingsPage(props: any = {}) {
                       }
                       className="w-4 h-4"
                     />
-                    <span className="text-sm">开放给访客使用</span>
+                    <span className="text-sm">{t("agent.settings.aiForm.public")}</span>
                   </label>
                 </div>
 
                 <div className="flex gap-2">
                   <Button type="submit" disabled={submitting}>
                     {submitting
-                      ? "提交中..."
+                      ? t("agent.settings.aiForm.submitting")
                       : editingId
-                        ? "更新配置"
-                        : "创建配置"}
+                        ? t("agent.settings.aiForm.submitUpdate")
+                        : t("agent.settings.aiForm.submitCreate")}
                   </Button>
                   {editingId && (
                     <Button
@@ -627,7 +656,7 @@ export default function SettingsPage(props: any = {}) {
                       variant="outline"
                       onClick={resetForm}
                     >
-                      取消
+                      {t("agent.common.cancel")}
                     </Button>
                   )}
                 </div>
@@ -638,16 +667,16 @@ export default function SettingsPage(props: any = {}) {
           {/* 配置列表 */}
           <Card>
             <CardHeader>
-              <CardTitle>已配置的 AI 服务</CardTitle>
+              <CardTitle>{t("agent.settings.list.title")}</CardTitle>
             </CardHeader>
             <CardContent>
               {loading ? (
                 <div className="text-center py-8 text-gray-500">
-                  加载中...
+                  {t("common.loading")}
                 </div>
               ) : configs.length === 0 ? (
                 <div className="text-center py-8 text-gray-500">
-                  暂无配置,请添加
+                  {t("agent.settings.list.empty")}
                 </div>
               ) : (
                 <div className="space-y-4">
@@ -664,27 +693,27 @@ export default function SettingsPage(props: any = {}) {
                             </h3>
                             {config.is_active && (
                               <span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded">
-                                启用
+                                {t("agent.settings.badge.active")}
                               </span>
                             )}
                             {config.is_public && (
                               <span className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded">
-                                开放
+                                {t("agent.settings.badge.public")}
                               </span>
                             )}
                           </div>
                           <div className="text-sm text-gray-600 space-y-1">
                             <p>
-                              <span className="font-medium">API 地址:</span>
+                              <span className="font-medium">{t("agent.settings.list.apiUrlLabel")}</span>
                               {config.api_url}
                             </p>
                             <p>
-                              <span className="font-medium">模型类型:</span>
-                              {config.model_type}
+                              <span className="font-medium">{t("agent.settings.list.modelTypeLabel")}</span>
+                              {modelTypeLabel(config.model_type)}
                             </p>
                             {config.description && (
                               <p>
-                                <span className="font-medium">描述:</span>
+                                <span className="font-medium">{t("agent.settings.list.descLabel")}</span>
                                 {config.description}
                               </p>
                             )}
@@ -696,14 +725,14 @@ export default function SettingsPage(props: any = {}) {
                             variant="outline"
                             onClick={() => handleEdit(config)}
                           >
-                            编辑
+                            {t("agent.common.edit")}
                           </Button>
                           <Button
                             size="sm"
                             variant="destructive"
                             onClick={() => handleDelete(config.id)}
                           >
-                            删除
+                            {t("agent.common.delete")}
                           </Button>
                         </div>
                       </div>

+ 136 - 96
frontend/app/agent/users/page.tsx

@@ -32,21 +32,42 @@ import {
   defaultAgentPermissions,
   type PermissionKey,
 } from "@/lib/constants/agent-permissions";
+import type { I18nKey } from "@/lib/i18n/dict";
+import { useI18n } from "@/lib/i18n/provider";
 import {
-  Plus,
   Edit,
   Trash2,
   Lock,
   Search,
   UserPlus,
-  Save,
-  X,
 } from "lucide-react";
 
+const PERM_LABEL: Record<PermissionKey, I18nKey> = {
+  chat: "agent.perm.chat",
+  kb_test: "agent.perm.kb_test",
+  knowledge: "agent.perm.knowledge",
+  faqs: "agent.perm.faqs",
+  analytics: "agent.perm.analytics",
+  logs: "agent.perm.logs",
+  prompts: "agent.perm.prompts",
+  settings: "agent.perm.settings",
+  users: "agent.perm.users",
+};
+
 export default function UsersPage(props: any = {}) {
   const { embedded = false } = props;
   const router = useRouter();
   const { agent } = useAuth();
+  const { t, lang } = useI18n();
+
+  const tr = (key: I18nKey, vars?: Record<string, string>) => {
+    let s = t(key);
+    if (!vars) return s;
+    for (const k of Object.keys(vars)) {
+      s = s.replaceAll(`{{${k}}}`, vars[k] ?? "");
+    }
+    return s;
+  };
   const [users, setUsers] = useState<UserSummary[]>([]);
   const [loading, setLoading] = useState(true);
   const [searchQuery, setSearchQuery] = useState("");
@@ -100,11 +121,11 @@ export default function UsersPage(props: any = {}) {
       setUsers(data);
     } catch (error) {
       console.error("加载用户列表失败:", error);
-      toast.error((error as Error).message || "加载用户列表失败");
+      toast.error((error as Error).message || t("agent.users.toast.loadFailed"));
     } finally {
       setLoading(false);
     }
-  }, [agent?.id]);
+  }, [agent?.id, t]);
 
   // 初始加载
   useEffect(() => {
@@ -143,7 +164,7 @@ export default function UsersPage(props: any = {}) {
       return;
     }
     if (!createForm.username.trim() || !createForm.password.trim()) {
-      toast.error("用户名和密码不能为空");
+      toast.error(t("agent.users.toast.usernamePasswordRequired"));
       return;
     }
     setSubmitting(true);
@@ -151,9 +172,9 @@ export default function UsersPage(props: any = {}) {
       await createUser(createForm, agent.id);
       setCreateDialogOpen(false);
       await loadUsers();
-      toast.success("创建成功");
+      toast.success(t("agent.users.toast.createSuccess"));
     } catch (error) {
-      toast.error((error as Error).message || "创建用户失败");
+      toast.error((error as Error).message || t("agent.users.toast.createFailed"));
     } finally {
       setSubmitting(false);
     }
@@ -187,9 +208,9 @@ export default function UsersPage(props: any = {}) {
       setEditDialogOpen(false);
       setSelectedUser(null);
       await loadUsers();
-      toast.success("更新成功");
+      toast.success(t("agent.users.toast.updateSuccess"));
     } catch (error) {
-      toast.error((error as Error).message || "更新用户失败");
+      toast.error((error as Error).message || t("agent.users.toast.updateFailed"));
     } finally {
       setSubmitting(false);
     }
@@ -198,7 +219,7 @@ export default function UsersPage(props: any = {}) {
   // 打开修改密码对话框
   const handleOpenPassword = (user: UserSummary) => {
     if (user.role === "admin") {
-      toast.error("管理员密码仅支持数据库修改,前端已禁用");
+      toast.error(t("agent.users.toast.adminPasswordDisabled"));
       return;
     }
     setSelectedUser(user);
@@ -215,13 +236,13 @@ export default function UsersPage(props: any = {}) {
       return;
     }
     if (!passwordForm.new_password.trim()) {
-      toast.error("新密码不能为空");
+      toast.error(t("agent.users.toast.newPasswordRequired"));
       return;
     }
     // 如果修改的是当前用户,需要旧密码;如果是其他用户,不需要旧密码
     const isCurrentUser = selectedUser.id === agent.id;
     if (isCurrentUser && !passwordForm.old_password?.trim()) {
-      toast.error("修改自己的密码需要提供旧密码");
+      toast.error(t("agent.users.toast.oldPasswordRequired"));
       return;
     }
 
@@ -235,9 +256,9 @@ export default function UsersPage(props: any = {}) {
       setPasswordDialogOpen(false);
       setSelectedUser(null);
       setPasswordForm({ old_password: "", new_password: "" });
-      toast.success("密码更新成功");
+      toast.success(t("agent.users.toast.passwordSuccess"));
     } catch (error) {
-      toast.error((error as Error).message || "更新密码失败");
+      toast.error((error as Error).message || t("agent.users.toast.passwordFailed"));
     } finally {
       setSubmitting(false);
     }
@@ -246,7 +267,7 @@ export default function UsersPage(props: any = {}) {
   // 打开删除对话框
   const handleOpenDelete = (user: UserSummary) => {
     if (user.role === "admin") {
-      toast.error("管理员账号仅支持数据库删除,前端已禁用");
+      toast.error(t("agent.users.toast.adminDeleteDisabled"));
       return;
     }
     setSelectedUser(user);
@@ -266,13 +287,15 @@ export default function UsersPage(props: any = {}) {
       await loadUsers();
       if (result.transferredAIConfigs > 0) {
         toast.success(
-          `删除成功,已自动转移 ${result.transferredAIConfigs} 条 AI 配置到当前管理员`
+          tr("agent.users.toast.deleteTransferred", {
+            count: String(result.transferredAIConfigs),
+          })
         );
       } else {
-        toast.success("删除成功");
+        toast.success(t("agent.users.toast.deleteSuccess"));
       }
     } catch (error) {
-      toast.error((error as Error).message || "删除用户失败");
+      toast.error((error as Error).message || t("agent.users.toast.deleteFailed"));
     } finally {
       setSubmitting(false);
     }
@@ -281,7 +304,7 @@ export default function UsersPage(props: any = {}) {
   // 格式化时间
   const formatTime = (dateStr: string) => {
     const date = new Date(dateStr);
-    return date.toLocaleString("zh-CN", {
+    return date.toLocaleString(lang === "en" ? "en-US" : "zh-CN", {
       year: "numeric",
       month: "2-digit",
       day: "2-digit",
@@ -296,16 +319,16 @@ export default function UsersPage(props: any = {}) {
 
   // 构建头部内容
   const headerContent = (
-    <div className="bg-card border-b p-4 shadow-sm">
+    <div className="border-b bg-card p-3 shadow-sm sm:p-4">
       <div className="flex items-center justify-between mb-4">
-        <h1 className="text-xl font-bold text-foreground">用户管理</h1>
+        <h1 className="text-xl font-bold text-foreground">{t("agent.users.title")}</h1>
         {!embedded && (
           <Button
             variant="ghost"
             size="sm"
             onClick={() => router.push("/agent/dashboard")}
           >
-            返回
+            {t("agent.common.back")}
           </Button>
         )}
       </div>
@@ -316,7 +339,7 @@ export default function UsersPage(props: any = {}) {
           <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
           <Input
             type="text"
-            placeholder="搜索用户(用户名、昵称、邮箱)..."
+            placeholder={t("agent.users.search.placeholder")}
             value={searchQuery}
             onChange={(e) => setSearchQuery(e.target.value)}
             className="pl-10"
@@ -327,7 +350,7 @@ export default function UsersPage(props: any = {}) {
           className="w-full sm:w-auto"
         >
           <UserPlus className="w-4 h-4 mr-2" />
-          创建用户
+          {t("agent.users.createButton")}
         </Button>
       </div>
     </div>
@@ -335,15 +358,15 @@ export default function UsersPage(props: any = {}) {
 
   // 构建主内容区
   const mainContent = (
-    <div className="flex-1 overflow-y-auto p-4 scrollbar-auto">
+    <div className="scrollbar-auto flex-1 overflow-y-auto p-3 sm:p-4">
         {loading ? (
           <div className="flex items-center justify-center h-full">
-            <span className="text-muted-foreground">加载中...</span>
+            <span className="text-muted-foreground">{t("common.loading")}</span>
           </div>
         ) : filteredUsers.length === 0 ? (
           <div className="flex items-center justify-center h-full">
             <span className="text-muted-foreground">
-              {searchQuery ? "没有找到匹配的用户" : "暂无用户"}
+              {searchQuery ? t("agent.users.empty.filtered") : t("agent.users.empty")}
             </span>
           </div>
         ) : (
@@ -358,15 +381,23 @@ export default function UsersPage(props: any = {}) {
                     <Badge
                       variant={user.role === "admin" ? "default" : "secondary"}
                     >
-                      {user.role === "admin" ? "管理员" : "客服"}
+                      {user.role === "admin"
+                        ? t("agent.users.role.admin")
+                        : t("agent.users.role.agent")}
                     </Badge>
                   </div>
                   <div className="text-sm text-muted-foreground space-y-1 mb-2">
-                    <div>用户名: {user.username}</div>
-                    {user.email && <div>邮箱: {user.email}</div>}
+                    <div>
+                      {t("agent.users.field.username")}: {user.username}
+                    </div>
+                    {user.email && (
+                      <div>
+                        {t("agent.users.field.email")}: {user.email}
+                      </div>
+                    )}
                   </div>
                   <div className="text-xs text-muted-foreground">
-                    创建时间: {formatTime(user.created_at)}
+                    {t("agent.users.field.createdAt")}: {formatTime(user.created_at)}
                   </div>
                 </div>
 
@@ -378,7 +409,7 @@ export default function UsersPage(props: any = {}) {
                     className="flex-1"
                   >
                     <Edit className="w-4 h-4 mr-1" />
-                    编辑
+                    {t("agent.users.card.edit")}
                   </Button>
                   <Button
                     variant="outline"
@@ -386,10 +417,14 @@ export default function UsersPage(props: any = {}) {
                     onClick={() => handleOpenPassword(user)}
                     className="flex-1"
                     disabled={user.role === "admin"}
-                    title={user.role === "admin" ? "管理员密码仅支持数据库修改" : ""}
+                    title={
+                      user.role === "admin"
+                        ? t("agent.users.tooltip.adminPasswordDbOnly")
+                        : ""
+                    }
                   >
                     <Lock className="w-4 h-4 mr-1" />
-                    密码
+                    {t("agent.users.card.password")}
                   </Button>
                   <Button
                     variant="destructive"
@@ -398,9 +433,9 @@ export default function UsersPage(props: any = {}) {
                     disabled={user.id === agent.id || user.role === "admin"}
                     title={
                       user.role === "admin"
-                        ? "管理员账号仅支持数据库删除"
+                        ? t("agent.users.tooltip.adminDeleteDbOnly")
                         : user.id === agent.id
-                          ? "不能删除当前登录用户"
+                          ? t("agent.users.tooltip.cannotDeleteSelf")
                           : ""
                     }
                   >
@@ -414,36 +449,28 @@ export default function UsersPage(props: any = {}) {
     </div>
   );
 
-  // 如果是嵌入模式,只返回内容,不包含 ResponsiveLayout
-  if (embedded) {
-    return (
-      <>
-        <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
-          {headerContent}
-          {mainContent}
-        </div>
-        {/* 对话框 */}
-
+  const userDialogs = (
+    <>
       {/* 创建用户对话框 */}
       <Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
         <DialogContent>
           <DialogHeader>
-            <DialogTitle>创建新用户</DialogTitle>
+            <DialogTitle>{t("agent.users.dialog.createTitle")}</DialogTitle>
           </DialogHeader>
           <div className="space-y-4">
             <div>
-              <Label htmlFor="create-username">用户名 *</Label>
+              <Label htmlFor="create-username">{t("agent.users.form.username")} *</Label>
               <Input
                 id="create-username"
                 value={createForm.username}
                 onChange={(e) =>
                   setCreateForm({ ...createForm, username: e.target.value })
                 }
-                placeholder="请输入用户名"
+                placeholder={t("agent.users.placeholder.username")}
               />
             </div>
             <div>
-              <Label htmlFor="create-password">密码 *</Label>
+              <Label htmlFor="create-password">{t("agent.users.form.password")} *</Label>
               <Input
                 id="create-password"
                 type="password"
@@ -451,11 +478,11 @@ export default function UsersPage(props: any = {}) {
                 onChange={(e) =>
                   setCreateForm({ ...createForm, password: e.target.value })
                 }
-                placeholder="请输入密码"
+                placeholder={t("agent.users.placeholder.password")}
               />
             </div>
             <div>
-              <Label htmlFor="create-role">角色 *</Label>
+              <Label htmlFor="create-role">{t("agent.users.form.role")} *</Label>
               <select
                 id="create-role"
                 value={createForm.role}
@@ -471,15 +498,15 @@ export default function UsersPage(props: any = {}) {
                 }
                 className="w-full px-3 py-2 border border-border rounded-md bg-background"
               >
-                <option value="agent">客服</option>
-                <option value="admin">管理员</option>
+                <option value="agent">{t("agent.users.role.agent")}</option>
+                <option value="admin">{t("agent.users.role.admin")}</option>
               </select>
             </div>
 
             {/* 功能权限(开/关一级;role=admin 默认全开) */}
             {createForm.role !== "admin" && (
               <div>
-                <Label>功能权限</Label>
+                <Label>{t("agent.users.form.permissions")}</Label>
                 <div className="mt-2 grid grid-cols-2 gap-2">
                   {PERMISSION_OPTIONS.map((p) => {
                     const checked = (createForm.permissions ?? []).includes(p.key);
@@ -498,29 +525,29 @@ export default function UsersPage(props: any = {}) {
                             setCreateForm({ ...createForm, permissions: Array.from(next) });
                           }}
                         />
-                        <span>{p.label}</span>
+                        <span>{t(PERM_LABEL[p.key])}</span>
                       </label>
                     );
                   })}
                 </div>
                 <p className="mt-1 text-xs text-muted-foreground">
-                  默认仅开启“对话”。关闭后对应菜单不可见且后端接口会返回 403。
+                  {t("agent.users.form.permissionsHint")}
                 </p>
               </div>
             )}
             <div>
-              <Label htmlFor="create-nickname">昵称</Label>
+              <Label htmlFor="create-nickname">{t("agent.users.form.nickname")}</Label>
               <Input
                 id="create-nickname"
                 value={createForm.nickname}
                 onChange={(e) =>
                   setCreateForm({ ...createForm, nickname: e.target.value })
                 }
-                placeholder="请输入昵称(可选)"
+                placeholder={t("agent.users.placeholder.nicknameOptional")}
               />
             </div>
             <div>
-              <Label htmlFor="create-email">邮箱</Label>
+              <Label htmlFor="create-email">{t("agent.users.form.email")}</Label>
               <Input
                 id="create-email"
                 type="email"
@@ -528,7 +555,7 @@ export default function UsersPage(props: any = {}) {
                 onChange={(e) =>
                   setCreateForm({ ...createForm, email: e.target.value })
                 }
-                placeholder="请输入邮箱(可选)"
+                placeholder={t("agent.users.placeholder.emailOptional")}
               />
             </div>
             <div className="flex justify-end gap-2">
@@ -537,10 +564,10 @@ export default function UsersPage(props: any = {}) {
                 onClick={() => setCreateDialogOpen(false)}
                 disabled={submitting}
               >
-                取消
+                {t("agent.common.cancel")}
               </Button>
               <Button onClick={handleCreate} disabled={submitting}>
-                {submitting ? "创建中..." : "创建"}
+                {submitting ? t("agent.users.submit.creating") : t("agent.common.create")}
               </Button>
             </div>
           </div>
@@ -551,19 +578,19 @@ export default function UsersPage(props: any = {}) {
       <Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
         <DialogContent>
           <DialogHeader>
-            <DialogTitle>编辑用户</DialogTitle>
+            <DialogTitle>{t("agent.users.dialog.editTitle")}</DialogTitle>
           </DialogHeader>
           {selectedUser && (
             <div className="space-y-4">
               <div>
-                <Label>用户名</Label>
+                <Label>{t("agent.users.field.username")}</Label>
                 <Input value={selectedUser.username} disabled />
                 <p className="text-xs text-muted-foreground mt-1">
-                  用户名不能修改
+                  {t("agent.users.usernameImmutableHint")}
                 </p>
               </div>
               <div>
-                <Label htmlFor="edit-role">角色 *</Label>
+                <Label htmlFor="edit-role">{t("agent.users.form.role")} *</Label>
                 <select
                   id="edit-role"
                   value={editForm.role}
@@ -579,14 +606,14 @@ export default function UsersPage(props: any = {}) {
                   }
                   className="w-full px-3 py-2 border border-border rounded-md bg-background"
                 >
-                  <option value="agent">客服</option>
-                  <option value="admin">管理员</option>
+                  <option value="agent">{t("agent.users.role.agent")}</option>
+                  <option value="admin">{t("agent.users.role.admin")}</option>
                 </select>
               </div>
 
               {editForm.role !== "admin" && (
                 <div>
-                  <Label>功能权限</Label>
+                  <Label>{t("agent.users.form.permissions")}</Label>
                   <div className="mt-2 grid grid-cols-2 gap-2">
                     {PERMISSION_OPTIONS.map((p) => {
                       const checked = (editForm.permissions ?? []).includes(p.key);
@@ -605,7 +632,7 @@ export default function UsersPage(props: any = {}) {
                               setEditForm({ ...editForm, permissions: Array.from(next) });
                             }}
                           />
-                          <span>{p.label}</span>
+                          <span>{t(PERM_LABEL[p.key])}</span>
                         </label>
                       );
                     })}
@@ -613,18 +640,18 @@ export default function UsersPage(props: any = {}) {
                 </div>
               )}
               <div>
-                <Label htmlFor="edit-nickname">昵称</Label>
+                <Label htmlFor="edit-nickname">{t("agent.users.form.nickname")}</Label>
                 <Input
                   id="edit-nickname"
                   value={editForm.nickname || ""}
                   onChange={(e) =>
                     setEditForm({ ...editForm, nickname: e.target.value })
                   }
-                  placeholder="请输入昵称"
+                  placeholder={t("agent.users.placeholder.nickname")}
                 />
               </div>
               <div>
-                <Label htmlFor="edit-email">邮箱</Label>
+                <Label htmlFor="edit-email">{t("agent.users.form.email")}</Label>
                 <Input
                   id="edit-email"
                   type="email"
@@ -632,7 +659,7 @@ export default function UsersPage(props: any = {}) {
                   onChange={(e) =>
                     setEditForm({ ...editForm, email: e.target.value })
                   }
-                  placeholder="请输入邮箱"
+                  placeholder={t("agent.users.placeholder.email")}
                 />
               </div>
               <div className="flex items-center gap-2">
@@ -649,7 +676,7 @@ export default function UsersPage(props: any = {}) {
                   className="w-4 h-4"
                 />
                 <Label htmlFor="edit-receive-ai" className="cursor-pointer">
-                  接收 AI 对话
+                  {t("agent.users.receiveAiLabel")}
                 </Label>
               </div>
               <div className="flex justify-end gap-2">
@@ -658,10 +685,10 @@ export default function UsersPage(props: any = {}) {
                   onClick={() => setEditDialogOpen(false)}
                   disabled={submitting}
                 >
-                  取消
+                  {t("agent.common.cancel")}
                 </Button>
                 <Button onClick={handleUpdate} disabled={submitting}>
-                  {submitting ? "更新中..." : "更新"}
+                  {submitting ? t("agent.users.submit.updating") : t("agent.common.update")}
                 </Button>
               </div>
             </div>
@@ -673,17 +700,17 @@ export default function UsersPage(props: any = {}) {
       <Dialog open={passwordDialogOpen} onOpenChange={setPasswordDialogOpen}>
         <DialogContent>
           <DialogHeader>
-            <DialogTitle>修改密码</DialogTitle>
+            <DialogTitle>{t("agent.users.dialog.passwordTitle")}</DialogTitle>
           </DialogHeader>
           {selectedUser && (
             <div className="space-y-4">
               <div>
-                <Label>用户名</Label>
+                <Label>{t("agent.users.field.username")}</Label>
                 <Input value={selectedUser.username} disabled />
               </div>
               {selectedUser.id === agent?.id && (
                 <div>
-                  <Label htmlFor="password-old">旧密码 *</Label>
+                  <Label htmlFor="password-old">{t("agent.users.form.oldPassword")} *</Label>
                   <Input
                     id="password-old"
                     type="password"
@@ -694,12 +721,12 @@ export default function UsersPage(props: any = {}) {
                         old_password: e.target.value,
                       })
                     }
-                    placeholder="请输入旧密码"
+                    placeholder={t("agent.users.placeholder.oldPassword")}
                   />
                 </div>
               )}
               <div>
-                <Label htmlFor="password-new">新密码 *</Label>
+                <Label htmlFor="password-new">{t("agent.users.form.newPassword")} *</Label>
                 <Input
                   id="password-new"
                   type="password"
@@ -710,7 +737,7 @@ export default function UsersPage(props: any = {}) {
                       new_password: e.target.value,
                     })
                   }
-                  placeholder="请输入新密码"
+                  placeholder={t("agent.users.placeholder.password")}
                 />
               </div>
               <div className="flex justify-end gap-2">
@@ -719,10 +746,10 @@ export default function UsersPage(props: any = {}) {
                   onClick={() => setPasswordDialogOpen(false)}
                   disabled={submitting}
                 >
-                  取消
+                  {t("agent.common.cancel")}
                 </Button>
                 <Button onClick={handleUpdatePassword} disabled={submitting}>
-                  {submitting ? "更新中..." : "更新"}
+                  {submitting ? t("agent.users.submit.updating") : t("agent.common.update")}
                 </Button>
               </div>
             </div>
@@ -734,15 +761,17 @@ export default function UsersPage(props: any = {}) {
       <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
         <DialogContent>
           <DialogHeader>
-            <DialogTitle>删除用户</DialogTitle>
+            <DialogTitle>{t("agent.users.dialog.deleteTitle")}</DialogTitle>
           </DialogHeader>
           {selectedUser && (
             <div className="space-y-4">
               <p className="text-foreground">
-                确定要删除用户 <strong>{selectedUser.username}</strong> 吗?
+                {tr("agent.users.dialog.deleteConfirm", {
+                  username: selectedUser.username,
+                })}
               </p>
               <p className="text-sm text-muted-foreground">
-                此操作不可恢复。若该用户有 AI 配置,系统会自动转移给当前管理员,避免配置丢失。
+                {t("agent.users.dialog.deleteNote")}
               </p>
               <div className="flex justify-end gap-2">
                 <Button
@@ -750,14 +779,14 @@ export default function UsersPage(props: any = {}) {
                   onClick={() => setDeleteDialogOpen(false)}
                   disabled={submitting}
                 >
-                  取消
+                  {t("agent.common.cancel")}
                 </Button>
                 <Button
                   variant="destructive"
                   onClick={handleDelete}
                   disabled={submitting}
                 >
-                  {submitting ? "删除中..." : "删除"}
+                  {submitting ? t("agent.users.submit.deleting") : t("agent.common.delete")}
                 </Button>
               </div>
             </div>
@@ -766,13 +795,24 @@ export default function UsersPage(props: any = {}) {
       </Dialog>
     </>
   );
+
+  if (embedded) {
+    return (
+      <>
+        <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
+          {headerContent}
+          {mainContent}
+        </div>
+        {userDialogs}
+      </>
+    );
   }
 
   return (
-    <ResponsiveLayout
-      main={mainContent}
-      header={headerContent}
-    />
+    <>
+      <ResponsiveLayout main={mainContent} header={headerContent} />
+      {userDialogs}
+    </>
   );
 }
 

+ 10 - 0
frontend/app/globals.css

@@ -92,6 +92,16 @@ html {
   scroll-behavior: smooth;
 }
 
+/* 移动端:刘海 / 手势条安全区(与 Tailwind 任意值 env(safe-area-inset-*) 配合) */
+@supports (padding: env(safe-area-inset-bottom)) {
+  .pb-safe {
+    padding-bottom: env(safe-area-inset-bottom);
+  }
+  .pt-safe {
+    padding-top: env(safe-area-inset-top);
+  }
+}
+
 body {
   background-color: hsl(var(--background));
   color: hsl(var(--foreground));

+ 14 - 2
frontend/app/layout.tsx

@@ -1,9 +1,10 @@
-import type { Metadata } from "next";
+import type { Metadata, Viewport } from "next";
 import { Geist, Geist_Mono } from "next/font/google";
 import "./globals.css";
 import MatomoTracker from "@/components/MatomoTracker";
 import { Toaster } from "@/components/ui/toaster";
 import { getSiteUrl } from "@/lib/site";
+import { I18nProvider } from "@/lib/i18n/provider";
 
 const geistSans = Geist({
   variable: "--font-geist-sans",
@@ -20,6 +21,17 @@ export const metadata: Metadata = {
   description: "融合 AI 技术与人工客服,为企业提供高效、智能的客户服务解决方案",
 };
 
+/** 移动端:正确缩放、刘海屏 safe-area、禁止误触极小字号(仍允许用户双指放大) */
+export const viewport: Viewport = {
+  width: "device-width",
+  initialScale: 1,
+  viewportFit: "cover",
+  themeColor: [
+    { media: "(prefers-color-scheme: light)", color: "#f8fafc" },
+    { media: "(prefers-color-scheme: dark)", color: "#0f172a" },
+  ],
+};
+
 // Matomo 容器 URL(格式:container_*.js)
 const MATOMO_CONTAINER_URL = process.env.NEXT_PUBLIC_MATOMO_CONTAINER_URL || '';
 
@@ -43,7 +55,7 @@ export default function RootLayout({
       <body
         className={`${geistSans.variable} ${geistMono.variable} antialiased`}
       >
-        {children}
+        <I18nProvider>{children}</I18nProvider>
         <Toaster />
         {MATOMO_CONTAINER_URL && <MatomoTracker containerUrl={MATOMO_CONTAINER_URL} />}
       </body>

+ 22 - 10
frontend/components/dashboard/ChatHeader.tsx

@@ -3,6 +3,9 @@
 import { formatConversationTime } from "@/utils/format";
 import { Button } from "@/components/ui/button";
 import { Separator } from "@/components/ui/separator";
+import { useI18n } from "@/lib/i18n/provider";
+import { cn } from "@/lib/utils";
+import { Bot } from "lucide-react";
 
 interface ChatHeaderProps {
   conversationId: number;
@@ -16,6 +19,8 @@ interface ChatHeaderProps {
   soundEnabled?: boolean;
   onToggleSound?: () => void;
   hideAIToggle?: boolean; // 内部对话时隐藏「显示 AI 消息」切换
+  /** 为工作台移动端悬浮菜单(左/右圆钮)留出内边距,避免盖住标题与操作区 */
+  mobileGutters?: { left?: boolean; right?: boolean };
 }
 
 export function ChatHeader({
@@ -31,27 +36,34 @@ export function ChatHeader({
   onToggleSound,
   hideAIToggle = false,
 }: ChatHeaderProps) {
+  const { t } = useI18n();
   return (
     <div className="h-16 flex items-center justify-between px-4 bg-background flex-shrink-0 relative">
       <div className="z-10">
-        <div className="font-semibold text-foreground">对话 #{conversationId}</div>
+        <div className="font-semibold text-foreground">
+          {t("agent.chat.conversation")} #{conversationId}
+        </div>
         <div className="text-xs text-muted-foreground mt-0.5">
           {lastSeenAt
-            ? `last seen ${formatConversationTime(lastSeenAt)}`
-            : "last seen 未知"}
+            ? `${t("agent.chat.lastSeen")} ${formatConversationTime(lastSeenAt)}`
+            : t("agent.chat.lastSeenUnknown")}
         </div>
       </div>
-      <div className="flex items-center gap-2">
+      <div className="flex items-center gap-1 sm:gap-2 flex-shrink-0">
         {/* 显示/隐藏 AI 消息切换按钮(内部对话不显示,默认始终包含 AI 消息) */}
         {onToggleAIMessages && !hideAIToggle && (
           <Button
             variant={includeAIMessages ? "default" : "outline"}
             size="sm"
             onClick={onToggleAIMessages}
-            title={includeAIMessages ? "隐藏 AI 消息" : "显示 AI 消息"}
-            className="text-xs"
+            title={includeAIMessages ? t("agent.chat.hideAI") : t("agent.chat.showAI")}
+            aria-label={includeAIMessages ? t("agent.chat.hideAI") : t("agent.chat.showAI")}
+            className="text-xs gap-1 px-2 sm:px-3"
           >
-            {includeAIMessages ? "隐藏 AI 消息" : "显示 AI 消息"}
+            <Bot className="h-4 w-4 sm:hidden shrink-0" aria-hidden />
+            <span className="hidden sm:inline">
+              {includeAIMessages ? t("agent.chat.hideAI") : t("agent.chat.showAI")}
+            </span>
           </Button>
         )}
         {/* 声音开关按钮 */}
@@ -59,7 +71,7 @@ export function ChatHeader({
           <Button
             variant="ghost"
             size="icon"
-            title={soundEnabled ? "关闭声音提示" : "开启声音提示"}
+            title={soundEnabled ? t("agent.chat.soundOn") : t("agent.chat.soundOff")}
             onClick={onToggleSound}
           >
             {soundEnabled ? (
@@ -102,7 +114,7 @@ export function ChatHeader({
         <Button
           variant="ghost"
           size="icon"
-          title="关闭会话"
+          title={t("agent.chat.closeConversation")}
           onClick={onCloseConversation}
           disabled={!onCloseConversation}
         >
@@ -123,7 +135,7 @@ export function ChatHeader({
         <Button
           variant="ghost"
           size="icon"
-          title="刷新"
+          title={t("agent.chat.refresh")}
           onClick={onRefresh}
         >
           <svg

+ 13 - 4
frontend/components/dashboard/ConversationHeader.tsx

@@ -2,6 +2,7 @@
 
 import { useState, useRef, useEffect } from "react";
 import { ChevronDown } from "lucide-react";
+import { useI18n } from "@/lib/i18n/provider";
 
 export type ConversationFilter = "all" | "mine" | "others";
 
@@ -27,10 +28,18 @@ export function ConversationHeader({
   listStatus,
   onListStatusChange,
 }: ConversationHeaderProps) {
+  const { t } = useI18n();
   const [open, setOpen] = useState(false);
   const ref = useRef<HTMLDivElement>(null);
 
-  const currentLabel = FILTER_OPTIONS.find((o) => o.value === filter)?.label ?? "全部对话";
+  const options = [
+    { value: "all" as const, label: t("agent.conversations.filter.all") },
+    { value: "mine" as const, label: t("agent.conversations.filter.mine") },
+    { value: "others" as const, label: t("agent.conversations.filter.others") },
+  ];
+  const currentLabel =
+    options.find((o) => o.value === filter)?.label ??
+    t("agent.conversations.filter.all");
 
   useEffect(() => {
     const handleClickOutside = (e: MouseEvent) => {
@@ -58,7 +67,7 @@ export function ConversationHeader({
         </button>
         {open && (
           <div className="absolute top-full left-0 mt-1 py-1 rounded-lg border border-border bg-popover shadow-md z-50 min-w-[theme(spacing.32)]">
-            {FILTER_OPTIONS.map((opt) => (
+            {options.map((opt) => (
               <button
                 key={opt.value}
                 type="button"
@@ -90,7 +99,7 @@ export function ConversationHeader({
               }`}
               onClick={() => onListStatusChange!("open")}
             >
-              进行中
+              {t("agent.conversations.status.open")}
             </button>
             <button
               type="button"
@@ -101,7 +110,7 @@ export function ConversationHeader({
               }`}
               onClick={() => onListStatusChange!("closed")}
             >
-              历史
+              {t("agent.conversations.status.closed")}
             </button>
           </div>
         </div>

+ 11 - 5
frontend/components/dashboard/ConversationListItem.tsx

@@ -8,6 +8,7 @@ import {
 } from "@/utils/format";
 import { Badge } from "@/components/ui/badge";
 import { Card } from "@/components/ui/card";
+import { useI18n } from "@/lib/i18n/provider";
 
 interface ConversationListItemProps {
   conversation: ConversationSummary;
@@ -20,12 +21,13 @@ export function ConversationListItem({
   selected,
   onSelect,
 }: ConversationListItemProps) {
+  const { t } = useI18n();
   const avatarColor = `hsl(${(conversation.id * 137.5) % 360}, 70%, 50%)`;
   const unreadCount = conversation.unread_count ?? 0;
   const lastMessage = conversation.last_message;
   const lastMessagePreview = lastMessage
     ? buildMessagePreview(lastMessage.content)
-    : "暂无消息";
+    : t("agent.conversation.noMessage");
   // 根据 last_seen_at 判断是否在线(最近 10 秒内认为在线)
   const isOnline = isVisitorOnline(conversation.last_seen_at);
 
@@ -57,13 +59,13 @@ export function ConversationListItem({
         <div className="flex-1 min-w-0">
           <div className="flex items-center gap-2 mb-1">
             <span className="font-medium text-foreground text-sm truncate">
-              对话 #{conversation.id}
+              {t("agent.chat.conversation")} #{conversation.id}
             </span>
             {/* 在线/离线状态图标 */}
             {isOnline && (
               <span
                 className="w-2 h-2 rounded-full flex-shrink-0"
-                title="在线"
+                title={t("agent.conversation.online")}
                 style={{ backgroundColor: "#10b981" }}
               />
             )}
@@ -76,7 +78,9 @@ export function ConversationListItem({
               variant={conversation.status === "open" ? "default" : "secondary"}
               className="flex-shrink-0"
             >
-              {conversation.status === "open" ? "进行中" : "已关闭"}
+              {conversation.status === "open"
+                ? t("agent.conversations.status.open")
+                : t("agent.conversations.status.closed")}
             </Badge>
           </div>
           <div className="text-xs text-muted-foreground mb-1 flex items-center gap-1">
@@ -92,7 +96,9 @@ export function ConversationListItem({
             <span className="truncate">{lastMessagePreview}</span>
           </div>
           <div className="flex items-center justify-between gap-2 text-xs text-muted-foreground min-w-0">
-            <span className="truncate">访客 #{conversation.visitor_id}</span>
+            <span className="truncate">
+              {t("agent.conversation.visitor")} #{conversation.visitor_id}
+            </span>
             <span className="flex-shrink-0 whitespace-nowrap">{formatConversationTime(conversation.updated_at)}</span>
           </div>
         </div>

+ 4 - 2
frontend/components/dashboard/ConversationSidebar.tsx

@@ -6,6 +6,7 @@ import { ConversationSearch } from "./ConversationSearch";
 import { ConversationList } from "./ConversationList";
 import { Button } from "@/components/ui/button";
 import { Plus } from "lucide-react";
+import { useI18n } from "@/lib/i18n/provider";
 
 type ConversationStatus = "open" | "closed";
 
@@ -37,15 +38,16 @@ export function ConversationSidebar({
   mode = "visitor",
   onNewClick,
 }: ConversationSidebarProps) {
+  const { t } = useI18n();
   return (
     <div className="w-80 min-w-0 flex-1 flex flex-col bg-white border-r border-gray-200 min-h-0 overflow-hidden">
       {mode === "internal" ? (
         <div className="h-14 flex items-center justify-between px-3 border-b border-border bg-background flex-shrink-0">
-          <span className="text-sm font-medium text-foreground truncate">知识库测试</span>
+          <span className="text-sm font-medium text-foreground truncate">{t("agent.internalChat.title")}</span>
           {onNewClick && (
             <Button size="sm" variant="outline" onClick={onNewClick} className="flex-shrink-0 gap-1">
               <Plus className="w-4 h-4" />
-              新建
+              {t("agent.internalChat.new")}
             </Button>
           )}
         </div>

+ 14 - 7
frontend/components/dashboard/DashboardShell.tsx

@@ -30,8 +30,10 @@ import { VisitorDetailPanel } from "./VisitorDetailPanel";
 import { useSoundNotification } from "@/hooks/useSoundNotification";
 import { usePageTitle } from "@/hooks/usePageTitle";
 import { reportFrontendLog } from "@/features/agent/services/systemLogApi";
+import { useI18n } from "@/lib/i18n/provider";
 
 export function DashboardShell() {
+  const { t } = useI18n();
   const pathname = usePathname();
   const router = useRouter();
   const searchParams = useSearchParams();
@@ -221,12 +223,12 @@ export function DashboardShell() {
     if (!selectedConversationId) return;
     try {
       await closeConversation(selectedConversationId);
-      toast.success("已关闭会话");
+      toast.success(t("agent.chat.toast.conversationClosed"));
       // 清空选中并刷新列表/详情
       selectConversation(null);
       refreshConversations();
     } catch (e) {
-      toast.error((e as Error).message || "关闭会话失败");
+      toast.error((e as Error).message || t("agent.chat.toast.closeFailed"));
     }
   }, [refreshConversations, selectConversation, selectedConversationId]);
 
@@ -280,7 +282,7 @@ export function DashboardShell() {
       selectConversation(conversation_id);
     } catch (e) {
       console.error("创建内部对话失败:", e);
-      toast.error((e as Error).message || "创建内部对话失败");
+      toast.error((e as Error).message || t("agent.internalChat.createFailed"));
     }
   }, [agent?.id, refreshConversations, selectConversation]);
 
@@ -354,6 +356,11 @@ export function DashboardShell() {
               soundEnabled={soundEnabled}
               onToggleSound={toggleSound}
               hideAIToggle={isInternalChat}
+              mobileGutters={{
+                left: true,
+                right:
+                  currentPage === "dashboard" && selectedConversationId != null,
+              }}
             />
             <MessageList
               messages={messages}
@@ -382,7 +389,7 @@ export function DashboardShell() {
                       <div className="flex justify-start mt-2">
                         <div className="inline-flex items-center gap-2 px-4 py-2 rounded-2xl rounded-bl-none bg-card border border-border/50 shadow-sm text-sm text-muted-foreground">
                           <Loader2 className="w-4 h-4 animate-spin flex-shrink-0" />
-                          <span>AI 正在思考...</span>
+                          <span>{t("agent.internalChat.aiThinking")}</span>
                         </div>
                       </div>
                     ) : null}
@@ -399,7 +406,7 @@ export function DashboardShell() {
                     onCheckedChange={(v) => setNeedWebSearch(Boolean(v))}
                   />
                   <Label htmlFor="internal-need-web-search" className="cursor-pointer font-normal">
-                    本回合联网搜索
+                    {t("agent.internalChat.webSearchThisTurn")}
                   </Label>
                 </div>
               </div>
@@ -413,8 +420,8 @@ export function DashboardShell() {
             />
           </>
         ) : (
-          <div className="flex-1 flex items-center justify-center text-muted-foreground text-sm">
-            {isInternalChat ? "选择或新建内部对话,测试知识库效果" : "选择一个对话开始聊天"}
+          <div className="flex-1 flex items-center justify-center text-muted-foreground text-sm px-6 text-center">
+            {isInternalChat ? t("agent.internalChat.emptyHint") : t("agent.chat.emptyPick")}
           </div>
         )
       ) : (

+ 12 - 0
frontend/components/dashboard/DashboardSuspenseFallback.tsx

@@ -0,0 +1,12 @@
+"use client";
+
+import { useI18n } from "@/lib/i18n/provider";
+
+export function DashboardSuspenseFallback() {
+  const { t } = useI18n();
+  return (
+    <div className="flex justify-center items-center min-h-screen bg-background">
+      <div className="text-muted-foreground">{t("common.loading")}</div>
+    </div>
+  );
+}

+ 20 - 7
frontend/components/dashboard/MessageInput.tsx

@@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input";
 import { uploadFile, UploadFileResult } from "@/features/agent/services/messageApi";
 import { X, Paperclip, Image as ImageIcon } from "lucide-react";
 import { toast } from "@/hooks/useToast";
+import { useI18n } from "@/lib/i18n/provider";
 
 interface MessageInputProps {
   value: string;
@@ -27,6 +28,7 @@ export function MessageInput({
   sending,
   conversationId,
 }: MessageInputProps) {
+  const { t } = useI18n();
   // 输入框引用,用于发送消息后自动聚焦
   const inputRef = useRef<HTMLInputElement>(null);
   // 文件输入框引用
@@ -58,7 +60,7 @@ export function MessageInput({
       // 验证文件大小(10MB)
       const MAX_FILE_SIZE = 10 * 1024 * 1024;
       if (file.size > MAX_FILE_SIZE) {
-        toast.error("文件大小超过限制(最大10MB)");
+        toast.error(t("agent.input.fileTooLarge"));
         return;
       }
 
@@ -66,7 +68,7 @@ export function MessageInput({
       const ext = file.name.toLowerCase().split(".").pop();
       const allowedExts = ["jpg", "jpeg", "png", "gif", "webp", "pdf", "doc", "docx", "txt"];
       if (!ext || !allowedExts.includes(ext)) {
-        toast.error("不支持的文件类型");
+        toast.error(t("agent.input.fileTypeNotSupported"));
         return;
       }
 
@@ -178,7 +180,7 @@ export function MessageInput({
         try {
           fileInfo = await uploadFile(filePreview.file, conversationId);
         } catch (error) {
-          toast.error((error as Error).message || "文件上传失败");
+          toast.error((error as Error).message || t("agent.input.uploadFailed"));
           setUploading(false);
           return;
         }
@@ -254,7 +256,10 @@ export function MessageInput({
       )}
 
       {/* 输入区域 */}
-      <form onSubmit={handleSubmit} className="px-4 py-3 flex items-center gap-2">
+      <form
+        onSubmit={handleSubmit}
+        className="px-4 py-3 flex items-center gap-2 pb-[max(0.75rem,env(safe-area-inset-bottom,0px))]"
+      >
         <input
           ref={fileInputRef}
           type="file"
@@ -268,7 +273,7 @@ export function MessageInput({
           size="sm"
           onClick={() => fileInputRef.current?.click()}
           disabled={sending || uploading}
-          title="上传文件"
+          title={t("agent.input.upload")}
           className="hover:bg-primary/10 hover:text-primary transition-colors"
         >
           <Paperclip className="w-4 h-4" />
@@ -276,7 +281,11 @@ export function MessageInput({
         <Input
           ref={inputRef}
           type="text"
-          placeholder={filePreview ? "添加消息(可选)..." : "输入消息..."}
+          placeholder={
+            filePreview
+              ? t("agent.input.placeholder.withAttachment")
+              : t("agent.input.placeholder")
+          }
           value={value}
           onChange={(event) => onChange(event.target.value)}
           className="flex-1 border-border/50 focus:border-primary/50 focus:ring-primary/20"
@@ -289,7 +298,11 @@ export function MessageInput({
           size="default"
           className="bg-primary hover:bg-primary/90 shadow-md hover:shadow-lg transition-all"
         >
-          {uploading ? "上传中..." : sending ? "发送中..." : "发送"}
+          {uploading
+            ? t("agent.input.uploading")
+            : sending
+              ? t("agent.input.sending")
+              : t("agent.input.send")}
         </Button>
       </form>
     </div>

+ 86 - 6
frontend/components/dashboard/MessageList.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import { useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
 import { MessageItem } from "@/features/agent/types";
 import { formatMessageTime } from "@/utils/format";
 import { highlightText } from "@/utils/highlight";
@@ -10,6 +10,7 @@ import { Dialog, DialogContent } from "@/components/ui/dialog";
 import { Paperclip, Download, X } from "lucide-react";
 import { API_BASE_URL } from "@/lib/config";
 import { getAvatarUrl } from "@/utils/avatar";
+import { useI18n } from "@/lib/i18n/provider";
 
 function TypewriterText({
   text,
@@ -81,6 +82,7 @@ export function MessageList({
   internalChatMode = false,
   leftAvatarBySenderId,
 }: MessageListProps) {
+  const { t } = useI18n();
   const containerRef = useRef<HTMLDivElement>(null);
   const messageRefs = useRef<Record<number, HTMLDivElement | null>>({});
   const shouldStickToBottomRef = useRef(true);
@@ -90,10 +92,61 @@ export function MessageList({
   const lastMessageIdRef = useRef<number | null>(null);
   const lastMessageCountRef = useRef<number>(0);
   const hasInitialScrolledRef = useRef(false); // 标记是否已经完成初始滚动
+  /** 逐字打字效果:避免历史消息在重进会话/重开小窗时重复播放 */
+  const typewriterInitializedRef = useRef(false);
+  const typewriterSeenIdsRef = useRef<Set<number>>(new Set());
   // 图片预览状态(必须在所有条件返回之前声明)
   const [imagePreviewOpen, setImagePreviewOpen] = useState(false);
   const [previewImageUrl, setPreviewImageUrl] = useState<string | null>(null);
 
+  const typewriterStorageKey =
+    conversationId != null ? `ai_cs_typewriter_seen_ai_${conversationId}` : null;
+
+  const loadTypewriterSeenSet = useCallback(() => {
+    if (typeof window === "undefined" || !typewriterStorageKey) {
+      typewriterSeenIdsRef.current = new Set();
+      return;
+    }
+    try {
+      const raw = window.sessionStorage.getItem(typewriterStorageKey);
+      if (!raw) {
+        typewriterSeenIdsRef.current = new Set();
+        return;
+      }
+      const parsed = JSON.parse(raw);
+      if (!Array.isArray(parsed)) {
+        typewriterSeenIdsRef.current = new Set();
+        return;
+      }
+      typewriterSeenIdsRef.current = new Set(
+        parsed.map((n) => Number(n)).filter((n) => Number.isFinite(n))
+      );
+    } catch {
+      typewriterSeenIdsRef.current = new Set();
+    }
+  }, [typewriterStorageKey]);
+
+  const persistTypewriterSeenSet = useCallback(() => {
+    if (typeof window === "undefined" || !typewriterStorageKey) return;
+    try {
+      const ids = Array.from(typewriterSeenIdsRef.current);
+      const sliced = ids.length > 600 ? ids.slice(ids.length - 600) : ids;
+      window.sessionStorage.setItem(typewriterStorageKey, JSON.stringify(sliced));
+    } catch {
+      // ignore
+    }
+  }, [typewriterStorageKey]);
+
+  const markTypewriterSeen = useCallback(
+    (messageId: number) => {
+      if (!Number.isFinite(messageId)) return;
+      if (typewriterSeenIdsRef.current.has(messageId)) return;
+      typewriterSeenIdsRef.current.add(messageId);
+      persistTypewriterSeenSet();
+    },
+    [persistTypewriterSeenSet]
+  );
+
   useEffect(() => {
     if (conversationId !== lastConversationIdRef.current) {
       lastConversationIdRef.current = conversationId;
@@ -101,9 +154,28 @@ export function MessageList({
       lastMessageIdRef.current = null;
       lastMessageCountRef.current = 0;
       hasInitialScrolledRef.current = false; // 重置初始滚动标记
+      typewriterInitializedRef.current = false;
+      loadTypewriterSeenSet();
     }
   }, [conversationId]);
 
+  // 首次加载某个会话的历史消息:全部视作“已展示过打字”,避免重复播放
+  useEffect(() => {
+    if (typewriterInitializedRef.current) return;
+    if (!messages || messages.length === 0) return;
+    // 确保已载入 storage
+    if (typewriterSeenIdsRef.current.size === 0) {
+      loadTypewriterSeenSet();
+    }
+    for (const msg of messages) {
+      const isAIMessage = Boolean(msg.sender_is_agent) && msg.sender_id === 0;
+      if (isAIMessage) {
+        markTypewriterSeen(msg.id);
+      }
+    }
+    typewriterInitializedRef.current = true;
+  }, [messages, loadTypewriterSeenSet, markTypewriterSeen]);
+
   // 监听滚动事件,当滚动到底部附近时标记消息为已读
   // 注意:即使 disableAutoScroll 为 true,也应该允许通过滚动来标记消息为已读
   useEffect(() => {
@@ -442,9 +514,11 @@ export function MessageList({
               : message.content;
 
           const isAIMessage = Boolean(message.sender_is_agent) && message.sender_id === 0;
-          // 仅当不需要高亮搜索关键词、且该消息为 AI 回复时才启用逐字显示
+          const hasShownTypewriter = typewriterSeenIdsRef.current.has(message.id);
+          // 仅当不需要高亮搜索关键词、且该消息为 AI 回复、且从未展示过打字效果时才启用逐字显示
           const shouldTypewriter =
             isAIMessage &&
+            !hasShownTypewriter &&
             keyword === "" &&
             !message.file_url &&
             typeof message.content === "string" &&
@@ -553,7 +627,13 @@ export function MessageList({
                   {message.content && (
                     <div className="whitespace-pre-wrap break-words text-sm">
                       {shouldTypewriter ? (
-                        <TypewriterText text={message.content} animateKey={message.id} />
+                        (() => {
+                          // 标记为已展示,避免重新进入会话/重开小窗时重复打字
+                          markTypewriterSeen(message.id);
+                          return (
+                            <TypewriterText text={message.content} animateKey={message.id} />
+                          );
+                        })()
                       ) : (
                         bubbleContent
                       )}
@@ -621,9 +701,9 @@ export function MessageList({
                   <div className="mt-1 text-[10px] text-muted-foreground flex flex-wrap gap-x-2 gap-y-0">
                     {message.sources_used.split(",").map((s) => s.trim()).filter(Boolean).map((src) => (
                       <span key={src}>
-                        {src === "knowledge_base" && "已使用知识库"}
-                        {src === "llm" && "已使用大模型"}
-                        {src === "web" && "已使用联网搜索"}
+                        {src === "knowledge_base" && t("agent.aiSource.kb")}
+                        {src === "llm" && t("agent.aiSource.llm")}
+                        {src === "web" && t("agent.aiSource.web")}
                       </span>
                     ))}
                   </div>

+ 13 - 7
frontend/components/dashboard/NavigationSidebar.tsx

@@ -7,8 +7,11 @@ import { Button } from "@/components/ui/button";
 import { websiteConfig } from "@/lib/website-config";
 import { Badge } from "@/components/ui/badge";
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import { LanguageSwitcher } from "@/components/i18n/LanguageSwitcher";
+import { useI18n } from "@/lib/i18n/provider";
 import {
   AGENT_PAGES,
+  type AgentPageItem,
   type NavigationPage,
 } from "@/lib/constants/agent-pages";
 
@@ -33,6 +36,7 @@ export function NavigationSidebar({
   unreadChatCount = 0,
 }: NavigationSidebarProps) {
   const { agent } = useAuth();
+  const { t } = useI18n();
   const [profileMenuOpen, setProfileMenuOpen] = useState(false);
   const menuRef = useRef<HTMLDivElement>(null);
 
@@ -68,7 +72,7 @@ export function NavigationSidebar({
     const need = p.requiredPermission;
     if (!need) return true;
     return permissions.includes(need);
-  });
+  }) as unknown as AgentPageItem[];
 
   return (
     <TooltipProvider delayDuration={0}>
@@ -78,6 +82,7 @@ export function NavigationSidebar({
             const isActive = currentPage === page.id;
             const Icon = page.Icon;
             const showUnread = page.id === "dashboard" && unreadChatCount > 0;
+            const pageTitle = page.titleKey ? t(page.titleKey) : page.title;
             return (
               <Tooltip key={page.id}>
                 <TooltipTrigger asChild>
@@ -88,7 +93,7 @@ export function NavigationSidebar({
                         : "bg-white border border-gray-200 hover:bg-gray-100 text-gray-700"
                     }`}
                     onClick={() => handleNavigate(page.id as NavigationPage)}
-                    aria-label={page.title}
+                    aria-label={pageTitle}
                   >
                     <div className="relative flex items-center justify-center">
                       <Icon className={`h-5 w-5 ${isActive ? "text-white" : "text-gray-600"}`} />
@@ -103,7 +108,7 @@ export function NavigationSidebar({
                     </div>
                   </button>
                 </TooltipTrigger>
-                <TooltipContent side="right">{page.title}</TooltipContent>
+                <TooltipContent side="right">{pageTitle}</TooltipContent>
               </Tooltip>
             );
           })}
@@ -111,6 +116,7 @@ export function NavigationSidebar({
 
       {/* 个人资料按钮和 GitHub 按钮(固定在底部) */}
         <div className="mt-auto flex flex-col items-center gap-2">
+          <LanguageSwitcher variant="ghost" size="icon" className="text-gray-700 hover:text-gray-900" />
           <div className="relative" ref={menuRef}>
             <Tooltip>
               <TooltipTrigger asChild>
@@ -121,7 +127,7 @@ export function NavigationSidebar({
                       : "bg-white border border-gray-200 hover:bg-gray-100"
                   }`}
                   onClick={() => setProfileMenuOpen(!profileMenuOpen)}
-                  aria-label="个人资料"
+                  aria-label={t("agent.profile")}
                 >
                   <div className="flex items-center justify-center">
                     {fullAvatarUrl ? (
@@ -141,7 +147,7 @@ export function NavigationSidebar({
                   </div>
                 </button>
               </TooltipTrigger>
-              <TooltipContent side="right">个人资料</TooltipContent>
+              <TooltipContent side="right">{t("agent.profile")}</TooltipContent>
             </Tooltip>
 
           {profileMenuOpen && (
@@ -196,7 +202,7 @@ export function NavigationSidebar({
                       d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
                     />
                   </svg>
-                  个人资料
+                  {t("agent.profile")}
                 </Button>
                 <Button
                   variant="ghost"
@@ -220,7 +226,7 @@ export function NavigationSidebar({
                       d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
                     />
                   </svg>
-                  退出登录
+                  {t("agent.logout")}
                 </Button>
               </div>
             </div>

+ 36 - 0
frontend/components/i18n/LanguageSwitcher.tsx

@@ -0,0 +1,36 @@
+"use client";
+
+import { useMemo } from "react";
+import { useI18n } from "@/lib/i18n/provider";
+import type { Lang } from "@/lib/i18n/dict";
+import { Button } from "@/components/ui/button";
+
+export function LanguageSwitcher({
+  variant = "ghost",
+  size = "sm",
+  className = "",
+}: {
+  variant?: "ghost" | "outline" | "default";
+  size?: "sm" | "icon" | "default";
+  className?: string;
+}) {
+  const { lang, setLang } = useI18n();
+
+  const next = useMemo<Lang>(() => (lang === "zh-CN" ? "en" : "zh-CN"), [lang]);
+  const label = lang === "zh-CN" ? "EN" : "中文";
+
+  return (
+    <Button
+      type="button"
+      variant={variant}
+      size={size}
+      onClick={() => setLang(next)}
+      className={className}
+      aria-label="Language"
+      title="Language"
+    >
+      {label}
+    </Button>
+  );
+}
+

+ 46 - 44
frontend/components/layout/Footer.tsx

@@ -3,66 +3,64 @@
 import Link from "next/link";
 import { Github, Mail, MessageSquare } from "lucide-react";
 import { websiteConfig } from "@/lib/website-config";
+import { useI18n } from "@/lib/i18n/provider";
 
 /**
  * 官网底部页脚
- * 包含公司信息、友情链接、联系方式等
  */
 export function Footer() {
+  const { t } = useI18n();
+
   return (
     <footer className="border-t bg-muted/30">
       <div className="container mx-auto px-4 py-12">
-        <div className="grid grid-cols-1 md:grid-cols-4 gap-8">
-          {/* 关于产品 */}
+        <div className="grid grid-cols-1 gap-8 md:grid-cols-4">
           <div>
-            <div className="flex items-center space-x-2 mb-4">
-              <div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
-                <span className="text-primary-foreground font-bold text-lg">AI</span>
+            <div className="mb-4 flex items-center space-x-2">
+              <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary">
+                <span className="text-lg font-bold text-primary-foreground">AI</span>
               </div>
               <span className="text-lg font-bold">AI-CS</span>
             </div>
-            <p className="text-sm text-muted-foreground mb-4">
-              AI-CS 是一款 AI 驱动的智能客服系统,融合 AI 技术与人工客服,为企业提供高效、智能的客户服务解决方案。
-            </p>
+            <p className="mb-4 text-sm text-muted-foreground">{t("footer.blurb")}</p>
             <div className="flex items-center space-x-4">
               <a
                 href={websiteConfig.github.repo}
                 target="_blank"
                 rel="noopener noreferrer"
-                className="text-muted-foreground hover:text-foreground transition-colors"
+                className="text-muted-foreground transition-colors hover:text-foreground"
                 aria-label="GitHub"
               >
-                <Github className="w-5 h-5" />
+                <Github className="h-5 w-5" />
               </a>
             </div>
           </div>
 
-          {/* 产品链接 */}
           <div>
-            <h3 className="font-semibold mb-4">产品</h3>
+            <h3 className="mb-4 font-semibold">{t("footer.column.product")}</h3>
             <ul className="space-y-2 text-sm">
               <li>
                 <Link
                   href="#features"
-                  className="text-muted-foreground hover:text-foreground transition-colors"
+                  className="text-muted-foreground transition-colors hover:text-foreground"
                 >
-                  核心能力
+                  {t("nav.features")}
                 </Link>
               </li>
               <li>
                 <Link
                   href="#screenshots"
-                  className="text-muted-foreground hover:text-foreground transition-colors"
+                  className="text-muted-foreground transition-colors hover:text-foreground"
                 >
-                  界面展示
+                  {t("nav.screenshots")}
                 </Link>
               </li>
               <li>
                 <Link
                   href="#quick-start"
-                  className="text-muted-foreground hover:text-foreground transition-colors"
+                  className="text-muted-foreground transition-colors hover:text-foreground"
                 >
-                  快速接入
+                  {t("nav.quickStart")}
                 </Link>
               </li>
               <li>
@@ -70,17 +68,16 @@ export function Footer() {
                   href="/agent/login"
                   target="_blank"
                   rel="noopener noreferrer"
-                  className="text-muted-foreground hover:text-foreground transition-colors"
+                  className="text-muted-foreground transition-colors hover:text-foreground"
                 >
-                  客服登录
+                  {t("nav.agentLogin")}
                 </Link>
               </li>
             </ul>
           </div>
 
-          {/* 友情链接 */}
           <div>
-            <h3 className="font-semibold mb-4">友情链接</h3>
+            <h3 className="mb-4 font-semibold">{t("footer.column.friendLinks")}</h3>
             <ul className="space-y-2 text-sm">
               {websiteConfig.friendLinks.length > 0 ? (
                 websiteConfig.friendLinks.map((link, index) => (
@@ -89,62 +86,68 @@ export function Footer() {
                       href={link.url}
                       target="_blank"
                       rel="noopener noreferrer"
-                      className="text-muted-foreground hover:text-foreground transition-colors"
+                      className="text-muted-foreground transition-colors hover:text-foreground"
                     >
                       {link.name}
                     </a>
                   </li>
                 ))
               ) : (
-                <li className="text-muted-foreground text-xs">
-                  暂无友情链接
-                </li>
+                <li className="text-xs text-muted-foreground">{t("footer.noFriendLinks")}</li>
               )}
             </ul>
           </div>
 
-          {/* 联系我们 */}
           <div>
-            <h3 className="font-semibold mb-4">联系我们</h3>
+            <h3 className="mb-4 font-semibold">{t("footer.column.contact")}</h3>
             <ul className="space-y-3 text-sm">
+              {websiteConfig.contact.email ? (
+                <li className="flex items-center space-x-2 text-muted-foreground">
+                  <Mail className="h-4 w-4 shrink-0" />
+                  <a
+                    href={`mailto:${websiteConfig.contact.email}`}
+                    aria-label={t("footer.emailLabel")}
+                    className="break-all transition-colors hover:text-foreground"
+                  >
+                    {websiteConfig.contact.email}
+                  </a>
+                </li>
+              ) : null}
               <li className="flex items-center space-x-2 text-muted-foreground">
-                <Github className="w-4 h-4" />
+                <Github className="h-4 w-4" />
                 <a
                   href={websiteConfig.github.repo}
                   target="_blank"
                   rel="noopener noreferrer"
-                  className="hover:text-foreground transition-colors"
+                  className="transition-colors hover:text-foreground"
                 >
                   GitHub
                 </a>
               </li>
               <li className="flex items-center space-x-2 text-muted-foreground">
-                <MessageSquare className="w-4 h-4" />
-                <Link
-                  href="/chat"
-                  className="hover:text-foreground transition-colors"
-                >
-                  在线客服
+                <MessageSquare className="h-4 w-4" />
+                <Link href="/chat" className="transition-colors hover:text-foreground">
+                  {t("footer.onlineChat")}
                 </Link>
               </li>
             </ul>
           </div>
         </div>
 
-        {/* 版权信息 */}
-        <div className="mt-8 pt-8 border-t text-center text-sm text-muted-foreground">
+        <div className="mt-8 border-t pt-8 text-center text-sm text-muted-foreground">
           <p className="mb-2">
-            © {websiteConfig.copyright.year} {websiteConfig.copyright.company}. All rights reserved.
+            © {websiteConfig.copyright.year} {websiteConfig.copyright.company}.{" "}
+            {t("footer.allRightsReserved")}
           </p>
           <p>
-            Powered by Next.js & Go | 
+            {t("footer.poweredBy")}{" "}
             <a
               href={websiteConfig.github.repo}
               target="_blank"
               rel="noopener noreferrer"
-              className="ml-1 hover:text-foreground transition-colors"
+              className="hover:text-foreground transition-colors"
             >
-              开源协议
+              {t("footer.openSourceLicense")}
             </a>
           </p>
         </div>
@@ -152,4 +155,3 @@ export function Footer() {
     </footer>
   );
 }
-

+ 74 - 7
frontend/components/layout/Header.tsx

@@ -2,14 +2,25 @@
 
 import Link from "next/link";
 import { Button } from "@/components/ui/button";
-import { Github } from "lucide-react";
+import {
+  Sheet,
+  SheetContent,
+  SheetHeader,
+  SheetTitle,
+  SheetTrigger,
+  SheetClose,
+} from "@/components/ui/sheet";
+import { Github, Menu } from "lucide-react";
 import { websiteConfig } from "@/lib/website-config";
+import { LanguageSwitcher } from "@/components/i18n/LanguageSwitcher";
+import { useI18n } from "@/lib/i18n/provider";
 
 /**
  * 官网顶部导航栏
  * 包含 Logo、导航链接和 GitHub 链接
  */
 export function Header() {
+  const { t } = useI18n();
   return (
     <header className="sticky top-0 z-50 w-full border-b border-border/50 bg-background/80 backdrop-blur-md">
       <div className="container mx-auto px-6">
@@ -21,25 +32,79 @@ export function Header() {
             <span className="text-[19px] font-semibold text-foreground tracking-tight">AI-CS</span>
           </Link>
 
-          <div className="flex items-center gap-6 md:gap-8">
+          <div className="flex items-center gap-2 sm:gap-4 md:gap-8">
+            <Sheet>
+              <SheetTrigger asChild>
+                <Button
+                  variant="ghost"
+                  size="icon"
+                  className="md:hidden"
+                  aria-label={t("nav.menu")}
+                >
+                  <Menu className="w-5 h-5" />
+                </Button>
+              </SheetTrigger>
+              <SheetContent side="right" className="w-[min(100vw-2rem,20rem)]">
+                <SheetHeader>
+                  <SheetTitle className="text-left">{t("nav.menu")}</SheetTitle>
+                </SheetHeader>
+                <nav className="flex flex-col gap-1 mt-8">
+                  <SheetClose asChild>
+                    <Link
+                      href="#features"
+                      className="py-3 text-[15px] text-muted-foreground hover:text-foreground transition-colors border-b border-border/60"
+                    >
+                      {t("nav.features")}
+                    </Link>
+                  </SheetClose>
+                  <SheetClose asChild>
+                    <Link
+                      href="#screenshots"
+                      className="py-3 text-[15px] text-muted-foreground hover:text-foreground transition-colors border-b border-border/60"
+                    >
+                      {t("nav.screenshots")}
+                    </Link>
+                  </SheetClose>
+                  <SheetClose asChild>
+                    <Link
+                      href="#quick-start"
+                      className="py-3 text-[15px] text-muted-foreground hover:text-foreground transition-colors border-b border-border/60"
+                    >
+                      {t("nav.quickStart")}
+                    </Link>
+                  </SheetClose>
+                  <SheetClose asChild>
+                    <Link
+                      href="/agent/login"
+                      target="_blank"
+                      rel="noopener noreferrer"
+                      className="py-3 text-[15px] text-muted-foreground hover:text-foreground transition-colors"
+                    >
+                      {t("nav.agentLogin")}
+                    </Link>
+                  </SheetClose>
+                </nav>
+              </SheetContent>
+            </Sheet>
+
             <nav className="hidden md:flex items-center gap-6">
               <Link
                 href="#features"
                 className="text-[15px] text-muted-foreground hover:text-foreground transition-colors"
               >
-                核心能力
+                {t("nav.features")}
               </Link>
               <Link
                 href="#screenshots"
                 className="text-[15px] text-muted-foreground hover:text-foreground transition-colors"
               >
-                界面展示
+                {t("nav.screenshots")}
               </Link>
               <Link
                 href="#quick-start"
                 className="text-[15px] text-muted-foreground hover:text-foreground transition-colors"
               >
-                快速接入
+                {t("nav.quickStart")}
               </Link>
               <Link
                 href="/agent/login"
@@ -47,10 +112,11 @@ export function Header() {
                 rel="noopener noreferrer"
                 className="text-[15px] text-muted-foreground hover:text-foreground transition-colors"
               >
-                客服登录
+                {t("nav.agentLogin")}
               </Link>
             </nav>
 
+            <LanguageSwitcher variant="ghost" size="sm" className="hidden sm:flex text-muted-foreground hover:text-foreground" />
             <Button variant="ghost" size="sm" asChild className="hidden sm:flex text-muted-foreground hover:text-foreground">
               <a
                 href={websiteConfig.github.repo}
@@ -59,7 +125,7 @@ export function Header() {
                 className="flex items-center gap-2"
               >
                 <Github className="w-4 h-4" />
-                <span>GitHub</span>
+                <span>{t("common.github")}</span>
               </a>
             </Button>
             <Button variant="ghost" size="icon" asChild className="sm:hidden text-muted-foreground hover:text-foreground">
@@ -72,6 +138,7 @@ export function Header() {
                 <Github className="w-5 h-5" />
               </a>
             </Button>
+            <LanguageSwitcher variant="ghost" size="icon" className="sm:hidden text-muted-foreground hover:text-foreground" />
           </div>
         </div>
       </div>

+ 42 - 42
frontend/components/layout/ResponsiveLayout.tsx

@@ -3,23 +3,15 @@
 import * as React from "react";
 import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
 import { Button } from "@/components/ui/button";
-import { Menu } from "lucide-react";
+import { Menu, PanelRight } from "lucide-react";
 import { LAYOUT } from "@/lib/constants/breakpoints";
+import { useI18n } from "@/lib/i18n/provider";
 
 /**
  * ResponsiveLayout - 响应式布局组件
- * 
+ *
  * 提供统一的响应式布局,支持桌面端和移动端自适应。
- * 
- * @example
- * ```tsx
- * <ResponsiveLayout
- *   sidebar={<ConversationSidebar />}
- *   main={<MessageList />}
- *   rightPanel={<VisitorDetailPanel />}
- * />
- * ```
- * 
+ *
  * @param sidebar - 侧边栏内容(桌面端显示,移动端可折叠)
  * @param main - 主内容区(所有设备都显示)
  * @param rightPanel - 右侧面板(大屏幕显示,小屏幕隐藏或折叠)
@@ -32,7 +24,7 @@ export interface ResponsiveLayoutProps {
   rightPanel?: React.ReactNode;
   header?: React.ReactNode;
   className?: string;
-  sidebarWidth?: string; // 侧边栏宽度(可选,默认使用 LAYOUT.sidebarWidth)
+  sidebarWidth?: string;
 }
 
 export function ResponsiveLayout({
@@ -44,70 +36,79 @@ export function ResponsiveLayout({
   sidebarWidth,
 }: ResponsiveLayoutProps) {
   const actualSidebarWidth = sidebarWidth || LAYOUT.sidebarWidth;
-  
+  const { t } = useI18n();
+
+  /** 与顶部安全区对齐;与 ChatHeader(h-16)上圆钮视觉对齐 */
+  const mobileFabTop =
+    "top-[max(0.75rem,env(safe-area-inset-top,0px)+0.25rem)]";
+
   return (
-    <div className={`flex h-screen bg-background overflow-hidden ${className || ""}`}>
-      {/* 桌面端侧边栏:中等屏幕及以上显示 */}
+    <div
+      className={`flex h-[100dvh] max-h-[100dvh] bg-background overflow-hidden ${className || ""}`}
+    >
       {sidebar && (
-        <aside className={`hidden md:block border-r bg-background flex-shrink-0`} style={{ width: actualSidebarWidth }}>
+        <aside
+          className="hidden md:block border-r bg-background flex-shrink-0"
+          style={{ width: actualSidebarWidth }}
+        >
           {sidebar}
         </aside>
       )}
 
-      {/* 移动端侧边栏:使用 Sheet 组件实现可折叠侧边栏 */}
       {sidebar && (
         <Sheet>
           <SheetTrigger asChild>
             <Button
-              variant="ghost"
+              variant="outline"
               size="icon"
-              className="fixed top-4 left-4 z-50 md:hidden bg-background/80 backdrop-blur-sm"
+              className={`fixed left-3 z-50 md:hidden h-10 w-10 rounded-full border-border/80 bg-background/90 shadow-sm backdrop-blur-sm ${mobileFabTop}`}
+              aria-label={t("agent.layout.openNavMenu")}
             >
-              <Menu className="h-6 w-6" />
-              <span className="sr-only">打开菜单</span>
+              <Menu className="h-5 w-5" />
             </Button>
           </SheetTrigger>
-          <SheetContent side="left" className="w-80 p-0" style={{ width: actualSidebarWidth }}>
+          <SheetContent
+            side="left"
+            className="w-[min(100vw-2rem,24rem)] max-w-[24rem] p-0"
+            style={{ width: actualSidebarWidth }}
+          >
             {sidebar}
           </SheetContent>
         </Sheet>
       )}
 
-      {/* 主内容区 */}
       <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
-        {/* 顶部栏(如果提供) */}
         {header && (
-          <header className="flex-shrink-0 border-b bg-background">
-            {header}
-          </header>
+          <header className="flex-shrink-0 border-b bg-background">{header}</header>
         )}
 
-        {/* 主内容区和右侧面板容器 */}
         <div className="flex flex-1 min-h-0 overflow-hidden">
-          {/* 主内容区 */}
-          <main className="flex-1 flex flex-col min-h-0 overflow-hidden">
-            {main}
-          </main>
+          <main className="flex-1 flex flex-col min-h-0 overflow-hidden">{main}</main>
 
-          {/* 右侧面板:大屏幕显示,小屏幕隐藏 */}
           {rightPanel && (
             <>
-              <aside className={`hidden lg:block border-l bg-background flex-shrink-0`} style={{ width: LAYOUT.rightPanelWidth }}>
+              <aside
+                className="hidden lg:block border-l bg-background flex-shrink-0"
+                style={{ width: LAYOUT.rightPanelWidth }}
+              >
                 {rightPanel}
               </aside>
-              {/* 移动端右侧面板:使用 Sheet 组件实现可折叠面板 */}
               <Sheet>
                 <SheetTrigger asChild>
                   <Button
-                    variant="ghost"
+                    variant="outline"
                     size="icon"
-                    className="fixed top-4 right-4 z-50 lg:hidden bg-background/80 backdrop-blur-sm"
+                    className={`fixed right-3 z-50 lg:hidden h-10 w-10 rounded-full border-border/80 bg-background/90 shadow-sm backdrop-blur-sm ${mobileFabTop}`}
+                    aria-label={t("agent.layout.openVisitorPanel")}
                   >
-                    <Menu className="h-6 w-6" />
-                    <span className="sr-only">打开详情</span>
+                    <PanelRight className="h-5 w-5" />
                   </Button>
                 </SheetTrigger>
-                <SheetContent side="right" className="w-80 p-0" style={{ width: LAYOUT.rightPanelWidth }}>
+                <SheetContent
+                  side="right"
+                  className="w-[min(100vw-2rem,20rem)] max-w-[20rem] p-0"
+                  style={{ width: LAYOUT.rightPanelWidth }}
+                >
                   {rightPanel}
                 </SheetContent>
               </Sheet>
@@ -118,4 +119,3 @@ export function ResponsiveLayout({
     </div>
   );
 }
-

+ 116 - 141
frontend/components/marketing/HomePageClient.tsx

@@ -29,145 +29,121 @@ import { Footer } from "@/components/layout/Footer";
 import { FadeIn, FadeInStagger, FadeInItem } from "@/components/ui/fade-in";
 import { websiteConfig } from "@/lib/website-config";
 import { stats } from "@/lib/stats-config";
+import type { I18nKey } from "@/lib/i18n/dict";
+import { useI18n } from "@/lib/i18n/provider";
+import type { LucideIcon } from "lucide-react";
 
-const capabilityCards = [
-  {
-    icon: Bot,
-    title: "多模型 AI 客服",
-    description:
-      "支持配置多家大模型与绘画等能力,访客与后台可统一管理模型与使用方式,便于替换供应商、控制成本。",
-  },
-  {
-    icon: BookOpen,
-    title: "知识库与 RAG",
-    description:
-      "文档入库、向量检索,让回答贴近你的业务资料;回复可标记是否使用知识库、模型或联网,便于核对与优化。",
-  },
-  {
-    icon: Wand2,
-    title: "提示词工程",
-    description:
-      "配置系统中使用的提示词模板,用于不同领域 RAG、联网等不同的业务场景。",
-  },
-  {
-    icon: Users,
-    title: "人工客服与实时协作",
-    description:
-      "在线状态、会话实时推送(WebSocket),支持人工接管与日常协作;访客小窗可嵌入任意站点。",
-  },
-  {
-    icon: LineChart,
-    title: "可视化报表",
-    description:
-      "按日或自定义区间查看访客小窗打开、会话与消息、AI 回复与失败率、知识库命中率等指标,快速掌握运营态势。",
-  },
-  {
-    icon: ScrollText,
-    title: "日志中心",
-    description:
-      "结构化日志按分类与事件落库,支持 trace_id 与关键字筛选,关键链路与异常可追溯,便于排障与审计。",
-  },
+const CAPABILITY_ITEMS: {
+  icon: LucideIcon;
+  titleKey: I18nKey;
+  descKey: I18nKey;
+}[] = [
+  { icon: Bot, titleKey: "home.cap.multimodel.title", descKey: "home.cap.multimodel.desc" },
+  { icon: BookOpen, titleKey: "home.cap.kb.title", descKey: "home.cap.kb.desc" },
+  { icon: Wand2, titleKey: "home.cap.prompt.title", descKey: "home.cap.prompt.desc" },
+  { icon: Users, titleKey: "home.cap.human.title", descKey: "home.cap.human.desc" },
+  { icon: LineChart, titleKey: "home.cap.reports.title", descKey: "home.cap.reports.desc" },
+  { icon: ScrollText, titleKey: "home.cap.logs.title", descKey: "home.cap.logs.desc" },
 ];
 
-const steps = [
-  {
-    title: "克隆与配置",
-    body: "复制 .env 模板,填好数据库与管理员等必填项。",
-  },
-  {
-    title: "一键启动",
-    body: "使用 Docker Compose 拉起前后端与依赖服务(详见 README)。",
-  },
-  {
-    title: "嵌入访客端",
-    body: "在站点中挂载聊天小窗,后台完成模型与知识库配置后即可对外服务。",
-  },
+const QUICK_STEPS: { titleKey: I18nKey; bodyKey: I18nKey }[] = [
+  { titleKey: "home.step1.title", bodyKey: "home.step1.body" },
+  { titleKey: "home.step2.title", bodyKey: "home.step2.body" },
+  { titleKey: "home.step3.title", bodyKey: "home.step3.body" },
 ];
 
-const screenshotCards = [
+const SCREENSHOT_ITEMS: {
+  slug: string;
+  imageName: string;
+  placeholderIcon: LucideIcon;
+  titleKey: I18nKey;
+  placeholderKey: I18nKey;
+  altKey: I18nKey;
+}[] = [
   {
-    key: "dashboard",
-    title: "工作台",
+    slug: "dashboard",
     imageName: "dashboard.png",
     placeholderIcon: LayoutDashboard,
-    placeholderText: "工作台界面",
-    alt: "AI-CS 工作台界面",
+    titleKey: "home.ss.dashboard.title",
+    placeholderKey: "home.ss.dashboard.placeholder",
+    altKey: "home.ss.dashboard.alt",
   },
   {
-    key: "visitor",
-    title: "访客端",
+    slug: "visitor",
     imageName: "visitor.png",
     placeholderIcon: Globe,
-    placeholderText: "访客端界面",
-    alt: "AI-CS 访客端界面",
+    titleKey: "home.ss.visitor.title",
+    placeholderKey: "home.ss.visitor.placeholder",
+    altKey: "home.ss.visitor.alt",
   },
   {
-    key: "ai-config",
-    title: "AI配置",
+    slug: "ai-config",
     imageName: "ai-config.png",
     placeholderIcon: Bot,
-    placeholderText: "AI配置界面",
-    alt: "AI-CS AI配置界面",
+    titleKey: "home.ss.aiconfig.title",
+    placeholderKey: "home.ss.aiconfig.placeholder",
+    altKey: "home.ss.aiconfig.alt",
   },
   {
-    key: "users",
-    title: "用户管理",
+    slug: "users",
     imageName: "users.png",
     placeholderIcon: Users,
-    placeholderText: "用户管理界面",
-    alt: "AI-CS 用户管理界面",
+    titleKey: "home.ss.users.title",
+    placeholderKey: "home.ss.users.placeholder",
+    altKey: "home.ss.users.alt",
   },
   {
-    key: "faq",
-    title: "FAQ管理",
+    slug: "faq",
     imageName: "faq.png",
     placeholderIcon: FileText,
-    placeholderText: "FAQ管理界面",
-    alt: "AI-CS FAQ管理界面",
+    titleKey: "home.ss.faq.title",
+    placeholderKey: "home.ss.faq.placeholder",
+    altKey: "home.ss.faq.alt",
   },
   {
-    key: "knowledge",
-    title: "知识库管理",
+    slug: "knowledge",
     imageName: "knowledge.png",
     placeholderIcon: BookOpen,
-    placeholderText: "知识库管理界面",
-    alt: "AI-CS 知识库管理界面",
+    titleKey: "home.ss.knowledge.title",
+    placeholderKey: "home.ss.knowledge.placeholder",
+    altKey: "home.ss.knowledge.alt",
   },
   {
-    key: "conversations",
-    title: "知识库测试",
+    slug: "conversations",
     imageName: "conversations.png",
     placeholderIcon: MessageSquare,
-    placeholderText: "知识库测试界面",
-    alt: "AI-CS 知识库测试界面",
+    titleKey: "home.ss.kbtest.title",
+    placeholderKey: "home.ss.kbtest.placeholder",
+    altKey: "home.ss.kbtest.alt",
   },
   {
-    key: "prompts",
-    title: "提示词工程",
+    slug: "prompts",
     imageName: "prompts.png",
     placeholderIcon: Wand2,
-    placeholderText: "提示词工程界面",
-    alt: "AI-CS 提示词工程界面",
+    titleKey: "home.ss.prompts.title",
+    placeholderKey: "home.ss.prompts.placeholder",
+    altKey: "home.ss.prompts.alt",
   },
   {
-    key: "logs",
-    title: "日志中心",
+    slug: "logs",
     imageName: "logs.png",
     placeholderIcon: ScrollText,
-    placeholderText: "日志中心界面",
-    alt: "AI-CS 日志中心界面",
+    titleKey: "home.ss.logs.title",
+    placeholderKey: "home.ss.logs.placeholder",
+    altKey: "home.ss.logs.alt",
   },
   {
-    key: "analytics",
-    title: "可视化报表",
+    slug: "analytics",
     imageName: "analytics.png",
     placeholderIcon: LineChart,
-    placeholderText: "可视化报表界面",
-    alt: "AI-CS 可视化报表界面",
+    titleKey: "home.ss.analytics.title",
+    placeholderKey: "home.ss.analytics.placeholder",
+    altKey: "home.ss.analytics.alt",
   },
 ];
 
 export function HomePageClient() {
+  const { t } = useI18n();
   const [visitorId, setVisitorId] = useState<number | null>(null);
   const [isChatOpen, setIsChatOpen] = useState(false);
   const [activeScreenshot, setActiveScreenshot] = useState(0);
@@ -192,7 +168,7 @@ export function HomePageClient() {
     }
   };
 
-  const totalScreenshots = screenshotCards.length;
+  const totalScreenshots = SCREENSHOT_ITEMS.length;
   const prevScreenshotIndex =
     (activeScreenshot - 1 + totalScreenshots) % totalScreenshots;
   const nextScreenshotIndex = (activeScreenshot + 1) % totalScreenshots;
@@ -230,13 +206,13 @@ export function HomePageClient() {
           <FadeIn>
             <div className="mx-auto max-w-4xl text-center">
               <p className="mb-4 text-sm font-medium text-muted-foreground tracking-wide uppercase">
-                AI 智能客服
+                {t("home.hero.tagline")}
               </p>
               <h1 className="mb-6 text-balance text-4xl font-bold tracking-tight text-foreground sm:text-5xl md:text-6xl md:leading-[1.12]">
-                让客户服务更简单、更高效
+                {t("home.hero.title")}
               </h1>
               <p className="mx-auto mb-10 max-w-3xl text-pretty text-lg sm:text-xl text-muted-foreground leading-relaxed">
-                7×24 小时智能应答,AI 与人工无缝切换,释放团队时间专注更有价值的事
+                {t("home.hero.subtitle")}
               </p>
               <div className="flex flex-col items-stretch justify-center gap-3 sm:flex-row sm:flex-wrap sm:items-center">
                 <Button
@@ -244,7 +220,7 @@ export function HomePageClient() {
                   className="rounded-xl bg-blue-600 px-8 py-6 text-[15px] shadow-sm transition-all hover:bg-blue-500 hover:shadow-md"
                   onClick={handleOpenChat}
                 >
-                  立即体验
+                  {t("home.hero.cta.tryNow")}
                   <ArrowRight className="ml-2 h-4 w-4" />
                 </Button>
                 <Button
@@ -259,11 +235,11 @@ export function HomePageClient() {
                     rel="noopener noreferrer"
                     className="inline-flex items-center justify-center gap-2"
                   >
-                    客服登录
+                    {t("home.hero.cta.agentLogin")}
                   </Link>
                 </Button>
               </div>
-              <p className="mt-4 text-sm text-muted-foreground">无需等待,可立即使用</p>
+              <p className="mt-4 text-sm text-muted-foreground">{t("home.hero.hint")}</p>
             </div>
           </FadeIn>
         </div>
@@ -274,13 +250,15 @@ export function HomePageClient() {
         <FadeIn>
           <div className="container mx-auto px-6">
             <p className="text-xs font-medium text-muted-foreground text-center mb-8 tracking-wide">
-              深受企业信赖
+              {t("home.stats.trustedBy")}
             </p>
             <div className="grid grid-cols-2 md:grid-cols-4 gap-10 max-w-6xl mx-auto">
               {stats.map((stat) => (
-                <div key={stat.label} className="text-center">
-                  <div className="text-3xl md:text-4xl font-semibold text-foreground">{stat.value}</div>
-                  <div className="text-sm text-muted-foreground mt-1">{stat.label}</div>
+                <div key={stat.labelKey} className="text-center">
+                  <div className="text-3xl md:text-4xl font-semibold text-foreground">
+                    {t(stat.valueKey)}
+                  </div>
+                  <div className="mt-1 text-sm text-muted-foreground">{t(stat.labelKey)}</div>
                 </div>
               ))}
             </div>
@@ -295,28 +273,28 @@ export function HomePageClient() {
           <FadeIn>
             <div className="mb-14 text-center px-4">
               <h2 className="mb-3 text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
-                核心能力
+                {t("home.features.title")}
               </h2>
               <p className="mx-auto max-w-xl text-base text-muted-foreground">
-                从模型、知识库、提示词到人工协作、报表与日志,一套系统串起来。
+                {t("home.features.lead")}
               </p>
             </div>
           </FadeIn>
           <FadeInStagger className="mx-auto grid max-w-6xl grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 lg:gap-6">
-            {capabilityCards.map((item) => {
+            {CAPABILITY_ITEMS.map((item) => {
               const Icon = item.icon;
               return (
-                <FadeInItem key={item.title}>
+                <FadeInItem key={item.titleKey}>
                   <Card className="group h-full border border-border/60 bg-card/90 shadow-sm backdrop-blur-sm transition-all duration-300 hover:-translate-y-0.5 hover:border-blue-200/70 hover:shadow-md">
                     <CardHeader className="pb-3">
                       <div className="mb-4 flex h-11 w-11 items-center justify-center rounded-xl border border-blue-100/80 bg-gradient-to-br from-blue-50 to-background text-blue-700 transition-transform duration-300 group-hover:scale-[1.03]">
                         <Icon className="h-5 w-5" />
                       </div>
                       <CardTitle className="text-lg font-semibold tracking-tight">
-                        {item.title}
+                        {t(item.titleKey)}
                       </CardTitle>
                       <CardDescription className="text-sm leading-relaxed text-muted-foreground">
-                        {item.description}
+                        {t(item.descKey)}
                       </CardDescription>
                     </CardHeader>
                   </Card>
@@ -336,17 +314,17 @@ export function HomePageClient() {
           <div className="container mx-auto px-6">
             <div className="mb-14 text-center px-4">
               <h2 className="mb-3 text-3xl font-semibold tracking-tight sm:text-4xl">
-                界面展示
+                {t("home.screenshots.title")}
               </h2>
               <p className="mx-auto max-w-xl text-muted-foreground">
-                精心设计的界面,让管理更轻松
+                {t("home.screenshots.lead")}
               </p>
             </div>
             <div className="mx-auto max-w-6xl">
               <div className="mb-8 flex flex-wrap justify-center gap-2">
-                {screenshotCards.map((item, idx) => (
+                {SCREENSHOT_ITEMS.map((item, idx) => (
                   <button
-                    key={item.key}
+                    key={item.slug}
                     type="button"
                     onClick={() => setActiveScreenshot(idx)}
                     className={`rounded-full px-4 py-1.5 text-sm transition-all ${
@@ -355,7 +333,7 @@ export function HomePageClient() {
                         : "bg-background text-muted-foreground border border-border/70 hover:text-foreground hover:border-blue-200"
                     }`}
                   >
-                    {item.title}
+                    {t(item.titleKey)}
                   </button>
                 ))}
               </div>
@@ -368,7 +346,7 @@ export function HomePageClient() {
                     type="button"
                     onClick={goPrevScreenshot}
                     className="absolute left-0 top-1/2 z-40 -translate-y-1/2 rounded-full border border-border/70 bg-background/90 p-2.5 shadow-sm backdrop-blur transition hover:border-blue-200 hover:bg-background"
-                    aria-label="查看上一张"
+                    aria-label={t("home.screenshots.prevAria")}
                   >
                     <ChevronLeft className="h-5 w-5" />
                   </button>
@@ -377,7 +355,7 @@ export function HomePageClient() {
                     type="button"
                     onClick={goNextScreenshot}
                     className="absolute right-0 top-1/2 z-40 -translate-y-1/2 rounded-full border border-border/70 bg-background/90 p-2.5 shadow-sm backdrop-blur transition hover:border-blue-200 hover:bg-background"
-                    aria-label="查看下一张"
+                    aria-label={t("home.screenshots.nextAria")}
                   >
                     <ChevronRight className="h-5 w-5" />
                   </button>
@@ -386,10 +364,10 @@ export function HomePageClient() {
                   <div className="absolute left-6 right-[42%] top-7 z-10 hidden overflow-hidden rounded-2xl border border-border/60 bg-background/85 shadow-md md:block">
                     <div className="pointer-events-none absolute inset-0 z-10 bg-background/18" />
                     <ScreenshotDisplay
-                      imageName={screenshotCards[prevScreenshotIndex].imageName}
-                      placeholderIcon={screenshotCards[prevScreenshotIndex].placeholderIcon}
-                      placeholderText={screenshotCards[prevScreenshotIndex].placeholderText}
-                      alt={screenshotCards[prevScreenshotIndex].alt}
+                      imageName={SCREENSHOT_ITEMS[prevScreenshotIndex].imageName}
+                      placeholderIcon={SCREENSHOT_ITEMS[prevScreenshotIndex].placeholderIcon}
+                      placeholderText={t(SCREENSHOT_ITEMS[prevScreenshotIndex].placeholderKey)}
+                      alt={t(SCREENSHOT_ITEMS[prevScreenshotIndex].altKey)}
                     />
                   </div>
 
@@ -397,20 +375,20 @@ export function HomePageClient() {
                   <div className="absolute left-[42%] right-6 top-7 z-10 hidden overflow-hidden rounded-2xl border border-border/60 bg-background/85 shadow-md md:block">
                     <div className="pointer-events-none absolute inset-0 z-10 bg-background/18" />
                     <ScreenshotDisplay
-                      imageName={screenshotCards[nextScreenshotIndex].imageName}
-                      placeholderIcon={screenshotCards[nextScreenshotIndex].placeholderIcon}
-                      placeholderText={screenshotCards[nextScreenshotIndex].placeholderText}
-                      alt={screenshotCards[nextScreenshotIndex].alt}
+                      imageName={SCREENSHOT_ITEMS[nextScreenshotIndex].imageName}
+                      placeholderIcon={SCREENSHOT_ITEMS[nextScreenshotIndex].placeholderIcon}
+                      placeholderText={t(SCREENSHOT_ITEMS[nextScreenshotIndex].placeholderKey)}
+                      alt={t(SCREENSHOT_ITEMS[nextScreenshotIndex].altKey)}
                     />
                   </div>
 
                   {/* 中间主卡片 */}
                   <div className="absolute inset-x-8 top-0 z-30 overflow-hidden rounded-2xl border border-border/70 bg-background shadow-xl ring-1 ring-blue-100/60 md:inset-x-16 lg:inset-x-24">
                     <ScreenshotDisplay
-                      imageName={screenshotCards[activeScreenshot].imageName}
-                      placeholderIcon={screenshotCards[activeScreenshot].placeholderIcon}
-                      placeholderText={screenshotCards[activeScreenshot].placeholderText}
-                      alt={screenshotCards[activeScreenshot].alt}
+                      imageName={SCREENSHOT_ITEMS[activeScreenshot].imageName}
+                      placeholderIcon={SCREENSHOT_ITEMS[activeScreenshot].placeholderIcon}
+                      placeholderText={t(SCREENSHOT_ITEMS[activeScreenshot].placeholderKey)}
+                      alt={t(SCREENSHOT_ITEMS[activeScreenshot].altKey)}
                     />
                   </div>
                 </div>
@@ -430,20 +408,20 @@ export function HomePageClient() {
           <FadeIn>
             <div className="mb-12 text-center px-4">
               <h2 className="mb-3 text-3xl font-semibold tracking-tight sm:text-4xl">
-                快速接入
+                {t("home.quickStart.title")}
               </h2>
-              <p className="text-muted-foreground">三步跑通,从仓库到访客小窗。</p>
+              <p className="text-muted-foreground">{t("home.quickStart.lead")}</p>
             </div>
           </FadeIn>
           <FadeInStagger className="mx-auto grid max-w-4xl grid-cols-1 gap-8 md:grid-cols-3">
-            {steps.map((step, i) => (
-              <FadeInItem key={step.title}>
+            {QUICK_STEPS.map((step, i) => (
+              <FadeInItem key={step.titleKey}>
                 <div className="relative rounded-2xl border border-border/60 bg-card/50 p-6 text-center md:text-left transition-all duration-300 hover:border-blue-200/70 hover:shadow-md hover:-translate-y-0.5">
                   <div className="mx-auto mb-4 flex h-10 w-10 items-center justify-center rounded-full border border-blue-200/80 bg-blue-50/80 text-sm font-semibold text-blue-800 md:mx-0">
                     {i + 1}
                   </div>
-                  <h3 className="mb-2 font-semibold">{step.title}</h3>
-                  <p className="text-sm leading-relaxed text-muted-foreground">{step.body}</p>
+                  <h3 className="mb-2 font-semibold">{t(step.titleKey)}</h3>
+                  <p className="text-sm leading-relaxed text-muted-foreground">{t(step.bodyKey)}</p>
                 </div>
               </FadeInItem>
             ))}
@@ -460,10 +438,10 @@ export function HomePageClient() {
         <div className="container relative mx-auto px-6 py-20 text-center md:py-28">
           <FadeIn>
             <h2 className="mb-4 text-3xl font-semibold tracking-tight sm:text-4xl">
-              准备好把 AI-CS 接到你的产品里了吗?
+              {t("home.cta.title")}
             </h2>
             <p className="mx-auto mb-10 max-w-lg text-muted-foreground leading-relaxed">
-              从开源仓库开始,或用在线 Demo 先看交互与能力边界。
+              {t("home.cta.subtitle")}
             </p>
             <div className="flex flex-col items-stretch justify-center gap-3 sm:flex-row sm:flex-wrap sm:justify-center">
               <Button size="lg" className="rounded-xl bg-blue-600 px-8 shadow-sm hover:bg-blue-500" asChild>
@@ -474,18 +452,15 @@ export function HomePageClient() {
                   className="inline-flex items-center justify-center gap-2"
                 >
                   <Github className="h-4 w-4" />
-                  Star / Fork 仓库
+                  {t("home.cta.starRepo")}
                 </a>
               </Button>
               <Button size="lg" variant="outline" className="rounded-xl border-border/80 px-8 bg-background/80" asChild>
                 <a
-                  // 点击后直接唤起邮箱客户端(可在客户端自动带上主题/正文)
-                  href={`mailto:2930134478@qq.com?subject=${encodeURIComponent("AI-CS 建议反馈")}&body=${encodeURIComponent(
-                    "你好,我想反馈:\n\n1)问题/建议:\n2)影响范围/环境:\n3)期望结果:\n\n---\n联系方式(可选):"
-                  )}`}
+                  href={`mailto:2930134478@qq.com?subject=${encodeURIComponent(t("home.cta.mailSubject"))}&body=${encodeURIComponent(t("home.cta.mailBody"))}`}
                   className="inline-flex items-center justify-center gap-2"
                 >
-                  建议反馈
+                  {t("home.cta.feedback")}
                   <Mail className="h-3.5 w-3.5" />
                 </a>
               </Button>

+ 11 - 3
frontend/components/visitor/ChatWidget.tsx

@@ -39,6 +39,8 @@ import { playNotificationSound } from "@/utils/sound";
 import { getAvatarUrl } from "@/utils/avatar";
 import { cn } from "@/lib/utils";
 import { Check, ChevronDown, Loader2 } from "lucide-react";
+import { useI18n } from "@/lib/i18n/provider";
+import { LanguageSwitcher } from "@/components/i18n/LanguageSwitcher";
 
 interface ChatWidgetProps {
   visitorId: number;
@@ -93,6 +95,7 @@ const CHAT_WIDGET_PANEL_MAX_W =
  * 提供小窗形式的聊天界面,支持展开/收起
  */
 export function ChatWidget({ visitorId, isOpen, onToggle }: ChatWidgetProps) {
+  const { t } = useI18n();
   const WEB_SEARCH_PREF_KEY = "visitor_widget_need_web_search";
   // 数据分析:每次由关→开上报一次小窗打开(供后台「小窗打开次数」统计)
   const prevIsOpenRef = useRef(false);
@@ -741,9 +744,14 @@ export function ChatWidget({ visitorId, isOpen, onToggle }: ChatWidgetProps) {
               />
             </svg>
           </div>
-          <h2 className="text-base font-bold text-white truncate">客服聊天</h2>
+          <h2 className="text-base font-bold text-white truncate">{t("chat.title")}</h2>
         </div>
         <div className="flex items-center gap-2">
+          <LanguageSwitcher
+            variant="ghost"
+            size="sm"
+            className="text-white/90 hover:text-white hover:bg-white/20 h-8 px-2 rounded-lg transition-colors"
+          />
           {/* 声音开关按钮 */}
           <Button
             variant="ghost"
@@ -830,7 +838,7 @@ export function ChatWidget({ visitorId, isOpen, onToggle }: ChatWidgetProps) {
                 : "bg-white text-slate-700 hover:text-slate-900 hover:bg-slate-100 border border-slate-300"
             }
           >
-            人工客服
+            {t("chat.mode.human")}
           </Button>
           <Button
             variant={chatMode === "ai" ? "default" : "outline"}
@@ -844,7 +852,7 @@ export function ChatWidget({ visitorId, isOpen, onToggle }: ChatWidgetProps) {
                 : "bg-white text-slate-700 hover:text-slate-900 hover:bg-slate-100 border border-slate-300"
             }
           >
-            AI 客服
+            {t("chat.mode.ai")}
           </Button>
         </div>
         {/* 模型选择已下沉到输入区发送按钮左侧(仅 AI 模式显示) */}

+ 50 - 0
frontend/features/agent/hooks/useMessages.ts

@@ -202,6 +202,56 @@ export function useMessages({
     refreshConversationDetail(conversationId);
   }, [conversationId, agentId, effectiveIncludeAIMessages, loadMessages, refreshConversationDetail]);
 
+  /**
+   * 关闭「显示 AI 消息」时,列表接口会过滤 chat_mode=ai 的访客消息;
+   * 它们仍会计入 unread_count,但 MessageList 里看不到未读,滚动逻辑永远不会触发标记已读。
+   * 此时在加载完成后自动调用一次 mark,清掉「仅存在于 AI 分段」里的访客未读。
+   */
+  useEffect(() => {
+    if (!conversationId || !agentId) {
+      return;
+    }
+    if (loadingMessages) {
+      return;
+    }
+    if (effectiveIncludeAIMessages) {
+      return;
+    }
+    if (conversationDetail && conversationDetail.id !== conversationId) {
+      return;
+    }
+
+    const serverUnread = Number(conversationDetail?.unread_count ?? 0);
+    if (serverUnread <= 0) {
+      return;
+    }
+
+    const messagesBelongToConv =
+      messages.length === 0 ||
+      messages.every((m) => m.conversation_id === conversationId);
+    if (!messagesBelongToConv) {
+      return;
+    }
+
+    const visibleVisitorUnread = messages.filter(
+      (msg) => !msg.sender_is_agent && !msg.is_read
+    ).length;
+    if (visibleVisitorUnread > 0) {
+      return;
+    }
+
+    void handleMarkMessagesRead(conversationId, true);
+  }, [
+    conversationId,
+    agentId,
+    loadingMessages,
+    effectiveIncludeAIMessages,
+    messages,
+    conversationDetail?.id,
+    conversationDetail?.unread_count,
+    handleMarkMessagesRead,
+  ]);
+
   const handleNewMessageRef = useRef<(message: MessageItem) => void>(() => {});
 
   const handleNewMessage = useCallback(

+ 11 - 0
frontend/lib/constants/agent-pages.tsx

@@ -49,6 +49,8 @@ export interface AgentPageItem {
   id: string;
   label: string;
   title: string;
+  /** i18n:title 对应的 key(可选,未接入时回退 title) */
+  titleKey?: import("@/lib/i18n/dict").I18nKey;
   Icon: LucideIcon;
   /** 需要的功能权限键(单级开关)。admin 视为全权限 */
   requiredPermission?: string;
@@ -67,6 +69,7 @@ export const AGENT_PAGES = [
     id: "dashboard",
     label: "会话对话",
     title: "对话",
+    titleKey: "agent.page.dashboard",
     Icon: MessageCircle,
     requiredPermission: "chat",
     isChatPage: true,
@@ -75,6 +78,7 @@ export const AGENT_PAGES = [
     id: "internal-chat",
     label: "知识测试",
     title: "知识库测试",
+    titleKey: "agent.page.internalChat",
     Icon: Lightbulb,
     requiredPermission: "kb_test",
     isChatPage: true,
@@ -83,6 +87,7 @@ export const AGENT_PAGES = [
     id: "knowledge",
     label: "知识管理",
     title: "知识库",
+    titleKey: "agent.page.knowledge",
     Icon: BookOpen,
     requiredPermission: "knowledge",
     component: KnowledgePage,
@@ -91,6 +96,7 @@ export const AGENT_PAGES = [
     id: "faqs",
     label: "事件管理",
     title: "事件管理",
+    titleKey: "agent.page.faqs",
     Icon: ClipboardList,
     requiredPermission: "faqs",
     component: FAQsPage,
@@ -99,6 +105,7 @@ export const AGENT_PAGES = [
     id: "analytics",
     label: "数据报表",
     title: "数据报表",
+    titleKey: "agent.page.analytics",
     Icon: BarChart3,
     requiredPermission: "analytics",
     component: AnalyticsPage,
@@ -107,6 +114,7 @@ export const AGENT_PAGES = [
     id: "logs",
     label: "日志中心",
     title: "日志中心",
+    titleKey: "agent.page.logs",
     Icon: ScrollText,
     requiredPermission: "logs",
     component: LogsPage,
@@ -115,6 +123,7 @@ export const AGENT_PAGES = [
     id: "users",
     label: "用户管理",
     title: "用户管理",
+    titleKey: "agent.page.users",
     Icon: Users,
     requiredPermission: "users",
     component: UsersPage,
@@ -123,6 +132,7 @@ export const AGENT_PAGES = [
     id: "prompts",
     label: "提示配置",
     title: "提示词",
+    titleKey: "agent.page.prompts",
     Icon: FileText,
     requiredPermission: "prompts",
     component: PromptsPage,
@@ -131,6 +141,7 @@ export const AGENT_PAGES = [
     id: "settings",
     label: "AI配置",
     title: "AI 配置",
+    titleKey: "agent.page.settings",
     Icon: Settings,
     requiredPermission: "settings",
     component: SettingsPage,

+ 1672 - 0
frontend/lib/i18n/dict.ts

@@ -0,0 +1,1672 @@
+export type Lang = "zh-CN" | "en";
+
+export type I18nKey =
+  | "nav.features"
+  | "nav.screenshots"
+  | "nav.quickStart"
+  | "nav.agentLogin"
+  | "nav.menu"
+  | "common.github"
+  | "common.to"
+  | "common.save"
+  | "common.saving"
+  | "common.restoreEnv"
+  | "common.loading"
+  | "common.search"
+  | "common.prevPage"
+  | "common.nextPage"
+  | "common.copy"
+  | "home.hero.tagline"
+  | "home.hero.title"
+  | "home.hero.subtitle"
+  | "home.hero.cta.tryNow"
+  | "home.hero.cta.agentLogin"
+  | "home.hero.hint"
+  | "home.stats.trustedBy"
+  | "home.stats.clients"
+  | "home.stats.conversations"
+  | "home.stats.latency"
+  | "home.stats.satisfaction"
+  | "home.stats.val.clients"
+  | "home.stats.val.conversations"
+  | "home.stats.val.latency"
+  | "home.stats.val.satisfaction"
+  | "home.features.title"
+  | "home.features.lead"
+  | "home.cap.multimodel.title"
+  | "home.cap.multimodel.desc"
+  | "home.cap.kb.title"
+  | "home.cap.kb.desc"
+  | "home.cap.prompt.title"
+  | "home.cap.prompt.desc"
+  | "home.cap.human.title"
+  | "home.cap.human.desc"
+  | "home.cap.reports.title"
+  | "home.cap.reports.desc"
+  | "home.cap.logs.title"
+  | "home.cap.logs.desc"
+  | "home.screenshots.title"
+  | "home.screenshots.lead"
+  | "home.screenshots.prevAria"
+  | "home.screenshots.nextAria"
+  | "home.ss.dashboard.title"
+  | "home.ss.dashboard.placeholder"
+  | "home.ss.dashboard.alt"
+  | "home.ss.visitor.title"
+  | "home.ss.visitor.placeholder"
+  | "home.ss.visitor.alt"
+  | "home.ss.aiconfig.title"
+  | "home.ss.aiconfig.placeholder"
+  | "home.ss.aiconfig.alt"
+  | "home.ss.users.title"
+  | "home.ss.users.placeholder"
+  | "home.ss.users.alt"
+  | "home.ss.faq.title"
+  | "home.ss.faq.placeholder"
+  | "home.ss.faq.alt"
+  | "home.ss.knowledge.title"
+  | "home.ss.knowledge.placeholder"
+  | "home.ss.knowledge.alt"
+  | "home.ss.kbtest.title"
+  | "home.ss.kbtest.placeholder"
+  | "home.ss.kbtest.alt"
+  | "home.ss.prompts.title"
+  | "home.ss.prompts.placeholder"
+  | "home.ss.prompts.alt"
+  | "home.ss.logs.title"
+  | "home.ss.logs.placeholder"
+  | "home.ss.logs.alt"
+  | "home.ss.analytics.title"
+  | "home.ss.analytics.placeholder"
+  | "home.ss.analytics.alt"
+  | "home.quickStart.title"
+  | "home.quickStart.lead"
+  | "home.step1.title"
+  | "home.step1.body"
+  | "home.step2.title"
+  | "home.step2.body"
+  | "home.step3.title"
+  | "home.step3.body"
+  | "home.cta.title"
+  | "home.cta.subtitle"
+  | "home.cta.starRepo"
+  | "home.cta.feedback"
+  | "home.cta.mailSubject"
+  | "home.cta.mailBody"
+  | "footer.blurb"
+  | "footer.column.product"
+  | "footer.column.friendLinks"
+  | "footer.column.contact"
+  | "footer.noFriendLinks"
+  | "footer.onlineChat"
+  | "footer.openSourceLicense"
+  | "footer.poweredBy"
+  | "footer.allRightsReserved"
+  | "footer.emailLabel"
+  | "agent.page.dashboard"
+  | "agent.page.internalChat"
+  | "agent.page.knowledge"
+  | "agent.page.faqs"
+  | "agent.page.analytics"
+  | "agent.page.logs"
+  | "agent.page.users"
+  | "agent.page.prompts"
+  | "agent.page.settings"
+  | "agent.profile"
+  | "agent.logout"
+  | "agent.chat.conversation"
+  | "agent.chat.lastSeen"
+  | "agent.chat.lastSeenUnknown"
+  | "agent.chat.showAI"
+  | "agent.chat.hideAI"
+  | "agent.chat.closeConversation"
+  | "agent.chat.refresh"
+  | "agent.chat.soundOn"
+  | "agent.chat.soundOff"
+  | "agent.chat.toast.conversationClosed"
+  | "agent.chat.toast.closeFailed"
+  | "agent.chat.emptyPick"
+  | "agent.layout.openNavMenu"
+  | "agent.layout.openVisitorPanel"
+  | "agent.internalChat.webSearchThisTurn"
+  | "agent.internalChat.aiThinking"
+  | "agent.internalChat.emptyHint"
+  | "agent.internalChat.createFailed"
+  | "agent.login.title"
+  | "agent.login.subtitle"
+  | "agent.login.username"
+  | "agent.login.password"
+  | "agent.login.submit"
+  | "agent.login.submitting"
+  | "agent.login.error.empty"
+  | "agent.login.error.failed"
+  | "agent.login.error.network"
+  | "agent.login.demoHint"
+  | "agent.logs.title"
+  | "agent.logs.subtitle"
+  | "agent.logs.policy.title"
+  | "agent.logs.policy.desc"
+  | "agent.logs.policy.current"
+  | "agent.logs.policy.env"
+  | "agent.logs.policy.overridden"
+  | "agent.logs.level.all"
+  | "agent.logs.category.all"
+  | "agent.logs.source.all"
+  | "agent.logs.event.placeholder"
+  | "agent.logs.conversationId.placeholder"
+  | "agent.logs.keyword.placeholder"
+  | "agent.logs.table.time"
+  | "agent.logs.table.level"
+  | "agent.logs.table.category"
+  | "agent.logs.table.event"
+  | "agent.logs.table.conversation"
+  | "agent.logs.table.source"
+  | "agent.logs.table.message"
+  | "agent.logs.paginationSummary"
+  | "agent.logs.empty"
+  | "agent.logs.detail.title"
+  | "agent.logs.detail.time"
+  | "agent.logs.detail.sourceEvent"
+  | "agent.logs.detail.category"
+  | "agent.logs.detail.traceId"
+  | "agent.logs.detail.conversationId"
+  | "agent.logs.detail.userVisitor"
+  | "agent.logs.detail.message"
+  | "agent.logs.detail.metaJson"
+  | "agent.logs.detail.noMeta"
+  | "agent.logs.toast.loadPolicyFailed"
+  | "agent.logs.toast.loadLogsFailed"
+  | "agent.logs.toast.savePolicyFailed"
+  | "agent.logs.toast.restorePolicyFailed"
+  | "agent.logs.toast.policySaved"
+  | "agent.logs.toast.policyRestored"
+  | "agent.logs.toast.messageCopied"
+  | "agent.logs.toast.copyFailed"
+  | "agent.conversationsPage.title"
+  | "agent.conversationsPage.loading"
+  | "agent.conversationsPage.empty"
+  | "agent.conversationsPage.convLabel"
+  | "agent.conversationsPage.visitorLabel"
+  | "agent.conversationsPage.createdAt"
+  | "agent.conversationsPage.updatedAt"
+  | "agent.conversations.filter.all"
+  | "agent.conversations.filter.mine"
+  | "agent.conversations.filter.others"
+  | "agent.conversations.status.open"
+  | "agent.conversations.status.closed"
+  | "agent.internalChat.title"
+  | "agent.internalChat.new"
+  | "agent.conversation.noMessage"
+  | "agent.conversation.online"
+  | "agent.conversation.visitor"
+  | "agent.input.upload"
+  | "agent.input.placeholder"
+  | "agent.input.placeholder.withAttachment"
+  | "agent.input.sending"
+  | "agent.input.uploading"
+  | "agent.input.send"
+  | "agent.input.fileTooLarge"
+  | "agent.input.fileTypeNotSupported"
+  | "agent.input.uploadFailed"
+  | "agent.aiSource.kb"
+  | "agent.aiSource.llm"
+  | "agent.aiSource.web"
+  | "agent.common.back"
+  | "agent.common.cancel"
+  | "agent.common.create"
+  | "agent.common.update"
+  | "agent.common.delete"
+  | "agent.common.edit"
+  | "agent.common.keywordSearch"
+  | "agent.common.noMatch"
+  | "agent.common.none"
+  | "agent.common.confirm"
+  | "agent.faqs.title"
+  | "agent.faqs.subtitle"
+  | "agent.faqs.search.placeholder"
+  | "agent.faqs.createButton"
+  | "agent.faqs.empty"
+  | "agent.faqs.empty.filtered"
+  | "agent.faqs.dialog.createTitle"
+  | "agent.faqs.dialog.editTitle"
+  | "agent.faqs.dialog.deleteTitle"
+  | "agent.faqs.form.question"
+  | "agent.faqs.form.answer"
+  | "agent.faqs.form.keywords"
+  | "agent.faqs.form.keywordsHint"
+  | "agent.faqs.toast.loadFailed"
+  | "agent.faqs.toast.createFailed"
+  | "agent.faqs.toast.updateFailed"
+  | "agent.faqs.toast.deleteFailed"
+  | "agent.faqs.toast.createSuccess"
+  | "agent.faqs.toast.updateSuccess"
+  | "agent.faqs.toast.deleteSuccess"
+  | "agent.faqs.toast.emptyRequired"
+  | "agent.faqs.card.keywords"
+  | "agent.faqs.card.createdAt"
+  | "agent.faqs.card.edit"
+  | "agent.faqs.dialog.createTitle2"
+  | "agent.faqs.dialog.createDesc"
+  | "agent.faqs.dialog.editDesc"
+  | "agent.faqs.dialog.deleteConfirm"
+  | "agent.faqs.form.placeholder.question"
+  | "agent.faqs.form.placeholder.answer"
+  | "agent.faqs.form.placeholder.keywords"
+  | "agent.faqs.form.keywordsOptional"
+  | "agent.faqs.form.keywordsTip"
+  | "agent.faqs.submit.creating"
+  | "agent.faqs.submit.deleting"
+  | "agent.perm.analytics"
+  | "agent.perm.chat"
+  | "agent.perm.faqs"
+  | "agent.perm.kb_test"
+  | "agent.perm.knowledge"
+  | "agent.perm.logs"
+  | "agent.perm.prompts"
+  | "agent.perm.settings"
+  | "agent.perm.users"
+  | "agent.settings.aiCard.titleAdd"
+  | "agent.settings.aiCard.titleEdit"
+  | "agent.settings.aiForm.active"
+  | "agent.settings.aiForm.apiKey"
+  | "agent.settings.aiForm.apiUrl"
+  | "agent.settings.aiForm.apiUrlPh"
+  | "agent.settings.aiForm.descPh"
+  | "agent.settings.aiForm.description"
+  | "agent.settings.aiForm.model"
+  | "agent.settings.aiForm.modelType"
+  | "agent.settings.aiForm.modelPh"
+  | "agent.settings.aiForm.provider"
+  | "agent.settings.aiForm.providerPh"
+  | "agent.settings.aiForm.public"
+  | "agent.settings.aiForm.submitCreate"
+  | "agent.settings.aiForm.submitUpdate"
+  | "agent.settings.aiForm.submitting"
+  | "agent.settings.backDashboard"
+  | "agent.settings.badge.active"
+  | "agent.settings.badge.public"
+  | "agent.settings.confirmDeleteConfig"
+  | "agent.settings.embedding.apiKey"
+  | "agent.settings.embedding.apiKeyKeepEmpty"
+  | "agent.settings.embedding.apiKeyInput"
+  | "agent.settings.embedding.apiUrl"
+  | "agent.settings.embedding.apiUrlPh"
+  | "agent.settings.embedding.bgeLocal"
+  | "agent.settings.embedding.customerKb"
+  | "agent.settings.embedding.lead"
+  | "agent.settings.embedding.model"
+  | "agent.settings.embedding.modelPh"
+  | "agent.settings.embedding.openaiCompatible"
+  | "agent.settings.embedding.save"
+  | "agent.settings.embedding.title"
+  | "agent.settings.embedding.type"
+  | "agent.settings.error.delete"
+  | "agent.settings.error.loadConfigs"
+  | "agent.settings.error.loadEmbedding"
+  | "agent.settings.error.operation"
+  | "agent.settings.global.noReceiveAi"
+  | "agent.settings.global.noReceiveAiHint"
+  | "agent.settings.list.apiUrlLabel"
+  | "agent.settings.list.descLabel"
+  | "agent.settings.list.empty"
+  | "agent.settings.list.modelTypeLabel"
+  | "agent.settings.list.title"
+  | "agent.settings.modelType.audio"
+  | "agent.settings.modelType.image"
+  | "agent.settings.modelType.text"
+  | "agent.settings.modelType.video"
+  | "agent.settings.section.global"
+  | "agent.settings.subtitle"
+  | "agent.settings.title"
+  | "agent.settings.toast.embeddingSaved"
+  | "agent.settings.toast.profileUpdateFailed"
+  | "agent.settings.webSearch.lead"
+  | "agent.settings.webSearch.mode"
+  | "agent.settings.webSearch.modeCustom"
+  | "agent.settings.webSearch.modeHint"
+  | "agent.settings.webSearch.modeVendor"
+  | "agent.settings.webSearch.save"
+  | "agent.settings.webSearch.title"
+  | "agent.settings.webSearch.visitorToggle"
+  | "agent.users.card.edit"
+  | "agent.users.card.password"
+  | "agent.users.createButton"
+  | "agent.users.dialog.createTitle"
+  | "agent.users.dialog.deleteConfirm"
+  | "agent.users.dialog.deleteNote"
+  | "agent.users.dialog.deleteTitle"
+  | "agent.users.dialog.editTitle"
+  | "agent.users.dialog.passwordTitle"
+  | "agent.users.empty"
+  | "agent.users.empty.filtered"
+  | "agent.users.field.createdAt"
+  | "agent.users.field.email"
+  | "agent.users.field.username"
+  | "agent.users.form.email"
+  | "agent.users.form.newPassword"
+  | "agent.users.form.oldPassword"
+  | "agent.users.form.password"
+  | "agent.users.form.permissions"
+  | "agent.users.form.permissionsHint"
+  | "agent.users.form.role"
+  | "agent.users.form.username"
+  | "agent.users.placeholder.email"
+  | "agent.users.placeholder.emailOptional"
+  | "agent.users.placeholder.nickname"
+  | "agent.users.placeholder.nicknameOptional"
+  | "agent.users.placeholder.oldPassword"
+  | "agent.users.placeholder.password"
+  | "agent.users.placeholder.username"
+  | "agent.users.receiveAiLabel"
+  | "agent.users.role.admin"
+  | "agent.users.role.agent"
+  | "agent.users.search.placeholder"
+  | "agent.users.submit.creating"
+  | "agent.users.submit.deleting"
+  | "agent.users.submit.updating"
+  | "agent.users.title"
+  | "agent.users.toast.adminDeleteDisabled"
+  | "agent.users.toast.adminPasswordDisabled"
+  | "agent.users.toast.createFailed"
+  | "agent.users.toast.createSuccess"
+  | "agent.users.toast.deleteFailed"
+  | "agent.users.toast.deleteSuccess"
+  | "agent.users.toast.deleteTransferred"
+  | "agent.users.toast.loadFailed"
+  | "agent.users.toast.newPasswordRequired"
+  | "agent.users.toast.oldPasswordRequired"
+  | "agent.users.toast.passwordFailed"
+  | "agent.users.toast.passwordSuccess"
+  | "agent.users.toast.updateFailed"
+  | "agent.users.toast.updateSuccess"
+  | "agent.users.toast.usernamePasswordRequired"
+  | "agent.users.tooltip.adminDeleteDbOnly"
+  | "agent.users.tooltip.adminPasswordDbOnly"
+  | "agent.users.tooltip.cannotDeleteSelf"
+  | "agent.users.usernameImmutableHint"
+  | "agent.users.form.nickname"
+  | "agent.knowledge.title"
+  | "agent.knowledge.rag"
+  | "agent.knowledge.kb.create"
+  | "agent.knowledge.kb.empty"
+  | "agent.knowledge.kb.selectOne"
+  | "agent.knowledge.kb.docCount"
+  | "agent.knowledge.import.url"
+  | "agent.knowledge.import.file"
+  | "agent.knowledge.import.tabFile"
+  | "agent.knowledge.import.tabUrl"
+  | "agent.knowledge.import.pickFiles"
+  | "agent.knowledge.import.filesSelected"
+  | "agent.knowledge.import.action"
+  | "agent.knowledge.import.urlListLabel"
+  | "agent.knowledge.doc.create"
+  | "agent.knowledge.doc.searchPh"
+  | "agent.knowledge.doc.empty"
+  | "agent.knowledge.doc.empty.filtered"
+  | "agent.knowledge.doc.type"
+  | "agent.knowledge.doc.createdAt"
+  | "agent.knowledge.doc.publish"
+  | "agent.knowledge.doc.unpublish"
+  | "agent.knowledge.filter.all"
+  | "agent.knowledge.pagination"
+  | "agent.knowledge.status.draft"
+  | "agent.knowledge.status.published"
+  | "agent.knowledge.embedding.pending"
+  | "agent.knowledge.embedding.processing"
+  | "agent.knowledge.embedding.completed"
+  | "agent.knowledge.embedding.failed"
+  | "agent.knowledge.dialog.kbCreateTitle"
+  | "agent.knowledge.dialog.kbCreateDesc"
+  | "agent.knowledge.dialog.kbEditTitle"
+  | "agent.knowledge.dialog.kbEditDesc"
+  | "agent.knowledge.dialog.kbDeleteTitle"
+  | "agent.knowledge.dialog.kbDeleteConfirm"
+  | "agent.knowledge.dialog.kbDeleteHint"
+  | "agent.knowledge.dialog.docCreateTitle"
+  | "agent.knowledge.dialog.docCreateDesc"
+  | "agent.knowledge.dialog.docEditTitle"
+  | "agent.knowledge.dialog.docEditDesc"
+  | "agent.knowledge.dialog.docDeleteTitle"
+  | "agent.knowledge.dialog.docDeleteConfirm"
+  | "agent.knowledge.dialog.importTitle"
+  | "agent.knowledge.dialog.importDesc"
+  | "agent.knowledge.field.name"
+  | "agent.knowledge.field.descOptional"
+  | "agent.knowledge.field.title"
+  | "agent.knowledge.field.summaryOptional"
+  | "agent.knowledge.field.content"
+  | "agent.knowledge.ph.kbName"
+  | "agent.knowledge.ph.kbDesc"
+  | "agent.knowledge.ph.docTitle"
+  | "agent.knowledge.ph.docSummary"
+  | "agent.knowledge.ph.docContent"
+  | "agent.knowledge.submitting.creating"
+  | "agent.knowledge.submitting.updating"
+  | "agent.knowledge.submitting.deleting"
+  | "agent.knowledge.submitting.importing"
+  | "agent.knowledge.toast.loadKbFailed"
+  | "agent.knowledge.toast.loadDocFailed"
+  | "agent.knowledge.toast.kbNameRequired"
+  | "agent.knowledge.toast.selectKbFirst"
+  | "agent.knowledge.toast.docTitleContentRequired"
+  | "agent.knowledge.toast.createSuccess"
+  | "agent.knowledge.toast.updateSuccess"
+  | "agent.knowledge.toast.deleteSuccess"
+  | "agent.knowledge.toast.updateFailed"
+  | "agent.knowledge.toast.createKbFailed"
+  | "agent.knowledge.toast.updateKbFailed"
+  | "agent.knowledge.toast.deleteKbFailed"
+  | "agent.knowledge.toast.createDocFailed"
+  | "agent.knowledge.toast.updateDocFailed"
+  | "agent.knowledge.toast.deleteDocFailed"
+  | "agent.knowledge.toast.publishSuccess"
+  | "agent.knowledge.toast.publishFailed"
+  | "agent.knowledge.toast.unpublishSuccess"
+  | "agent.knowledge.toast.unpublishFailed"
+  | "agent.knowledge.toast.selectFiles"
+  | "agent.knowledge.toast.urlRequired"
+  | "agent.knowledge.toast.importDocFailed"
+  | "agent.knowledge.toast.importUrlFailed"
+  | "agent.knowledge.toast.importRefreshFailed"
+  | "agent.knowledge.toast.importFailed.files"
+  | "agent.knowledge.toast.importFailed.urls"
+  | "agent.knowledge.toast.importDone.files"
+  | "agent.knowledge.toast.importDone.urls"
+  | "agent.knowledge.toast.importDone.partial"
+  | "agent.prompts.title"
+  | "agent.prompts.subtitle"
+  | "agent.prompts.loadFailed"
+  | "agent.prompts.saveSuccess"
+  | "agent.prompts.saveFailed"
+  | "agent.prompts.usageLabel"
+  | "agent.prompts.ph.shortReply"
+  | "agent.prompts.ph.withPlaceholders"
+  | "agent.prompts.saving"
+  | "agent.prompts.save"
+  | "agent.prompts.hint.rag_prompt"
+  | "agent.prompts.hint.rag_prompt_with_web_optional"
+  | "agent.prompts.hint.no_kb_prompt"
+  | "agent.prompts.hint.web_search_result_prompt"
+  | "agent.prompts.hint.no_source_reply"
+  | "agent.prompts.hint.ai_fail_reply"
+  | "agent.prompts.hint.default"
+  | "agent.prompts.usage.rag_prompt"
+  | "agent.prompts.usage.rag_prompt_with_web_optional"
+  | "agent.prompts.usage.no_kb_prompt"
+  | "agent.prompts.usage.web_search_result_prompt"
+  | "agent.prompts.usage.no_source_reply"
+  | "agent.prompts.usage.ai_fail_reply"
+  | "agent.analytics.title"
+  | "agent.analytics.subtitle"
+  | "agent.analytics.from"
+  | "agent.analytics.to"
+  | "agent.analytics.query"
+  | "agent.analytics.loading"
+  | "agent.analytics.empty"
+  | "agent.analytics.emptyOrFailed"
+  | "agent.analytics.stat.widgetOpens"
+  | "agent.analytics.stat.widgetOpensSub"
+  | "agent.analytics.stat.sessions"
+  | "agent.analytics.stat.messages"
+  | "agent.analytics.stat.aiReplies"
+  | "agent.analytics.stat.aiFailed"
+  | "agent.analytics.stat.aiFailureRate"
+  | "agent.analytics.stat.aiFailureRateSub"
+  | "agent.analytics.stat.kbHits"
+  | "agent.analytics.stat.kbHitRate"
+  | "agent.analytics.stat.kbHitRateSub"
+  | "agent.analytics.stat.maxAiRounds"
+  | "agent.analytics.stat.maxAiRoundsSub"
+  | "agent.analytics.stat.sessionsWithAi"
+  | "agent.analytics.stat.sessionsWithAiSub"
+  | "agent.analytics.stat.aiToHuman"
+  | "agent.analytics.stat.aiToHumanSub"
+  | "agent.analytics.stat.humanToAi"
+  | "agent.analytics.stat.humanToAiSub"
+  | "agent.analytics.chart.widgetOpens"
+  | "agent.analytics.chart.sessions"
+  | "agent.analytics.chart.messages"
+  | "agent.analytics.chart.aiReplies"
+  | "common.irreversibleHint"
+  | "chat.title"
+  | "chat.mode.human"
+  | "chat.mode.ai";
+
+export const DEFAULT_LANG: Lang = "zh-CN";
+export const LANG_STORAGE_KEY = "aics_lang";
+
+export const DICT: Record<Lang, Record<I18nKey, string>> = {
+  "zh-CN": {
+    "nav.features": "核心能力",
+    "nav.screenshots": "界面展示",
+    "nav.quickStart": "快速接入",
+    "nav.agentLogin": "客服登录",
+    "nav.menu": "菜单",
+    "common.github": "GitHub",
+    "common.to": "到",
+    "common.save": "保存到服务器",
+    "common.saving": "保存中...",
+    "common.restoreEnv": "恢复环境变量",
+    "common.loading": "加载中...",
+    "common.search": "查询",
+    "common.prevPage": "上一页",
+    "common.nextPage": "下一页",
+    "common.copy": "复制",
+    "home.hero.tagline": "AI 智能客服",
+    "home.hero.title": "让客户服务更简单、更高效",
+    "home.hero.subtitle":
+      "7×24 小时智能应答,AI 与人工无缝切换,释放团队时间专注更有价值的事",
+    "home.hero.cta.tryNow": "立即体验",
+    "home.hero.cta.agentLogin": "客服登录",
+    "home.hero.hint": "无需等待,可立即使用",
+    "home.stats.trustedBy": "深受企业信赖",
+    "home.stats.clients": "服务企业",
+    "home.stats.conversations": "处理对话",
+    "home.stats.latency": "响应时间",
+    "home.stats.satisfaction": "满意度",
+    "home.stats.val.clients": "1000+",
+    "home.stats.val.conversations": "100万+",
+    "home.stats.val.latency": "<100ms",
+    "home.stats.val.satisfaction": "98%",
+    "home.features.title": "核心能力",
+    "home.features.lead": "从模型、知识库、提示词到人工协作、报表与日志,一套系统串起来。",
+    "home.cap.multimodel.title": "多模型 AI 客服",
+    "home.cap.multimodel.desc":
+      "支持配置多家大模型与绘画等能力,访客与后台可统一管理模型与使用方式,便于替换供应商、控制成本。",
+    "home.cap.kb.title": "知识库与 RAG",
+    "home.cap.kb.desc":
+      "文档入库、向量检索,让回答贴近你的业务资料;回复可标记是否使用知识库、模型或联网,便于核对与优化。",
+    "home.cap.prompt.title": "提示词工程",
+    "home.cap.prompt.desc":
+      "配置系统中使用的提示词模板,用于不同领域 RAG、联网等不同的业务场景。",
+    "home.cap.human.title": "人工客服与实时协作",
+    "home.cap.human.desc":
+      "在线状态、会话实时推送(WebSocket),支持人工接管与日常协作;访客小窗可嵌入任意站点。",
+    "home.cap.reports.title": "可视化报表",
+    "home.cap.reports.desc":
+      "按日或自定义区间查看访客小窗打开、会话与消息、AI 回复与失败率、知识库命中率等指标,快速掌握运营态势。",
+    "home.cap.logs.title": "日志中心",
+    "home.cap.logs.desc":
+      "结构化日志按分类与事件落库,支持 trace_id 与关键字筛选,关键链路与异常可追溯,便于排障与审计。",
+    "home.screenshots.title": "界面展示",
+    "home.screenshots.lead": "精心设计的界面,让管理更轻松",
+    "home.screenshots.prevAria": "查看上一张",
+    "home.screenshots.nextAria": "查看下一张",
+    "home.ss.dashboard.title": "工作台",
+    "home.ss.dashboard.placeholder": "工作台界面",
+    "home.ss.dashboard.alt": "AI-CS 工作台界面",
+    "home.ss.visitor.title": "访客端",
+    "home.ss.visitor.placeholder": "访客端界面",
+    "home.ss.visitor.alt": "AI-CS 访客端界面",
+    "home.ss.aiconfig.title": "AI 配置",
+    "home.ss.aiconfig.placeholder": "AI 配置界面",
+    "home.ss.aiconfig.alt": "AI-CS AI 配置界面",
+    "home.ss.users.title": "用户管理",
+    "home.ss.users.placeholder": "用户管理界面",
+    "home.ss.users.alt": "AI-CS 用户管理界面",
+    "home.ss.faq.title": "FAQ 管理",
+    "home.ss.faq.placeholder": "FAQ 管理界面",
+    "home.ss.faq.alt": "AI-CS FAQ 管理界面",
+    "home.ss.knowledge.title": "知识库管理",
+    "home.ss.knowledge.placeholder": "知识库管理界面",
+    "home.ss.knowledge.alt": "AI-CS 知识库管理界面",
+    "home.ss.kbtest.title": "知识库测试",
+    "home.ss.kbtest.placeholder": "知识库测试界面",
+    "home.ss.kbtest.alt": "AI-CS 知识库测试界面",
+    "home.ss.prompts.title": "提示词工程",
+    "home.ss.prompts.placeholder": "提示词工程界面",
+    "home.ss.prompts.alt": "AI-CS 提示词工程界面",
+    "home.ss.logs.title": "日志中心",
+    "home.ss.logs.placeholder": "日志中心界面",
+    "home.ss.logs.alt": "AI-CS 日志中心界面",
+    "home.ss.analytics.title": "可视化报表",
+    "home.ss.analytics.placeholder": "可视化报表界面",
+    "home.ss.analytics.alt": "AI-CS 可视化报表界面",
+    "home.quickStart.title": "快速接入",
+    "home.quickStart.lead": "三步跑通,从仓库到访客小窗。",
+    "home.step1.title": "克隆与配置",
+    "home.step1.body": "复制 .env 模板,填好数据库与管理员等必填项。",
+    "home.step2.title": "一键启动",
+    "home.step2.body": "使用 Docker Compose 拉起前后端与依赖服务(详见 README)。",
+    "home.step3.title": "嵌入访客端",
+    "home.step3.body": "在站点中挂载聊天小窗,后台完成模型与知识库配置后即可对外服务。",
+    "home.cta.title": "准备好把 AI-CS 接到你的产品里了吗?",
+    "home.cta.subtitle": "从开源仓库开始,或用在线 Demo 先看交互与能力边界。",
+    "home.cta.starRepo": "Star / Fork 仓库",
+    "home.cta.feedback": "建议反馈",
+    "home.cta.mailSubject": "AI-CS 建议反馈",
+    "home.cta.mailBody":
+      "你好,我想反馈:\n\n1)问题/建议:\n2)影响范围/环境:\n3)期望结果:\n\n---\n联系方式(可选):",
+    "footer.blurb":
+      "AI-CS 是一款 AI 驱动的智能客服系统,融合 AI 技术与人工客服,为企业提供高效、智能的客户服务解决方案。",
+    "footer.column.product": "产品",
+    "footer.column.friendLinks": "友情链接",
+    "footer.column.contact": "联系我们",
+    "footer.noFriendLinks": "暂无友情链接",
+    "footer.onlineChat": "在线客服",
+    "footer.openSourceLicense": "开源协议",
+    "footer.poweredBy": "Powered by Next.js & Go |",
+    "footer.allRightsReserved": "保留所有权利。",
+    "footer.emailLabel": "邮箱",
+    "agent.page.dashboard": "对话",
+    "agent.page.internalChat": "知识库测试",
+    "agent.page.knowledge": "知识库",
+    "agent.page.faqs": "事件管理",
+    "agent.page.analytics": "数据报表",
+    "agent.page.logs": "日志中心",
+    "agent.page.users": "用户管理",
+    "agent.page.prompts": "提示词",
+    "agent.page.settings": "AI 配置",
+    "agent.profile": "个人资料",
+    "agent.logout": "退出登录",
+    "agent.chat.conversation": "对话",
+    "agent.chat.lastSeen": "最后活跃",
+    "agent.chat.lastSeenUnknown": "最后活跃 未知",
+    "agent.chat.showAI": "显示 AI 消息",
+    "agent.chat.hideAI": "隐藏 AI 消息",
+    "agent.chat.closeConversation": "关闭会话",
+    "agent.chat.refresh": "刷新",
+    "agent.chat.soundOn": "关闭声音提示",
+    "agent.chat.soundOff": "开启声音提示",
+    "agent.chat.toast.conversationClosed": "已关闭会话",
+    "agent.chat.toast.closeFailed": "关闭会话失败",
+    "agent.chat.emptyPick": "选择一个对话开始聊天",
+    "agent.layout.openNavMenu": "打开导航与对话列表",
+    "agent.layout.openVisitorPanel": "打开访客详情",
+    "agent.internalChat.webSearchThisTurn": "本回合联网搜索",
+    "agent.internalChat.aiThinking": "AI 正在思考...",
+    "agent.internalChat.emptyHint": "选择或新建内部对话,测试知识库效果",
+    "agent.internalChat.createFailed": "创建内部对话失败",
+    "agent.login.title": "客服登录",
+    "agent.login.subtitle": "管理员和客服请在此登录",
+    "agent.login.username": "用户名",
+    "agent.login.password": "密码",
+    "agent.login.submit": "登录",
+    "agent.login.submitting": "登录中...",
+    "agent.login.error.empty": "用户名和密码不能为空",
+    "agent.login.error.failed": "登录失败",
+    "agent.login.error.network": "登录失败,请检查网络连接",
+    "agent.login.demoHint": "默认管理员账号:admin / admin123",
+    "agent.logs.title": "日志中心",
+    "agent.logs.subtitle": "按分类查看 AI / RAG / 系统 / 前端日志,用于排障定位。",
+    "agent.logs.policy.title": "落库级别(性能)",
+    "agent.logs.policy.desc":
+      "仅将不低于所选级别的记录写入数据库。设为 warn 可大幅减少成功类 info 写入。也可在根目录 SYSTEM_LOG_MIN_LEVEL 配置默认值;此处保存后会写入数据库并覆盖环境变量,直至点击「恢复环境变量」。",
+    "agent.logs.policy.current": "当前生效:",
+    "agent.logs.policy.env": "环境变量默认:",
+    "agent.logs.policy.overridden": "(已由控制台覆盖)",
+    "agent.logs.level.all": "全部级别",
+    "agent.logs.category.all": "全部分类",
+    "agent.logs.source.all": "全部来源",
+    "agent.logs.event.placeholder": "事件名(event)",
+    "agent.logs.conversationId.placeholder": "会话ID",
+    "agent.logs.keyword.placeholder": "关键词(message/meta)",
+    "agent.logs.table.time": "时间",
+    "agent.logs.table.level": "级别",
+    "agent.logs.table.category": "分类",
+    "agent.logs.table.event": "事件",
+    "agent.logs.table.conversation": "会话",
+    "agent.logs.table.source": "来源",
+    "agent.logs.table.message": "消息",
+    "agent.logs.paginationSummary": "共 {{total}} 条,当前第 {{page}}/{{pages}} 页",
+    "agent.logs.empty": "暂无日志",
+    "agent.logs.detail.title": "日志详情",
+    "agent.logs.detail.time": "时间",
+    "agent.logs.detail.sourceEvent": "source / event",
+    "agent.logs.detail.category": "category",
+    "agent.logs.detail.traceId": "trace_id",
+    "agent.logs.detail.conversationId": "conversation_id",
+    "agent.logs.detail.userVisitor": "user_id / visitor_id",
+    "agent.logs.detail.message": "message",
+    "agent.logs.detail.metaJson": "meta_json",
+    "agent.logs.detail.noMeta": "(无 meta_json)",
+    "agent.logs.toast.loadPolicyFailed": "加载落库策略失败",
+    "agent.logs.toast.loadLogsFailed": "加载日志失败",
+    "agent.logs.toast.savePolicyFailed": "保存失败",
+    "agent.logs.toast.restorePolicyFailed": "恢复失败",
+    "agent.logs.toast.policySaved": "已保存",
+    "agent.logs.toast.policyRestored": "已恢复为环境变量默认",
+    "agent.logs.toast.messageCopied": "已复制 message",
+    "agent.logs.toast.copyFailed": "复制失败",
+    "agent.conversationsPage.title": "对话列表",
+    "agent.conversationsPage.loading": "加载中...",
+    "agent.conversationsPage.empty": "暂无对话",
+    "agent.conversationsPage.convLabel": "对话 #{{id}}",
+    "agent.conversationsPage.visitorLabel": "访客ID: {{id}}",
+    "agent.conversationsPage.createdAt": "创建时间: {{time}}",
+    "agent.conversationsPage.updatedAt": "最后更新: {{time}}",
+    "agent.conversations.filter.all": "全部对话",
+    "agent.conversations.filter.mine": "我的对话",
+    "agent.conversations.filter.others": "他人对话",
+    "agent.conversations.status.open": "进行中",
+    "agent.conversations.status.closed": "历史",
+    "agent.internalChat.title": "知识库测试",
+    "agent.internalChat.new": "新建",
+    "agent.conversation.noMessage": "暂无消息",
+    "agent.conversation.online": "在线",
+    "agent.conversation.visitor": "访客",
+    "agent.input.upload": "上传文件",
+    "agent.input.placeholder": "输入消息...",
+    "agent.input.placeholder.withAttachment": "添加消息(可选)...",
+    "agent.input.sending": "发送中...",
+    "agent.input.uploading": "上传中...",
+    "agent.input.send": "发送",
+    "agent.input.fileTooLarge": "文件大小超过限制(最大10MB)",
+    "agent.input.fileTypeNotSupported": "不支持的文件类型",
+    "agent.input.uploadFailed": "文件上传失败",
+    "agent.aiSource.kb": "已使用知识库",
+    "agent.aiSource.llm": "已使用大模型",
+    "agent.aiSource.web": "已使用联网搜索",
+    "agent.common.back": "返回",
+    "agent.common.cancel": "取消",
+    "agent.common.create": "创建",
+    "agent.common.update": "更新",
+    "agent.common.delete": "删除",
+    "agent.common.edit": "编辑",
+    "agent.common.keywordSearch": "关键词搜索",
+    "agent.common.noMatch": "没有找到匹配的内容",
+    "agent.common.none": "暂无",
+    "agent.common.confirm": "确定",
+    "agent.faqs.title": "事件管理(FAQ)",
+    "agent.faqs.subtitle": "维护常见问题/事件模板,支持关键词搜索。",
+    "agent.faqs.search.placeholder": "关键词搜索(用 % 分隔,例如:openai%api%调用)...",
+    "agent.faqs.createButton": "创建事件",
+    "agent.faqs.empty": "暂无事件",
+    "agent.faqs.empty.filtered": "没有找到匹配的事件",
+    "agent.faqs.dialog.createTitle": "创建事件",
+    "agent.faqs.dialog.editTitle": "编辑事件",
+    "agent.faqs.dialog.deleteTitle": "删除事件",
+    "agent.faqs.form.question": "问题",
+    "agent.faqs.form.answer": "答案",
+    "agent.faqs.form.keywords": "关键词",
+    "agent.faqs.form.keywordsHint": "多个关键词建议用 % 分隔,便于检索命中",
+    "agent.faqs.toast.loadFailed": "加载 FAQ 列表失败",
+    "agent.faqs.toast.createFailed": "创建 FAQ 失败",
+    "agent.faqs.toast.updateFailed": "更新 FAQ 失败",
+    "agent.faqs.toast.deleteFailed": "删除 FAQ 失败",
+    "agent.faqs.toast.createSuccess": "创建成功",
+    "agent.faqs.toast.updateSuccess": "更新成功",
+    "agent.faqs.toast.deleteSuccess": "删除成功",
+    "agent.faqs.toast.emptyRequired": "问题和答案不能为空",
+    "agent.faqs.card.keywords": "关键词",
+    "agent.faqs.card.createdAt": "创建时间",
+    "agent.faqs.card.edit": "编辑",
+    "agent.faqs.dialog.createTitle2": "创建新事件",
+    "agent.faqs.dialog.createDesc": "填写问题和答案,可以添加关键词以便搜索",
+    "agent.faqs.dialog.editDesc": "修改问题和答案,可以更新关键词以便搜索",
+    "agent.faqs.dialog.deleteConfirm": "确定要删除事件 \"{{name}}\" 吗?",
+    "agent.faqs.form.placeholder.question": "请输入问题",
+    "agent.faqs.form.placeholder.answer": "请输入答案",
+    "agent.faqs.form.placeholder.keywords":
+      "例如:API、错误、配置(用逗号或空格分隔)",
+    "agent.faqs.form.keywordsOptional": "关键词(可选)",
+    "agent.faqs.form.keywordsTip":
+      "提示:即使不填写关键词,系统也会自动搜索问题和答案中的内容。关键词字段用于添加额外的搜索索引,帮助用户更快找到相关内容。",
+    "agent.faqs.submit.creating": "创建中...",
+    "agent.faqs.submit.deleting": "删除中...",
+    "agent.perm.analytics": "数据报表",
+    "agent.perm.chat": "对话",
+    "agent.perm.faqs": "事件管理",
+    "agent.perm.kb_test": "知识库测试",
+    "agent.perm.knowledge": "知识库",
+    "agent.perm.logs": "日志中心",
+    "agent.perm.prompts": "提示词",
+    "agent.perm.settings": "AI 配置",
+    "agent.perm.users": "用户管理",
+    "agent.settings.aiCard.titleAdd": "添加 AI 配置",
+    "agent.settings.aiCard.titleEdit": "编辑 AI 配置",
+    "agent.settings.aiForm.active": "启用配置",
+    "agent.settings.aiForm.apiKey": "API Key",
+    "agent.settings.aiForm.apiUrl": "API 地址",
+    "agent.settings.aiForm.apiUrlPh": "https://api.openai.com/v1/chat/completions",
+    "agent.settings.aiForm.descPh": "例如:OpenAI GPT-3.5 Turbo 模型",
+    "agent.settings.aiForm.description": "配置描述",
+    "agent.settings.aiForm.model": "模型名称",
+    "agent.settings.aiForm.modelType": "模型类型",
+    "agent.settings.aiForm.modelPh": "例如:gpt-3.5-turbo、gpt-4",
+    "agent.settings.aiForm.provider": "服务商名称",
+    "agent.settings.aiForm.providerPh": "例如:OpenAI、Claude、自定义",
+    "agent.settings.aiForm.public": "开放给访客使用",
+    "agent.settings.aiForm.submitCreate": "创建配置",
+    "agent.settings.aiForm.submitUpdate": "更新配置",
+    "agent.settings.aiForm.submitting": "提交中...",
+    "agent.settings.backDashboard": "返回工作台",
+    "agent.settings.badge.active": "启用",
+    "agent.settings.badge.public": "开放",
+    "agent.settings.confirmDeleteConfig": "确定要删除这个配置吗?",
+    "agent.settings.embedding.apiKey": "API Key",
+    "agent.settings.embedding.apiKeyKeepEmpty": "留空则不更新",
+    "agent.settings.embedding.apiKeyInput": "输入 API Key",
+    "agent.settings.embedding.apiUrl": "API 地址",
+    "agent.settings.embedding.apiUrlPh": "https://api.openai.com/v1 或兼容地址",
+    "agent.settings.embedding.bgeLocal": "BGE 本地",
+    "agent.settings.embedding.customerKb": "开放知识库给客服使用(允许创建知识库、上传文档、对话中引用)",
+    "agent.settings.embedding.lead":
+      "用于知识库文档向量化与 RAG 检索。仅管理员可修改;保存后立即生效,无需重启。",
+    "agent.settings.embedding.model": "模型",
+    "agent.settings.embedding.modelPh": "text-embedding-3-small",
+    "agent.settings.embedding.openaiCompatible": "OpenAI / 兼容 API",
+    "agent.settings.embedding.save": "保存配置",
+    "agent.settings.embedding.title": "知识库向量模型",
+    "agent.settings.embedding.type": "类型",
+    "agent.settings.error.delete": "删除失败",
+    "agent.settings.error.loadConfigs": "加载配置失败",
+    "agent.settings.error.loadEmbedding": "加载失败",
+    "agent.settings.error.operation": "操作失败",
+    "agent.settings.global.noReceiveAi": "客服不接收 AI 对话",
+    "agent.settings.global.noReceiveAiHint":
+      "开启后,AI 对话将不会显示在对话列表中,也不会收到 AI 消息通知。但您仍可在会话页面手动开启「显示 AI 消息」查看 AI 对话历史。",
+    "agent.settings.list.apiUrlLabel": "API 地址:",
+    "agent.settings.list.descLabel": "描述:",
+    "agent.settings.list.empty": "暂无配置,请添加",
+    "agent.settings.list.modelTypeLabel": "模型类型:",
+    "agent.settings.list.title": "已配置的 AI 服务",
+    "agent.settings.modelType.audio": "语音",
+    "agent.settings.modelType.image": "图片",
+    "agent.settings.modelType.text": "文本",
+    "agent.settings.modelType.video": "视频",
+    "agent.settings.section.global": "全局设置",
+    "agent.settings.subtitle": "管理 AI 服务商配置",
+    "agent.settings.title": "AI 配置管理",
+    "agent.settings.toast.embeddingSaved": "保存成功,配置已立即生效。",
+    "agent.settings.toast.profileUpdateFailed": "更新设置失败,请重试",
+    "agent.settings.webSearch.lead":
+      "控制对话中的联网搜索方式与访客端是否显示联网选项。与「知识库向量模型」无关,仅影响 AI 对话时的联网行为。",
+    "agent.settings.webSearch.mode": "联网方式",
+    "agent.settings.webSearch.modeCustom": "自建 (Serper)",
+    "agent.settings.webSearch.modeHint":
+      "自建:由后端通过 Serper(MCP 或 HTTP)执行;厂商内置:使用当前对话所用 AI 厂商自带的联网搜索,不占用 Serper。",
+    "agent.settings.webSearch.modeVendor": "厂商内置",
+    "agent.settings.webSearch.save": "保存联网设置",
+    "agent.settings.webSearch.title": "联网搜索设置",
+    "agent.settings.webSearch.visitorToggle": "访客小窗显示「本回合联网搜索」选项",
+    "agent.users.card.edit": "编辑",
+    "agent.users.card.password": "密码",
+    "agent.users.createButton": "创建用户",
+    "agent.users.dialog.createTitle": "创建新用户",
+    "agent.users.dialog.deleteConfirm": "确定要删除用户 {{username}} 吗?",
+    "agent.users.dialog.deleteNote":
+      "此操作不可恢复。若该用户有 AI 配置,系统会自动转移给当前管理员,避免配置丢失。",
+    "agent.users.dialog.deleteTitle": "删除用户",
+    "agent.users.dialog.editTitle": "编辑用户",
+    "agent.users.dialog.passwordTitle": "修改密码",
+    "agent.users.empty": "暂无用户",
+    "agent.users.empty.filtered": "没有找到匹配的用户",
+    "agent.users.field.createdAt": "创建时间",
+    "agent.users.field.email": "邮箱",
+    "agent.users.field.username": "用户名",
+    "agent.users.form.email": "邮箱",
+    "agent.users.form.newPassword": "新密码",
+    "agent.users.form.oldPassword": "旧密码",
+    "agent.users.form.password": "密码",
+    "agent.users.form.permissions": "功能权限",
+    "agent.users.form.permissionsHint":
+      "默认仅开启「对话」。关闭后对应菜单不可见且后端接口会返回 403。",
+    "agent.users.form.role": "角色",
+    "agent.users.form.username": "用户名",
+    "agent.users.form.nickname": "昵称",
+    "agent.users.placeholder.email": "请输入邮箱",
+    "agent.users.placeholder.emailOptional": "请输入邮箱(可选)",
+    "agent.users.placeholder.nickname": "请输入昵称",
+    "agent.users.placeholder.nicknameOptional": "请输入昵称(可选)",
+    "agent.users.placeholder.oldPassword": "请输入旧密码",
+    "agent.users.placeholder.password": "请输入密码",
+    "agent.users.placeholder.username": "请输入用户名",
+    "agent.users.receiveAiLabel": "接收 AI 对话",
+    "agent.users.role.admin": "管理员",
+    "agent.users.role.agent": "客服",
+    "agent.users.search.placeholder": "搜索用户(用户名、昵称、邮箱)...",
+    "agent.users.submit.creating": "创建中...",
+    "agent.users.submit.deleting": "删除中...",
+    "agent.users.submit.updating": "更新中...",
+    "agent.users.title": "用户管理",
+    "agent.users.toast.adminDeleteDisabled": "管理员账号仅支持数据库删除,前端已禁用",
+    "agent.users.toast.adminPasswordDisabled": "管理员密码仅支持数据库修改,前端已禁用",
+    "agent.users.toast.createFailed": "创建用户失败",
+    "agent.users.toast.createSuccess": "创建成功",
+    "agent.users.toast.deleteFailed": "删除用户失败",
+    "agent.users.toast.deleteSuccess": "删除成功",
+    "agent.users.toast.deleteTransferred": "删除成功,已自动转移 {{count}} 条 AI 配置到当前管理员",
+    "agent.users.toast.loadFailed": "加载用户列表失败",
+    "agent.users.toast.newPasswordRequired": "新密码不能为空",
+    "agent.users.toast.oldPasswordRequired": "修改自己的密码需要提供旧密码",
+    "agent.users.toast.passwordFailed": "更新密码失败",
+    "agent.users.toast.passwordSuccess": "密码更新成功",
+    "agent.users.toast.updateFailed": "更新用户失败",
+    "agent.users.toast.updateSuccess": "更新成功",
+    "agent.users.toast.usernamePasswordRequired": "用户名和密码不能为空",
+    "agent.users.tooltip.adminDeleteDbOnly": "管理员账号仅支持数据库删除",
+    "agent.users.tooltip.adminPasswordDbOnly": "管理员密码仅支持数据库修改",
+    "agent.users.tooltip.cannotDeleteSelf": "不能删除当前登录用户",
+    "agent.users.usernameImmutableHint": "用户名不能修改",
+    "agent.knowledge.title": "知识库管理",
+    "agent.knowledge.rag": "参与 RAG",
+    "agent.knowledge.kb.create": "新建知识库",
+    "agent.knowledge.kb.empty": "暂无知识库",
+    "agent.knowledge.kb.selectOne": "请选择一个知识库",
+    "agent.knowledge.kb.docCount": "{{count}} 篇文档",
+    "agent.knowledge.import.url": "导入 URL",
+    "agent.knowledge.import.file": "导入文件",
+    "agent.knowledge.import.tabFile": "文件上传",
+    "agent.knowledge.import.tabUrl": "URL 导入",
+    "agent.knowledge.import.pickFiles": "选择文件",
+    "agent.knowledge.import.filesSelected": "已选择 {{count}} 个文件",
+    "agent.knowledge.import.action": "导入",
+    "agent.knowledge.import.urlListLabel": "URL 列表(每行一个)",
+    "agent.knowledge.doc.create": "新建文档",
+    "agent.knowledge.doc.searchPh": "搜索文档...",
+    "agent.knowledge.doc.empty": "暂无文档",
+    "agent.knowledge.doc.empty.filtered": "没有找到匹配的文档",
+    "agent.knowledge.doc.type": "类型",
+    "agent.knowledge.doc.createdAt": "创建时间",
+    "agent.knowledge.doc.publish": "发布",
+    "agent.knowledge.doc.unpublish": "取消发布",
+    "agent.knowledge.filter.all": "全部状态",
+    "agent.knowledge.pagination": "第 {{page}} / {{totalPage}} 页,共 {{total}} 条",
+    "agent.knowledge.status.draft": "草稿",
+    "agent.knowledge.status.published": "已发布",
+    "agent.knowledge.embedding.pending": "待处理",
+    "agent.knowledge.embedding.processing": "处理中",
+    "agent.knowledge.embedding.completed": "已完成",
+    "agent.knowledge.embedding.failed": "失败",
+    "agent.knowledge.dialog.kbCreateTitle": "创建知识库",
+    "agent.knowledge.dialog.kbCreateDesc": "填写知识库名称和描述",
+    "agent.knowledge.dialog.kbEditTitle": "编辑知识库",
+    "agent.knowledge.dialog.kbEditDesc": "修改知识库名称和描述",
+    "agent.knowledge.dialog.kbDeleteTitle": "删除知识库",
+    "agent.knowledge.dialog.kbDeleteConfirm": "确定要删除知识库 \"{{name}}\" 吗?",
+    "agent.knowledge.dialog.kbDeleteHint":
+      "此操作将同时删除该知识库下的所有文档,此操作不可恢复,请谨慎操作。",
+    "agent.knowledge.dialog.docCreateTitle": "创建文档",
+    "agent.knowledge.dialog.docCreateDesc": "填写文档标题和内容",
+    "agent.knowledge.dialog.docEditTitle": "编辑文档",
+    "agent.knowledge.dialog.docEditDesc": "修改文档标题和内容",
+    "agent.knowledge.dialog.docDeleteTitle": "删除文档",
+    "agent.knowledge.dialog.docDeleteConfirm": "确定要删除文档 \"{{title}}\" 吗?",
+    "agent.knowledge.dialog.importTitle": "导入文档",
+    "agent.knowledge.dialog.importDesc":
+      "选择文件上传或输入 URL 批量导入。当前支持的文件格式:Markdown(.md、.markdown);PDF、Word 解析功能开发中。",
+    "agent.knowledge.field.name": "名称",
+    "agent.knowledge.field.descOptional": "描述(可选)",
+    "agent.knowledge.field.title": "标题",
+    "agent.knowledge.field.summaryOptional": "摘要(可选)",
+    "agent.knowledge.field.content": "内容",
+    "agent.knowledge.ph.kbName": "请输入知识库名称",
+    "agent.knowledge.ph.kbDesc": "请输入知识库描述",
+    "agent.knowledge.ph.docTitle": "请输入文档标题",
+    "agent.knowledge.ph.docSummary": "请输入文档摘要",
+    "agent.knowledge.ph.docContent": "请输入文档内容",
+    "agent.knowledge.submitting.creating": "创建中...",
+    "agent.knowledge.submitting.updating": "更新中...",
+    "agent.knowledge.submitting.deleting": "删除中...",
+    "agent.knowledge.submitting.importing": "导入中...",
+    "agent.knowledge.toast.loadKbFailed": "加载知识库列表失败",
+    "agent.knowledge.toast.loadDocFailed": "加载文档列表失败",
+    "agent.knowledge.toast.kbNameRequired": "知识库名称不能为空",
+    "agent.knowledge.toast.selectKbFirst": "请先选择知识库",
+    "agent.knowledge.toast.docTitleContentRequired": "标题和内容不能为空",
+    "agent.knowledge.toast.createSuccess": "创建成功",
+    "agent.knowledge.toast.updateSuccess": "更新成功",
+    "agent.knowledge.toast.deleteSuccess": "删除成功",
+    "agent.knowledge.toast.updateFailed": "更新失败",
+    "agent.knowledge.toast.createKbFailed": "创建知识库失败",
+    "agent.knowledge.toast.updateKbFailed": "更新知识库失败",
+    "agent.knowledge.toast.deleteKbFailed": "删除知识库失败",
+    "agent.knowledge.toast.createDocFailed": "创建文档失败",
+    "agent.knowledge.toast.updateDocFailed": "更新文档失败",
+    "agent.knowledge.toast.deleteDocFailed": "删除文档失败",
+    "agent.knowledge.toast.publishSuccess": "发布成功",
+    "agent.knowledge.toast.publishFailed": "发布文档失败",
+    "agent.knowledge.toast.unpublishSuccess": "取消发布成功",
+    "agent.knowledge.toast.unpublishFailed": "取消发布文档失败",
+    "agent.knowledge.toast.selectFiles": "请选择要导入的文件",
+    "agent.knowledge.toast.urlRequired": "请输入至少一个 URL",
+    "agent.knowledge.toast.importDocFailed": "导入文档失败",
+    "agent.knowledge.toast.importUrlFailed": "导入 URL 失败",
+    "agent.knowledge.toast.importRefreshFailed": "导入成功,但刷新列表失败,请手动刷新页面",
+    "agent.knowledge.toast.importFailed.files": "导入失败:{{count}} 个文件未成功",
+    "agent.knowledge.toast.importFailed.urls": "导入失败:{{count}} 个 URL 未成功",
+    "agent.knowledge.toast.importDone.files": "导入完成:成功 {{success}} 个文件",
+    "agent.knowledge.toast.importDone.urls": "导入完成:成功 {{success}} 个 URL",
+    "agent.knowledge.toast.importDone.partial": "导入完成:成功 {{success}},失败 {{failed}} {{err}}",
+    "agent.prompts.title": "提示词",
+    "agent.prompts.subtitle":
+      "配置系统中使用的提示词模板,用于 RAG、联网等场景。仅管理员可修改。占位符说明见下方各卡片。",
+    "agent.prompts.loadFailed": "加载提示词失败",
+    "agent.prompts.saveSuccess": "保存成功,将立即生效。",
+    "agent.prompts.saveFailed": "保存失败",
+    "agent.prompts.usageLabel": "使用场景:",
+    "agent.prompts.ph.shortReply": "请输入一句完整回复语",
+    "agent.prompts.ph.withPlaceholders": "请输入提示词内容,保留占位符",
+    "agent.prompts.saving": "保存中...",
+    "agent.prompts.save": "保存",
+    "agent.prompts.hint.rag_prompt":
+      "占位符:{{rag_context}} 为知识库检索内容,{{user_message}} 为用户问题。",
+    "agent.prompts.hint.rag_prompt_with_web_optional":
+      "占位符:{{rag_context}} 为知识库检索内容,{{user_message}} 为用户问题。",
+    "agent.prompts.hint.no_kb_prompt": "占位符:{{user_message}} 为用户问题。",
+    "agent.prompts.hint.web_search_result_prompt":
+      "占位符:{{web_context}} 为联网搜索结果,{{user_message}} 为用户问题。(当前流程未使用此模板)",
+    "agent.prompts.hint.no_source_reply": "无占位符,内容将作为完整回复语直接展示给用户。",
+    "agent.prompts.hint.ai_fail_reply": "无占位符,内容将作为完整回复语直接展示给用户。",
+    "agent.prompts.hint.default": "请勿删除占位符,保存后由系统替换为实际内容。",
+    "agent.prompts.usage.rag_prompt":
+      "有知识库检索结果,且本回合未勾选「联网搜索」时,用此模板拼成 prompt 发给模型。",
+    "agent.prompts.usage.rag_prompt_with_web_optional":
+      "有知识库检索结果且本回合勾选「联网搜索」时,用此模板并传入联网工具,由模型决定是否调用联网。",
+    "agent.prompts.usage.no_kb_prompt":
+      "没有知识库检索结果且本回合未走联网时,用此模板让模型仅凭自身知识回答。",
+    "agent.prompts.usage.web_search_result_prompt":
+      "预留:若将来有「先联网搜再拼成一段 prompt」的流程,会使用此模板。当前未使用。",
+    "agent.prompts.usage.no_source_reply":
+      "既未命中知识库、也未使用大模型或联网时(如用户关闭了所有数据源),直接向用户展示这句话。",
+    "agent.prompts.usage.ai_fail_reply": "调用 AI 接口失败(超时、报错等)时,向用户展示这句话。",
+    "agent.analytics.title": "数据报表",
+    "agent.analytics.subtitle":
+      "访客小窗与 AI 客服统计(按上海时区自然日,不含「知识库测试」内部会话)",
+    "agent.analytics.from": "从",
+    "agent.analytics.to": "到",
+    "agent.analytics.query": "查询",
+    "agent.analytics.loading": "加载中…",
+    "agent.analytics.empty": "暂无数据",
+    "agent.analytics.emptyOrFailed": "暂无数据或加载失败",
+    "agent.analytics.stat.widgetOpens": "小窗打开次数",
+    "agent.analytics.stat.widgetOpensSub": "需前端埋点,历史数据可能为 0",
+    "agent.analytics.stat.sessions": "新建会话数",
+    "agent.analytics.stat.messages": "消息数",
+    "agent.analytics.stat.aiReplies": "AI 回复次数",
+    "agent.analytics.stat.aiFailed": "AI 失败次数",
+    "agent.analytics.stat.aiFailureRate": "AI 失败率",
+    "agent.analytics.stat.aiFailureRateSub": "占 AI 回复条数",
+    "agent.analytics.stat.kbHits": "知识库命中次数",
+    "agent.analytics.stat.kbHitRate": "知识库命中率",
+    "agent.analytics.stat.kbHitRateSub": "占成功 AI 回复",
+    "agent.analytics.stat.maxAiRounds": "最大 AI 对话轮数",
+    "agent.analytics.stat.maxAiRoundsSub": "单会话内用户+AI 一轮",
+    "agent.analytics.stat.sessionsWithAi": "AI 参与会话",
+    "agent.analytics.stat.sessionsWithAiSub": "占新建会话 {{pct}}",
+    "agent.analytics.stat.aiToHuman": "AI→人工(会话数)",
+    "agent.analytics.stat.aiToHumanSub": "占有过 AI 发言的会话 {{pct}}",
+    "agent.analytics.stat.humanToAi": "人工→AI(会话数)",
+    "agent.analytics.stat.humanToAiSub": "占有过人工发言的会话 {{pct}}",
+    "agent.analytics.chart.widgetOpens": "每日小窗打开",
+    "agent.analytics.chart.sessions": "每日新建会话",
+    "agent.analytics.chart.messages": "每日消息数",
+    "agent.analytics.chart.aiReplies": "每日 AI 回复",
+    "common.irreversibleHint": "此操作不可恢复,请谨慎操作。",
+    "chat.title": "客服聊天",
+    "chat.mode.human": "人工客服",
+    "chat.mode.ai": "AI 客服",
+  },
+  en: {
+    "nav.features": "Features",
+    "nav.screenshots": "Screenshots",
+    "nav.quickStart": "Quick Start",
+    "nav.agentLogin": "Agent Login",
+    "nav.menu": "Menu",
+    "common.github": "GitHub",
+    "common.to": "to",
+    "common.save": "Save",
+    "common.saving": "Saving...",
+    "common.restoreEnv": "Use env default",
+    "common.loading": "Loading...",
+    "common.search": "Search",
+    "common.prevPage": "Prev",
+    "common.nextPage": "Next",
+    "common.copy": "Copy",
+    "home.hero.tagline": "AI Customer Support",
+    "home.hero.title": "Make customer support simpler and faster",
+    "home.hero.subtitle":
+      "24/7 AI responses with seamless handoff to human agents—free your team to focus on what matters.",
+    "home.hero.cta.tryNow": "Try now",
+    "home.hero.cta.agentLogin": "Agent Login",
+    "home.hero.hint": "No waiting—ready to use",
+    "home.stats.trustedBy": "Trusted by teams",
+    "home.stats.clients": "Teams served",
+    "home.stats.conversations": "Conversations handled",
+    "home.stats.latency": "Response time",
+    "home.stats.satisfaction": "Satisfaction",
+    "home.stats.val.clients": "1000+",
+    "home.stats.val.conversations": "1M+",
+    "home.stats.val.latency": "<100ms",
+    "home.stats.val.satisfaction": "98%",
+    "home.features.title": "Capabilities",
+    "home.features.lead":
+      "From models and knowledge bases to prompts, human collaboration, analytics, and logs—one cohesive system.",
+    "home.cap.multimodel.title": "Multi-model AI support",
+    "home.cap.multimodel.desc":
+      "Configure multiple LLM and multimodal providers; visitors and admins share one place to manage models and usage—easy to swap vendors and control cost.",
+    "home.cap.kb.title": "Knowledge base & RAG",
+    "home.cap.kb.desc":
+      "Ingest documents and retrieve with vectors so answers stay on-brand; replies can show whether KB, model, or web search was used for review and tuning.",
+    "home.cap.prompt.title": "Prompt engineering",
+    "home.cap.prompt.desc":
+      "Edit the prompt templates that power RAG, optional web search, and other flows across your scenarios.",
+    "home.cap.human.title": "Human agents & real-time collaboration",
+    "home.cap.human.desc":
+      "Online presence, live sessions over WebSocket, seamless handoff and teamwork; embed the visitor widget on any site.",
+    "home.cap.reports.title": "Analytics",
+    "home.cap.reports.desc":
+      "Daily or custom ranges for widget opens, sessions, messages, AI success/failure, KB hit rate, and more—see how operations are trending.",
+    "home.cap.logs.title": "Structured logs",
+    "home.cap.logs.desc":
+      "Persisted by category and event with trace_id and keyword search—trace critical paths and incidents for troubleshooting and audit.",
+    "home.screenshots.title": "Product screenshots",
+    "home.screenshots.lead": "A polished UI that makes day-to-day admin work easier.",
+    "home.screenshots.prevAria": "Previous screenshot",
+    "home.screenshots.nextAria": "Next screenshot",
+    "home.ss.dashboard.title": "Agent workspace",
+    "home.ss.dashboard.placeholder": "Workspace preview",
+    "home.ss.dashboard.alt": "AI-CS agent workspace",
+    "home.ss.visitor.title": "Visitor widget",
+    "home.ss.visitor.placeholder": "Visitor UI preview",
+    "home.ss.visitor.alt": "AI-CS visitor experience",
+    "home.ss.aiconfig.title": "AI configuration",
+    "home.ss.aiconfig.placeholder": "AI settings preview",
+    "home.ss.aiconfig.alt": "AI-CS AI configuration",
+    "home.ss.users.title": "User management",
+    "home.ss.users.placeholder": "User admin preview",
+    "home.ss.users.alt": "AI-CS user management",
+    "home.ss.faq.title": "FAQ management",
+    "home.ss.faq.placeholder": "FAQ admin preview",
+    "home.ss.faq.alt": "AI-CS FAQ management",
+    "home.ss.knowledge.title": "Knowledge base",
+    "home.ss.knowledge.placeholder": "Knowledge base preview",
+    "home.ss.knowledge.alt": "AI-CS knowledge base",
+    "home.ss.kbtest.title": "KB test chat",
+    "home.ss.kbtest.placeholder": "KB test preview",
+    "home.ss.kbtest.alt": "AI-CS knowledge base test",
+    "home.ss.prompts.title": "Prompts",
+    "home.ss.prompts.placeholder": "Prompts preview",
+    "home.ss.prompts.alt": "AI-CS prompt management",
+    "home.ss.logs.title": "Logs",
+    "home.ss.logs.placeholder": "Logs preview",
+    "home.ss.logs.alt": "AI-CS log center",
+    "home.ss.analytics.title": "Analytics",
+    "home.ss.analytics.placeholder": "Analytics preview",
+    "home.ss.analytics.alt": "AI-CS analytics",
+    "home.quickStart.title": "Quick start",
+    "home.quickStart.lead": "Three steps from the repo to the visitor widget.",
+    "home.step1.title": "Clone & configure",
+    "home.step1.body": "Copy the .env template and fill required database and admin settings.",
+    "home.step2.title": "Launch",
+    "home.step2.body": "Bring up backend, frontend, and dependencies with Docker Compose (see README).",
+    "home.step3.title": "Embed for visitors",
+    "home.step3.body": "Mount the chat widget on your site; configure models and KB in the console, then go live.",
+    "home.cta.title": "Ready to wire AI-CS into your product?",
+    "home.cta.subtitle": "Start from the open-source repo, or explore the online demo first.",
+    "home.cta.starRepo": "Star / Fork on GitHub",
+    "home.cta.feedback": "Send feedback",
+    "home.cta.mailSubject": "AI-CS feedback",
+    "home.cta.mailBody":
+      "Hi,\n\n1) Issue or suggestion:\n2) Scope / environment:\n3) Expected outcome:\n\n---\nContact (optional):",
+    "footer.blurb":
+      "AI-CS is an AI-powered customer support stack that blends automation and human agents for efficient, modern service.",
+    "footer.column.product": "Product",
+    "footer.column.friendLinks": "Friends",
+    "footer.column.contact": "Contact",
+    "footer.noFriendLinks": "No links yet",
+    "footer.onlineChat": "Live chat",
+    "footer.openSourceLicense": "Open-source license",
+    "footer.poweredBy": "Powered by Next.js & Go |",
+    "footer.allRightsReserved": "All rights reserved.",
+    "footer.emailLabel": "Email",
+    "agent.page.dashboard": "Chats",
+    "agent.page.internalChat": "KB Test",
+    "agent.page.knowledge": "Knowledge Base",
+    "agent.page.faqs": "FAQs",
+    "agent.page.analytics": "Analytics",
+    "agent.page.logs": "Logs",
+    "agent.page.users": "Users",
+    "agent.page.prompts": "Prompts",
+    "agent.page.settings": "AI Settings",
+    "agent.profile": "Profile",
+    "agent.logout": "Log out",
+    "agent.chat.conversation": "Chat",
+    "agent.chat.lastSeen": "Last seen",
+    "agent.chat.lastSeenUnknown": "Last seen unknown",
+    "agent.chat.showAI": "Show AI messages",
+    "agent.chat.hideAI": "Hide AI messages",
+    "agent.chat.closeConversation": "Close",
+    "agent.chat.refresh": "Refresh",
+    "agent.chat.soundOn": "Turn sound off",
+    "agent.chat.soundOff": "Turn sound on",
+    "agent.chat.toast.conversationClosed": "Conversation closed",
+    "agent.chat.toast.closeFailed": "Failed to close conversation",
+    "agent.chat.emptyPick": "Select a conversation to start",
+    "agent.layout.openNavMenu": "Open navigation and conversation list",
+    "agent.layout.openVisitorPanel": "Open visitor details",
+    "agent.internalChat.webSearchThisTurn": "Web search this turn",
+    "agent.internalChat.aiThinking": "AI is thinking...",
+    "agent.internalChat.emptyHint": "Select or create an internal chat to test the knowledge base",
+    "agent.internalChat.createFailed": "Failed to create internal chat",
+    "agent.login.title": "Agent Login",
+    "agent.login.subtitle": "Admins and agents sign in here",
+    "agent.login.username": "Username",
+    "agent.login.password": "Password",
+    "agent.login.submit": "Sign in",
+    "agent.login.submitting": "Signing in...",
+    "agent.login.error.empty": "Username and password are required",
+    "agent.login.error.failed": "Sign-in failed",
+    "agent.login.error.network": "Sign-in failed. Check your connection.",
+    "agent.login.demoHint": "Default admin: admin / admin123",
+    "agent.logs.title": "Logs",
+    "agent.logs.subtitle": "Filter AI / RAG / system / frontend logs for troubleshooting.",
+    "agent.logs.policy.title": "Persist level (performance)",
+    "agent.logs.policy.desc":
+      "Only logs at or above this level are persisted. Set to warn to reduce successful info writes. You can set SYSTEM_LOG_MIN_LEVEL in .env as default; saving here persists to DB and overrides env until restored.",
+    "agent.logs.policy.current": "Effective:",
+    "agent.logs.policy.env": "Env default:",
+    "agent.logs.policy.overridden": "(overridden in console)",
+    "agent.logs.level.all": "All levels",
+    "agent.logs.category.all": "All categories",
+    "agent.logs.source.all": "All sources",
+    "agent.logs.event.placeholder": "Event",
+    "agent.logs.conversationId.placeholder": "Conversation ID",
+    "agent.logs.keyword.placeholder": "Keyword (message/meta)",
+    "agent.logs.table.time": "Time",
+    "agent.logs.table.level": "Level",
+    "agent.logs.table.category": "Category",
+    "agent.logs.table.event": "Event",
+    "agent.logs.table.conversation": "Conversation",
+    "agent.logs.table.source": "Source",
+    "agent.logs.table.message": "Message",
+    "agent.logs.paginationSummary": "{{total}} rows · page {{page}} / {{pages}}",
+    "agent.logs.empty": "No logs",
+    "agent.logs.detail.title": "Log detail",
+    "agent.logs.detail.time": "Time",
+    "agent.logs.detail.sourceEvent": "source / event",
+    "agent.logs.detail.category": "category",
+    "agent.logs.detail.traceId": "trace_id",
+    "agent.logs.detail.conversationId": "conversation_id",
+    "agent.logs.detail.userVisitor": "user_id / visitor_id",
+    "agent.logs.detail.message": "message",
+    "agent.logs.detail.metaJson": "meta_json",
+    "agent.logs.detail.noMeta": "(no meta_json)",
+    "agent.logs.toast.loadPolicyFailed": "Failed to load persist policy",
+    "agent.logs.toast.loadLogsFailed": "Failed to load logs",
+    "agent.logs.toast.savePolicyFailed": "Save failed",
+    "agent.logs.toast.restorePolicyFailed": "Restore failed",
+    "agent.logs.toast.policySaved": "Saved",
+    "agent.logs.toast.policyRestored": "Restored to env default",
+    "agent.logs.toast.messageCopied": "Message copied",
+    "agent.logs.toast.copyFailed": "Copy failed",
+    "agent.conversationsPage.title": "Conversations",
+    "agent.conversationsPage.loading": "Loading...",
+    "agent.conversationsPage.empty": "No conversations",
+    "agent.conversationsPage.convLabel": "Chat #{{id}}",
+    "agent.conversationsPage.visitorLabel": "Visitor ID: {{id}}",
+    "agent.conversationsPage.createdAt": "Created: {{time}}",
+    "agent.conversationsPage.updatedAt": "Updated: {{time}}",
+    "agent.conversations.filter.all": "All chats",
+    "agent.conversations.filter.mine": "My chats",
+    "agent.conversations.filter.others": "Others",
+    "agent.conversations.status.open": "Open",
+    "agent.conversations.status.closed": "History",
+    "agent.internalChat.title": "KB Test",
+    "agent.internalChat.new": "New",
+    "agent.conversation.noMessage": "No messages yet",
+    "agent.conversation.online": "Online",
+    "agent.conversation.visitor": "Visitor",
+    "agent.input.upload": "Upload",
+    "agent.input.placeholder": "Type a message...",
+    "agent.input.placeholder.withAttachment": "Add a message (optional)...",
+    "agent.input.sending": "Sending...",
+    "agent.input.uploading": "Uploading...",
+    "agent.input.send": "Send",
+    "agent.input.fileTooLarge": "File is too large (max 10MB)",
+    "agent.input.fileTypeNotSupported": "Unsupported file type",
+    "agent.input.uploadFailed": "Upload failed",
+    "agent.aiSource.kb": "Knowledge base used",
+    "agent.aiSource.llm": "LLM used",
+    "agent.aiSource.web": "Web search used",
+    "agent.common.back": "Back",
+    "agent.common.cancel": "Cancel",
+    "agent.common.create": "Create",
+    "agent.common.update": "Update",
+    "agent.common.delete": "Delete",
+    "agent.common.edit": "Edit",
+    "agent.common.keywordSearch": "Keyword search",
+    "agent.common.noMatch": "No results found",
+    "agent.common.none": "None",
+    "agent.common.confirm": "Confirm",
+    "agent.faqs.title": "FAQs",
+    "agent.faqs.subtitle": "Manage FAQ/event templates with keyword search.",
+    "agent.faqs.search.placeholder": "Keyword search (use % as separator)...",
+    "agent.faqs.createButton": "Create",
+    "agent.faqs.empty": "No FAQs",
+    "agent.faqs.empty.filtered": "No matching FAQs",
+    "agent.faqs.dialog.createTitle": "Create FAQ",
+    "agent.faqs.dialog.editTitle": "Edit FAQ",
+    "agent.faqs.dialog.deleteTitle": "Delete FAQ",
+    "agent.faqs.form.question": "Question",
+    "agent.faqs.form.answer": "Answer",
+    "agent.faqs.form.keywords": "Keywords",
+    "agent.faqs.form.keywordsHint": "Separate keywords with % for better matching",
+    "agent.faqs.toast.loadFailed": "Failed to load FAQs",
+    "agent.faqs.toast.createFailed": "Failed to create FAQ",
+    "agent.faqs.toast.updateFailed": "Failed to update FAQ",
+    "agent.faqs.toast.deleteFailed": "Failed to delete FAQ",
+    "agent.faqs.toast.createSuccess": "Created",
+    "agent.faqs.toast.updateSuccess": "Updated",
+    "agent.faqs.toast.deleteSuccess": "Deleted",
+    "agent.faqs.toast.emptyRequired": "Question and answer are required",
+    "agent.faqs.card.keywords": "Keywords",
+    "agent.faqs.card.createdAt": "Created",
+    "agent.faqs.card.edit": "Edit",
+    "agent.faqs.dialog.createTitle2": "Create FAQ",
+    "agent.faqs.dialog.createDesc": "Provide question and answer. Add keywords for search.",
+    "agent.faqs.dialog.editDesc": "Update question/answer and keywords for search.",
+    "agent.faqs.dialog.deleteConfirm": "Delete \"{{name}}\"?",
+    "agent.faqs.form.placeholder.question": "Enter question",
+    "agent.faqs.form.placeholder.answer": "Enter answer",
+    "agent.faqs.form.placeholder.keywords":
+      "e.g. API, error, config (comma or space separated)",
+    "agent.faqs.form.keywordsOptional": "Keywords (optional)",
+    "agent.faqs.form.keywordsTip":
+      "Tip: Even without keywords, the system searches question and answer content. Keywords add extra search index for faster matching.",
+    "agent.faqs.submit.creating": "Creating...",
+    "agent.faqs.submit.deleting": "Deleting...",
+    "agent.perm.analytics": "Analytics",
+    "agent.perm.chat": "Chat",
+    "agent.perm.faqs": "FAQs",
+    "agent.perm.kb_test": "KB test",
+    "agent.perm.knowledge": "Knowledge base",
+    "agent.perm.logs": "Logs",
+    "agent.perm.prompts": "Prompts",
+    "agent.perm.settings": "AI settings",
+    "agent.perm.users": "Users",
+    "agent.settings.aiCard.titleAdd": "Add AI config",
+    "agent.settings.aiCard.titleEdit": "Edit AI config",
+    "agent.settings.aiForm.active": "Enabled",
+    "agent.settings.aiForm.apiKey": "API key",
+    "agent.settings.aiForm.apiUrl": "API URL",
+    "agent.settings.aiForm.apiUrlPh": "https://api.openai.com/v1/chat/completions",
+    "agent.settings.aiForm.descPh": "e.g. OpenAI GPT-3.5 Turbo",
+    "agent.settings.aiForm.description": "Description",
+    "agent.settings.aiForm.model": "Model name",
+    "agent.settings.aiForm.modelType": "Model type",
+    "agent.settings.aiForm.modelPh": "e.g. gpt-3.5-turbo, gpt-4",
+    "agent.settings.aiForm.provider": "Provider",
+    "agent.settings.aiForm.providerPh": "e.g. OpenAI, Claude, Custom",
+    "agent.settings.aiForm.public": "Available to visitors",
+    "agent.settings.aiForm.submitCreate": "Create",
+    "agent.settings.aiForm.submitUpdate": "Update",
+    "agent.settings.aiForm.submitting": "Submitting...",
+    "agent.settings.backDashboard": "Back to workspace",
+    "agent.settings.badge.active": "On",
+    "agent.settings.badge.public": "Public",
+    "agent.settings.confirmDeleteConfig": "Delete this configuration?",
+    "agent.settings.embedding.apiKey": "API key",
+    "agent.settings.embedding.apiKeyKeepEmpty": "Leave blank to keep unchanged",
+    "agent.settings.embedding.apiKeyInput": "Enter API key",
+    "agent.settings.embedding.apiUrl": "API URL",
+    "agent.settings.embedding.apiUrlPh": "https://api.openai.com/v1 or compatible URL",
+    "agent.settings.embedding.bgeLocal": "BGE local",
+    "agent.settings.embedding.customerKb":
+      "Let agents use the knowledge base (create KBs, upload docs, cite in chat)",
+    "agent.settings.embedding.lead":
+      "Embeddings for KB documents and RAG. Admins only; changes apply immediately without restart.",
+    "agent.settings.embedding.model": "Model",
+    "agent.settings.embedding.modelPh": "text-embedding-3-small",
+    "agent.settings.embedding.openaiCompatible": "OpenAI / compatible API",
+    "agent.settings.embedding.save": "Save",
+    "agent.settings.embedding.title": "Embedding model (knowledge base)",
+    "agent.settings.embedding.type": "Type",
+    "agent.settings.error.delete": "Delete failed",
+    "agent.settings.error.loadConfigs": "Failed to load configs",
+    "agent.settings.error.loadEmbedding": "Load failed",
+    "agent.settings.error.operation": "Operation failed",
+    "agent.settings.global.noReceiveAi": "Do not receive AI conversations",
+    "agent.settings.global.noReceiveAiHint":
+      "When enabled, AI conversations won't appear in your list or notify you. You can still open a chat and turn on “Show AI messages” to view history.",
+    "agent.settings.list.apiUrlLabel": "API URL:",
+    "agent.settings.list.descLabel": "Description:",
+    "agent.settings.list.empty": "No configs yet — add one",
+    "agent.settings.list.modelTypeLabel": "Model type:",
+    "agent.settings.list.title": "Configured providers",
+    "agent.settings.modelType.audio": "Audio",
+    "agent.settings.modelType.image": "Image",
+    "agent.settings.modelType.text": "Text",
+    "agent.settings.modelType.video": "Video",
+    "agent.settings.section.global": "Global",
+    "agent.settings.subtitle": "Manage AI provider settings",
+    "agent.settings.title": "AI configuration",
+    "agent.settings.toast.embeddingSaved": "Saved. Changes are live.",
+    "agent.settings.toast.profileUpdateFailed": "Failed to update settings. Try again.",
+    "agent.settings.webSearch.lead":
+      "Web search mode and whether visitors see the toggle. Unrelated to embedding settings above.",
+    "agent.settings.webSearch.mode": "Web search mode",
+    "agent.settings.webSearch.modeCustom": "Self-hosted (Serper)",
+    "agent.settings.webSearch.modeHint":
+      "Custom: backend uses Serper (MCP or HTTP). Vendor: use the model provider’s built-in web search (no Serper).",
+    "agent.settings.webSearch.modeVendor": "Vendor built-in",
+    "agent.settings.webSearch.save": "Save web search settings",
+    "agent.settings.webSearch.title": "Web search",
+    "agent.settings.webSearch.visitorToggle":
+      "Show “web search this turn” in the visitor widget",
+    "agent.users.card.edit": "Edit",
+    "agent.users.card.password": "Password",
+    "agent.users.createButton": "Create user",
+    "agent.users.dialog.createTitle": "Create user",
+    "agent.users.dialog.deleteConfirm": "Delete user {{username}}?",
+    "agent.users.dialog.deleteNote":
+      "This cannot be undone. If this user has AI configs, they are transferred to the current admin.",
+    "agent.users.dialog.deleteTitle": "Delete user",
+    "agent.users.dialog.editTitle": "Edit user",
+    "agent.users.dialog.passwordTitle": "Change password",
+    "agent.users.empty": "No users",
+    "agent.users.empty.filtered": "No matching users",
+    "agent.users.field.createdAt": "Created",
+    "agent.users.field.email": "Email",
+    "agent.users.field.username": "Username",
+    "agent.users.form.email": "Email",
+    "agent.users.form.newPassword": "New password",
+    "agent.users.form.oldPassword": "Old password",
+    "agent.users.form.password": "Password",
+    "agent.users.form.permissions": "Permissions",
+    "agent.users.form.permissionsHint":
+      "Only “Chat” is on by default. Turning off hides the menu and returns 403 from APIs.",
+    "agent.users.form.role": "Role",
+    "agent.users.form.username": "Username",
+    "agent.users.form.nickname": "Nickname",
+    "agent.users.placeholder.email": "Email",
+    "agent.users.placeholder.emailOptional": "Email (optional)",
+    "agent.users.placeholder.nickname": "Nickname",
+    "agent.users.placeholder.nicknameOptional": "Nickname (optional)",
+    "agent.users.placeholder.oldPassword": "Current password",
+    "agent.users.placeholder.password": "Password",
+    "agent.users.placeholder.username": "Username",
+    "agent.users.receiveAiLabel": "Receive AI conversations",
+    "agent.users.role.admin": "Admin",
+    "agent.users.role.agent": "Agent",
+    "agent.users.search.placeholder": "Search by username, nickname, email…",
+    "agent.users.submit.creating": "Creating...",
+    "agent.users.submit.deleting": "Deleting...",
+    "agent.users.submit.updating": "Updating...",
+    "agent.users.title": "Users",
+    "agent.users.toast.adminDeleteDisabled":
+      "Admin accounts can only be removed via database; disabled in UI",
+    "agent.users.toast.adminPasswordDisabled":
+      "Admin passwords can only be changed via database; disabled in UI",
+    "agent.users.toast.createFailed": "Failed to create user",
+    "agent.users.toast.createSuccess": "Created",
+    "agent.users.toast.deleteFailed": "Failed to delete user",
+    "agent.users.toast.deleteSuccess": "Deleted",
+    "agent.users.toast.deleteTransferred":
+      "Deleted. {{count}} AI config(s) moved to the current admin",
+    "agent.users.toast.loadFailed": "Failed to load users",
+    "agent.users.toast.newPasswordRequired": "New password is required",
+    "agent.users.toast.oldPasswordRequired": "Current password is required to change your own password",
+    "agent.users.toast.passwordFailed": "Failed to update password",
+    "agent.users.toast.passwordSuccess": "Password updated",
+    "agent.users.toast.updateFailed": "Failed to update user",
+    "agent.users.toast.updateSuccess": "Updated",
+    "agent.users.toast.usernamePasswordRequired": "Username and password are required",
+    "agent.users.tooltip.adminDeleteDbOnly": "Admin deletion is database-only",
+    "agent.users.tooltip.adminPasswordDbOnly": "Admin password is database-only",
+    "agent.users.tooltip.cannotDeleteSelf": "You cannot delete the signed-in user",
+    "agent.users.usernameImmutableHint": "Username cannot be changed",
+    "agent.knowledge.title": "Knowledge base",
+    "agent.knowledge.rag": "RAG",
+    "agent.knowledge.kb.create": "New knowledge base",
+    "agent.knowledge.kb.empty": "No knowledge bases",
+    "agent.knowledge.kb.selectOne": "Select a knowledge base",
+    "agent.knowledge.kb.docCount": "{{count}} docs",
+    "agent.knowledge.import.url": "Import URL",
+    "agent.knowledge.import.file": "Import files",
+    "agent.knowledge.import.tabFile": "Files",
+    "agent.knowledge.import.tabUrl": "URLs",
+    "agent.knowledge.import.pickFiles": "Choose files",
+    "agent.knowledge.import.filesSelected": "{{count}} file(s) selected",
+    "agent.knowledge.import.action": "Import",
+    "agent.knowledge.import.urlListLabel": "URL list (one per line)",
+    "agent.knowledge.doc.create": "New doc",
+    "agent.knowledge.doc.searchPh": "Search docs...",
+    "agent.knowledge.doc.empty": "No docs",
+    "agent.knowledge.doc.empty.filtered": "No matching docs",
+    "agent.knowledge.doc.type": "Type",
+    "agent.knowledge.doc.createdAt": "Created",
+    "agent.knowledge.doc.publish": "Publish",
+    "agent.knowledge.doc.unpublish": "Unpublish",
+    "agent.knowledge.filter.all": "All statuses",
+    "agent.knowledge.pagination": "Page {{page}} / {{totalPage}}, total {{total}}",
+    "agent.knowledge.status.draft": "Draft",
+    "agent.knowledge.status.published": "Published",
+    "agent.knowledge.embedding.pending": "Pending",
+    "agent.knowledge.embedding.processing": "Processing",
+    "agent.knowledge.embedding.completed": "Completed",
+    "agent.knowledge.embedding.failed": "Failed",
+    "agent.knowledge.dialog.kbCreateTitle": "Create knowledge base",
+    "agent.knowledge.dialog.kbCreateDesc": "Enter name and description",
+    "agent.knowledge.dialog.kbEditTitle": "Edit knowledge base",
+    "agent.knowledge.dialog.kbEditDesc": "Update name and description",
+    "agent.knowledge.dialog.kbDeleteTitle": "Delete knowledge base",
+    "agent.knowledge.dialog.kbDeleteConfirm": "Delete knowledge base \"{{name}}\"?",
+    "agent.knowledge.dialog.kbDeleteHint":
+      "This will also delete all docs in the knowledge base. This action cannot be undone.",
+    "agent.knowledge.dialog.docCreateTitle": "Create doc",
+    "agent.knowledge.dialog.docCreateDesc": "Enter title and content",
+    "agent.knowledge.dialog.docEditTitle": "Edit doc",
+    "agent.knowledge.dialog.docEditDesc": "Update title and content",
+    "agent.knowledge.dialog.docDeleteTitle": "Delete doc",
+    "agent.knowledge.dialog.docDeleteConfirm": "Delete doc \"{{title}}\"?",
+    "agent.knowledge.dialog.importTitle": "Import docs",
+    "agent.knowledge.dialog.importDesc":
+      "Upload files or import by URL. Supported: Markdown (.md, .markdown). PDF/Word parsing is in progress.",
+    "agent.knowledge.field.name": "Name",
+    "agent.knowledge.field.descOptional": "Description (optional)",
+    "agent.knowledge.field.title": "Title",
+    "agent.knowledge.field.summaryOptional": "Summary (optional)",
+    "agent.knowledge.field.content": "Content",
+    "agent.knowledge.ph.kbName": "Knowledge base name",
+    "agent.knowledge.ph.kbDesc": "Knowledge base description",
+    "agent.knowledge.ph.docTitle": "Doc title",
+    "agent.knowledge.ph.docSummary": "Doc summary",
+    "agent.knowledge.ph.docContent": "Doc content",
+    "agent.knowledge.submitting.creating": "Creating...",
+    "agent.knowledge.submitting.updating": "Updating...",
+    "agent.knowledge.submitting.deleting": "Deleting...",
+    "agent.knowledge.submitting.importing": "Importing...",
+    "agent.knowledge.toast.loadKbFailed": "Failed to load knowledge bases",
+    "agent.knowledge.toast.loadDocFailed": "Failed to load docs",
+    "agent.knowledge.toast.kbNameRequired": "Knowledge base name is required",
+    "agent.knowledge.toast.selectKbFirst": "Please select a knowledge base first",
+    "agent.knowledge.toast.docTitleContentRequired": "Title and content are required",
+    "agent.knowledge.toast.createSuccess": "Created",
+    "agent.knowledge.toast.updateSuccess": "Updated",
+    "agent.knowledge.toast.deleteSuccess": "Deleted",
+    "agent.knowledge.toast.updateFailed": "Update failed",
+    "agent.knowledge.toast.createKbFailed": "Failed to create knowledge base",
+    "agent.knowledge.toast.updateKbFailed": "Failed to update knowledge base",
+    "agent.knowledge.toast.deleteKbFailed": "Failed to delete knowledge base",
+    "agent.knowledge.toast.createDocFailed": "Failed to create doc",
+    "agent.knowledge.toast.updateDocFailed": "Failed to update doc",
+    "agent.knowledge.toast.deleteDocFailed": "Failed to delete doc",
+    "agent.knowledge.toast.publishSuccess": "Published",
+    "agent.knowledge.toast.publishFailed": "Failed to publish doc",
+    "agent.knowledge.toast.unpublishSuccess": "Unpublished",
+    "agent.knowledge.toast.unpublishFailed": "Failed to unpublish doc",
+    "agent.knowledge.toast.selectFiles": "Please choose files to import",
+    "agent.knowledge.toast.urlRequired": "Please enter at least one URL",
+    "agent.knowledge.toast.importDocFailed": "Failed to import docs",
+    "agent.knowledge.toast.importUrlFailed": "Failed to import URLs",
+    "agent.knowledge.toast.importRefreshFailed":
+      "Imported, but failed to refresh list. Please refresh the page.",
+    "agent.knowledge.toast.importFailed.files": "Import failed: {{count}} file(s)",
+    "agent.knowledge.toast.importFailed.urls": "Import failed: {{count}} URL(s)",
+    "agent.knowledge.toast.importDone.files": "Imported: {{success}} file(s)",
+    "agent.knowledge.toast.importDone.urls": "Imported: {{success}} URL(s)",
+    "agent.knowledge.toast.importDone.partial":
+      "Imported: {{success}} success, {{failed}} failed {{err}}",
+    "agent.prompts.title": "Prompts",
+    "agent.prompts.subtitle":
+      "Edit system prompt templates for RAG and web search. Admin only. Placeholder hints are shown per card.",
+    "agent.prompts.loadFailed": "Failed to load prompts",
+    "agent.prompts.saveSuccess": "Saved. Changes apply immediately.",
+    "agent.prompts.saveFailed": "Save failed",
+    "agent.prompts.usageLabel": "When used:",
+    "agent.prompts.ph.shortReply": "Enter a full short reply sentence",
+    "agent.prompts.ph.withPlaceholders": "Enter prompt text; keep placeholders",
+    "agent.prompts.saving": "Saving...",
+    "agent.prompts.save": "Save",
+    "agent.prompts.hint.rag_prompt":
+      "Placeholders: {{rag_context}} = retrieved KB text, {{user_message}} = user question.",
+    "agent.prompts.hint.rag_prompt_with_web_optional":
+      "Placeholders: {{rag_context}} = retrieved KB text, {{user_message}} = user question.",
+    "agent.prompts.hint.no_kb_prompt": "Placeholder: {{user_message}} = user question.",
+    "agent.prompts.hint.web_search_result_prompt":
+      "Placeholders: {{web_context}} = web results, {{user_message}} = user question. (Not used in current flow)",
+    "agent.prompts.hint.no_source_reply": "No placeholders; shown to the user as a full reply.",
+    "agent.prompts.hint.ai_fail_reply": "No placeholders; shown to the user as a full reply.",
+    "agent.prompts.hint.default": "Do not remove placeholders; they are filled in by the system.",
+    "agent.prompts.usage.rag_prompt":
+      "When KB retrieval succeeded and web search is off for this turn, this template is sent to the model.",
+    "agent.prompts.usage.rag_prompt_with_web_optional":
+      "When KB retrieval succeeded and web search is on, this template enables the model to optionally use web tools.",
+    "agent.prompts.usage.no_kb_prompt":
+      "When there is no KB hit and no web flow, the model answers from its own knowledge only.",
+    "agent.prompts.usage.web_search_result_prompt":
+      "Reserved for a future “web first, then prompt” flow. Not used today.",
+    "agent.prompts.usage.no_source_reply":
+      "When no KB/model/web sources apply, show this sentence to the user.",
+    "agent.prompts.usage.ai_fail_reply": "When the AI API errors or times out, show this sentence to the user.",
+    "agent.analytics.title": "Analytics",
+    "agent.analytics.subtitle":
+      "Visitor widget & AI support stats (calendar day in Shanghai; excludes internal “KB test” chats)",
+    "agent.analytics.from": "From",
+    "agent.analytics.to": "To",
+    "agent.analytics.query": "Query",
+    "agent.analytics.loading": "Loading…",
+    "agent.analytics.empty": "No data",
+    "agent.analytics.emptyOrFailed": "No data or failed to load",
+    "agent.analytics.stat.widgetOpens": "Widget opens",
+    "agent.analytics.stat.widgetOpensSub": "Requires frontend tracking; history may be 0",
+    "agent.analytics.stat.sessions": "New sessions",
+    "agent.analytics.stat.messages": "Messages",
+    "agent.analytics.stat.aiReplies": "AI replies",
+    "agent.analytics.stat.aiFailed": "AI failures",
+    "agent.analytics.stat.aiFailureRate": "AI failure rate",
+    "agent.analytics.stat.aiFailureRateSub": "Of AI reply rows",
+    "agent.analytics.stat.kbHits": "KB hits",
+    "agent.analytics.stat.kbHitRate": "KB hit rate",
+    "agent.analytics.stat.kbHitRateSub": "Of successful AI replies",
+    "agent.analytics.stat.maxAiRounds": "Max AI rounds / session",
+    "agent.analytics.stat.maxAiRoundsSub": "One round = user + AI once",
+    "agent.analytics.stat.sessionsWithAi": "Sessions with AI",
+    "agent.analytics.stat.sessionsWithAiSub": "{{pct}} of new sessions",
+    "agent.analytics.stat.aiToHuman": "AI → human (sessions)",
+    "agent.analytics.stat.aiToHumanSub": "{{pct}} of sessions that had AI messages",
+    "agent.analytics.stat.humanToAi": "Human → AI (sessions)",
+    "agent.analytics.stat.humanToAiSub": "{{pct}} of sessions that had human messages",
+    "agent.analytics.chart.widgetOpens": "Widget opens by day",
+    "agent.analytics.chart.sessions": "New sessions by day",
+    "agent.analytics.chart.messages": "Messages by day",
+    "agent.analytics.chart.aiReplies": "AI replies by day",
+    "common.irreversibleHint": "This action cannot be undone.",
+    "chat.title": "Chat",
+    "chat.mode.human": "Human",
+    "chat.mode.ai": "AI",
+  },
+};
+

+ 71 - 0
frontend/lib/i18n/provider.tsx

@@ -0,0 +1,71 @@
+"use client";
+
+import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
+import { DEFAULT_LANG, DICT, LANG_STORAGE_KEY, type I18nKey, type Lang } from "./dict";
+
+function isLang(x: string | null | undefined): x is Lang {
+  return x === "zh-CN" || x === "en";
+}
+
+function getLangFromLocation(): Lang | null {
+  if (typeof window === "undefined") return null;
+  const url = new URL(window.location.href);
+  const q = url.searchParams.get("lang");
+  if (isLang(q)) return q;
+  const stored = window.localStorage.getItem(LANG_STORAGE_KEY);
+  if (isLang(stored)) return stored;
+  return null;
+}
+
+type I18nContextValue = {
+  lang: Lang;
+  setLang: (lang: Lang) => void;
+  t: (key: I18nKey) => string;
+};
+
+const I18nContext = createContext<I18nContextValue | null>(null);
+
+export function I18nProvider({ children }: { children: React.ReactNode }) {
+  const [lang, setLangState] = useState<Lang>(DEFAULT_LANG);
+
+  useEffect(() => {
+    const initial = getLangFromLocation();
+    if (initial) setLangState(initial);
+  }, []);
+
+  const setLang = useCallback((next: Lang) => {
+    setLangState(next);
+    if (typeof window === "undefined") return;
+    window.localStorage.setItem(LANG_STORAGE_KEY, next);
+    // 同步 URL(可分享)
+    const url = new URL(window.location.href);
+    url.searchParams.set("lang", next);
+    window.history.replaceState(null, "", url.toString());
+  }, []);
+
+  const t = useCallback(
+    (key: I18nKey) => {
+      const v = DICT[lang]?.[key];
+      if (typeof v === "string" && v) return v;
+      return DICT[DEFAULT_LANG][key] ?? key;
+    },
+    [lang]
+  );
+
+  const value = useMemo(() => ({ lang, setLang, t }), [lang, setLang, t]);
+
+  return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
+}
+
+export function useI18n(): I18nContextValue {
+  const ctx = useContext(I18nContext);
+  if (!ctx) {
+    return {
+      lang: DEFAULT_LANG,
+      setLang: () => {},
+      t: (key) => DICT[DEFAULT_LANG][key] ?? key,
+    };
+  }
+  return ctx;
+}
+

+ 8 - 6
frontend/lib/stats-config.ts

@@ -1,9 +1,11 @@
-// 统计数据配置
-export const stats = [
-  { label: "服务企业", value: "1000+" },
-  { label: "处理对话", value: "100万+" },
-  { label: "响应时间", value: "<100ms" },
-  { label: "满意度", value: "98%" },
+import type { I18nKey } from "@/lib/i18n/dict";
+
+/** 首页数字条:标签与展示数值均支持中英 */
+export const stats: { labelKey: I18nKey; valueKey: I18nKey }[] = [
+  { labelKey: "home.stats.clients", valueKey: "home.stats.val.clients" },
+  { labelKey: "home.stats.conversations", valueKey: "home.stats.val.conversations" },
+  { labelKey: "home.stats.latency", valueKey: "home.stats.val.latency" },
+  { labelKey: "home.stats.satisfaction", valueKey: "home.stats.val.satisfaction" },
 ];
 
 // 客户评价