fix(site): add MCP tools for query examples and jump examples (#3623)

This commit is contained in:
申君健 2025-07-30 10:20:15 +08:00 committed by GitHub
parent 41b9fbaade
commit 96cd780f26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 88 additions and 13 deletions

View File

@ -10,7 +10,7 @@
<div class="right-panel" :class="{ collapsed: !showTinyRobot }"> <div class="right-panel" :class="{ collapsed: !showTinyRobot }">
<tiny-robot-chat /> <tiny-robot-chat />
</div> </div>
<IconAi @click="handleShowTinyRobot" class="style-settings-icon"></IconAi> <IconAi v-show="!showTinyRobot" @click="handleShowTinyRobot" class="style-settings-icon"></IconAi>
<tiny-dialog-box <tiny-dialog-box
v-model:visible="boxVisibility" v-model:visible="boxVisibility"
:close-on-click-modal="false" :close-on-click-modal="false"
@ -136,7 +136,7 @@ const handleShowTinyRobot = () => {
padding: 34px 0 0; padding: 34px 0 0;
} }
} }
.right-panel { .right-panel:not(.collapsed) {
:deep(.tr-container) { :deep(.tr-container) {
z-index: 9999; z-index: 9999;
} }

View File

@ -3,7 +3,7 @@
<tr-container v-model:show="showTinyRobot" v-model:fullscreen="fullscreen"> <tr-container v-model:show="showTinyRobot" v-model:fullscreen="fullscreen">
<tr-bubble-provider :message-renderers="messageRenderers"> <tr-bubble-provider :message-renderers="messageRenderers">
<div v-if="showMessages.length === 0"> <div class="robot-top-msg" v-if="showMessages.length === 0">
<tr-welcome title="智能助手" description="您好我是Opentiny AI智能助手" :icon="welcomeIcon"> <tr-welcome title="智能助手" description="您好我是Opentiny AI智能助手" :icon="welcomeIcon">
<template #footer> <template #footer>
<div class="welcome-footer"></div> <div class="welcome-footer"></div>
@ -17,7 +17,8 @@
@item-click="handlePromptItemClick" @item-click="handlePromptItemClick"
></tr-prompts> ></tr-prompts>
</div> </div>
<tr-bubble-list v-else :items="showMessages" :roles="roles" auto-scroll> </tr-bubble-list> <tr-bubble-list v-else class="robot-top-msg markdown-body" :items="showMessages" :roles="roles" auto-scroll>
</tr-bubble-list>
</tr-bubble-provider> </tr-bubble-provider>
<template #footer> <template #footer>
@ -172,6 +173,11 @@ watch(() => messages.value[messages.value.length - 1]?.content, scrollToBottom)
height: 600px; height: 600px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
} }
/** 聊天顶部要撑开空间 */
.robot-top-msg {
flex: 1;
}
</style> </style>
<style> <style>

View File

@ -22,8 +22,13 @@ export const useTinyRobot = () => {
const promptItems = [ const promptItems = [
{ {
label: '智能操作网页', label: '快速跳到文档',
description: '帮我选中最贵的手机商品', description: '帮我切换到国际化指南',
icon: h('span', { style: { fontSize: '18px' } }, '🕹')
},
{
label: '快速跳到组件',
description: '帮我切换到 Select 组件',
icon: h('span', { style: { fontSize: '18px' } }, '🕹') icon: h('span', { style: { fontSize: '18px' } }, '🕹')
} }
] ]
@ -54,14 +59,24 @@ export const useTinyRobot = () => {
const suggestionPillItems = [ const suggestionPillItems = [
{ {
id: '1', id: '1',
text: '帮我选中最贵的手机商品', text: 'Select',
icon: h('span', { style: { fontSize: '18px' } }, '🕹') icon: h('span', { style: { fontSize: '18px' } }, '🧩')
},
{
id: '1',
text: '表格',
icon: h('span', { style: { fontSize: '18px' } }, '🧩')
},
{
id: '1',
text: '树组件',
icon: h('span', { style: { fontSize: '18px' } }, '🧩')
} }
] ]
function handleSuggestionPillItemClick(item: SuggestionItem) { function handleSuggestionPillItemClick(item: SuggestionItem) {
if (item.id === '1') { if (item.id === '1') {
let templateText = `请对 [目标组件] ,执行 [操作]` let templateText = `帮我跳转到 [目标组件]`
let currentInitialValue = { 目标组件: item.text, : '' } let currentInitialValue = { 目标组件: item.text, : '' }
if (senderRef.value) { if (senderRef.value) {

View File

@ -2,6 +2,9 @@ import { genMenus } from '../menus'
import { z } from 'zod' import { z } from 'zod'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
// 组件页面的右上导航的数据回调函数
export const cmpAnchorDataCallback = { value: null }
export const createGlobalMcpTool = (server) => { export const createGlobalMcpTool = (server) => {
const router = useRouter() const router = useRouter()
server.registerResource( server.registerResource(
@ -26,11 +29,13 @@ export const createGlobalMcpTool = (server) => {
'swtich-router', 'swtich-router',
{ {
title: 'router', title: 'router',
description: '可以帮用户跳转页面比如跳转到组件文档页面和API文档页面', description: '可以帮用户跳转到文档页面组件示例的总页面或组件API文档页面或组件库的概览页面',
inputSchema: { inputSchema: {
key: z.string().describe('跳转页面路径'), key: z.string().describe('跳转页面路径'),
type: z.enum(['components', 'docs', 'overview', 'features']).describe('跳转页面类型'), type: z
isOpenApi: z.boolean().describe('是否打开API文档') .enum(['components', 'docs', 'overview', 'features'])
.describe('跳转页面类型,比如:组件的页面,文档的页面,组件的概览页面'),
isOpenApi: z.boolean().describe('跳转到组件页面时是否打开API文档')
} }
}, },
async ({ key, type, isOpenApi }) => { async ({ key, type, isOpenApi }) => {
@ -51,6 +56,49 @@ export const createGlobalMcpTool = (server) => {
} }
) )
server.registerTool(
'get-component-demos',
{
title: '查询全部示例的信息',
description:
'查询当前组件的全部示例信息demos信息。返回值是一个数组其中每一项的 demoId 属性是示例的键通过键可以跳转到该示例。desc属性是示例的详细描述。',
inputSchema: {}
},
async () => {
// 通知组件页面返回右侧导航的数据
if (cmpAnchorDataCallback.value != null) {
const links = cmpAnchorDataCallback.value()
return {
content: [{ type: 'text', text: JSON.stringify(links) }]
}
} else {
return {
content: [{ type: 'text', text: '找不到示例' }]
}
}
}
)
server.registerTool(
'jump-to-demo',
{
title: '跳转到组件的示例demo',
description: '根据参数demoId, 跳转到指定的示例demo。',
inputSchema: {
demoId: z.string().describe('示例的id,唯一标识。')
}
},
async ({ demoId }) => {
// 通知组件页面返回右侧导航的数据
location.hash = '#' + demoId
return {
content: [{ type: 'text', text: '跳转示例成功' }]
}
}
)
// 长任务示例 // 长任务示例
server.registerTool( server.registerTool(
'long-task', 'long-task',

View File

@ -91,7 +91,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { reactive, computed, watch, onMounted, nextTick, ref } from 'vue' import { reactive, computed, watch, onMounted, nextTick, ref, onUnmounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { TinyTabs, TinyTabItem } from '@opentiny/vue' import { TinyTabs, TinyTabItem } from '@opentiny/vue'
import { debounce } from '@opentiny/utils' import { debounce } from '@opentiny/utils'
@ -106,6 +106,7 @@ import ApiDocs from '../../components/api-docs.vue'
import McpDocs from '../../components/mcp-docs.vue' import McpDocs from '../../components/mcp-docs.vue'
import useTasksFinish from '../../composable/useTasksFinish' import useTasksFinish from '../../composable/useTasksFinish'
import { appData } from '../../tools/appData' import { appData } from '../../tools/appData'
import { cmpAnchorDataCallback } from '../../tools/globalMcpTool'
const props = defineProps({ loadData: {}, appMode: {}, demoKey: {} }) const props = defineProps({ loadData: {}, appMode: {}, demoKey: {} })
@ -442,6 +443,11 @@ const handleAnchorClick = (e, data) => {
} }
} }
cmpAnchorDataCallback.value = () => state.currJson.demos
onUnmounted(() => {
cmpAnchorDataCallback.value = null
})
defineExpose({ loadPage }) defineExpose({ loadPage })
</script> </script>