Skip to content

Commit 12b6384

Browse files
authored
fix: ai plugin and mcp issue (opentiny#1586)
* fix(ai): fix ai plugin mcp issue
1 parent 27195a0 commit 12b6384

9 files changed

Lines changed: 281 additions & 161 deletions

File tree

packages/plugins/page/src/mcp/tools/editSpecificPage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const inputSchema = z.object({
66
})
77

88
export const editSpecificPage = {
9-
name: 'Edit_page_in_canvas.',
9+
name: 'edit_page_in_canvas',
1010
title: '在画布中编辑页面',
1111
order: 9,
1212
description: 'Edit a specific page in canvas. Use this tool when you need to edit some page in canvas.',
70.5 KB
Loading

packages/plugins/robot/src/Main.vue

Lines changed: 107 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,27 @@
3636
<icon-new-session />
3737
</button>
3838
</template>
39-
<div v-if="activeMessages.length === 0">
40-
<tr-welcome title="AI助手" description="您好,我是您的开发小助手" :icon="welcomeIcon" class="robot-welcome">
41-
</tr-welcome>
42-
<tr-prompts
43-
:items="promptItems"
44-
:wrap="true"
45-
item-class="prompt-item"
46-
class="tiny-prompts"
47-
@item-click="handlePromptItemClick"
48-
></tr-prompts>
39+
<div class="robot-chat-container-content" ref="chatContainerRef">
40+
<div v-if="activeMessages.length === 0">
41+
<tr-welcome
42+
title="AI助手"
43+
description="您好,我是您的开发小助手"
44+
:icon="welcomeIcon"
45+
class="robot-welcome"
46+
>
47+
</tr-welcome>
48+
<tr-prompts
49+
:items="promptItems"
50+
:wrap="true"
51+
item-class="prompt-item"
52+
class="tiny-prompts"
53+
@item-click="handlePromptItemClick"
54+
></tr-prompts>
55+
</div>
56+
<tr-bubble-provider :content-renderers="contentRenderers" v-else>
57+
<tr-bubble-list :items="activeMessages" :roles="roles" autoScroll></tr-bubble-list>
58+
</tr-bubble-provider>
4959
</div>
50-
<tr-bubble-provider :message-renderers="{ markdown: MarkdownRenderer }" v-else>
51-
<tr-bubble-list :items="activeMessages" :roles="roles" autoScroll></tr-bubble-list>
52-
</tr-bubble-provider>
5360
<template #footer>
5461
<tr-sender
5562
:maxlength="4000"
@@ -61,7 +68,7 @@
6168
@submit="sendContent(inputContent, false)"
6269
>
6370
<template #footer-left>
64-
<mcp-server></mcp-server>
71+
<mcp-server :position="mcpDrawerPosition"></mcp-server>
6572
</template>
6673
</tr-sender>
6774
</template>
@@ -73,7 +80,18 @@
7380

7481
<script lang="ts">
7582
/* metaService: engine.plugins.robot.Main */
76-
import { ref, onMounted, watchEffect, type CSSProperties, h, resolveComponent } from 'vue'
83+
import {
84+
ref,
85+
onMounted,
86+
watchEffect,
87+
type CSSProperties,
88+
h,
89+
resolveComponent,
90+
computed,
91+
watch,
92+
nextTick,
93+
type Component
94+
} from 'vue'
7795
import { Notify, Loading, TinyPopover } from '@opentiny/vue'
7896
import { useCanvas, useHistory, usePage, useModal, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
7997
import { extend } from '@opentiny/vue-renderless/common/object'
@@ -85,6 +103,7 @@ import { getBlockContent, initBlockList, getAIModelOptions } from './js/robotSet
85103
import McpServer from './mcp/McpServer.vue'
86104
import useMcpServer from './mcp/useMcp'
87105
import MarkdownRenderer from './mcp/MarkdownRenderer.vue'
106+
import LoadingRenderer from './mcp/LoadingRenderer.vue'
88107
import { sendMcpRequest } from './mcp/utils'
89108
90109
export default {
@@ -118,6 +137,8 @@ export default {
118137
const tokenValue = ref('')
119138
const showPopover = ref(false)
120139
140+
const chatContainerRef = ref(null)
141+
121142
const { pageSettingState, getDefaultPage } = usePage()
122143
const ROOT_ID = pageSettingState.ROOT_ID
123144
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))
@@ -142,6 +163,14 @@ export default {
142163
)
143164
}
144165
166+
const scrollContent = async () => {
167+
await nextTick()
168+
const el = chatContainerRef.value as HTMLElement | null
169+
if (el) {
170+
el.scrollTop = el.scrollHeight
171+
}
172+
}
173+
145174
const createNewPage = (schema) => {
146175
if (!(pageSettingState.isNew && pageSettingState.isAIPage)) {
147176
pageSettingState.isNew = true
@@ -188,8 +217,7 @@ export default {
188217
189218
const getAiRespMessage = (role = 'assistant', content) => ({
190219
role,
191-
content,
192-
name: 'AI'
220+
content
193221
})
194222
195223
const requestLoading = ref(false)
@@ -198,6 +226,7 @@ export default {
198226
if (useMcpServer().isToolsEnabled) {
199227
try {
200228
requestLoading.value = true
229+
await scrollContent()
201230
await sendMcpRequest(messages.value, {
202231
model: selectedModel.value.value,
203232
headers: {
@@ -209,6 +238,7 @@ export default {
209238
} finally {
210239
inProcesing.value = false
211240
requestLoading.value = false
241+
await scrollContent()
212242
}
213243
return
214244
}
@@ -236,14 +266,6 @@ export default {
236266
})
237267
}
238268
239-
const scrollContent = async () => {
240-
await sleep(100)
241-
const scrollElement = document.getElementById('chatgpt-window')
242-
if (scrollElement) {
243-
scrollElement.scrollTop = scrollElement.scrollHeight
244-
}
245-
}
246-
247269
const resetContent = async () => {
248270
activeMessages.value = messages.value
249271
await scrollContent()
@@ -257,8 +279,7 @@ export default {
257279
258280
const getMessage = (content) => ({
259281
role: 'user',
260-
content,
261-
name: 'John'
282+
content
262283
})
263284
264285
const sendContent = async (content, isModel) => {
@@ -412,11 +433,38 @@ export default {
412433
413434
// 对话角色配置
414435
const roles: Record<string, BubbleRoleConfig> = {
415-
assistant: { placement: 'start', avatar: aiAvatar, maxWidth: '90%', contentRenderer: MarkdownRenderer },
436+
assistant: {
437+
placement: 'start',
438+
avatar: aiAvatar,
439+
maxWidth: '90%',
440+
contentRenderer: MarkdownRenderer,
441+
customContentField: 'renderContent'
442+
},
416443
user: { placement: 'end', avatar: userAvatar, maxWidth: '90%', contentRenderer: MarkdownRenderer }
417444
}
418445
446+
watch([() => activeMessages.value.length, () => activeMessages.value.at(-1)?.renderContent?.length ?? 0], () => {
447+
scrollContent()
448+
})
449+
450+
const contentRenderers: Record<string, Component> = {
451+
markdown: MarkdownRenderer,
452+
loading: LoadingRenderer
453+
}
454+
455+
const mcpDrawerPosition = computed(() => {
456+
return {
457+
type: 'fixed',
458+
position: {
459+
top: 'var(--base-top-panel-height)',
460+
bottom: 0,
461+
...(fullscreen.value ? { left: 0 } : { right: 'var(--tr-container-width)' })
462+
}
463+
}
464+
})
465+
419466
return {
467+
chatContainerRef,
420468
robotVisible,
421469
avatarUrl,
422470
chatWindowOpened,
@@ -439,8 +487,9 @@ export default {
439487
handlePromptItemClick,
440488
welcomeIcon,
441489
roles,
442-
MarkdownRenderer,
443-
requestLoading
490+
contentRenderers,
491+
requestLoading,
492+
mcpDrawerPosition
444493
}
445494
}
446495
}
@@ -486,10 +535,38 @@ export default {
486535
}
487536
488537
.tr-bubble-list {
538+
font-size: 14px;
489539
flex: 1;
490540
.tr-bubble {
491541
word-break: break-word;
492542
}
543+
ul,
544+
ol {
545+
padding-left: 10px;
546+
}
547+
ul > li {
548+
list-style: disc;
549+
}
550+
ol > li {
551+
list-style: decimal;
552+
}
553+
table {
554+
border-collapse: collapse; // 合并边框
555+
border: 1px solid #ccc;
556+
width: 100%;
557+
margin: 1rem 0;
558+
th,
559+
td {
560+
border: 1px solid #ccc; /* 单元格边框 */
561+
padding: 8px;
562+
}
563+
tr:nth-child(even) {
564+
background-color: #f2f2f2;
565+
}
566+
tr:hover {
567+
background-color: #e6f7ff;
568+
}
569+
}
493570
}
494571
495572
.robot-welcome > div {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<img src="../../assets/loading.webp" alt="loading" style="width: 24px; height: 24px" />
3+
</template>

packages/plugins/robot/src/mcp/MarkdownRenderer.vue

Lines changed: 85 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,109 @@
11
<template>
2-
<div v-html="renderContent" class="robot-markdown-renderer"></div>
2+
<div v-html="renderContent" class="markdown-renderer" :class="themeClass"></div>
33
</template>
44

55
<script setup lang="ts">
6-
import DOMPurify from 'dompurify'
7-
import MarkdownIt, { type Options as MarkdownItOptions } from 'markdown-it'
8-
import hljs from 'highlight.js'
96
import { computed } from 'vue'
10-
import 'highlight.js/styles/github.min.css'
7+
import DOMPurify from 'dompurify'
8+
import MarkdownIt, { type Options } from 'markdown-it'
9+
import hljs from 'highlight.js/lib/core'
10+
import 'highlight.js/styles/github.css'
11+
12+
// 按需加载语言
13+
import bash from 'highlight.js/lib/languages/bash'
14+
import javascript from 'highlight.js/lib/languages/javascript'
15+
import typescript from 'highlight.js/lib/languages/typescript'
16+
import json from 'highlight.js/lib/languages/json'
17+
import yaml from 'highlight.js/lib/languages/yaml'
18+
import xml from 'highlight.js/lib/languages/xml'
19+
import shell from 'highlight.js/lib/languages/shell'
20+
21+
// 注册语言
22+
hljs.registerLanguage('bash', bash)
23+
hljs.registerLanguage('javascript', javascript)
24+
hljs.registerLanguage('typescript', typescript)
25+
hljs.registerLanguage('json', json)
26+
hljs.registerLanguage('yaml', yaml)
27+
hljs.registerLanguage('xml', xml)
28+
hljs.registerLanguage('shell', shell)
29+
30+
const props = defineProps({
31+
content: {
32+
type: String,
33+
required: true
34+
},
35+
theme: {
36+
type: String as () => 'light' | 'dark',
37+
default: 'light'
38+
},
39+
options: {
40+
type: Object as () => Options,
41+
default: () => ({})
42+
}
43+
})
1144
12-
const defaultMarkdownItOptions = {
45+
const themeClass = computed(() => `hljs-theme-${props.theme}`)
46+
47+
const escapeHtml = (s: string) =>
48+
s.replace(/[&<>"']/g, (ch) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch]!))
49+
50+
const markdownIt = new MarkdownIt({
51+
html: true,
1352
breaks: true,
1453
typographer: true,
15-
highlight: function (str: string, lang: string) {
54+
highlight: (str: string, lang: string) => {
1655
if (lang && hljs.getLanguage(lang)) {
1756
try {
18-
return hljs.highlightAuto(str).value
19-
} catch (__) {
57+
const { value } = hljs.highlight(str, { language: lang, ignoreIllegals: true })
58+
return `<pre class="hljs"><code class="language-${lang}">${value}</code></pre>`
59+
} catch (e) {
2060
/* ignore */
2161
}
2262
}
23-
24-
return str
25-
}
26-
}
27-
28-
const props = defineProps<{
29-
type?: 'markdown' | 'text'
30-
content: string
31-
options?: MarkdownItOptions
32-
}>()
33-
34-
const markdownIt = computed(
35-
() =>
36-
new MarkdownIt({
37-
...defaultMarkdownItOptions,
38-
...props.options
39-
})
40-
)
63+
return `<pre class="hljs"><code>${escapeHtml(str)}</code></pre>`
64+
},
65+
...props.options
66+
})
4167
4268
const renderContent = computed(() => {
43-
const htmlContent = markdownIt.value.render(props.content)
44-
return DOMPurify.sanitize(htmlContent)
69+
return DOMPurify.sanitize(markdownIt.render(props.content))
4570
})
4671
</script>
4772

48-
<style lang="less" scoped>
49-
.robot-markdown-renderer {
73+
<style lang="less">
74+
.markdown-renderer {
5075
word-break: break-word;
51-
& > *:first-child {
76+
77+
pre {
78+
border-radius: 6px;
79+
padding: 1em;
80+
overflow: auto;
81+
line-height: 1.45;
82+
> code {
83+
font-size: 12px;
84+
}
85+
}
86+
87+
// TODO: 适配TInyEngine主题,实现跟随主题自动切换
88+
/* 亮色主题 */
89+
&.hljs-theme-light {
90+
pre {
91+
background-color: #f6f8fa;
92+
}
93+
}
94+
95+
/* 暗色主题 */
96+
&.hljs-theme-dark {
97+
pre {
98+
background-color: #0d1117;
99+
}
100+
}
101+
102+
> *:first-child {
52103
margin-top: 0 !important;
53104
}
54-
& > *:last-child {
105+
106+
> *:last-child {
55107
margin-bottom: 0 !important;
56108
}
57109
}

0 commit comments

Comments
 (0)