@docsector/docsector-reader 4.3.3 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +18 -0
- package/README.md +135 -4
- package/bin/docsector.js +36 -1
- package/docsector.config.js +44 -0
- package/package.json +3 -2
- package/public/robots.txt +4 -0
- package/src/ai-assistant/config.js +91 -0
- package/src/ai-assistant/indexing.js +50 -0
- package/src/ai-assistant/layout.js +56 -0
- package/src/ai-assistant/messages.js +41 -0
- package/src/ai-assistant/panel.js +22 -0
- package/src/ai-assistant/server.js +348 -0
- package/src/ai-assistant/session.js +91 -0
- package/src/ai-assistant/stream.js +125 -0
- package/src/components/DAssistantPanel.vue +701 -0
- package/src/components/DPage.vue +114 -4
- package/src/components/DPageRichContent.vue +105 -0
- package/src/components/DPageTokens.vue +27 -16
- package/src/components/api-block-model.js +77 -1
- package/src/components/inline-code-copy.js +58 -0
- package/src/components/page-section-tokens.js +6 -4
- package/src/components/quasar-api-extends.json +235 -0
- package/src/composables/useAssistant.js +201 -0
- package/src/i18n/helpers.js +2 -0
- package/src/i18n/languages/en-US.hjson +22 -0
- package/src/i18n/languages/pt-BR.hjson +22 -0
- package/src/layouts/DefaultLayout.vue +22 -0
- package/src/markdown-agent.js +32 -0
- package/src/pages/manual/basic/ai-assistant.overview.en-US.md +69 -0
- package/src/pages/manual/basic/ai-assistant.overview.pt-BR.md +69 -0
- package/src/pages/manual.index.js +29 -0
- package/src/quasar.factory.js +166 -33
- package/src/sitemap.js +103 -0
- package/src/store/Layout.js +9 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
## Visão Geral
|
|
2
|
+
|
|
3
|
+
O AI Assistant adiciona um painel opcional de chat às páginas do Docsector. Ele foi desenhado para fluxos de RAG semântico na Cloudflare, mantendo a integração do navegador simples: usuários abrem um drawer pelo header global, e a Cloudflare Pages Function conversa com o provedor de IA configurado.
|
|
4
|
+
|
|
5
|
+
O primeiro provedor é o Cloudflare AI Search. Ele pode rastrear o sitemap Markdown gerado pelo Docsector, recuperar trechos relevantes com busca híbrida e transmitir uma resposta com trechos de origem que o painel mostra como citações.
|
|
6
|
+
|
|
7
|
+
## O Que Ele Adiciona
|
|
8
|
+
|
|
9
|
+
- Um drawer lateral direito no desktop.
|
|
10
|
+
- Um diálogo em tela cheia no mobile.
|
|
11
|
+
- Prompts sugeridos, contexto da página atual, respostas em streaming e links de fontes.
|
|
12
|
+
- Artefatos de build para AI Search quando `siteUrl` está configurado.
|
|
13
|
+
- Um endpoint interno same-origin para manter credenciais no servidor.
|
|
14
|
+
|
|
15
|
+
## Habilitar
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
export default {
|
|
19
|
+
siteUrl: 'https://docs.example.com',
|
|
20
|
+
|
|
21
|
+
aiAssistant: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
provider: 'aiSearch',
|
|
24
|
+
endpoint: '/assistant',
|
|
25
|
+
aiSearch: {
|
|
26
|
+
binding: 'AI_SEARCH',
|
|
27
|
+
instanceNameEnv: 'AI_SEARCH_INSTANCE_NAME',
|
|
28
|
+
accountIdEnv: 'CLOUDFLARE_ACCOUNT_ID',
|
|
29
|
+
apiTokenEnv: 'CLOUDFLARE_API_TOKEN',
|
|
30
|
+
retrievalType: 'hybrid',
|
|
31
|
+
maxResults: 6,
|
|
32
|
+
stream: true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Defina `AI_SEARCH_INSTANCE_NAME` nas variáveis de ambiente do Cloudflare Pages em deploy, ou em `.dev.vars` quando usar `wrangler pages dev` localmente.
|
|
39
|
+
|
|
40
|
+
## Cloudflare AI Search
|
|
41
|
+
|
|
42
|
+
Crie uma instância do AI Search e configure uma fonte de dados Website. O Docsector sempre publica `/sitemap.xml` no build e anuncia esse arquivo em `robots.txt`, então o crawler da Cloudflare consegue descobrir o site automaticamente.
|
|
43
|
+
|
|
44
|
+
Para uma recuperação mais limpa, aponte a configuração de sitemap específico para:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
https://docs.example.com/ai-search-sitemap.xml
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
O sitemap do AI Search aponta para URLs Markdown, que são mais limpas para recuperação do que HTML renderizado pela SPA. O manifest em `/.well-known/ai-search/manifest.json` lista títulos, rotas, locales, books, versões e subpáginas do mesmo conjunto de fontes.
|
|
51
|
+
|
|
52
|
+
## Endpoint Runtime
|
|
53
|
+
|
|
54
|
+
A Pages Function gerada aceita mensagens de chat, metadados da rota atual, locale e texto selecionado opcional. Ela encaminha a solicitação ao AI Search por binding quando disponível, ou por REST usando variáveis de ambiente criptografadas da Cloudflare. O endpoint é uma API interna do drawer, não uma página para o usuário navegar.
|
|
55
|
+
|
|
56
|
+
O navegador nunca precisa de um token da API Cloudflare.
|
|
57
|
+
|
|
58
|
+
## Validar
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx docsector build
|
|
62
|
+
cat dist/spa/sitemap.xml
|
|
63
|
+
cat dist/spa/robots.txt
|
|
64
|
+
cat dist/spa/ai-search-sitemap.xml
|
|
65
|
+
cat dist/spa/.well-known/ai-search/manifest.json
|
|
66
|
+
npx wrangler pages dev dist/spa
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Bindings remotos de AI Search e Workers AI podem gerar uso cobrado pela Cloudflare durante o desenvolvimento local.
|
|
@@ -265,6 +265,35 @@ export default {
|
|
|
265
265
|
}
|
|
266
266
|
},
|
|
267
267
|
|
|
268
|
+
'/basic/ai-assistant': {
|
|
269
|
+
config: {
|
|
270
|
+
icon: 'auto_awesome',
|
|
271
|
+
status: 'new',
|
|
272
|
+
version: 'v4.4.0',
|
|
273
|
+
meta: {
|
|
274
|
+
description: {
|
|
275
|
+
'en-US': 'AI Assistant — Documentation of Docsector Reader',
|
|
276
|
+
'pt-BR': 'Assistente de IA — Documentacao do Docsector Reader'
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
book: 'manual',
|
|
280
|
+
menu: {},
|
|
281
|
+
subpages: {
|
|
282
|
+
showcase: false
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
data: {
|
|
286
|
+
'en-US': { title: 'AI Assistant' },
|
|
287
|
+
'pt-BR': { title: 'Assistente de IA' }
|
|
288
|
+
},
|
|
289
|
+
metadata: {
|
|
290
|
+
tags: {
|
|
291
|
+
'en-US': 'ai assistant cloudflare ai search rag drawer semantic search citations streaming',
|
|
292
|
+
'pt-BR': 'ia assistente cloudflare ai search rag drawer busca semântica citações streaming'
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
|
|
268
297
|
'/basic/agent-skills': {
|
|
269
298
|
config: {
|
|
270
299
|
icon: 'psychology',
|
package/src/quasar.factory.js
CHANGED
|
@@ -30,6 +30,11 @@ import { resolve } from 'path'
|
|
|
30
30
|
import { pathToFileURL } from 'url'
|
|
31
31
|
import HJSON from 'hjson'
|
|
32
32
|
|
|
33
|
+
import { normalizeAiAssistantConfig } from './ai-assistant/config.js'
|
|
34
|
+
import { createAiSearchIndexArtifacts } from './ai-assistant/indexing.js'
|
|
35
|
+
import { MARKDOWN_AGENT_USER_AGENT_SOURCE, matchesMarkdownAgentUserAgent } from './markdown-agent.js'
|
|
36
|
+
import { appendSitemapsToRobots, createSitemap } from './sitemap.js'
|
|
37
|
+
|
|
33
38
|
/**
|
|
34
39
|
* No-op configure wrapper.
|
|
35
40
|
* Quasar's `configure` from `quasar/wrappers` is a TypeScript identity function.
|
|
@@ -1751,9 +1756,6 @@ function createMarkdownEndpointPlugin (projectRoot) {
|
|
|
1751
1756
|
return Math.max(1, Math.ceil(markdown.length / 4))
|
|
1752
1757
|
}
|
|
1753
1758
|
|
|
1754
|
-
// LLM bot user-agent patterns
|
|
1755
|
-
const LLM_BOT_PATTERN = /GPTBot|ChatGPT-User|OAI-SearchBot|ClaudeBot|Claude-User|Claude-SearchBot|anthropic-ai|Google-Extended|Gemini-Deep-Research|PerplexityBot|Perplexity-User|Bytespider|CCBot|Meta-ExternalAgent|FacebookBot|Amazonbot|Applebot-Extended|cohere-ai|DuckAssistBot|GrokBot|AI2Bot|YouBot|PetalBot/i
|
|
1756
|
-
|
|
1757
1759
|
return {
|
|
1758
1760
|
name: 'docsector-markdown-endpoint',
|
|
1759
1761
|
|
|
@@ -1806,7 +1808,7 @@ function createMarkdownEndpointPlugin (projectRoot) {
|
|
|
1806
1808
|
}
|
|
1807
1809
|
|
|
1808
1810
|
if (homepagePath && typeof remoteHomepage === 'string' && remoteHomepage.length > 0) {
|
|
1809
|
-
if ((markdownNegotiationEnabled && wantsMarkdown) || (markdownAgentFallback &&
|
|
1811
|
+
if ((markdownNegotiationEnabled && wantsMarkdown) || (markdownAgentFallback && matchesMarkdownAgentUserAgent(req.headers['user-agent'] || ''))) {
|
|
1810
1812
|
res.setHeader('Content-Type', 'text/markdown; charset=utf-8')
|
|
1811
1813
|
res.setHeader('Vary', 'Accept')
|
|
1812
1814
|
res.setHeader('x-markdown-tokens', String(estimateMarkdownTokens(remoteHomepage)))
|
|
@@ -1843,7 +1845,7 @@ function createMarkdownEndpointPlugin (projectRoot) {
|
|
|
1843
1845
|
|
|
1844
1846
|
// Auto-serve markdown to LLM bot crawlers
|
|
1845
1847
|
const ua = req.headers['user-agent'] || ''
|
|
1846
|
-
if (markdownAgentFallback &&
|
|
1848
|
+
if (markdownAgentFallback && matchesMarkdownAgentUserAgent(ua)) {
|
|
1847
1849
|
const file = resolveNegotiatedFile(url.pathname, lang)
|
|
1848
1850
|
if (file) {
|
|
1849
1851
|
const content = readFileSync(file, 'utf-8')
|
|
@@ -1863,6 +1865,74 @@ function createMarkdownEndpointPlugin (projectRoot) {
|
|
|
1863
1865
|
}
|
|
1864
1866
|
}
|
|
1865
1867
|
|
|
1868
|
+
function collectAiSearchIndexEntries ({ pagesDir, pageEntries = [], defaultLang = 'en-US' } = {}) {
|
|
1869
|
+
const entries = []
|
|
1870
|
+
|
|
1871
|
+
for (const entry of pageEntries) {
|
|
1872
|
+
const { book, pagePath, page } = entry
|
|
1873
|
+
if (page?.config === null) continue
|
|
1874
|
+
if (page?.config?.status === 'empty') continue
|
|
1875
|
+
|
|
1876
|
+
const title = page?.data?.['*']?.title
|
|
1877
|
+
|| page?.data?.[defaultLang]?.title
|
|
1878
|
+
|| page?.data?.['en-US']?.title
|
|
1879
|
+
|| pagePath?.split('/').pop()
|
|
1880
|
+
|| pagePath
|
|
1881
|
+
|
|
1882
|
+
const subpages = ['overview']
|
|
1883
|
+
if (page?.config?.subpages?.showcase) subpages.push('showcase')
|
|
1884
|
+
if (page?.config?.subpages?.vs) subpages.push('vs')
|
|
1885
|
+
|
|
1886
|
+
for (const subpage of subpages) {
|
|
1887
|
+
const srcFile = resolveMarkdownSourceFile(pagesDir, entry, subpage, defaultLang)
|
|
1888
|
+
if (!existsSync(srcFile)) continue
|
|
1889
|
+
|
|
1890
|
+
const routePath = buildPageRoutePath(entry, subpage)
|
|
1891
|
+
entries.push({
|
|
1892
|
+
title,
|
|
1893
|
+
path: routePath,
|
|
1894
|
+
markdownPath: `${routePath}.md`,
|
|
1895
|
+
locale: defaultLang,
|
|
1896
|
+
book,
|
|
1897
|
+
version: entry.version,
|
|
1898
|
+
subpage
|
|
1899
|
+
})
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
return entries
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function collectStandardSitemapEntries ({ pagesDir, pageEntries = [], defaultLang = 'en-US' } = {}) {
|
|
1907
|
+
const entries = [
|
|
1908
|
+
{ path: '/', priority: '1.0' }
|
|
1909
|
+
]
|
|
1910
|
+
const seenPaths = new Set(['/'])
|
|
1911
|
+
|
|
1912
|
+
for (const entry of pageEntries) {
|
|
1913
|
+
const { page } = entry
|
|
1914
|
+
if (page?.config === null) continue
|
|
1915
|
+
if (page?.config?.status === 'empty') continue
|
|
1916
|
+
|
|
1917
|
+
const subpages = ['overview']
|
|
1918
|
+
if (page?.config?.subpages?.showcase) subpages.push('showcase')
|
|
1919
|
+
if (page?.config?.subpages?.vs) subpages.push('vs')
|
|
1920
|
+
|
|
1921
|
+
for (const subpage of subpages) {
|
|
1922
|
+
const srcFile = resolveMarkdownSourceFile(pagesDir, entry, subpage, defaultLang)
|
|
1923
|
+
if (!existsSync(srcFile)) continue
|
|
1924
|
+
|
|
1925
|
+
const routePath = buildPageRoutePath(entry, subpage, { leadingSlash: true })
|
|
1926
|
+
if (seenPaths.has(routePath)) continue
|
|
1927
|
+
|
|
1928
|
+
seenPaths.add(routePath)
|
|
1929
|
+
entries.push({ path: routePath })
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
return entries
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1866
1936
|
/**
|
|
1867
1937
|
* Create a Vite plugin that generates static `.md` files at build time.
|
|
1868
1938
|
*
|
|
@@ -1883,6 +1953,7 @@ function createMarkdownBuildPlugin (projectRoot) {
|
|
|
1883
1953
|
|
|
1884
1954
|
const { default: config } = await import(configUrl)
|
|
1885
1955
|
const { pageEntries } = await loadBooksRegistry(projectRoot)
|
|
1956
|
+
const assistantConfig = normalizeAiAssistantConfig(config)
|
|
1886
1957
|
|
|
1887
1958
|
const defaultLang = config.defaultLanguage || config.languages?.[0]?.value || 'en-US'
|
|
1888
1959
|
let count = 0
|
|
@@ -1929,34 +2000,23 @@ function createMarkdownBuildPlugin (projectRoot) {
|
|
|
1929
2000
|
|
|
1930
2001
|
console.log(`\x1b[36m[docsector]\x1b[0m Generated ${count} static .md files`)
|
|
1931
2002
|
|
|
1932
|
-
// Generate sitemap.xml if siteUrl is configured
|
|
1933
2003
|
const siteUrl = (config.siteUrl || '').replace(/\/+$/, '')
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
if (page.config.subpages?.vs) subpages.push('vs')
|
|
1946
|
-
|
|
1947
|
-
for (const subpage of subpages) {
|
|
1948
|
-
const srcFile = resolveMarkdownSourceFile(pagesDir, entry, subpage, defaultLang)
|
|
1949
|
-
if (!existsSync(srcFile)) continue
|
|
1950
|
-
|
|
1951
|
-
const routePath = buildPageRoutePath(entry, subpage, { leadingSlash: true })
|
|
1952
|
-
urls += ` <url>\n <loc>${siteUrl}${routePath}</loc>\n <lastmod>${today}</lastmod>\n </url>\n`
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}</urlset>\n`
|
|
2004
|
+
const generatedAt = new Date().toISOString()
|
|
2005
|
+
const sitemapConfig = config.sitemap || {}
|
|
2006
|
+
const sitemapEnabled = sitemapConfig.enabled !== false
|
|
2007
|
+
|
|
2008
|
+
if (sitemapEnabled) {
|
|
2009
|
+
const sitemapEntries = collectStandardSitemapEntries({ pagesDir, pageEntries, defaultLang })
|
|
2010
|
+
const sitemap = createSitemap({
|
|
2011
|
+
entries: sitemapEntries,
|
|
2012
|
+
generatedAt,
|
|
2013
|
+
siteUrl
|
|
2014
|
+
})
|
|
1957
2015
|
writeFileSync(resolve(distDir, 'sitemap.xml'), sitemap)
|
|
1958
|
-
console.log(`\x1b[36m[docsector]\x1b[0m Generated sitemap.xml`)
|
|
2016
|
+
console.log(`\x1b[36m[docsector]\x1b[0m Generated sitemap.xml (${sitemapEntries.length} URLs)`)
|
|
2017
|
+
}
|
|
1959
2018
|
|
|
2019
|
+
if (siteUrl) {
|
|
1960
2020
|
// Generate llms.txt and llms-full.txt (LLM-friendly page index and full content)
|
|
1961
2021
|
const brandingName = config.branding?.name || 'Documentation'
|
|
1962
2022
|
const brandingVersion = config.branding?.version || ''
|
|
@@ -2047,6 +2107,8 @@ function createMarkdownBuildPlugin (projectRoot) {
|
|
|
2047
2107
|
const agentSkillsEnabled = agentSkillsConfig.enabled === true
|
|
2048
2108
|
const mcpServerCardConfig = config.mcpServerCard || {}
|
|
2049
2109
|
const mcpServerCardEnabled = mcpServerCardConfig.enabled === true
|
|
2110
|
+
const aiAssistantEnabled = assistantConfig.enabled === true
|
|
2111
|
+
let aiSearchSitemapGenerated = false
|
|
2050
2112
|
|
|
2051
2113
|
const toUrl = (href) => {
|
|
2052
2114
|
if (!href) return null
|
|
@@ -2061,11 +2123,57 @@ function createMarkdownBuildPlugin (projectRoot) {
|
|
|
2061
2123
|
return path || null
|
|
2062
2124
|
}
|
|
2063
2125
|
|
|
2126
|
+
if (aiAssistantEnabled && assistantConfig.provider === 'aiSearch') {
|
|
2127
|
+
const aiSearchEntries = collectAiSearchIndexEntries({
|
|
2128
|
+
pagesDir,
|
|
2129
|
+
pageEntries,
|
|
2130
|
+
defaultLang
|
|
2131
|
+
})
|
|
2132
|
+
const artifacts = createAiSearchIndexArtifacts({
|
|
2133
|
+
siteUrl,
|
|
2134
|
+
entries: aiSearchEntries
|
|
2135
|
+
})
|
|
2136
|
+
const manifestPath = '.well-known/ai-search/manifest.json'
|
|
2137
|
+
const sitemapPath = 'ai-search-sitemap.xml'
|
|
2138
|
+
|
|
2139
|
+
mkdirSync(resolve(distDir, '.well-known', 'ai-search'), { recursive: true })
|
|
2140
|
+
writeFileSync(resolve(distDir, manifestPath), JSON.stringify(artifacts.manifest, null, 2) + '\n')
|
|
2141
|
+
if (artifacts.sitemap) {
|
|
2142
|
+
writeFileSync(resolve(distDir, sitemapPath), artifacts.sitemap)
|
|
2143
|
+
aiSearchSitemapGenerated = true
|
|
2144
|
+
}
|
|
2145
|
+
console.log(`\x1b[36m[docsector]\x1b[0m Generated AI Search index artifacts (${aiSearchEntries.length} pages)`)
|
|
2146
|
+
|
|
2147
|
+
const headersWithAiSearch = readFileSync(headersPath, 'utf-8')
|
|
2148
|
+
const aiSearchHeaders = [
|
|
2149
|
+
'/.well-known/ai-search/manifest.json\n Content-Type: application/json; charset=utf-8',
|
|
2150
|
+
'/ai-search-sitemap.xml\n Content-Type: application/xml; charset=utf-8'
|
|
2151
|
+
].filter(rule => !headersWithAiSearch.includes(rule.split('\n')[0])).join('\n\n')
|
|
2152
|
+
if (aiSearchHeaders) {
|
|
2153
|
+
writeFileSync(headersPath, headersWithAiSearch.trimEnd() + '\n\n' + aiSearchHeaders + '\n')
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
if (aiAssistantEnabled) {
|
|
2158
|
+
const functionsDir = resolve(projectRoot, 'functions')
|
|
2159
|
+
const packageRoot = getPackageRoot(projectRoot)
|
|
2160
|
+
const templatePath = resolve(packageRoot, 'src', 'ai-assistant', 'server.js')
|
|
2161
|
+
mkdirSync(functionsDir, { recursive: true })
|
|
2162
|
+
|
|
2163
|
+
if (existsSync(templatePath)) {
|
|
2164
|
+
const serverCode = readFileSync(templatePath, 'utf-8')
|
|
2165
|
+
.replaceAll('__AI_ASSISTANT_CONFIG__', JSON.stringify(assistantConfig, null, 2))
|
|
2166
|
+
|
|
2167
|
+
writeFileSync(resolve(functionsDir, 'assistant.js'), serverCode)
|
|
2168
|
+
console.log(`\x1b[36m[docsector]\x1b[0m Generated AI Assistant endpoint at functions/assistant.js`)
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2064
2172
|
if (markdownNegotiationEnabled || webBotAuthEnabled) {
|
|
2065
2173
|
const functionsDir = resolve(projectRoot, 'functions')
|
|
2066
2174
|
mkdirSync(functionsDir, { recursive: true })
|
|
2067
2175
|
|
|
2068
|
-
const middlewareCode = `const LLM_BOT_PATTERN =
|
|
2176
|
+
const middlewareCode = `const LLM_BOT_PATTERN = new RegExp(${JSON.stringify(MARKDOWN_AGENT_USER_AGENT_SOURCE)}, 'i')
|
|
2069
2177
|
|
|
2070
2178
|
const DEFAULT_LANG = ${JSON.stringify(defaultLang)}
|
|
2071
2179
|
const MARKDOWN_ENABLED = ${markdownNegotiationEnabled ? 'true' : 'false'}
|
|
@@ -2227,6 +2335,7 @@ function estimateTokens (markdown = '') {
|
|
|
2227
2335
|
|
|
2228
2336
|
function shouldBypass (pathname) {
|
|
2229
2337
|
if (pathname === '/mcp' || pathname.startsWith('/mcp/')) return true
|
|
2338
|
+
if (pathname === '/assistant' || pathname.startsWith('/assistant/')) return true
|
|
2230
2339
|
if (pathname.startsWith('/.well-known/')) return true
|
|
2231
2340
|
return /\\.(js|css|map|png|jpg|jpeg|gif|webp|svg|ico|woff2?|ttf|eot|xml|json|txt)$/i.test(pathname)
|
|
2232
2341
|
}
|
|
@@ -2327,6 +2436,26 @@ export async function onRequest (context) {
|
|
|
2327
2436
|
}
|
|
2328
2437
|
}
|
|
2329
2438
|
|
|
2439
|
+
const robotsSitemapPaths = []
|
|
2440
|
+
if (sitemapEnabled) robotsSitemapPaths.push('/sitemap.xml')
|
|
2441
|
+
if (aiSearchSitemapGenerated) robotsSitemapPaths.push('/ai-search-sitemap.xml')
|
|
2442
|
+
|
|
2443
|
+
if (robotsSitemapPaths.length > 0) {
|
|
2444
|
+
const robotsPath = resolve(distDir, 'robots.txt')
|
|
2445
|
+
const existingRobots = existsSync(robotsPath)
|
|
2446
|
+
? readFileSync(robotsPath, 'utf-8')
|
|
2447
|
+
: 'User-agent: *\nAllow: /\n'
|
|
2448
|
+
const patchedRobots = appendSitemapsToRobots(existingRobots, {
|
|
2449
|
+
sitemaps: robotsSitemapPaths,
|
|
2450
|
+
siteUrl
|
|
2451
|
+
})
|
|
2452
|
+
|
|
2453
|
+
if (patchedRobots !== existingRobots) {
|
|
2454
|
+
writeFileSync(robotsPath, patchedRobots)
|
|
2455
|
+
console.log(`\x1b[36m[docsector]\x1b[0m Updated robots.txt with sitemap discovery`)
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2330
2459
|
if (agentSkillsEnabled) {
|
|
2331
2460
|
const agentSkillsPath = agentSkillsConfig.path || '/.well-known/agent-skills/index.json'
|
|
2332
2461
|
const agentSkillsSchema = agentSkillsConfig.schema || 'https://schemas.agentskills.io/discovery/0.2.0/schema.json'
|
|
@@ -2653,7 +2782,7 @@ export async function onRequest (context) {
|
|
|
2653
2782
|
}
|
|
2654
2783
|
|
|
2655
2784
|
// Generate or merge _routes.json for Cloudflare Pages functions
|
|
2656
|
-
if (config.mcp || markdownNegotiationEnabled || webBotAuthEnabled) {
|
|
2785
|
+
if (config.mcp || aiAssistantEnabled || markdownNegotiationEnabled || webBotAuthEnabled) {
|
|
2657
2786
|
const routesPath = resolve(distDir, '_routes.json')
|
|
2658
2787
|
let routes = { version: 1, include: [], exclude: [] }
|
|
2659
2788
|
if (existsSync(routesPath)) {
|
|
@@ -2672,6 +2801,10 @@ export async function onRequest (context) {
|
|
|
2672
2801
|
routes.include.push('/mcp')
|
|
2673
2802
|
}
|
|
2674
2803
|
|
|
2804
|
+
if (aiAssistantEnabled && !markdownNegotiationEnabled && !routes.include.includes('/assistant')) {
|
|
2805
|
+
routes.include.push('/assistant')
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2675
2808
|
if (webBotAuthEnabled && !markdownNegotiationEnabled && !routes.include.includes(webBotAuthDirectoryPath)) {
|
|
2676
2809
|
routes.include.push(webBotAuthDirectoryPath)
|
|
2677
2810
|
}
|
|
@@ -3144,7 +3277,7 @@ export function createQuasarConfig (options = {}) {
|
|
|
3144
3277
|
config: {},
|
|
3145
3278
|
lang: 'en-US',
|
|
3146
3279
|
plugins: [
|
|
3147
|
-
'Meta', 'LocalStorage', 'SessionStorage'
|
|
3280
|
+
'Meta', 'Notify', 'LocalStorage', 'SessionStorage'
|
|
3148
3281
|
]
|
|
3149
3282
|
},
|
|
3150
3283
|
|
package/src/sitemap.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
function escapeXml (value) {
|
|
2
|
+
return String(value || '')
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"')
|
|
7
|
+
.replace(/'/g, ''')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeBaseUrl (siteUrl) {
|
|
11
|
+
return String(siteUrl || '').replace(/\/+$/g, '')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeLocalUrl (value) {
|
|
15
|
+
const path = String(value || '').trim()
|
|
16
|
+
if (!path || path === '/') return '/'
|
|
17
|
+
if (/^https?:\/\//i.test(path)) return path
|
|
18
|
+
return `/${path.replace(/^\/+/, '')}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveSitemapUrl (path, siteUrl) {
|
|
22
|
+
const localUrl = normalizeLocalUrl(path)
|
|
23
|
+
if (/^https?:\/\//i.test(localUrl)) return localUrl
|
|
24
|
+
|
|
25
|
+
const baseUrl = normalizeBaseUrl(siteUrl)
|
|
26
|
+
return baseUrl ? `${baseUrl}${localUrl}` : localUrl
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeSitemapIdentity (value) {
|
|
30
|
+
const normalized = normalizeLocalUrl(String(value || '').trim())
|
|
31
|
+
if (!/^https?:\/\//i.test(normalized)) return normalized.toLowerCase()
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(normalized)
|
|
35
|
+
return `${url.pathname}${url.search}`.toLowerCase()
|
|
36
|
+
} catch {
|
|
37
|
+
return normalized.toLowerCase()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeSitemapEntry (entry) {
|
|
42
|
+
if (typeof entry === 'string') return { path: entry }
|
|
43
|
+
return entry && typeof entry === 'object' ? entry : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createSitemap ({ entries = [], siteUrl = '', generatedAt = new Date().toISOString() } = {}) {
|
|
47
|
+
const lastmod = String(generatedAt || new Date().toISOString()).slice(0, 10)
|
|
48
|
+
const urls = (Array.isArray(entries) ? entries : [])
|
|
49
|
+
.map(normalizeSitemapEntry)
|
|
50
|
+
.filter(entry => entry?.path)
|
|
51
|
+
.map(entry => {
|
|
52
|
+
const lines = [
|
|
53
|
+
' <url>',
|
|
54
|
+
` <loc>${escapeXml(resolveSitemapUrl(entry.path, siteUrl))}</loc>`,
|
|
55
|
+
` <lastmod>${escapeXml(entry.lastmod || lastmod)}</lastmod>`
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
if (entry.changefreq) {
|
|
59
|
+
lines.push(` <changefreq>${escapeXml(entry.changefreq)}</changefreq>`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (entry.priority !== undefined && entry.priority !== null) {
|
|
63
|
+
lines.push(` <priority>${escapeXml(entry.priority)}</priority>`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
lines.push(' </url>')
|
|
67
|
+
return lines.join('\n')
|
|
68
|
+
})
|
|
69
|
+
.join('\n')
|
|
70
|
+
|
|
71
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls ? `${urls}\n` : ''}</urlset>\n`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function appendSitemapsToRobots (robotsContent, { sitemaps = [], siteUrl = '' } = {}) {
|
|
75
|
+
const input = typeof robotsContent === 'string' && robotsContent.trim()
|
|
76
|
+
? robotsContent
|
|
77
|
+
: 'User-agent: *\nAllow: /\n'
|
|
78
|
+
|
|
79
|
+
const existingIdentities = new Set(
|
|
80
|
+
input
|
|
81
|
+
.replace(/\r\n/g, '\n')
|
|
82
|
+
.split('\n')
|
|
83
|
+
.map(line => line.match(/^\s*Sitemap\s*:\s*(.+?)\s*$/i)?.[1])
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.map(normalizeSitemapIdentity)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const addedIdentities = new Set()
|
|
89
|
+
const sitemapLines = (Array.isArray(sitemaps) ? sitemaps : [sitemaps])
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.map(sitemap => resolveSitemapUrl(sitemap, siteUrl))
|
|
92
|
+
.filter(sitemap => {
|
|
93
|
+
const identity = normalizeSitemapIdentity(sitemap)
|
|
94
|
+
if (existingIdentities.has(identity) || addedIdentities.has(identity)) return false
|
|
95
|
+
addedIdentities.add(identity)
|
|
96
|
+
return true
|
|
97
|
+
})
|
|
98
|
+
.map(sitemap => `Sitemap: ${sitemap}`)
|
|
99
|
+
|
|
100
|
+
if (sitemapLines.length === 0) return input
|
|
101
|
+
|
|
102
|
+
return `${input.replace(/\s+$/g, '')}\n${sitemapLines.join('\n')}\n`
|
|
103
|
+
}
|
package/src/store/Layout.js
CHANGED
|
@@ -29,7 +29,9 @@ export default {
|
|
|
29
29
|
// Main
|
|
30
30
|
scrolling: true,
|
|
31
31
|
meta: true,
|
|
32
|
-
metaToggle: false
|
|
32
|
+
metaToggle: false,
|
|
33
|
+
assistant: false,
|
|
34
|
+
assistantWidth: 380
|
|
33
35
|
},
|
|
34
36
|
getters: {
|
|
35
37
|
view (state) {
|
|
@@ -118,6 +120,12 @@ export default {
|
|
|
118
120
|
},
|
|
119
121
|
setMetaToggle (state, val) {
|
|
120
122
|
state.metaToggle = val
|
|
123
|
+
},
|
|
124
|
+
setAssistant (state, val) {
|
|
125
|
+
state.assistant = val
|
|
126
|
+
},
|
|
127
|
+
setAssistantWidth (state, val) {
|
|
128
|
+
state.assistantWidth = val
|
|
121
129
|
}
|
|
122
130
|
},
|
|
123
131
|
actions: {}
|