@movk/nuxt-docs 1.8.0 → 1.9.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/README.md CHANGED
@@ -20,6 +20,10 @@
20
20
 
21
21
  ### 🤖 AI 增强体验
22
22
 
23
+ <div style="padding: 40px 0; display: flex; justify-content: center;">
24
+ <img src="https://docs.mhaibaraai.cn/ai/AiChat.png" alt="AiChat" width="400">
25
+ </div>
26
+
23
27
  - **AI 聊天助手** - 内置智能文档助手,基于 Vercel AI SDK 支持多种 LLM 模型(Mistral、Qwen、OpenRouter)
24
28
  - **MCP Server 支持** - 集成 Model Context Protocol 服务器,为 AI 助手提供结构化的文档访问能力
25
29
  - **LLM 优化** - 通过 `nuxt-llms` 模块自动生成 `llms.txt` 和 `llms-full.txt`,为 AI 工具提供优化的文档索引
package/app/app.config.ts CHANGED
@@ -108,8 +108,7 @@ export default defineAppConfig({
108
108
  streaming: 'i-lucide-chevron-down',
109
109
  providers: {
110
110
  mistral: 'i-simple-icons-mistralai',
111
- kwaipilot: 'i-lucide-wand',
112
- zai: 'i-lucide-wand'
111
+ kwaipilot: 'i-lucide-wand'
113
112
  }
114
113
  }
115
114
  }
@@ -81,10 +81,11 @@ const diagramRef = ref<HTMLElement | null>(null)
81
81
  const isRendered = ref(false)
82
82
  const hasError = ref(false)
83
83
  const errorMessage = ref('')
84
+ const hasBeenVisible = ref(false)
84
85
 
85
86
  const [isFullscreen, toggleFullscreen] = useToggle(false)
86
87
  const { copy, copied } = useClipboard({ source: () => props.code })
87
- const isVisible = useElementVisibility(containerRef)
88
+ const isVisible = useElementVisibility(containerRef, { threshold: 0.1 })
88
89
 
89
90
  async function renderMermaid() {
90
91
  if (!props.code || isRendered.value || !diagramRef.value) return
@@ -127,7 +128,11 @@ async function reRender() {
127
128
  watch(
128
129
  [isVisible, diagramRef],
129
130
  ([visible, el]) => {
130
- if (visible && el && !isRendered.value) {
131
+ // 记录曾经可见状态,避免快速滚动时错过渲染
132
+ if (visible) {
133
+ hasBeenVisible.value = true
134
+ }
135
+ if (hasBeenVisible.value && el && !isRendered.value) {
131
136
  renderMermaid()
132
137
  }
133
138
  },
@@ -1,7 +1,9 @@
1
1
  export function useToolCall() {
2
2
  const tools: Record<string, string | ((args: any) => string)> = {
3
3
  'list-pages': '列出所有文档页面',
4
- 'get-page': (args: any) => `检索 ${args?.path || '页面'}`
4
+ 'get-page': (args: any) => `检索 ${args?.path || '页面'}`,
5
+ 'list-examples': '列出所有示例',
6
+ 'get-example': (args: any) => `获取示例:${args?.exampleName || '示例'}`
5
7
  }
6
8
  return {
7
9
  tools
@@ -14,7 +14,7 @@ const { toc, github } = useAppConfig()
14
14
  const { data: page } = await useAsyncData(`docs-${kebabCase(route.path)}`, () => queryCollection('docs').path(route.path).first())
15
15
 
16
16
  if (!page.value) {
17
- throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
17
+ throw createError({ status: 404, statusText: 'Page not found', fatal: true })
18
18
  }
19
19
 
20
20
  const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  const { data: page } = await useAsyncData('landing', () => queryCollection('landing').path('/').first())
3
3
  if (!page.value) {
4
- throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
4
+ throw createError({ status: 404, statusText: 'Page not found', fatal: true })
5
5
  }
6
6
 
7
7
  const title = page.value.seo?.title || page.value.title
@@ -3,7 +3,7 @@ import type { ButtonProps } from '@nuxt/ui'
3
3
 
4
4
  const { data: page } = await useAsyncData('releases', () => queryCollection('releases').first())
5
5
  if (!page.value) {
6
- throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
6
+ throw createError({ status: 404, statusText: 'Page not found', fatal: true })
7
7
  }
8
8
 
9
9
  const title = page.value.seo?.title || page.value.title
package/content.config.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { defineCollection, defineContentConfig } from '@nuxt/content'
3
3
  import { useNuxt } from '@nuxt/kit'
4
- import { asSeoCollection } from '@nuxtjs/seo/content'
5
4
  import { joinURL } from 'ufo'
6
- import { z } from 'zod/v4'
5
+ import { z } from 'zod'
7
6
 
8
7
  const { options } = useNuxt()
9
8
  const cwd = joinURL(options.rootDir, 'content')
@@ -38,14 +37,14 @@ const PageHero = z.object({
38
37
 
39
38
  export default defineContentConfig({
40
39
  collections: {
41
- landing: defineCollection(asSeoCollection({
40
+ landing: defineCollection({
42
41
  type: 'page',
43
42
  source: {
44
43
  cwd,
45
44
  include: 'index.md'
46
45
  }
47
- })),
48
- docs: defineCollection(asSeoCollection({
46
+ }),
47
+ docs: defineCollection({
49
48
  type: 'page',
50
49
  source: {
51
50
  cwd,
@@ -58,8 +57,8 @@ export default defineContentConfig({
58
57
  title: z.string().optional()
59
58
  })
60
59
  })
61
- })),
62
- releases: defineCollection(asSeoCollection({
60
+ }),
61
+ releases: defineCollection({
63
62
  type: 'page',
64
63
  source: {
65
64
  cwd,
@@ -71,6 +70,6 @@ export default defineContentConfig({
71
70
  releases: z.string(),
72
71
  hero: PageHero
73
72
  })
74
- }))
73
+ })
75
74
  }
76
75
  })
@@ -22,7 +22,7 @@ export interface AiChatModuleOptions {
22
22
  */
23
23
  mcpPath?: string
24
24
  /**
25
- * 通过 AI SDK Gateway 、OpenRouter 使用的 AI 模型
25
+ * 使用的 AI 模型
26
26
  */
27
27
  model?: string
28
28
  /**
@@ -46,7 +46,7 @@ export default defineNuxtModule<AiChatModuleOptions>({
46
46
  models: []
47
47
  },
48
48
  setup(options, nuxt) {
49
- const hasApiKey = !!(process.env.AI_GATEWAY_API_KEY || process.env.OPENROUTER_API_KEY)
49
+ const hasApiKey = !!(process.env.AI_GATEWAY_API_KEY || process.env.OPENROUTER_API_KEY || process.env.ZHIPU_API_KEY)
50
50
 
51
51
  const { resolve } = createResolver(import.meta.url)
52
52
 
@@ -77,7 +77,7 @@ export default defineNuxtModule<AiChatModuleOptions>({
77
77
  }
78
78
 
79
79
  if (!hasApiKey) {
80
- log.warn('[ai-chat] Module disabled: no AI_GATEWAY_API_KEY or OPENROUTER_API_KEY found')
80
+ log.warn('[ai-chat] Module disabled: no API key found in environment variables.')
81
81
  return
82
82
  }
83
83
 
@@ -1,5 +1,5 @@
1
1
  import { tool, stepCountIs, generateText } from 'ai'
2
- import { z } from 'zod/v4'
2
+ import { z } from 'zod'
3
3
 
4
4
  function getSubAgentSystemPrompt(siteName: string) {
5
5
  return `您是 ${siteName} 的文档搜索代理。您的工作是从文档中查找并检索相关信息。
@@ -1,23 +1,29 @@
1
1
  import { createGateway } from '@ai-sdk/gateway'
2
2
  import { createOpenRouter } from '@openrouter/ai-sdk-provider'
3
+ import { modelProviderRegistry } from './modelProviders'
4
+
5
+ modelProviderRegistry.register('openrouter', ({ config, modelId }) => {
6
+ const openRouter = createOpenRouter({
7
+ apiKey: config.openRouterApiKey as string | undefined
8
+ })
9
+ return openRouter.chat(modelId)
10
+ })
3
11
 
4
12
  /**
5
13
  * 获取 AI 模型实例
6
- * @param modelId - 模型标识符,格式为 "provider/model" 或 "model"
7
- * @returns AI SDK 模型实例
14
+ * 优先使用注册的提供商,否则回退到 AI Gateway
8
15
  */
9
16
  export function getModel(modelId: string) {
10
17
  const config = useRuntimeConfig()
18
+ const parsed = modelProviderRegistry.parseModelId(modelId)
11
19
 
12
- // OpenRouter 模型:以 "openrouter/" 开头
13
- if (modelId.startsWith('openrouter/')) {
14
- const openRouter = createOpenRouter({
15
- apiKey: config.openRouterApiKey as string | undefined
16
- })
17
- return openRouter.chat(modelId.replace('openrouter/', ''))
20
+ if (parsed) {
21
+ const factory = modelProviderRegistry.get(parsed.prefix)
22
+ if (factory) {
23
+ return factory({ config, modelId: parsed.modelId })
24
+ }
18
25
  }
19
26
 
20
- // AI Gateway 模型(默认)
21
27
  const gateway = createGateway({
22
28
  apiKey: config.aiGatewayApiKey as string | undefined
23
29
  })
@@ -0,0 +1,34 @@
1
+ import type { LanguageModel } from 'ai'
2
+
3
+ export interface ModelProviderContext {
4
+ config: ReturnType<typeof useRuntimeConfig>
5
+ modelId: string
6
+ }
7
+
8
+ export type ModelProviderFactory = (context: ModelProviderContext) => LanguageModel
9
+
10
+ /**
11
+ * 模型提供商注册表
12
+ */
13
+ class ModelProviderRegistry {
14
+ private providers = new Map<string, ModelProviderFactory>()
15
+
16
+ register(prefix: string, factory: ModelProviderFactory): void {
17
+ this.providers.set(prefix, factory)
18
+ }
19
+
20
+ get(prefix: string): ModelProviderFactory | undefined {
21
+ return this.providers.get(prefix)
22
+ }
23
+
24
+ parseModelId(modelId: string): { prefix: string, modelId: string } | null {
25
+ const separatorIndex = modelId.indexOf('/')
26
+ if (separatorIndex === -1) return null
27
+ return {
28
+ prefix: modelId.slice(0, separatorIndex),
29
+ modelId: modelId.slice(separatorIndex + 1)
30
+ }
31
+ }
32
+ }
33
+
34
+ export const modelProviderRegistry = new ModelProviderRegistry()
package/modules/config.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createResolver, defineNuxtModule } from '@nuxt/kit'
2
- import { join } from 'pathe'
3
2
  import { defu } from 'defu'
4
3
  import { getGitBranch, getGitEnv, getLocalGitInfo } from '../utils/git'
5
4
  import { getPackageJsonMetadata, inferSiteURL } from '../utils/meta'
5
+ import { createComponentMetaExcludeFilters } from '../utils/component-meta'
6
6
 
7
7
  export default defineNuxtModule({
8
8
  meta: {
@@ -10,32 +10,32 @@ export default defineNuxtModule({
10
10
  },
11
11
  async setup(_options, nuxt) {
12
12
  const { resolve } = createResolver(import.meta.url)
13
+
14
+ nuxt.options.alias['#ai-chat'] = resolve('./ai-chat/runtime')
15
+
13
16
  const dir = nuxt.options.rootDir
14
17
  const url = inferSiteURL()
15
18
  const meta = await getPackageJsonMetadata(dir)
16
19
  const gitInfo = await getLocalGitInfo(dir) || getGitEnv()
17
- const siteName = nuxt.options?.site?.name || meta.name || gitInfo?.name || ''
20
+ const siteName = meta.name || gitInfo?.name || ''
21
+
22
+ nuxt.options.site = defu(nuxt.options.site, {
23
+ url,
24
+ name: siteName,
25
+ debug: false
26
+ })
18
27
 
19
28
  nuxt.options.llms = defu(nuxt.options.llms, {
20
29
  domain: url || 'https://example.com',
21
30
  title: siteName,
22
31
  description: meta.description || '',
32
+ contentRawMarkdown: false as const,
23
33
  full: {
24
34
  title: siteName,
25
35
  description: meta.description || ''
26
36
  }
27
37
  })
28
38
 
29
- nuxt.options.site = defu(nuxt.options.site, {
30
- url,
31
- name: siteName,
32
- debug: false
33
- })
34
-
35
- nuxt.options.robots = defu(nuxt.options.robots, {
36
- sitemap: url ? `${url.replace(/\/$/, '')}/sitemap.xml` : undefined
37
- })
38
-
39
39
  nuxt.options.appConfig.header = defu(nuxt.options.appConfig.header, {
40
40
  title: siteName
41
41
  })
@@ -59,34 +59,16 @@ export default defineNuxtModule({
59
59
  })
60
60
 
61
61
  const layerPath = resolve('..')
62
- const allowedComponents = [
63
- resolve('../app/components/content/CommitChangelog.vue'),
64
- resolve('../app/components/content/ComponentEmits.vue'),
65
- resolve('../app/components/content/ComponentExample.vue'),
66
- resolve('../app/components/content/ComponentProps.vue'),
67
- resolve('../app/components/content/ComponentSlots.vue'),
68
- resolve('../app/components/content/PageLastCommit.vue'),
69
- resolve('../app/components/content/Mermaid.vue'),
70
- resolve('./ai-chat/runtime/components/AiChatToolCall.vue'),
71
- resolve('./ai-chat/runtime/components/AiChatReasoning.vue'),
72
- resolve('./ai-chat/runtime/components/AiChatSlideoverFaq.vue'),
73
- resolve('./ai-chat/runtime/components/AiChatPreStream.vue')
74
- ]
75
- const userComponentPaths = [
76
- join(dir, 'app/components'),
77
- join(dir, 'components'),
78
- join(dir, 'docs/app/components'),
79
- join(dir, 'templates/*/app/components')
80
- ]
81
62
 
82
63
  // @ts-ignore - component-meta is not typed
83
64
  nuxt.hook('component-meta:extend', (options: any) => {
65
+ const userInclude = (nuxt.options.componentMeta && typeof nuxt.options.componentMeta === 'object')
66
+ ? nuxt.options.componentMeta.include || []
67
+ : []
68
+
84
69
  options.exclude = [
85
70
  ...(options.exclude || []),
86
- ({ filePath }: { filePath: string }) =>
87
- filePath.startsWith(layerPath) && !allowedComponents.includes(filePath),
88
- ({ filePath }: { filePath: string }) =>
89
- userComponentPaths.some(path => filePath.startsWith(path))
71
+ ...createComponentMetaExcludeFilters(resolve, dir, layerPath, userInclude)
90
72
  ]
91
73
  })
92
74
  }
package/modules/css.ts CHANGED
@@ -44,6 +44,8 @@ export default defineNuxtModule({
44
44
  }
45
45
  })
46
46
 
47
- nuxt.options.css.unshift(cssTemplate.dst)
47
+ if (Array.isArray(nuxt.options.css)) {
48
+ nuxt.options.css.unshift(cssTemplate.dst)
49
+ }
48
50
  }
49
51
  })
package/nuxt.config.ts CHANGED
@@ -12,10 +12,10 @@ export default defineNuxtConfig({
12
12
  '@nuxt/image',
13
13
  '@nuxt/a11y',
14
14
  '@nuxtjs/mcp-toolkit',
15
- '@nuxtjs/seo',
16
15
  '@vueuse/nuxt',
17
16
  'nuxt-component-meta',
18
17
  'nuxt-llms',
18
+ 'nuxt-og-image',
19
19
  'motion-v/nuxt',
20
20
  () => {
21
21
  extendViteConfig((config) => {
@@ -94,8 +94,8 @@ export default defineNuxtConfig({
94
94
  metaFields: {
95
95
  type: false,
96
96
  props: true,
97
- slots: 'no-schema' as const,
98
- events: 'no-schema' as const,
97
+ slots: 'no-schema',
98
+ events: 'no-schema',
99
99
  exposed: false
100
100
  },
101
101
  exclude: [
@@ -134,8 +134,5 @@ export default defineNuxtConfig({
134
134
  'Inter:400',
135
135
  'Inter:700'
136
136
  ]
137
- },
138
- sitemap: {
139
- zeroRuntime: true
140
137
  }
141
138
  })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@movk/nuxt-docs",
3
3
  "type": "module",
4
- "version": "1.8.0",
4
+ "version": "1.9.0",
5
5
  "private": false,
6
6
  "description": "Modern Nuxt 4 documentation theme with auto-generated component docs, AI chat assistant, MCP server, and complete developer experience optimization.",
7
7
  "author": "YiXuan <mhaibaraai@gmail.com>",
@@ -28,9 +28,9 @@
28
28
  "README.md"
29
29
  ],
30
30
  "dependencies": {
31
- "@ai-sdk/gateway": "^3.0.17",
32
- "@ai-sdk/mcp": "^1.0.11",
33
- "@ai-sdk/vue": "^3.0.42",
31
+ "@ai-sdk/gateway": "^3.0.22",
32
+ "@ai-sdk/mcp": "^1.0.13",
33
+ "@ai-sdk/vue": "^3.0.48",
34
34
  "@iconify-json/lucide": "^1.2.86",
35
35
  "@iconify-json/ph": "^1.2.2",
36
36
  "@iconify-json/simple-icons": "^1.2.67",
@@ -40,36 +40,36 @@
40
40
  "@nuxt/a11y": "^1.0.0-alpha.1",
41
41
  "@nuxt/content": "^3.11.0",
42
42
  "@nuxt/image": "^2.0.0",
43
- "@nuxt/kit": "^4.2.2",
44
- "@nuxt/ui": "^4.3.0",
43
+ "@nuxt/kit": "^4.3.0",
44
+ "@nuxt/ui": "^4.4.0",
45
45
  "@nuxtjs/mcp-toolkit": "^0.6.2",
46
- "@nuxtjs/seo": "^3.3.0",
47
46
  "@octokit/rest": "^22.0.1",
48
47
  "@openrouter/ai-sdk-provider": "^2.0.0",
49
48
  "@vercel/analytics": "^1.6.1",
50
49
  "@vercel/speed-insights": "^1.3.1",
51
50
  "@vueuse/core": "^14.1.0",
52
51
  "@vueuse/nuxt": "^14.1.0",
53
- "ai": "^6.0.42",
52
+ "ai": "^6.0.48",
54
53
  "defu": "^6.1.4",
55
- "dompurify": "^3.2.6",
54
+ "dompurify": "^3.3.1",
56
55
  "exsolve": "^1.0.8",
57
56
  "git-url-parse": "^16.1.0",
58
57
  "mermaid": "^11.12.2",
59
58
  "motion-v": "^1.9.0",
60
- "nuxt": "^4.2.2",
59
+ "nuxt": "^4.3.0",
61
60
  "nuxt-component-meta": "^0.17.1",
62
61
  "nuxt-llms": "^0.2.0",
62
+ "nuxt-og-image": "^5.1.13",
63
63
  "ohash": "^2.0.11",
64
64
  "pathe": "^2.0.3",
65
65
  "pkg-types": "^2.3.0",
66
- "prettier": "^3.8.0",
66
+ "prettier": "^3.8.1",
67
67
  "scule": "^1.3.0",
68
68
  "shiki": "^3.21.0",
69
69
  "shiki-stream": "^0.1.4",
70
70
  "shiki-transformer-color-highlight": "^1.0.0",
71
71
  "tailwindcss": "^4.1.18",
72
72
  "ufo": "^1.6.3",
73
- "zod": "^4.3.5"
73
+ "zod": "^4.3.6"
74
74
  }
75
75
  }
@@ -10,8 +10,8 @@ export default defineEventHandler((event) => {
10
10
  const component = components[pascalCase(componentName)]
11
11
  if (!component) {
12
12
  throw createError({
13
- statusMessage: 'Example not found!',
14
- statusCode: 404
13
+ statusText: 'Example not found!',
14
+ status: 404
15
15
  })
16
16
  }
17
17
  return component
@@ -10,8 +10,8 @@ export default defineCachedEventHandler(async (event) => {
10
10
 
11
11
  if (!paths.length || !paths[0]) {
12
12
  throw createError({
13
- statusCode: 400,
14
- statusMessage: 'Path is required'
13
+ status: 400,
14
+ statusText: 'Path is required'
15
15
  })
16
16
  }
17
17
 
@@ -19,8 +19,8 @@ export default defineCachedEventHandler(async (event) => {
19
19
 
20
20
  if (!github || typeof github === 'boolean') {
21
21
  throw createError({
22
- statusCode: 500,
23
- statusMessage: 'GitHub configuration is not available'
22
+ status: 500,
23
+ statusText: 'GitHub configuration is not available'
24
24
  })
25
25
  }
26
26
 
@@ -8,8 +8,8 @@ export default defineCachedEventHandler(async (event) => {
8
8
  const { path } = getQuery(event) as { path: string }
9
9
  if (!path) {
10
10
  throw createError({
11
- statusCode: 400,
12
- statusMessage: 'Path is required'
11
+ status: 400,
12
+ statusText: 'Path is required'
13
13
  })
14
14
  }
15
15
 
@@ -17,8 +17,8 @@ export default defineCachedEventHandler(async (event) => {
17
17
 
18
18
  if (!github || typeof github === 'boolean') {
19
19
  throw createError({
20
- statusCode: 500,
21
- statusMessage: 'GitHub configuration is not available'
20
+ status: 500,
21
+ statusText: 'GitHub configuration is not available'
22
22
  })
23
23
  }
24
24
 
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod'
2
+
3
+ export default defineMcpTool({
4
+ description: '检索特定的 UI 示例实现代码和详细信息',
5
+ inputSchema: {
6
+ exampleName: z.string().describe('示例名称(PascalCase)')
7
+ },
8
+ cache: '30m',
9
+ async handler({ exampleName }) {
10
+ try {
11
+ const result = await $fetch<{ code: string }>(`/api/component-example/${exampleName}.json`)
12
+ return {
13
+ content: [{ type: 'text' as const, text: result.code }]
14
+ }
15
+ } catch {
16
+ return errorResult(`示例 '${exampleName}' 未找到。使用 list_examples 工具查看所有可用示例。`)
17
+ }
18
+ }
19
+ })
@@ -1,9 +1,9 @@
1
- import { z } from 'zod/v4'
1
+ import { z } from 'zod'
2
2
  import { queryCollection } from '@nuxt/content/server'
3
3
  import { inferSiteURL } from '../../../utils/meta'
4
4
 
5
5
  export default defineMcpTool({
6
- description: `检索特定文档页面的完整内容和详细信息。
6
+ description: `检索特定文档页面的完整内容和详细信息,使用 \`sections\` 参数仅获取特定的 h2 部分以减少响应大小。'
7
7
 
8
8
  何时使用:当你知道文档页面的确切路径时使用。常见用例:
9
9
  - 用户请求特定页面:「显示入门指南」→ /docs/getting-started
@@ -15,10 +15,11 @@ export default defineMcpTool({
15
15
 
16
16
  工作流程:此工具返回完整的页面内容,包括标题、描述和完整的 markdown。当你需要从特定文档页面提供详细答案或代码示例时使用。`,
17
17
  inputSchema: {
18
- path: z.string().describe('从 list-pages 获取或用户提供的页面路径(例如 /docs/getting-started/installation)')
18
+ path: z.string().describe('从 list-pages 获取或用户提供的页面路径(例如 /docs/getting-started/installation)'),
19
+ sections: z.array(z.string()).optional().describe('要返回的特定 h2 部分标题(例如 ["Usage","API"])。如果省略,则返回完整文档。')
19
20
  },
20
- cache: '1h',
21
- handler: async ({ path }) => {
21
+ cache: '30m',
22
+ handler: async ({ path, sections }) => {
22
23
  const event = useEvent()
23
24
  const siteUrl = import.meta.dev ? 'http://localhost:3000' : inferSiteURL()
24
25
 
@@ -35,10 +36,17 @@ export default defineMcpTool({
35
36
  }
36
37
  }
37
38
 
38
- const content = await $fetch<string>(`/raw${path}.md`, {
39
+ const fullContent = await $fetch<string>(`/raw${path}.md`, {
39
40
  baseURL: siteUrl
40
41
  })
41
42
 
43
+ let content = fullContent
44
+
45
+ // If sections are specified, extract only those sections
46
+ if (sections && sections.length > 0) {
47
+ content = extractSections(fullContent, sections)
48
+ }
49
+
42
50
  const result = {
43
51
  title: page.title,
44
52
  path: page.path,
@@ -58,3 +66,64 @@ export default defineMcpTool({
58
66
  }
59
67
  }
60
68
  })
69
+
70
+ /**
71
+ * Extract specific sections from markdown content based on h2 headings
72
+ */
73
+ function extractSections(markdown: string, sectionTitles: string[]): string {
74
+ const lines = markdown.split('\n')
75
+ const result: string[] = []
76
+
77
+ // Normalize section titles for matching
78
+ const normalizedTitles = sectionTitles.map(t => t.toLowerCase().trim())
79
+
80
+ // Always include title (h1) and description (first blockquote)
81
+ let inHeader = true
82
+ for (const line of lines) {
83
+ if (inHeader) {
84
+ result.push(line)
85
+ // Stop after the description blockquote
86
+ if (line.startsWith('>') && result.length > 1) {
87
+ result.push('')
88
+ inHeader = false
89
+ }
90
+ continue
91
+ }
92
+ break
93
+ }
94
+
95
+ // Find and extract requested sections
96
+ let currentSection: string | null = null
97
+ let sectionContent: string[] = []
98
+
99
+ for (let i = 0; i < lines.length; i++) {
100
+ const line = lines[i]
101
+ if (!line) continue
102
+
103
+ // Check for h2 heading
104
+ if (line.startsWith('## ')) {
105
+ // Save previous section if it was requested
106
+ if (currentSection && normalizedTitles.includes(currentSection.toLowerCase())) {
107
+ result.push(...sectionContent)
108
+ result.push('')
109
+ }
110
+
111
+ // Start new section
112
+ currentSection = line.replace('## ', '').trim()
113
+ sectionContent = [line]
114
+ continue
115
+ }
116
+
117
+ // Add line to current section
118
+ if (currentSection) {
119
+ sectionContent.push(line)
120
+ }
121
+ }
122
+
123
+ // Don't forget the last section
124
+ if (currentSection && normalizedTitles.includes(currentSection.toLowerCase())) {
125
+ result.push(...sectionContent)
126
+ }
127
+
128
+ return result.join('\n').trim()
129
+ }
@@ -0,0 +1,14 @@
1
+ // @ts-expect-error - no types available
2
+ import components from '#component-example/nitro'
3
+
4
+ export default defineMcpTool({
5
+ description: '列出所有可用的 UI 示例和代码演示',
6
+ cache: '1h',
7
+ handler() {
8
+ const examples = Object.entries<{ pascalName: string }>(components).map(([_key, value]) => {
9
+ return value.pascalName
10
+ })
11
+
12
+ return jsonResult(examples)
13
+ }
14
+ })
@@ -19,7 +19,7 @@ export default defineMcpTool({
19
19
  - path:用于 get-page 的确切路径
20
20
  - description:页面内容的简要摘要
21
21
  - url:完整 URL 供参考`,
22
- cache: '1h',
22
+ cache: '30m',
23
23
  handler: async () => {
24
24
  const event = useEvent()
25
25
  const siteUrl = import.meta.dev ? 'http://localhost:3000' : getRequestURL(event).origin