diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index ed2ba00..e570f1f 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,6 +1,9 @@ import { streamText, UIMessage, convertToModelMessages } from "ai"; import { getModel, requiresApiKey, type AIProvider } from "@/lib/ai/models"; import { buildSystemMessage } from "@/lib/ai/prompt"; +import { source } from "@/lib/source"; +import fs from "fs/promises"; +import path from "path"; // 流式响应最长30秒 export const maxDuration = 30; @@ -39,6 +42,27 @@ export async function POST(req: Request) { ); } + // 如果有 slug 但没有 content,尝试在服务端读取内容 + if (pageContext?.slug && !pageContext.content) { + try { + const slugArray = pageContext.slug.split("/"); + const page = source.getPage(slugArray); + + if (page) { + const fullFilePath = path.join(process.cwd(), "app/docs", page.path); + const rawContent = await fs.readFile(fullFilePath, "utf-8"); + pageContext.content = extractTextFromMDX(rawContent); + } + } catch (error) { + console.warn( + "Failed to fetch content for slug:", + pageContext.slug, + error, + ); + // 出错时不中断,只是缺少上下文 + } + } + // 构建系统消息,包含页面上下文 const systemMessage = buildSystemMessage(system, pageContext); @@ -67,3 +91,28 @@ export async function POST(req: Request) { ); } } + +// 提取纯文本内容,过滤掉 MDX 语法 +function extractTextFromMDX(content: string): string { + let text = content + .replace(/^---[\s\S]*?---/m, "") // 移除头部元数据 (frontmatter) + .replace(/```[\s\S]*?```/g, "") // 移除代码块 + .replace(/`([^`]+)`/g, "$1"); // 移除内联代码符号,保留内容 + + // 递归移除 HTML/MDX 标签,防止嵌套标签清理不干净 + let prevText; + do { + prevText = text; + text = text.replace(/<[^>]+>/g, ""); + } while (text !== prevText); + + return text + .replace(/\*\*([^*]+)\*\*/g, "$1") // 移除粗体符号,保留文字 + .replace(/\*([^*]+)\*/g, "$1") // 移除斜体符号,保留文字 + .replace(/#{1,6}\s+/g, "") // 移除标题符号 (#) + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // 移除链接语法,仅保留链接文本 + .replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1") // 移除图片语法,保留 alt 文本 + .replace(/[#*`()[!\]!]/g, "") // 移除剩余的常用 Markdown 符号 + .replace(/\n{2,}/g, "\n") // 规范化换行,将多余的空行合并 + .trim(); +} diff --git a/app/components/DocsAssistant.tsx b/app/components/DocsAssistant.tsx index 139f98a..91459cf 100644 --- a/app/components/DocsAssistant.tsx +++ b/app/components/DocsAssistant.tsx @@ -15,7 +15,6 @@ import { interface PageContext { title?: string; description?: string; - content?: string; slug?: string; } diff --git a/app/docs/[...slug]/page.tsx b/app/docs/[...slug]/page.tsx index 7964685..64c62e2 100644 --- a/app/docs/[...slug]/page.tsx +++ b/app/docs/[...slug]/page.tsx @@ -14,31 +14,8 @@ import { Contributors } from "@/app/components/Contributors"; import { DocsAssistant } from "@/app/components/DocsAssistant"; import { LicenseNotice } from "@/app/components/LicenseNotice"; import { PageFeedback } from "@/app/components/PageFeedback"; -import fs from "fs/promises"; -import path from "path"; - -// Extract clean text content from MDX -function extractTextFromMDX(content: string): string { - let text = content - .replace(/^---[\s\S]*?---/m, "") // Remove frontmatter - .replace(/```[\s\S]*?```/g, "") // Remove code blocks - .replace(/`([^`]+)`/g, "$1"); // Remove inline code - // Remove HTML/MDX tags recursively to prevent incomplete multi-character sanitization - let prevText; - do { - prevText = text; - text = text.replace(/<[^>]+>/g, ""); - } while (text !== prevText); - return text - .replace(/\*\*([^*]+)\*\*/g, "$1") // Remove bold - .replace(/\*([^*]+)\*/g, "$1") // Remove italic - .replace(/#{1,6}\s+/g, "") // Remove headers - .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // Remove links, keep text - .replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1") // Remove images, keep alt text - .replace(/[#*`()[!\]!]/g, "") // Remove common markdown symbols - .replace(/\n{2,}/g, "\n") // Normalize line breaks - .trim(); -} +// Extract clean text content from MDX - no longer used on client/page side +// content fetching moved to API route for performance interface Param { params: Promise<{ @@ -67,20 +44,6 @@ export default async function DocPage({ params }: Param) { getDocContributorsByDocId(docIdFromPage); const Mdx = page.data.body; - // Prepare page content for AI assistant - let pageContentForAI = ""; - try { - const fullFilePath = path.join(process.cwd(), "app/docs", page.file.path); - const rawContent = await fs.readFile(fullFilePath, "utf-8"); - const extractedText = extractTextFromMDX(rawContent); - // Use full extracted content without truncation - pageContentForAI = extractedText; - } catch (error) { - console.warn("Failed to read file content for AI assistant:", error); - // Fallback to using page metadata - pageContentForAI = `${page.data.title}\n${page.data.description || ""}`; - } - return ( <> @@ -104,7 +67,6 @@ export default async function DocPage({ params }: Param) { pageContext={{ title: page.data.title, description: page.data.description, - content: pageContentForAI, slug: slug?.join("/"), }} />