UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

353 lines (313 loc) 10.8 kB
import { flatten } from 'lodash-es'; import { MetadataRoute } from 'next'; import qs from 'query-string'; import urlJoin from 'url-join'; import { serverFeatureFlags } from '@/config/featureFlags'; import { DEFAULT_LANG } from '@/const/locale'; import { SITEMAP_BASE_URL } from '@/const/url'; import { Locales, locales as allLocales } from '@/locales/resources'; import { DiscoverService } from '@/server/services/discover'; import { getCanonicalUrl } from '@/server/utils/url'; import { isDev } from '@/utils/env'; export interface SitemapItem { alternates?: { languages?: string; }; changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'; lastModified?: string | Date; priority?: number; url: string; } export enum SitemapType { Assistants = 'assistants', Mcp = 'mcp', Models = 'models', Pages = 'pages', Plugins = 'plugins', Providers = 'providers', } export const LAST_MODIFIED = new Date().toISOString(); // 每页条目数量 const ITEMS_PER_PAGE = 100; export class Sitemap { sitemapIndexs = [{ id: SitemapType.Pages }, { id: SitemapType.Providers }]; private discoverService = new DiscoverService(); // 获取插件总页数 async getPluginPageCount(): Promise<number> { const list = await this.discoverService.getPluginIdentifiers(); return Math.ceil(list.length / ITEMS_PER_PAGE); } // 获取助手总页数 async getAssistantPageCount(): Promise<number> { const list = await this.discoverService.getAssistantIdentifiers(); return Math.ceil(list.length / ITEMS_PER_PAGE); } // 获取MCP总页数 async getMcpPageCount(): Promise<number> { const list = await this.discoverService.getMcpIdentifiers(); return Math.ceil(list.length / ITEMS_PER_PAGE); } // 获取模型总页数 async getModelPageCount(): Promise<number> { const list = await this.discoverService.getModelIdentifiers(); return Math.ceil(list.length / ITEMS_PER_PAGE); } private _generateSitemapLink(url: string) { return [ '<sitemap>', `<loc>${url}</loc>`, `<lastmod>${LAST_MODIFIED}</lastmod>`, '</sitemap>', ].join('\n'); } private _formatTime(time?: string) { try { if (!time) return LAST_MODIFIED; return new Date(time).toISOString() || LAST_MODIFIED; } catch { return LAST_MODIFIED; } } private _genSitemapItem = ( lang: Locales, url: string, { lastModified, changeFrequency = 'monthly', priority = 0.4, noLocales, locales = allLocales, }: { changeFrequency?: SitemapItem['changeFrequency']; lastModified?: string; locales?: typeof allLocales; noLocales?: boolean; priority?: number; } = {}, ) => { const sitemap = { changeFrequency, lastModified: this._formatTime(lastModified), priority, url: lang === DEFAULT_LANG ? getCanonicalUrl(url) : qs.stringifyUrl({ query: { hl: lang }, url: getCanonicalUrl(url) }), }; if (noLocales) return sitemap; const languages: any = {}; for (const locale of locales) { if (locale === lang) continue; languages[locale] = qs.stringifyUrl({ query: { hl: locale }, url: getCanonicalUrl(url), }); } return { alternates: { languages, }, ...sitemap, }; }; private _genSitemap( url: string, { lastModified, changeFrequency = 'monthly', priority = 0.4, noLocales, locales = allLocales, }: { changeFrequency?: SitemapItem['changeFrequency']; lastModified?: string; locales?: typeof allLocales; noLocales?: boolean; priority?: number; } = {}, ) { if (noLocales) return [ this._genSitemapItem(DEFAULT_LANG, url, { changeFrequency, lastModified, locales, noLocales, priority, }), ]; return locales.map((lang) => this._genSitemapItem(lang, url, { changeFrequency, lastModified, locales, noLocales, priority, }), ); } async getIndex(): Promise<string> { const staticSitemaps = this.sitemapIndexs.map((item) => this._generateSitemapLink( getCanonicalUrl(SITEMAP_BASE_URL, isDev ? item.id : `${item.id}.xml`), ), ); // 获取需要分页的类型的页数 const [pluginPages, assistantPages, mcpPages, modelPages] = await Promise.all([ this.getPluginPageCount(), this.getAssistantPageCount(), this.getMcpPageCount(), this.getModelPageCount(), ]); // 生成分页sitemap链接 const paginatedSitemaps = [ ...Array.from({ length: pluginPages }, (_, i) => this._generateSitemapLink( getCanonicalUrl(SITEMAP_BASE_URL, isDev ? `plugins-${i + 1}` : `plugins-${i + 1}.xml`), ), ), ...Array.from({ length: assistantPages }, (_, i) => this._generateSitemapLink( getCanonicalUrl( SITEMAP_BASE_URL, isDev ? `assistants-${i + 1}` : `assistants-${i + 1}.xml`, ), ), ), ...Array.from({ length: mcpPages }, (_, i) => this._generateSitemapLink( getCanonicalUrl(SITEMAP_BASE_URL, isDev ? `mcp-${i + 1}` : `mcp-${i + 1}.xml`), ), ), ...Array.from({ length: modelPages }, (_, i) => this._generateSitemapLink( getCanonicalUrl(SITEMAP_BASE_URL, isDev ? `models-${i + 1}` : `models-${i + 1}.xml`), ), ), ]; return [ '<?xml version="1.0" encoding="UTF-8"?>', '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">', ...staticSitemaps, ...paginatedSitemaps, '</sitemapindex>', ].join('\n'); } async getAssistants(page?: number): Promise<MetadataRoute.Sitemap> { const list = await this.discoverService.getAssistantIdentifiers(); if (page !== undefined) { const startIndex = (page - 1) * ITEMS_PER_PAGE; const endIndex = startIndex + ITEMS_PER_PAGE; const pageAssistants = list.slice(startIndex, endIndex); const sitmap = pageAssistants.map((item) => this._genSitemap(urlJoin('/discover/assistant', item.identifier), { lastModified: item?.lastModified || LAST_MODIFIED, }), ); return flatten(sitmap); } // 如果没有指定页数,返回所有(向后兼容) const sitmap = list.map((item) => this._genSitemap(urlJoin('/discover/assistant', item.identifier), { lastModified: item?.lastModified || LAST_MODIFIED, }), ); return flatten(sitmap); } async getMcp(page?: number): Promise<MetadataRoute.Sitemap> { const list = await this.discoverService.getMcpIdentifiers(); if (page !== undefined) { const startIndex = (page - 1) * ITEMS_PER_PAGE; const endIndex = startIndex + ITEMS_PER_PAGE; const pageMcps = list.slice(startIndex, endIndex); const sitmap = pageMcps.map((item) => this._genSitemap(urlJoin('/discover/mcp', item.identifier), { lastModified: item?.lastModified || LAST_MODIFIED, }), ); return flatten(sitmap); } // 如果没有指定页数,返回所有(向后兼容) const sitmap = list.map((item) => this._genSitemap(urlJoin('/discover/mcp', item.identifier), { lastModified: item?.lastModified || LAST_MODIFIED, }), ); return flatten(sitmap); } async getPlugins(page?: number): Promise<MetadataRoute.Sitemap> { const list = await this.discoverService.getPluginIdentifiers(); if (page !== undefined) { const startIndex = (page - 1) * ITEMS_PER_PAGE; const endIndex = startIndex + ITEMS_PER_PAGE; const pagePlugins = list.slice(startIndex, endIndex); const sitmap = pagePlugins.map((item) => this._genSitemap(urlJoin('/discover/plugin', item.identifier), { lastModified: item?.lastModified || LAST_MODIFIED, }), ); return flatten(sitmap); } // 如果没有指定页数,返回所有(向后兼容) const sitmap = list.map((item) => this._genSitemap(urlJoin('/discover/plugin', item.identifier), { lastModified: item?.lastModified || LAST_MODIFIED, }), ); return flatten(sitmap); } async getModels(page?: number): Promise<MetadataRoute.Sitemap> { const list = await this.discoverService.getModelIdentifiers(); if (page !== undefined) { const startIndex = (page - 1) * ITEMS_PER_PAGE; const endIndex = startIndex + ITEMS_PER_PAGE; const pageModels = list.slice(startIndex, endIndex); const sitmap = pageModels.map((item) => this._genSitemap(urlJoin('/discover/model', item.identifier), { lastModified: item?.lastModified || LAST_MODIFIED, }), ); return flatten(sitmap); } // 如果没有指定页数,返回所有(向后兼容) const sitmap = list.map((item) => this._genSitemap(urlJoin('/discover/model', item.identifier), { lastModified: item?.lastModified || LAST_MODIFIED, }), ); return flatten(sitmap); } async getProviders(): Promise<MetadataRoute.Sitemap> { const list = await this.discoverService.getProviderIdentifiers(); const sitmap = list.map((item) => this._genSitemap(urlJoin('/discover/provider', item.identifier), { lastModified: item?.lastModified || LAST_MODIFIED, }), ); return flatten(sitmap); } async getPage(): Promise<MetadataRoute.Sitemap> { const hideDocs = serverFeatureFlags().hideDocs; return [ ...this._genSitemap('/', { noLocales: true }), ...this._genSitemap('/chat', { noLocales: true }), ...(!hideDocs ? this._genSitemap('/changelog', { noLocales: true }) : []), /* ↓ cloud slot ↓ */ /* ↑ cloud slot ↑ */ ...this._genSitemap('/discover', { changeFrequency: 'daily', priority: 0.7 }), ...this._genSitemap('/discover/assistant', { changeFrequency: 'daily', priority: 0.7 }), ...this._genSitemap('/discover/mcp', { changeFrequency: 'daily', priority: 0.7 }), ...this._genSitemap('/discover/plugin', { changeFrequency: 'daily', priority: 0.7 }), ...this._genSitemap('/discover/model', { changeFrequency: 'daily', priority: 0.7 }), ...this._genSitemap('/discover/provider', { changeFrequency: 'daily', priority: 0.7 }), ].filter(Boolean); } getRobots() { return [ getCanonicalUrl('/sitemap-index.xml'), ...this.sitemapIndexs.map((index) => getCanonicalUrl(SITEMAP_BASE_URL, isDev ? index.id : `${index.id}.xml`), ), ]; } }