@movk/nuxt-docs 1.5.1 → 1.6.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 +54 -24
- package/app/components/DocsAsideLeftBody.vue +13 -0
- package/app/components/DocsAsideLeftTop.vue +6 -0
- package/app/components/DocsAsideRightBottom.vue +29 -0
- package/app/components/OgImage/Nuxt.vue +40 -16
- package/app/components/PageHeaderLinks.vue +35 -9
- package/app/components/content/CommitChangelog.vue +34 -10
- package/app/components/content/ComponentEmits.vue +2 -2
- package/app/components/content/ComponentExample.vue +10 -10
- package/app/components/content/ComponentProps.vue +5 -3
- package/app/components/content/ComponentPropsSchema.vue +5 -1
- package/app/components/content/ComponentSlots.vue +2 -2
- package/app/components/content/HighlightInlineType.vue +1 -1
- package/app/components/content/PageLastCommit.vue +12 -8
- package/app/components/footer/Footer.vue +22 -0
- package/app/components/footer/FooterLeft.vue +7 -0
- package/app/components/footer/FooterRight.vue +14 -0
- package/app/components/header/Header.vue +4 -7
- package/app/components/header/HeaderCTA.vue +18 -0
- package/app/components/header/HeaderCenter.vue +7 -0
- package/app/components/theme-picker/ThemePicker.vue +24 -201
- package/app/composables/useFaq.ts +21 -0
- package/app/composables/useHighlighter.ts +22 -0
- package/app/composables/useTheme.ts +223 -0
- package/app/layouts/docs.vue +2 -15
- package/app/pages/docs/[...slug].vue +1 -1
- package/app/types/index.d.ts +6 -0
- package/app/utils/shiki-transformer-icon-highlight.ts +1 -1
- package/app/utils/unicode.ts +12 -0
- package/content.config.ts +2 -1
- package/modules/ai-chat/index.ts +90 -0
- package/modules/ai-chat/runtime/components/AiChat.vue +22 -0
- package/modules/ai-chat/runtime/components/AiChatFloatingInput.vue +85 -0
- package/modules/ai-chat/runtime/components/AiChatModelSelect.vue +24 -0
- package/modules/ai-chat/runtime/components/AiChatPreStream.vue +58 -0
- package/modules/ai-chat/runtime/components/AiChatReasoning.vue +49 -0
- package/modules/ai-chat/runtime/components/AiChatSlideover.vue +245 -0
- package/modules/ai-chat/runtime/components/AiChatSlideoverFaq.vue +41 -0
- package/modules/ai-chat/runtime/components/AiChatToolCall.vue +31 -0
- package/modules/ai-chat/runtime/composables/useAIChat.ts +45 -0
- package/modules/ai-chat/runtime/composables/useModels.ts +58 -0
- package/modules/ai-chat/runtime/composables/useTools.ts +31 -0
- package/modules/ai-chat/runtime/server/api/search.ts +84 -0
- package/modules/ai-chat/runtime/server/utils/docs_agent.ts +49 -0
- package/modules/ai-chat/runtime/server/utils/getModel.ts +25 -0
- package/modules/css.ts +2 -0
- package/nuxt.config.ts +27 -39
- package/package.json +16 -6
- package/server/mcp/tools/get-page.ts +60 -0
- package/server/mcp/tools/list-pages.ts +49 -0
- package/app/components/AdsCarbon.vue +0 -3
- package/app/components/Footer.vue +0 -32
- package/modules/llms.ts +0 -27
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
export interface FaqCategory {
|
|
3
|
+
category: string
|
|
4
|
+
items: string[]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
defineProps<{
|
|
8
|
+
/**
|
|
9
|
+
* 聊天为空时显示的常见问题分类
|
|
10
|
+
*/
|
|
11
|
+
faqQuestions: FaqCategory[]
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
defineEmits<{
|
|
15
|
+
(e: 'ask-question', question: string): void
|
|
16
|
+
}>()
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<div class="flex flex-col gap-5">
|
|
21
|
+
<div
|
|
22
|
+
v-for="category in faqQuestions"
|
|
23
|
+
:key="category.category"
|
|
24
|
+
class="flex flex-col gap-1.5"
|
|
25
|
+
>
|
|
26
|
+
<h4 class="text-xs font-medium text-dimmed uppercase tracking-wide">
|
|
27
|
+
{{ category.category }}
|
|
28
|
+
</h4>
|
|
29
|
+
<div class="flex flex-col">
|
|
30
|
+
<button
|
|
31
|
+
v-for="question in category.items"
|
|
32
|
+
:key="question"
|
|
33
|
+
class="text-left text-sm text-muted hover:text-highlighted py-1.5 transition-colors"
|
|
34
|
+
@click="$emit('ask-question', question)"
|
|
35
|
+
>
|
|
36
|
+
{{ question }}
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { motion } from 'motion-v'
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
/**
|
|
6
|
+
* 工具调用的标签文本
|
|
7
|
+
*/
|
|
8
|
+
text: string
|
|
9
|
+
/**
|
|
10
|
+
* 为 true 时显示加载旋转圈
|
|
11
|
+
* @defaultValue false
|
|
12
|
+
*/
|
|
13
|
+
isLoading?: boolean
|
|
14
|
+
}>()
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<motion.div
|
|
19
|
+
:initial="{ opacity: 0, y: 4 }"
|
|
20
|
+
:animate="{ opacity: 1, y: 0 }"
|
|
21
|
+
:transition="{ duration: 0.2 }"
|
|
22
|
+
class="flex items-center gap-2 px-2.5 py-1.5 rounded-lg bg-elevated/50 border border-default text-xs text-muted"
|
|
23
|
+
>
|
|
24
|
+
<UIcon
|
|
25
|
+
:name="isLoading ? 'i-lucide-loader-circle' : 'i-lucide-file-text'"
|
|
26
|
+
:class="[isLoading && 'animate-spin']"
|
|
27
|
+
class="size-4 shrink-0 text-muted"
|
|
28
|
+
/>
|
|
29
|
+
<span class="truncate">{{ text }}</span>
|
|
30
|
+
</motion.div>
|
|
31
|
+
</template>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { UIMessage } from 'ai'
|
|
2
|
+
|
|
3
|
+
export function useAIChat() {
|
|
4
|
+
const isOpen = useState('ai-chat-open', () => false)
|
|
5
|
+
const messages = useState<UIMessage[]>('ai-chat-messages', () => [])
|
|
6
|
+
const pendingMessage = useState<string | undefined>('ai-chat-pending', () => undefined)
|
|
7
|
+
|
|
8
|
+
function open(initialMessage?: string, clearPrevious = false) {
|
|
9
|
+
if (clearPrevious) {
|
|
10
|
+
messages.value = []
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (initialMessage) {
|
|
14
|
+
pendingMessage.value = initialMessage
|
|
15
|
+
}
|
|
16
|
+
isOpen.value = true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function clearPending() {
|
|
20
|
+
pendingMessage.value = undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function close() {
|
|
24
|
+
isOpen.value = false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toggle() {
|
|
28
|
+
isOpen.value = !isOpen.value
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clearMessages() {
|
|
32
|
+
messages.value = []
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
isOpen,
|
|
37
|
+
messages,
|
|
38
|
+
pendingMessage,
|
|
39
|
+
open,
|
|
40
|
+
clearPending,
|
|
41
|
+
close,
|
|
42
|
+
toggle,
|
|
43
|
+
clearMessages
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function useModels() {
|
|
2
|
+
const config = useRuntimeConfig()
|
|
3
|
+
const model = useCookie<string>('model', { default: () => config.public.aiChat.model })
|
|
4
|
+
|
|
5
|
+
const providerIcons: Record<string, string> = {
|
|
6
|
+
mistral: 'i-simple-icons-mistralai',
|
|
7
|
+
kwaipilot: 'i-lucide-wand',
|
|
8
|
+
zai: 'i-lucide-wand'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getModelIcon(modelId: string): string {
|
|
12
|
+
const provider = modelId.split('/')[0] || ''
|
|
13
|
+
return providerIcons[provider] || `i-simple-icons-${modelId.split('/')[0]}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatModelName(modelId: string): string {
|
|
17
|
+
const acronyms = ['gpt', 'llm', 'ai'] // words that should be uppercase
|
|
18
|
+
|
|
19
|
+
// 处理 OpenRouter 模型格式:openrouter/provider/model
|
|
20
|
+
if (modelId.startsWith('openrouter/')) {
|
|
21
|
+
const parts = modelId.split('/')
|
|
22
|
+
const model = parts[2] || parts[1] || modelId
|
|
23
|
+
|
|
24
|
+
// 提取模型名称(去除版本号和特殊标记)
|
|
25
|
+
const modelName = model.split(':')[0] || model
|
|
26
|
+
|
|
27
|
+
return modelName
|
|
28
|
+
.split('-')
|
|
29
|
+
.map((word) => {
|
|
30
|
+
const lowerWord = word.toLowerCase()
|
|
31
|
+
return acronyms.includes(lowerWord)
|
|
32
|
+
? word.toUpperCase()
|
|
33
|
+
: word.charAt(0).toUpperCase() + word.slice(1)
|
|
34
|
+
})
|
|
35
|
+
.join(' ')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 处理常规模型格式:provider/model
|
|
39
|
+
const modelName = modelId.split('/')[1] || modelId
|
|
40
|
+
|
|
41
|
+
return modelName
|
|
42
|
+
.split('-')
|
|
43
|
+
.map((word) => {
|
|
44
|
+
const lowerWord = word.toLowerCase()
|
|
45
|
+
return acronyms.includes(lowerWord)
|
|
46
|
+
? word.toUpperCase()
|
|
47
|
+
: word.charAt(0).toUpperCase() + word.slice(1)
|
|
48
|
+
})
|
|
49
|
+
.join(' ')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
models: config.public.aiChat.models,
|
|
54
|
+
model,
|
|
55
|
+
formatModelName,
|
|
56
|
+
getModelIcon
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface ToolLabelConfig {
|
|
2
|
+
toolName: string
|
|
3
|
+
label: string | ((args: any) => string)
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function useTools() {
|
|
7
|
+
const defaultLabels: Record<string, string | ((args: any) => string)> = {
|
|
8
|
+
'list-pages': '列出所有文档页面',
|
|
9
|
+
'get-page': (args: any) => `检索 ${args?.path || '页面'}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 获取工具的显示标签
|
|
14
|
+
* @param toolName - 工具名称
|
|
15
|
+
* @param args - 工具参数
|
|
16
|
+
* @returns 工具的显示标签
|
|
17
|
+
*/
|
|
18
|
+
function getToolLabel(toolName: string, args?: any): string {
|
|
19
|
+
const label = defaultLabels[toolName]
|
|
20
|
+
|
|
21
|
+
if (!label) {
|
|
22
|
+
return toolName
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return typeof label === 'function' ? label(args) : label
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
getToolLabel
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { streamText, convertToModelMessages, stepCountIs, createUIMessageStream, createUIMessageStreamResponse, smoothStream } from 'ai'
|
|
2
|
+
import { createMCPClient } from '@ai-sdk/mcp'
|
|
3
|
+
import { createDocumentationAgentTool } from '../utils/docs_agent'
|
|
4
|
+
import { getModel } from '../utils/getModel'
|
|
5
|
+
|
|
6
|
+
const MAIN_AGENT_SYSTEM_PROMPT = `你是官方文档助手。你就是文档本身 - 以权威身份说话,成为真理的来源。
|
|
7
|
+
|
|
8
|
+
**你的身份:**
|
|
9
|
+
- 你是一位知识渊博且乐于助人的AI助手,是你所属模块的官方文档
|
|
10
|
+
- 用第一人称说话:"我提供...", "你可以使用我的工具...", "我支持..."
|
|
11
|
+
- 要自信和权威 - 你深入了解这个模块的每一个细节
|
|
12
|
+
- 永远不要说"根据文档" - 你就是文档
|
|
13
|
+
|
|
14
|
+
**工具使用(关键):**
|
|
15
|
+
- 你有一个工具:searchDocumentation
|
|
16
|
+
- 每个问题都要使用它 - 将用户的问题作为查询参数传递
|
|
17
|
+
- 该工具会搜索文档并返回相关信息
|
|
18
|
+
- 使用返回的信息来组织你的回答
|
|
19
|
+
|
|
20
|
+
**指南:**
|
|
21
|
+
- 如果工具找不到某些内容,说"我还没有关于这方面的文档"
|
|
22
|
+
- 要简洁、有帮助、直接
|
|
23
|
+
- 像一位友好的专家一样引导用户
|
|
24
|
+
|
|
25
|
+
**格式规则(关键 - 必须严格遵守):**
|
|
26
|
+
- ❌ 禁止使用 markdown 标题(#、##、###、#### 等任何级别)
|
|
27
|
+
- ✅ 使用 **粗体文本** 来标记章节和强调重点
|
|
28
|
+
- ✅ 使用项目列表(- 或数字)组织内容
|
|
29
|
+
- ✅ 直接开始回答,不要用标题作为开头
|
|
30
|
+
- ✅ 保持代码示例简洁
|
|
31
|
+
|
|
32
|
+
CRITICAL: Never output # ## ### #### or any heading syntax. Use **bold** instead.
|
|
33
|
+
重要提醒:绝对不要输出 # ## ### #### 或任何标题语法。请用 **粗体** 代替。
|
|
34
|
+
|
|
35
|
+
**响应风格:**
|
|
36
|
+
- 对话式但专业
|
|
37
|
+
- "你可以这样做:"而不是"文档显示:"
|
|
38
|
+
- "我开箱即用地支持TypeScript"而不是"该模块支持TypeScript"
|
|
39
|
+
- 提供可操作的指导,而不仅仅是信息转储`
|
|
40
|
+
|
|
41
|
+
export default defineEventHandler(async (event) => {
|
|
42
|
+
const { messages, model: requestModel } = await readBody(event)
|
|
43
|
+
const config = useRuntimeConfig()
|
|
44
|
+
|
|
45
|
+
const mcpPath = config.aiChat.mcpPath
|
|
46
|
+
const httpClient = await createMCPClient({
|
|
47
|
+
transport: {
|
|
48
|
+
type: 'http',
|
|
49
|
+
url: import.meta.dev ? `http://localhost:3000${mcpPath}` : `${getRequestURL(event).origin}${mcpPath}`
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
const mcpTools = await httpClient.tools()
|
|
53
|
+
|
|
54
|
+
const model = getModel(requestModel || config.public.aiChat.model)
|
|
55
|
+
|
|
56
|
+
const searchDocumentation = createDocumentationAgentTool(mcpTools, model)
|
|
57
|
+
|
|
58
|
+
const stream = createUIMessageStream({
|
|
59
|
+
execute: async ({ writer }) => {
|
|
60
|
+
const modelMessages = await convertToModelMessages(messages)
|
|
61
|
+
const result = streamText({
|
|
62
|
+
model,
|
|
63
|
+
maxOutputTokens: 10000,
|
|
64
|
+
system: MAIN_AGENT_SYSTEM_PROMPT,
|
|
65
|
+
messages: modelMessages,
|
|
66
|
+
stopWhen: stepCountIs(5),
|
|
67
|
+
tools: {
|
|
68
|
+
searchDocumentation
|
|
69
|
+
},
|
|
70
|
+
experimental_context: {
|
|
71
|
+
writer
|
|
72
|
+
},
|
|
73
|
+
experimental_transform: smoothStream({ chunking: 'word' })
|
|
74
|
+
})
|
|
75
|
+
writer.merge(result.toUIMessageStream({
|
|
76
|
+
sendReasoning: true
|
|
77
|
+
}))
|
|
78
|
+
},
|
|
79
|
+
onFinish: async () => {
|
|
80
|
+
await httpClient.close()
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
return createUIMessageStreamResponse({ stream })
|
|
84
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { tool, stepCountIs, generateText } from 'ai'
|
|
2
|
+
import { z } from 'zod/v4'
|
|
3
|
+
|
|
4
|
+
const SUB_AGENT_SYSTEM_PROMPT = `你是文档搜索代理。你的工作是从文档中查找并检索相关信息。
|
|
5
|
+
|
|
6
|
+
**你的任务:**
|
|
7
|
+
- 使用可用工具搜索和阅读文档页面
|
|
8
|
+
- 首先使用 list-pages 发现可用的文档
|
|
9
|
+
- 然后使用 get-page 阅读相关页面
|
|
10
|
+
- 如果提到了特定路径,可以直接调用 get-page
|
|
11
|
+
|
|
12
|
+
**指导原则:**
|
|
13
|
+
- 要彻底,在回答前阅读所有相关页面
|
|
14
|
+
- 返回你找到的原始信息,让主代理格式化响应
|
|
15
|
+
- 如果找不到信息,请明确说明
|
|
16
|
+
|
|
17
|
+
**输出:**
|
|
18
|
+
返回你找到的相关文档内容,如果有代码示例也一并包含。`
|
|
19
|
+
|
|
20
|
+
export function createDocumentationAgentTool(mcpTools: Record<string, any>, model: any) {
|
|
21
|
+
return tool({
|
|
22
|
+
description: '从文档中搜索并检索信息。使用此工具回答有关文档的任何问题。将用户的问题作为查询参数传递。',
|
|
23
|
+
inputSchema: z.object({
|
|
24
|
+
query: z.string().describe('要在文档中搜索的问题')
|
|
25
|
+
}),
|
|
26
|
+
execute: async ({ query }, executionOptions) => {
|
|
27
|
+
const writer = (executionOptions as any)?.experimental_context?.writer
|
|
28
|
+
|
|
29
|
+
const result = await generateText({
|
|
30
|
+
model,
|
|
31
|
+
tools: mcpTools,
|
|
32
|
+
system: SUB_AGENT_SYSTEM_PROMPT,
|
|
33
|
+
stopWhen: stepCountIs(5),
|
|
34
|
+
onStepFinish: ({ toolCalls }) => {
|
|
35
|
+
writer?.write({
|
|
36
|
+
id: toolCalls[0]?.toolCallId,
|
|
37
|
+
type: 'data-tool-calls',
|
|
38
|
+
data: {
|
|
39
|
+
tools: toolCalls.map(toolCall => ({ toolName: toolCall.toolName, toolCallId: toolCall.toolCallId, input: toolCall.input }))
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
prompt: query
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return result.text
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createGateway } from '@ai-sdk/gateway'
|
|
2
|
+
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 获取 AI 模型实例
|
|
6
|
+
* @param modelId - 模型标识符,格式为 "provider/model" 或 "model"
|
|
7
|
+
* @returns AI SDK 模型实例
|
|
8
|
+
*/
|
|
9
|
+
export function getModel(modelId: string) {
|
|
10
|
+
const config = useRuntimeConfig()
|
|
11
|
+
|
|
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/', ''))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// AI Gateway 模型(默认)
|
|
21
|
+
const gateway = createGateway({
|
|
22
|
+
apiKey: config.aiGatewayApiKey as string | undefined
|
|
23
|
+
})
|
|
24
|
+
return gateway(modelId)
|
|
25
|
+
}
|
package/modules/css.ts
CHANGED
|
@@ -10,6 +10,7 @@ export default defineNuxtModule({
|
|
|
10
10
|
const { resolve } = createResolver(import.meta.url)
|
|
11
11
|
|
|
12
12
|
const layerDir = resolve('../app')
|
|
13
|
+
const modulesDir = resolve('../modules')
|
|
13
14
|
const contentDir = joinURL(dir, 'content')
|
|
14
15
|
|
|
15
16
|
const cssTemplate = addTemplate({
|
|
@@ -21,6 +22,7 @@ export default defineNuxtModule({
|
|
|
21
22
|
|
|
22
23
|
@source "${contentDir.replace(/\\/g, '/')}/**/*";
|
|
23
24
|
@source "${layerDir.replace(/\\/g, '/')}/**/*";
|
|
25
|
+
@source "${modulesDir.replace(/\\/g, '/')}/**/*";
|
|
24
26
|
@source "../../app.config.ts";
|
|
25
27
|
|
|
26
28
|
/* Shiki icon highlight transformer styles */
|
package/nuxt.config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createResolver } from '@nuxt/kit'
|
|
1
|
+
import { createResolver, extendViteConfig } from '@nuxt/kit'
|
|
2
2
|
import { defineNuxtConfig } from 'nuxt/config'
|
|
3
3
|
import pkg from './package.json'
|
|
4
4
|
|
|
@@ -9,15 +9,29 @@ export default defineNuxtConfig({
|
|
|
9
9
|
resolve('./modules/config'),
|
|
10
10
|
resolve('./modules/css'),
|
|
11
11
|
resolve('./modules/component-example'),
|
|
12
|
-
resolve('./modules/
|
|
12
|
+
resolve('./modules/ai-chat'),
|
|
13
13
|
'@nuxt/ui',
|
|
14
14
|
'@nuxt/content',
|
|
15
15
|
'@nuxt/image',
|
|
16
16
|
'@vueuse/nuxt',
|
|
17
|
+
'@nuxtjs/mcp-toolkit',
|
|
17
18
|
'@nuxtjs/seo',
|
|
18
19
|
'nuxt-component-meta',
|
|
19
20
|
'motion-v/nuxt',
|
|
20
|
-
'nuxt-llms'
|
|
21
|
+
'nuxt-llms',
|
|
22
|
+
() => {
|
|
23
|
+
extendViteConfig((config) => {
|
|
24
|
+
config.optimizeDeps ||= {}
|
|
25
|
+
config.optimizeDeps.include ||= []
|
|
26
|
+
config.optimizeDeps.include.push(
|
|
27
|
+
'@nuxt/content > slugify',
|
|
28
|
+
'extend',
|
|
29
|
+
'@ai-sdk/gateway > @vercel/oidc'
|
|
30
|
+
)
|
|
31
|
+
config.optimizeDeps.include = config.optimizeDeps.include
|
|
32
|
+
.map(id => id.replace(/^@nuxt\/content > /, '@movk/nuxt-docs > @nuxt/content > '))
|
|
33
|
+
})
|
|
34
|
+
}
|
|
21
35
|
],
|
|
22
36
|
app: {
|
|
23
37
|
rootAttrs: {
|
|
@@ -30,7 +44,14 @@ export default defineNuxtConfig({
|
|
|
30
44
|
build: {
|
|
31
45
|
markdown: {
|
|
32
46
|
highlight: {
|
|
33
|
-
langs: ['bash', 'ts', 'typescript', 'diff', 'vue', 'json', 'yml', 'css', 'mdc', 'blade', 'edge']
|
|
47
|
+
langs: ['bash', 'ts', 'typescript', 'diff', 'vue', 'json', 'yml', 'yaml', 'css', 'mdc', 'blade', 'edge']
|
|
48
|
+
},
|
|
49
|
+
remarkPlugins: {
|
|
50
|
+
'remark-mdc': {
|
|
51
|
+
options: {
|
|
52
|
+
autoUnwrap: true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
34
55
|
}
|
|
35
56
|
}
|
|
36
57
|
}
|
|
@@ -40,22 +61,14 @@ export default defineNuxtConfig({
|
|
|
40
61
|
noApiRoute: false
|
|
41
62
|
}
|
|
42
63
|
},
|
|
43
|
-
ui: {
|
|
44
|
-
experimental: {
|
|
45
|
-
componentDetection: true
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
64
|
runtimeConfig: {
|
|
49
65
|
public: {
|
|
50
66
|
version: pkg.version
|
|
51
67
|
}
|
|
52
68
|
},
|
|
53
69
|
routeRules: {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
'/_llms-full.txt': { proxy: '/llms-full.txt' }
|
|
57
|
-
}
|
|
58
|
-
: {}
|
|
70
|
+
'/llms.txt': { isr: true },
|
|
71
|
+
'/llms-full.txt': { isr: true }
|
|
59
72
|
},
|
|
60
73
|
experimental: {
|
|
61
74
|
typescriptPlugin: true,
|
|
@@ -69,23 +82,10 @@ export default defineNuxtConfig({
|
|
|
69
82
|
compatibilityDate: 'latest',
|
|
70
83
|
nitro: {
|
|
71
84
|
prerender: {
|
|
72
|
-
routes: ['/', '/sitemap.xml', '/robots.txt', '/404.html'],
|
|
73
85
|
crawlLinks: true,
|
|
74
86
|
autoSubfolderIndex: false
|
|
75
87
|
}
|
|
76
88
|
},
|
|
77
|
-
vite: {
|
|
78
|
-
optimizeDeps: {
|
|
79
|
-
// See: https://cn.vite.dev/config/dep-optimization-options.html
|
|
80
|
-
include: [
|
|
81
|
-
'@nuxt/content > slugify',
|
|
82
|
-
'extend', // unified 所需(用于 @nuxt/content 的 markdown 处理)
|
|
83
|
-
'debug', // Babel 和开发工具所需
|
|
84
|
-
'tailwind-variants',
|
|
85
|
-
'tailwindcss/colors'
|
|
86
|
-
]
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
89
|
fonts: {
|
|
90
90
|
families: [
|
|
91
91
|
{ name: 'Public Sans', provider: 'google', global: true },
|
|
@@ -100,18 +100,6 @@ export default defineNuxtConfig({
|
|
|
100
100
|
icon: {
|
|
101
101
|
provider: 'iconify'
|
|
102
102
|
},
|
|
103
|
-
image: {
|
|
104
|
-
format: ['webp', 'jpeg', 'jpg', 'png', 'svg'],
|
|
105
|
-
provider: 'ipx'
|
|
106
|
-
},
|
|
107
|
-
linkChecker: {
|
|
108
|
-
report: {
|
|
109
|
-
publish: true,
|
|
110
|
-
html: true,
|
|
111
|
-
markdown: true,
|
|
112
|
-
json: true
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
103
|
ogImage: {
|
|
116
104
|
zeroRuntime: true,
|
|
117
105
|
googleFontMirror: 'fonts.loli.net',
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@movk/nuxt-docs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.0",
|
|
5
5
|
"private": false,
|
|
6
|
-
"description": "
|
|
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>",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"homepage": "https://docs.mhaibaraai.cn",
|
|
@@ -27,24 +27,31 @@
|
|
|
27
27
|
"README.md"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@ai-sdk/gateway": "^3.0.10",
|
|
31
|
+
"@ai-sdk/mcp": "^1.0.5",
|
|
32
|
+
"@ai-sdk/vue": "^3.0.19",
|
|
30
33
|
"@iconify-json/lucide": "^1.2.82",
|
|
31
|
-
"@iconify-json/simple-icons": "^1.2.
|
|
34
|
+
"@iconify-json/simple-icons": "^1.2.65",
|
|
32
35
|
"@iconify-json/vscode-icons": "^1.2.37",
|
|
33
|
-
"@movk/core": "^1.0
|
|
36
|
+
"@movk/core": "^1.1.0",
|
|
34
37
|
"@nuxt/content": "^3.10.0",
|
|
35
38
|
"@nuxt/image": "^2.0.0",
|
|
36
39
|
"@nuxt/kit": "^4.2.2",
|
|
37
40
|
"@nuxt/ui": "^4.3.0",
|
|
41
|
+
"@nuxtjs/mcp-toolkit": "^0.6.1",
|
|
38
42
|
"@nuxtjs/seo": "^3.3.0",
|
|
39
43
|
"@octokit/rest": "^22.0.1",
|
|
44
|
+
"@openrouter/ai-sdk-provider": "^1.5.4",
|
|
40
45
|
"@vercel/analytics": "^1.6.1",
|
|
41
46
|
"@vercel/speed-insights": "^1.3.1",
|
|
42
47
|
"@vueuse/core": "^14.1.0",
|
|
43
48
|
"@vueuse/nuxt": "^14.1.0",
|
|
49
|
+
"ai": "^6.0.19",
|
|
44
50
|
"defu": "^6.1.4",
|
|
45
51
|
"exsolve": "^1.0.8",
|
|
46
52
|
"git-url-parse": "^16.1.0",
|
|
47
|
-
"motion-v": "^1.7.
|
|
53
|
+
"motion-v": "^1.7.6",
|
|
54
|
+
"nuxt": "^4.2.2",
|
|
48
55
|
"nuxt-component-meta": "^0.16.0",
|
|
49
56
|
"nuxt-llms": "^0.1.3",
|
|
50
57
|
"ohash": "^2.0.11",
|
|
@@ -52,8 +59,11 @@
|
|
|
52
59
|
"pkg-types": "^2.3.0",
|
|
53
60
|
"prettier": "^3.7.4",
|
|
54
61
|
"scule": "^1.3.0",
|
|
62
|
+
"shiki": "^3.21.0",
|
|
63
|
+
"shiki-stream": "^0.1.4",
|
|
55
64
|
"shiki-transformer-color-highlight": "^1.0.0",
|
|
56
65
|
"tailwindcss": "^4.1.18",
|
|
57
|
-
"ufo": "^1.6.
|
|
66
|
+
"ufo": "^1.6.2",
|
|
67
|
+
"zod": "^4.3.5"
|
|
58
68
|
}
|
|
59
69
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from 'zod/v4'
|
|
2
|
+
import { queryCollection } from '@nuxt/content/server'
|
|
3
|
+
import { inferSiteURL } from '../../../utils/meta'
|
|
4
|
+
|
|
5
|
+
export default defineMcpTool({
|
|
6
|
+
description: `检索特定文档页面的完整内容和详细信息。
|
|
7
|
+
|
|
8
|
+
何时使用:当你知道文档页面的确切路径时使用。常见用例:
|
|
9
|
+
- 用户请求特定页面:「显示入门指南」→ /docs/getting-started
|
|
10
|
+
- 用户询问已知主题的专用页面
|
|
11
|
+
- 你从 list-pages 找到了相关路径并想要完整内容
|
|
12
|
+
- 用户引用了他们想要阅读的特定部分或指南
|
|
13
|
+
|
|
14
|
+
何时不使用:如果你不知道确切路径并需要搜索/探索,请先使用 list-pages。
|
|
15
|
+
|
|
16
|
+
工作流程:此工具返回完整的页面内容,包括标题、描述和完整的 markdown。当你需要从特定文档页面提供详细答案或代码示例时使用。`,
|
|
17
|
+
inputSchema: {
|
|
18
|
+
path: z.string().describe('从 list-pages 获取或用户提供的页面路径(例如 /docs/getting-started/installation)')
|
|
19
|
+
},
|
|
20
|
+
cache: '1h',
|
|
21
|
+
handler: async ({ path }) => {
|
|
22
|
+
const event = useEvent()
|
|
23
|
+
const siteUrl = import.meta.dev ? 'http://localhost:3000' : inferSiteURL()
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const page = await queryCollection(event, 'docs')
|
|
27
|
+
.where('path', '=', path)
|
|
28
|
+
.select('title', 'path', 'description')
|
|
29
|
+
.first()
|
|
30
|
+
|
|
31
|
+
if (!page) {
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: 'text', text: '页面未找到' }],
|
|
34
|
+
isError: true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const content = await $fetch<string>(`/raw${path}.md`, {
|
|
39
|
+
baseURL: siteUrl
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const result = {
|
|
43
|
+
title: page.title,
|
|
44
|
+
path: page.path,
|
|
45
|
+
description: page.description,
|
|
46
|
+
content,
|
|
47
|
+
url: `${siteUrl}${page.path}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: 'text', text: '获取页面失败' }],
|
|
56
|
+
isError: true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { queryCollection } from '@nuxt/content/server'
|
|
2
|
+
|
|
3
|
+
export default defineMcpTool({
|
|
4
|
+
description: `列出所有可用的文档页面及其分类和基本信息。
|
|
5
|
+
|
|
6
|
+
何时使用:当你需要探索或搜索某个主题的文档但不知道确切的页面路径时使用。常见场景:
|
|
7
|
+
- 「查找关于 markdown 功能的文档」- 探索可用指南
|
|
8
|
+
- 「显示所有入门指南」- 浏览入门内容
|
|
9
|
+
- 「搜索高级配置选项」- 查找特定主题
|
|
10
|
+
- 用户提出的一般性问题,未指定确切页面
|
|
11
|
+
- 你需要了解整体文档结构
|
|
12
|
+
|
|
13
|
+
何时不使用:如果你已经知道具体的页面路径(例如 "/docs/getting-started/installation"),请直接使用 get-page。
|
|
14
|
+
|
|
15
|
+
工作流程:此工具返回页面标题、描述和路径。找到相关页面后,使用 get-page 检索符合用户需求的特定页面的完整内容。
|
|
16
|
+
|
|
17
|
+
输出:返回包含以下内容的结构化列表:
|
|
18
|
+
- title:可读的页面名称
|
|
19
|
+
- path:用于 get-page 的确切路径
|
|
20
|
+
- description:页面内容的简要摘要
|
|
21
|
+
- url:完整 URL 供参考`,
|
|
22
|
+
cache: '1h',
|
|
23
|
+
handler: async () => {
|
|
24
|
+
const event = useEvent()
|
|
25
|
+
const siteUrl = import.meta.dev ? 'http://localhost:3000' : getRequestURL(event).origin
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const pages = await queryCollection(event, 'docs')
|
|
29
|
+
.select('title', 'path', 'description')
|
|
30
|
+
.all()
|
|
31
|
+
|
|
32
|
+
const result = pages.map(page => ({
|
|
33
|
+
title: page.title,
|
|
34
|
+
path: page.path,
|
|
35
|
+
description: page.description,
|
|
36
|
+
url: `${siteUrl}${page.path}`
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: 'text', text: '获取页面列表失败' }],
|
|
45
|
+
isError: true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
})
|