diff --git a/config/algolia.config.example.ts b/config/algolia.config.example.ts index f0db07e..01ee306 100644 --- a/config/algolia.config.example.ts +++ b/config/algolia.config.example.ts @@ -45,4 +45,4 @@ export const algoliaConfig: AlgoliaConfig = { // 3. 确认 LLM API Key(OpenAI/Anthropic)已正确配置 // 4. 如果暂时不需要 AI 功能,设置为 undefined assistantId: undefined, // 或 'YOUR_ASSISTANT_ID' -} +}; diff --git a/config/algolia.config.ts b/config/algolia.config.ts index 46482dd..e74ad14 100644 --- a/config/algolia.config.ts +++ b/config/algolia.config.ts @@ -40,10 +40,10 @@ */ export interface AlgoliaConfig { - appId: string - indexName: string - apiKey: string - assistantId?: string + appId: string; + indexName: string; + apiKey: string; + assistantId?: string; } // 直接使用配置值(浏览器端安全) @@ -60,9 +60,11 @@ export const algoliaConfig: AlgoliaConfig = { // 2. 确认域名已在 Algolia Dashboard 中添加到白名单 // 3. 确认 LLM API Key 已正确配置 assistantId: 'Wky70IkEk8wa', -} +}; // 验证必需的配置项 if (!algoliaConfig.appId || !algoliaConfig.indexName || !algoliaConfig.apiKey) { - console.warn('Algolia DocSearch 配置不完整,请检查 config/algolia.config.ts 或环境变量') + console.warn( + 'Algolia DocSearch 配置不完整,请检查 config/algolia.config.ts 或环境变量', + ); } diff --git a/docs/en/guides/advanced/env.md b/docs/en/guides/advanced/env.md index 68ef384..5f0a2a9 100644 --- a/docs/en/guides/advanced/env.md +++ b/docs/en/guides/advanced/env.md @@ -208,7 +208,7 @@ Database logging switch, default value `off`, options include: - `off` Off ```bash -DB_LOGGING=on +DB_LOGGING=true ``` ### LOGGER_TRANSPORT diff --git a/docs/zh/guides/advanced/env.md b/docs/zh/guides/advanced/env.md index 20d11f2..7612137 100644 --- a/docs/zh/guides/advanced/env.md +++ b/docs/zh/guides/advanced/env.md @@ -208,7 +208,7 @@ DB_TABLE_PREFIX=tachybase_ - `off` 关闭 ```bash -DB_LOGGING=on +DB_LOGGING=true ``` ### LOGGER_TRANSPORT diff --git a/rspress.config.ts b/rspress.config.ts index e7f3a55..f16547c 100644 --- a/rspress.config.ts +++ b/rspress.config.ts @@ -1,8 +1,8 @@ -import * as path from 'node:path' -import { pluginPreview } from '@rspress/plugin-preview' -import mermaid from 'rspress-plugin-mermaid' -import { defineConfig } from 'rspress/config' -import { algoliaConfig } from './config/algolia.config' +import * as path from 'node:path'; +import { pluginPreview } from '@rspress/plugin-preview'; +import mermaid from 'rspress-plugin-mermaid'; +import { defineConfig } from 'rspress/config'; +import { algoliaConfig } from './config/algolia.config'; export default defineConfig({ root: path.join(__dirname, 'docs'), @@ -120,4 +120,4 @@ export default defineConfig({ ], }, }, -}) +}); diff --git a/theme/components/AlgoliaSearch.tsx b/theme/components/AlgoliaSearch.tsx index 64f4ab6..eff0d0e 100644 --- a/theme/components/AlgoliaSearch.tsx +++ b/theme/components/AlgoliaSearch.tsx @@ -1,25 +1,31 @@ -import docsearch from '@docsearch/js' -import { usePageData } from '@rspress/core/runtime' -import { useEffect, useRef } from 'react' -import '@docsearch/css' -import { algoliaConfig } from '../../config/algolia.config' +import docsearch from '@docsearch/js'; +import { usePageData } from '@rspress/core/runtime'; +import { useEffect, useRef } from 'react'; +import '@docsearch/css'; +import { algoliaConfig } from '../../config/algolia.config'; interface AlgoliaSearchProps { - containerId?: string - assistantId?: string + containerId?: string; + assistantId?: string; } // 翻译配置 - 完整的 DocSearch v4 翻译选项 const getTranslations = (isEnglish: boolean, hasAI: boolean) => ({ button: { - buttonText: hasAI ? (isEnglish ? 'AI / Search' : 'AI / 搜索') : isEnglish ? 'Search' : '搜索', + buttonText: hasAI + ? isEnglish + ? 'AI / Search' + : 'AI / 搜索' + : isEnglish + ? 'Search' + : '搜索', buttonAriaLabel: hasAI ? isEnglish ? 'Search or ask AI' : '搜索或使用 AI 提问' : isEnglish - ? 'Search' - : '搜索', + ? 'Search' + : '搜索', }, modal: { searchBox: { @@ -36,7 +42,9 @@ const getTranslations = (isEnglish: boolean, hasAI: boolean) => ({ startScreen: { recentSearchesTitle: isEnglish ? 'Recent' : '最近搜索', noRecentSearchesText: isEnglish ? 'No recent searches' : '没有最近搜索', - saveRecentSearchButtonTitle: isEnglish ? 'Save this search' : '保存到最近搜索', + saveRecentSearchButtonTitle: isEnglish + ? 'Save this search' + : '保存到最近搜索', removeRecentSearchButtonTitle: isEnglish ? 'Remove this search from history' : '从最近搜索中删除', @@ -70,88 +78,103 @@ const getTranslations = (isEnglish: boolean, hasAI: boolean) => ({ reportMissingResultsLinkText: isEnglish ? 'Let us know.' : '反馈给我们。', }, }, -}) +}); // 获取搜索框占位符文本 const getPlaceholder = (isEnglish: boolean, hasAI: boolean) => { if (hasAI) { - return isEnglish ? 'Ask AI or search docs ...' : '使用 AI 提问或输入关键词搜索...' + return isEnglish + ? 'Ask AI or search docs ...' + : '使用 AI 提问或输入关键词搜索...'; } - return isEnglish ? 'Search docs...' : '输入关键词搜索...' -} + return isEnglish ? 'Search docs...' : '输入关键词搜索...'; +}; -export function AlgoliaSearch({ containerId = 'docsearch', assistantId }: AlgoliaSearchProps) { - const finalAssistantId = assistantId || algoliaConfig.assistantId - const containerRef = useRef(null) - const { page } = usePageData() - const isEnglish = page.lang === 'en' - const initializedRef = useRef(false) - const aiFailedRef = useRef(false) // 标记 AI 是否已失败 +export function AlgoliaSearch({ + containerId = 'docsearch', + assistantId, +}: AlgoliaSearchProps) { + const finalAssistantId = assistantId || algoliaConfig.assistantId; + const containerRef = useRef(null); + const { page } = usePageData(); + const isEnglish = page.lang === 'en'; + const initializedRef = useRef(false); + const aiFailedRef = useRef(false); // 标记 AI 是否已失败 useEffect(() => { const timer = setTimeout(() => { // 如果语言改变,需要重新初始化以更新翻译 if (initializedRef.current) { - const existingButton = document.querySelector(`#${containerId} .DocSearch-Button`) + const existingButton = document.querySelector( + `#${containerId} .DocSearch-Button`, + ); if (existingButton) { // 语言改变时,重置初始化状态以重新初始化 - initializedRef.current = false + initializedRef.current = false; } else { - return + return; } } // 获取或创建搜索容器 - let searchContainer = document.getElementById(containerId) + let searchContainer = document.getElementById(containerId); if (!searchContainer) { - searchContainer = document.createElement('div') - searchContainer.id = containerId - searchContainer.className = 'algolia-search-container' + searchContainer = document.createElement('div'); + searchContainer.id = containerId; + searchContainer.className = 'algolia-search-container'; } else { - searchContainer.className = 'algolia-search-container' + searchContainer.className = 'algolia-search-container'; } // 查找原有的 Rspress 搜索按钮并替换 const originalSearchButton = document.querySelector( - '.rspress-nav-search-button, .navSearchButton_df1fb' - ) as HTMLElement | null + '.rspress-nav-search-button, .navSearchButton_df1fb', + ) as HTMLElement | null; if (originalSearchButton) { // 隐藏原有搜索按钮 - originalSearchButton.style.display = 'none' + originalSearchButton.style.display = 'none'; // 将 Algolia 搜索框插入到原有搜索按钮的位置(完全替换) - const parent = originalSearchButton.parentElement + const parent = originalSearchButton.parentElement; if (parent) { - parent.insertBefore(searchContainer, originalSearchButton) + parent.insertBefore(searchContainer, originalSearchButton); // 确保搜索框在正确的 flex 容器中 - const flexContainer = parent.closest('.rp-flex.sm\\:rp-flex-1, [class*="flex"]') + const flexContainer = parent.closest( + '.rp-flex.sm\\:rp-flex-1, [class*="flex"]', + ); if (flexContainer && !flexContainer.contains(searchContainer)) { - flexContainer.insertBefore(searchContainer, flexContainer.firstChild) + flexContainer.insertBefore( + searchContainer, + flexContainer.firstChild, + ); } } } else { // 如果没有找到原有搜索按钮,插入到导航栏右侧区域 const navContainer = document.querySelector( - '.navContainer_d18b1 > .container_e4235' - ) as HTMLElement | null + '.navContainer_d18b1 > .container_e4235', + ) as HTMLElement | null; if (navContainer) { - const rightNav = navContainer.querySelector('.rightNav_a2fea') as HTMLElement | null + const rightNav = navContainer.querySelector( + '.rightNav_a2fea', + ) as HTMLElement | null; if (rightNav && !rightNav.contains(searchContainer)) { - rightNav.insertBefore(searchContainer, rightNav.firstChild) + rightNav.insertBefore(searchContainer, rightNav.firstChild); } } } // 初始化 DocSearch 的函数 const initializeSearch = (useAI: boolean) => { - if (!searchContainer) return + if (!searchContainer) return; // 如果已经初始化过,先清理 - const existingButton = searchContainer.querySelector('.DocSearch-Button') + const existingButton = + searchContainer.querySelector('.DocSearch-Button'); if (existingButton) { - searchContainer.innerHTML = '' + searchContainer.innerHTML = ''; } // 调试信息:输出配置(隐藏敏感信息) @@ -160,9 +183,9 @@ export function AlgoliaSearch({ containerId = 'docsearch', assistantId }: Algoli indexName: algoliaConfig.indexName, apiKey: `${algoliaConfig.apiKey.slice(0, 4)}...${algoliaConfig.apiKey.slice(-4)}`, hasAssistantId: !!finalAssistantId, - }) + }); - const hasAI = useAI && !!finalAssistantId + const hasAI = useAI && !!finalAssistantId; const searchConfig: Parameters[0] = { container: `#${containerId}`, appId: algoliaConfig.appId, @@ -171,39 +194,39 @@ export function AlgoliaSearch({ containerId = 'docsearch', assistantId }: Algoli placeholder: getPlaceholder(isEnglish, hasAI), ...(hasAI && { askAi: finalAssistantId }), translations: getTranslations(isEnglish, hasAI), - } + }; try { - docsearch(searchConfig) - initializedRef.current = true - console.log('Algolia DocSearch 初始化成功') - return true + docsearch(searchConfig); + initializedRef.current = true; + console.log('Algolia DocSearch 初始化成功'); + return true; } catch (error) { - console.error('Failed to initialize Algolia DocSearch:', error) + console.error('Failed to initialize Algolia DocSearch:', error); // 输出详细的错误信息 if (error instanceof Error) { console.error('错误详情:', { message: error.message, name: error.name, stack: error.stack, - }) + }); } - return false + return false; } - } + }; // 处理 AI 错误的函数 const handleAIFailure = () => { if (!aiFailedRef.current && finalAssistantId) { - aiFailedRef.current = true - console.warn('检测到 AI Assistant 401 错误,自动降级到基础搜索功能') + aiFailedRef.current = true; + console.warn('检测到 AI Assistant 401 错误,自动降级到基础搜索功能'); // 延迟一下再重新初始化,避免立即重试 setTimeout(() => { - initializedRef.current = false - initializeSearch(false) - }, 500) + initializedRef.current = false; + initializeSearch(false); + }, 500); } - } + }; // 监听全局错误,检测 AI 相关的 401 错误 const handleError = (event: ErrorEvent) => { @@ -215,83 +238,96 @@ export function AlgoliaSearch({ containerId = 'docsearch', assistantId }: Algoli (event.error as any)?.status === 401 ) { // 检查错误是否来自 Algolia - const errorUrl = (event.error as any)?.url || '' + const errorUrl = (event.error as any)?.url || ''; if ( errorUrl.includes('algolia') || errorUrl.includes('docsearch') || event.message?.includes('algolia') || event.message?.includes('docsearch') ) { - handleAIFailure() + handleAIFailure(); } } - } + }; // 监听自定义的 AI 错误事件 const handleAIError = (event: Event) => { - handleAIFailure() - } + handleAIFailure(); + }; // 监听 fetch 错误(AI 请求可能通过 fetch 发送) // 使用全局变量避免重复拦截 if (!(window as any).__docsearch_fetch_intercepted) { - const originalFetch = window.fetch + const originalFetch = window.fetch; window.fetch = async (...args) => { try { - const response = await originalFetch(...args) + const response = await originalFetch(...args); // 检查是否是 Algolia AI 相关的 401 错误 if ( response.status === 401 && - (args[0]?.toString().includes('algolia') || args[0]?.toString().includes('docsearch')) + (args[0]?.toString().includes('algolia') || + args[0]?.toString().includes('docsearch')) ) { - const url = args[0]?.toString() || '' + const url = args[0]?.toString() || ''; if (url.includes('ask') || url.includes('assistant')) { // 触发自定义事件,让组件处理 window.dispatchEvent( new CustomEvent('docsearch-ai-error', { detail: { status: 401, url }, - }) - ) + }), + ); } } - return response + return response; } catch (error) { - throw error + throw error; } - } - ;(window as any).__docsearch_fetch_intercepted = true + }; + (window as any).__docsearch_fetch_intercepted = true; } // 添加全局错误监听 - window.addEventListener('error', handleError) - window.addEventListener('docsearch-ai-error', handleAIError) + window.addEventListener('error', handleError); + window.addEventListener('docsearch-ai-error', handleAIError); // 初始化搜索(优先尝试带 AI) - if (searchContainer && !searchContainer.querySelector('.DocSearch-Button')) { + if ( + searchContainer && + !searchContainer.querySelector('.DocSearch-Button') + ) { if (finalAssistantId && !aiFailedRef.current) { // 尝试使用 AI 功能 - const success = initializeSearch(true) + const success = initializeSearch(true); if (!success && finalAssistantId) { // 如果初始化失败,降级到基础搜索 - console.warn('AI Assistant 初始化失败,使用基础搜索功能') - aiFailedRef.current = true - initializeSearch(false) + console.warn('AI Assistant 初始化失败,使用基础搜索功能'); + aiFailedRef.current = true; + initializeSearch(false); } } else { // 直接使用基础搜索 - initializeSearch(false) + initializeSearch(false); } } // 清理函数 return () => { - window.removeEventListener('error', handleError) - window.removeEventListener('docsearch-ai-error', handleAIError as EventListener) - } - }, 200) + window.removeEventListener('error', handleError); + window.removeEventListener( + 'docsearch-ai-error', + handleAIError as EventListener, + ); + }; + }, 200); - return () => clearTimeout(timer) - }, [containerId, isEnglish, finalAssistantId]) + return () => clearTimeout(timer); + }, [containerId, isEnglish, finalAssistantId]); - return
+ return ( +
+ ); }