diff --git a/README.zh-CN.md b/README.zh-CN.md
index cd64467c8..def665fa9 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -16,7 +16,7 @@
# SurfSense
-虽然像 NotebookLM 和 Perplexity 这样的工具在对任何主题/查询进行研究时令人印象深刻且非常有效,但 SurfSense 通过与您的个人知识库集成,将这一能力提升到了新的高度。它是一个高度可定制的 AI 研究助手,可以连接外部数据源,如搜索引擎(SearxNG、Tavily、LinkUp)、Slack、Linear、Jira、ClickUp、Confluence、Gmail、Notion、YouTube、GitHub、Discord、Airtable、Google Calendar、Luma、Elasticsearch 等,未来还会支持更多。
+虽然像 NotebookLM 和 Perplexity 这样的工具在对任何主题/查询进行研究时令人印象深刻且非常有效,但 SurfSense 通过与您的个人知识库集成,将这一能力提升到了新的高度。它是一个高度可定制的 AI 研究助手,可以连接外部数据源,如搜索引擎(SearxNG、Tavily、LinkUp)、Slack、Linear、Jira、ClickUp、Confluence、BookStack、Gmail、Notion、YouTube、GitHub、Discord、Airtable、Google Calendar、Luma、Elasticsearch 等,未来还会支持更多。

@@ -25,8 +25,7 @@
# 视频演示
-
-https://github.com/user-attachments/assets/d9221908-e0de-4b2f-ac3a-691cf4b202da
+https://github.com/user-attachments/assets/42a29ea1-d4d8-4213-9c69-972b5b806d58
## 播客示例
@@ -71,6 +70,27 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
- 支持本地 TTS 提供商(Kokoro TTS)
- 支持多个 TTS 提供商(OpenAI、Azure、Google Vertex AI)
+### 🤖 **深度代理架构**
+
+#### 内置代理工具
+| 工具 | 描述 |
+|------|------|
+| **search_knowledge_base** | 使用语义+全文混合搜索、日期过滤和连接器特定查询搜索您的个人知识库 |
+| **generate_podcast** | 从聊天对话或知识库内容生成音频播客 |
+| **link_preview** | 获取 URL 的 Open Graph 元数据以显示预览卡片 |
+| **display_image** | 在聊天中显示带有元数据和来源归属的图像 |
+| **scrape_webpage** | 从网页中提取完整内容用于分析和总结(支持 Firecrawl 或本地 Chromium/Trafilatura) |
+
+#### 可扩展工具注册表
+贡献者可以通过注册表模式轻松添加新工具:
+1. 在 `surfsense_backend/app/agents/new_chat/tools/` 中创建工具工厂函数
+2. 在 `registry.py` 的 `BUILTIN_TOOLS` 列表中注册
+
+#### 可配置的系统提示词
+- 通过 LLM 配置自定义系统指令
+- 按配置切换引用开关
+- 通过 LiteLLM 集成支持 100+ 种 LLM
+
### 📊 **先进的 RAG 技术**
- 支持 100+ 种大语言模型
- 支持 6000+ 种嵌入模型
@@ -86,6 +106,7 @@ https://github.com/user-attachments/assets/a0a16566-6967-4374-ac51-9b3e07fbecd7
- Jira
- ClickUp
- Confluence
+- BookStack
- Notion
- Gmail
- YouTube 视频
@@ -214,32 +235,6 @@ Docker 和手动安装指南都包含适用于 Windows、macOS 和 Linux 的详
- LlamaIndex API 密钥(增强解析,支持 50+ 种格式)
- 其他根据用例需要的 API 密钥
-## 截图
-
-**研究助手**
-
-
-
-**搜索空间**
-
-
-
-**管理文档**
-
-
-**播客助手**
-
-
-
-**对话助手**
-
-
-
-**浏览器扩展**
-
-
-
-
## 技术栈
@@ -257,11 +252,13 @@ Docker 和手动安装指南都包含适用于 Windows、macOS 和 Linux 的详
- **FastAPI Users**:使用 JWT 和 OAuth 支持的身份验证和用户管理
-- **LangGraph**:用于开发 AI 代理的框架
-
+- **深度代理**:基于 LangGraph 构建的自定义代理框架,用于推理和行动的 AI 代理,支持可配置工具
+
+- **LangGraph**:用于开发具有对话持久性的有状态 AI 代理的框架
+
- **LangChain**:用于开发 AI 驱动应用程序的框架
-- **LLM 集成**:通过 LiteLLM 与大语言模型集成
+- **LiteLLM**:通用 LLM 集成,支持 100+ 种模型(OpenAI、Anthropic、Ollama 等)
- **Rerankers**:先进的结果排序,提高搜索相关性
@@ -285,33 +282,19 @@ Docker 和手动安装指南都包含适用于 Windows、macOS 和 Linux 的详
---
### **前端**
-- **Next.js 15.2.3**:React 框架,具有应用路由器、服务器组件、自动代码拆分和优化渲染功能
+- **Next.js**:React 框架,具有应用路由器、服务器组件、自动代码拆分和优化渲染功能
-- **React 19.0.0**:用于构建用户界面的 JavaScript 库
+- **React**:用于构建用户界面的 JavaScript 库
- **TypeScript**:JavaScript 的静态类型检查,提升代码质量和开发体验
- **Vercel AI SDK Kit UI Stream Protocol**:创建可扩展的聊天 UI
-- **Tailwind CSS 4.x**:实用优先的 CSS 框架,用于构建自定义 UI 设计
+- **Tailwind CSS**:实用优先的 CSS 框架,用于构建自定义 UI 设计
- **Shadcn**:无头组件库
-- **Lucide React**:作为 React 组件实现的图标集
-
-- **Framer Motion**:React 动画库
-
-- **Sonner**:Toast 通知库
-
-- **Geist**:Vercel 的字体系列
-
-- **React Hook Form**:表单状态管理和验证
-
-- **Zod**:TypeScript 优先的模式验证,带静态类型推断
-
-- **@hookform/resolvers**:用于在 React Hook Form 中使用验证库的解析器
-
-- **@tanstack/react-table**:用于构建强大表格和数据网格的无头 UI
+- **Motion(Framer Motion)**:React 动画库
### **DevOps**
@@ -332,6 +315,25 @@ Docker 和手动安装指南都包含适用于 Windows、macOS 和 Linux 的详
非常欢迎贡献!贡献可以小到一个 ⭐,甚至是发现和创建问题。
后端的微调总是受欢迎的。
+### 添加新的代理工具
+
+想要为 SurfSense 代理添加新工具?非常简单:
+
+1. 在 `surfsense_backend/app/agents/new_chat/tools/my_tool.py` 中创建您的工具文件
+2. 在 `registry.py` 中注册:
+
+```python
+ToolDefinition(
+ name="my_tool",
+ description="What my tool does",
+ factory=lambda deps: create_my_tool(
+ search_space_id=deps["search_space_id"],
+ db_session=deps["db_session"],
+ ),
+ requires=["search_space_id", "db_session"],
+),
+```
+
有关详细的贡献指南,请参阅我们的 [CONTRIBUTING.md](CONTRIBUTING.md) 文件。
## Star 历史
diff --git a/surfsense_web/app/(home)/login/GoogleLoginButton.tsx b/surfsense_web/app/(home)/login/GoogleLoginButton.tsx
index 6eef58aed..d4d9d4b4a 100644
--- a/surfsense_web/app/(home)/login/GoogleLoginButton.tsx
+++ b/surfsense_web/app/(home)/login/GoogleLoginButton.tsx
@@ -3,12 +3,16 @@ import { IconBrandGoogleFilled } from "@tabler/icons-react";
import { motion } from "motion/react";
import { useTranslations } from "next-intl";
import { Logo } from "@/components/Logo";
+import { trackLoginAttempt, trackLoginFailure } from "@/lib/posthog/events";
import { AmbientBackground } from "./AmbientBackground";
export function GoogleLoginButton() {
const t = useTranslations("auth");
const handleGoogleLogin = () => {
+ // Track Google login attempt
+ trackLoginAttempt("google");
+
// Redirect to Google OAuth authorization URL
// credentials: 'include' is required to accept the CSRF cookie from cross-origin response
fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/google/authorize`, {
@@ -24,10 +28,12 @@ export function GoogleLoginButton() {
if (data.authorization_url) {
window.location.href = data.authorization_url;
} else {
+ trackLoginFailure("google", "No authorization URL received");
console.error("No authorization URL received");
}
})
.catch((error) => {
+ trackLoginFailure("google", error?.message || "Unknown error");
console.error("Error during Google login:", error);
});
};
diff --git a/surfsense_web/app/(home)/login/LocalLoginForm.tsx b/surfsense_web/app/(home)/login/LocalLoginForm.tsx
index 0157c9faf..44e9b27c2 100644
--- a/surfsense_web/app/(home)/login/LocalLoginForm.tsx
+++ b/surfsense_web/app/(home)/login/LocalLoginForm.tsx
@@ -10,6 +10,7 @@ import { toast } from "sonner";
import { loginMutationAtom } from "@/atoms/auth/auth-mutation.atoms";
import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors";
import { ValidationError } from "@/lib/error";
+import { trackLoginAttempt, trackLoginFailure, trackLoginSuccess } from "@/lib/posthog/events";
export function LocalLoginForm() {
const t = useTranslations("auth");
@@ -37,6 +38,9 @@ export function LocalLoginForm() {
e.preventDefault();
setError({ title: null, message: null }); // Clear any previous errors
+ // Track login attempt
+ trackLoginAttempt("local");
+
// Show loading toast
const loadingToast = toast.loading(tCommon("loading"));
@@ -47,6 +51,9 @@ export function LocalLoginForm() {
grant_type: "password",
});
+ // Track successful login
+ trackLoginSuccess("local");
+
// Success toast
toast.success(t("login_success"), {
id: loadingToast,
@@ -60,6 +67,7 @@ export function LocalLoginForm() {
}, 500);
} catch (err) {
if (err instanceof ValidationError) {
+ trackLoginFailure("local", err.message);
setError({ title: err.name, message: err.message });
toast.error(err.name, {
id: loadingToast,
@@ -78,6 +86,9 @@ export function LocalLoginForm() {
errorCode = "NETWORK_ERROR";
}
+ // Track login failure
+ trackLoginFailure("local", errorCode);
+
// Get detailed error information from auth-errors utility
const errorDetails = getAuthErrorDetails(errorCode);
diff --git a/surfsense_web/app/(home)/register/page.tsx b/surfsense_web/app/(home)/register/page.tsx
index c535832be..4a8dce546 100644
--- a/surfsense_web/app/(home)/register/page.tsx
+++ b/surfsense_web/app/(home)/register/page.tsx
@@ -11,6 +11,11 @@ import { registerMutationAtom } from "@/atoms/auth/auth-mutation.atoms";
import { Logo } from "@/components/Logo";
import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors";
import { AppError, ValidationError } from "@/lib/error";
+import {
+ trackRegistrationAttempt,
+ trackRegistrationFailure,
+ trackRegistrationSuccess,
+} from "@/lib/posthog/events";
import { AmbientBackground } from "../login/AmbientBackground";
export default function RegisterPage() {
@@ -52,6 +57,9 @@ export default function RegisterPage() {
setError({ title: null, message: null }); // Clear any previous errors
+ // Track registration attempt
+ trackRegistrationAttempt();
+
// Show loading toast
const loadingToast = toast.loading(t("creating_account"));
@@ -64,6 +72,9 @@ export default function RegisterPage() {
is_verified: false,
});
+ // Track successful registration
+ trackRegistrationSuccess();
+
// Success toast
toast.success(t("register_success"), {
id: loadingToast,
@@ -81,6 +92,7 @@ export default function RegisterPage() {
case 403: {
const friendlyMessage =
"Registrations are currently closed. If you need access, contact your administrator.";
+ trackRegistrationFailure("Registration disabled");
setError({ title: "Registration is disabled", message: friendlyMessage });
toast.error("Registration is disabled", {
id: loadingToast,
@@ -94,6 +106,7 @@ export default function RegisterPage() {
}
if (err instanceof ValidationError) {
+ trackRegistrationFailure(err.message);
setError({ title: err.name, message: err.message });
toast.error(err.name, {
id: loadingToast,
@@ -113,6 +126,9 @@ export default function RegisterPage() {
errorCode = "NETWORK_ERROR";
}
+ // Track registration failure
+ trackRegistrationFailure(errorCode);
+
// Get detailed error information from auth-errors utility
const errorDetails = getAuthErrorDetails(errorCode);
diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
index 5831a0069..1a6d173f3 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
@@ -44,6 +44,12 @@ import {
getThreadMessages,
type MessageRecord,
} from "@/lib/chat/thread-persistence";
+import {
+ trackChatCreated,
+ trackChatError,
+ trackChatMessageSent,
+ trackChatResponseReceived,
+} from "@/lib/posthog/events";
/**
* Extract thinking steps from message content
@@ -378,6 +384,10 @@ export default function NewChatPage() {
const newThread = await createThread(searchSpaceId, "New Chat");
currentThreadId = newThread.id;
setThreadId(currentThreadId);
+
+ // Track chat creation
+ trackChatCreated(searchSpaceId, currentThreadId);
+
isNewThread = true;
// Update URL silently using browser API (not router.replace) to avoid
// interrupting the ongoing fetch/streaming with React navigation
@@ -405,6 +415,13 @@ export default function NewChatPage() {
};
setMessages((prev) => [...prev, userMessage]);
+ // Track message sent
+ trackChatMessageSent(searchSpaceId, currentThreadId, {
+ hasAttachments: messageAttachments.length > 0,
+ hasMentionedDocuments: mentionedDocumentIds.length > 0,
+ messageLength: userQuery.length,
+ });
+
// Store mentioned documents with this message for display
if (mentionedDocuments.length > 0) {
const docsInfo: MentionedDocumentInfo[] = mentionedDocuments.map((doc) => ({
@@ -752,6 +769,9 @@ export default function NewChatPage() {
role: "assistant",
content: finalContent,
}).catch((err) => console.error("Failed to persist assistant message:", err));
+
+ // Track successful response
+ trackChatResponseReceived(searchSpaceId, currentThreadId);
}
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
@@ -773,6 +793,14 @@ export default function NewChatPage() {
return;
}
console.error("[NewChatPage] Chat error:", error);
+
+ // Track chat error
+ trackChatError(
+ searchSpaceId,
+ currentThreadId,
+ error instanceof Error ? error.message : "Unknown error"
+ );
+
toast.error("Failed to get response. Please try again.");
// Update assistant message with error
setMessages((prev) =>
diff --git a/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx
index ad96402a4..48efcd922 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/settings/page.tsx
@@ -13,11 +13,12 @@ import {
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
-import { useCallback, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { LLMRoleManager } from "@/components/settings/llm-role-manager";
import { ModelConfigManager } from "@/components/settings/model-config-manager";
import { PromptConfigManager } from "@/components/settings/prompt-config-manager";
import { Button } from "@/components/ui/button";
+import { trackSettingsViewed } from "@/lib/posthog/events";
import { cn } from "@/lib/utils";
interface SettingsNavItem {
@@ -271,6 +272,11 @@ export default function SettingsPage() {
const [activeSection, setActiveSection] = useState("models");
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
+ // Track settings section view
+ useEffect(() => {
+ trackSettingsViewed(searchSpaceId, activeSection);
+ }, [searchSpaceId, activeSection]);
+
const handleBackToApp = useCallback(() => {
router.push(`/dashboard/${searchSpaceId}/new-chat`);
}, [router, searchSpaceId]);
diff --git a/surfsense_web/app/dashboard/[search_space_id]/sources/add/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/sources/add/page.tsx
index 3c9b57f98..e0729e29b 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/sources/add/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/sources/add/page.tsx
@@ -9,6 +9,7 @@ import { ConnectorsTab } from "@/components/sources/ConnectorsTab";
import { DocumentUploadTab } from "@/components/sources/DocumentUploadTab";
import { YouTubeTab } from "@/components/sources/YouTubeTab";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { trackSourcesTabViewed } from "@/lib/posthog/events";
export default function AddSourcesPage() {
const params = useParams();
@@ -30,9 +31,16 @@ export default function AddSourcesPage() {
router.push(`/dashboard/${search_space_id}/connectors/add/webcrawler-connector`);
} else {
setActiveTab(value);
+ // Track tab view
+ trackSourcesTabViewed(Number(search_space_id), value);
}
};
+ // Track initial tab view
+ useEffect(() => {
+ trackSourcesTabViewed(Number(search_space_id), activeTab);
+ }, []);
+
return (
-
-
-
-
- {children}
-
-
-
-
-
+
+
+
+
+
+ {children}
+
+
+
+
+
+