From ee5c054b98fd25365c020db3411e720726a945d1 Mon Sep 17 00:00:00 2001 From: hexqi Date: Wed, 30 Apr 2025 08:27:33 +0800 Subject: [PATCH 1/6] feat: replace ui with tiny-robot --- packages/plugins/robot/index.ts | 1 + packages/plugins/robot/package.json | 5 +- packages/plugins/robot/src/Main.vue | 465 +++++++++------------------- 3 files changed, 157 insertions(+), 314 deletions(-) diff --git a/packages/plugins/robot/index.ts b/packages/plugins/robot/index.ts index f84692b4e6..e4ce8a93bd 100644 --- a/packages/plugins/robot/index.ts +++ b/packages/plugins/robot/index.ts @@ -13,6 +13,7 @@ import RobotIcon from './src/Main.vue' import metaData from './meta' import './src/styles/vars.less' +import '@opentiny/tiny-robot/dist/style.css' export default { ...metaData, diff --git a/packages/plugins/robot/package.json b/packages/plugins/robot/package.json index 0c2185f383..70e643ccd5 100644 --- a/packages/plugins/robot/package.json +++ b/packages/plugins/robot/package.json @@ -25,7 +25,10 @@ "license": "MIT", "homepage": "https://opentiny.design/tiny-engine", "dependencies": { - "@opentiny/tiny-engine-meta-register": "workspace:*" + "@opentiny/tiny-engine-meta-register": "workspace:*", + "@opentiny/tiny-robot": "0.2.0-alpha.1", + "@opentiny/tiny-robot-kit": "0.2.0-alpha.1", + "@opentiny/tiny-robot-svgs": "0.2.0-alpha.1" }, "devDependencies": { "@opentiny/tiny-engine-vite-plugin-meta-comments": "workspace:*", diff --git a/packages/plugins/robot/src/Main.vue b/packages/plugins/robot/src/Main.vue index 511909ba80..27aa995119 100644 --- a/packages/plugins/robot/src/Main.vue +++ b/packages/plugins/robot/src/Main.vue @@ -4,19 +4,14 @@ -
-
-
-
- - -
-
-
+
+ + + + + +
@@ -113,22 +68,26 @@ + + diff --git a/packages/plugins/robot/src/mcp/McpServer.vue b/packages/plugins/robot/src/mcp/McpServer.vue new file mode 100644 index 0000000000..8196a12cec --- /dev/null +++ b/packages/plugins/robot/src/mcp/McpServer.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/packages/plugins/robot/src/mcp/types.ts b/packages/plugins/robot/src/mcp/types.ts new file mode 100644 index 0000000000..652a2cf3a4 --- /dev/null +++ b/packages/plugins/robot/src/mcp/types.ts @@ -0,0 +1,75 @@ +import type { BubbleMessageProps } from '@opentiny/tiny-robot' + +export interface RequestOptions { + url?: string + model?: string + headers?: Record +} + +export interface RequestTool { + type: 'function' + function: { + name: string + description: string + parameters: { + type: 'object' + required?: string[] + properties: Record< + string, + { + type: string + description: string + [prop: string]: unknown + } + > + } + } +} + +export interface LLMRequestBody { + model?: string + stream: boolean + messages: BubbleMessageProps[] + tools?: RequestTool[] +} + +export interface ReponseToolCall { + id: string + function: { + name: string + arguments: string + } +} + +export interface LLMResponse { + choices: Array<{ + message: { + role?: string + content: string + tool_calls?: Array + [prop: string]: unknown + } + }> +} + +export interface McpTool { + name: string + description: string + inputSchema?: { + type: 'object' + properties: Record< + string, + { + type: string + description: string + [prop: string]: unknown + } + > + [prop: string]: unknown + } + [prop: string]: unknown +} + +export interface McpListToolsResponse { + tools: Array +} diff --git a/packages/plugins/robot/src/mcp/useMcp.ts b/packages/plugins/robot/src/mcp/useMcp.ts new file mode 100644 index 0000000000..e88ea9b7af --- /dev/null +++ b/packages/plugins/robot/src/mcp/useMcp.ts @@ -0,0 +1,185 @@ +import { computed, ref } from 'vue' +import type { PluginInfo, PluginTool } from '@opentiny/tiny-robot' +import { getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register' +import type { McpListToolsResponse, McpTool, RequestTool } from './types' + +const ENGINE_MCP_SERVER: PluginInfo = { + id: 'tiny-engine-mcp-server', + name: 'Tiny Engine MCP 服务器', + icon: 'https://res.hc-cdn.com/lowcode-portal/1.1.80.20250515160330/assets/opentiny-tinyengine-logo-4f8a3801.svg', + description: '使用TinyEngine设计器能力,如添加国际化', + added: true +} + +const MOCK_SERVERS: PluginInfo[] = [ + { + id: 'plugin-1', + name: 'Jira 集成 (Mock)', + icon: 'https://ts3.tc.mm.bing.net/th/id/ODLS.2a97aa8b-50c6-4e00-af97-3b563dfa07f4', + description: 'Jira 任务管理', + enabled: true, + added: false, + tools: [ + { id: 'tool-5', name: '创建任务', description: '创建 Jira 任务', enabled: true }, + { id: 'tool-6', name: '查询任务', description: '查询 Jira 任务', enabled: true } + ] + }, + { + id: 'plugin-2', + name: 'Notion 集成 (Mock)', + icon: 'https://www.notion.so/front-static/favicon.ico', + description: 'Notion 文档管理和协作', + enabled: false, + added: false, + tools: [ + { id: 'tool-7', name: '创建页面', description: '创建 Notion 页面', enabled: false }, + { id: 'tool-8', name: '查询数据库', description: '查询 Notion 数据库', enabled: false } + ] + }, + { + id: 'plugin-3', + name: 'Telegram 机器人 (Mock)', + icon: 'https://telegram.org/favicon.ico', + description: 'Telegram 消息推送和自动化', + enabled: false, + added: false, + tools: [{ id: 'tool-9', name: '发送消息', description: '发送 Telegram 消息', enabled: false }] + } +] + +const mcpServers = ref([ENGINE_MCP_SERVER, ...MOCK_SERVERS]) + +const inUseMcpServers = ref([ + { ...ENGINE_MCP_SERVER, enabled: true, expanded: false, tools: [], toolCount: 0 } +]) + +const updateServerTools = (serverId: string, tools: PluginTool[]) => { + const mcpServer = inUseMcpServers.value.find((item) => item.id === serverId) + if (mcpServer) { + mcpServer.tools = tools + mcpServer.toolCount = tools.length + } +} + +const updateEngineTools = async () => { + const tools: Array<{ name: string; description: string; status: string }> = + (await getMetaApi(META_SERVICE.McpService)?.getToolList?.()) || [] + const engineTools = tools.map((tool) => ({ + id: tool.name, + name: tool.name, + description: tool.description, + enabled: tool.status === 'enabled' + })) + updateServerTools(ENGINE_MCP_SERVER.id, engineTools) +} + +const convertMCPToOpenAITools = (mcpTools: McpTool[]): RequestTool[] => { + return mcpTools.map((tool: McpTool) => ({ + type: 'function', + function: { + name: tool.name, + description: tool.description || '', + parameters: { + type: 'object', + properties: Object.fromEntries( + Object.entries(tool.inputSchema?.properties || {}).map(([key, prop]: [string, any]) => [key, { ...prop }]) + ), + required: tool.inputSchema?.required || [] + } + } + })) as RequestTool[] +} + +const getEngineServer = () => { + return inUseMcpServers.value.find((item) => item.id === ENGINE_MCP_SERVER.id) +} + +const isToolsEnabled = computed(() => getEngineServer()?.tools?.some((tool) => tool.enabled)) + +const updateEngineServerToolStatus = (toolId: string, enabled: boolean) => { + getMetaApi(META_SERVICE.McpService)?.updateTool?.(toolId, { enabled }) +} + +const updateEngineServer = (engineServer: PluginInfo, enabled: boolean) => { + engineServer?.tools?.forEach((tool) => { + tool.enabled = enabled + updateEngineServerToolStatus(tool.id, enabled) + }) +} + +// TODO: 连接MCP Server +const connectMcpServer = (_server: PluginInfo) => {} + +// TODO: 断开连接 +const disconnectMcpServer = (_server: PluginInfo) => {} + +const updateMcpServerStatus = async (server: PluginInfo, added: boolean) => { + if (added) { + const newServer: PluginInfo = { + ...server, + id: server.id || `mcp-server-${Date.now()}`, + enabled: true, + added: true, + expanded: false, + tools: server.tools || [], + toolCount: server.tools?.length || 0 + } + inUseMcpServers.value.push(newServer) + if (server.id === ENGINE_MCP_SERVER.id) { + await updateEngineTools() + updateEngineServer(newServer, added) + } + // TODO: 连接MCP Server + connectMcpServer(newServer) + } else { + const index = inUseMcpServers.value.findIndex((p) => p.id === server.id) + if (index > -1) { + updateEngineServer(inUseMcpServers.value[index], added) + inUseMcpServers.value.splice(index, 1) + // TODO: 断开连接 + disconnectMcpServer(server) + } + } +} + +const updateMcpServerToolStatus = (currentServer: PluginInfo, toolId: string, enabled: boolean) => { + const tool = currentServer.tools?.find((t: PluginTool) => t.id === toolId) + if (tool) { + tool.enabled = enabled + if (currentServer.id === ENGINE_MCP_SERVER.id) { + updateEngineServerToolStatus(toolId, enabled) + } else { + // TODO: 更新MCP Server的Tool状态 + // 获取 tool 实例调用 enableTool 或 disableTool + } + } +} + +const refreshMcpServerTools = () => { + updateEngineTools() +} + +const listTools = async (): Promise => + getMetaApi(META_SERVICE.McpService)?.getMcpClient()?.listTools() + +const callTool = async (toolId: string, args: Record) => + getMetaApi(META_SERVICE.McpService)?.getMcpClient()?.callTool({ name: toolId, arguments: args }) || {} + +const getLLMTools = async () => { + const mcpTools = await listTools() + return convertMCPToOpenAITools(mcpTools?.tools || []) +} + +export default function useMcpServer() { + return { + mcpServers, + inUseMcpServers, + refreshMcpServerTools, + updateMcpServerStatus, + updateMcpServerToolStatus, + listTools, + callTool, + getLLMTools, + isToolsEnabled + } +} diff --git a/packages/plugins/robot/src/mcp/utils.ts b/packages/plugins/robot/src/mcp/utils.ts new file mode 100644 index 0000000000..5acc7c4332 --- /dev/null +++ b/packages/plugins/robot/src/mcp/utils.ts @@ -0,0 +1,117 @@ +import { toRaw } from 'vue' +import useMcpServer from './useMcp' +import type { BubbleMessageProps } from '@opentiny/tiny-robot' +import type { LLMRequestBody, LLMResponse, ReponseToolCall, RequestOptions, RequestTool } from './types' + +let requestOptions: RequestOptions = {} + +const fetchLLM = async ( + messages: BubbleMessageProps[], + tools: RequestTool[], + options: RequestOptions = requestOptions +) => { + const bodyObj: LLMRequestBody = { + model: options?.model || 'deepseek-ai/DeepSeek-V3', + stream: false, + messages: toRaw(messages) + } + if (tools.length > 0) { + bodyObj.tools = toRaw(tools) + } + return fetch(options?.url || '/app-center/api/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...options?.headers + }, + body: JSON.stringify(bodyObj) + }).then((res) => res.json()) +} + +const parseArgs = (args: string) => { + try { + return JSON.parse(args) + } catch (error) { + return args + } +} + +const handleToolCall = async ( + res: LLMResponse, + tools: RequestTool[], + messages: BubbleMessageProps[], + contextMessages?: BubbleMessageProps[] +) => { + if (messages.length < 1) { + return + } + const currentMessage = messages.at(-1)! + if (!currentMessage.messages) { + currentMessage.messages = [] + } + if (res.choices[0].message.content) { + currentMessage.messages.push({ + type: 'markdown', + content: res.choices[0].message.content + }) + } + const tool_calls: ReponseToolCall[] | undefined = res.choices[0].message.tool_calls + if (tool_calls && tool_calls.length) { + const historyMessages = contextMessages?.length ? contextMessages : toRaw(messages.slice(0, -1)) + const toolMessages = [...historyMessages, res.choices[0].message] as BubbleMessageProps[] + for (const tool of tool_calls) { + const { name, arguments: args } = tool.function + const parsedArgs = parseArgs(args) + const currentToolMessage = { + type: 'tool', + name, + status: 'running', + params: { + params: parsedArgs + }, + formatPretty: true + } + currentMessage.messages.push(currentToolMessage) + const toolCallResult = await useMcpServer().callTool(name, parsedArgs) + toolMessages.push({ + type: 'text', + content: toolCallResult.content, + role: 'tool', + tool_call_id: tool.id + }) + + currentMessage.messages.at(-1).status = 'success' + currentMessage.messages.at(-1).params = { + params: parsedArgs, + result: toolCallResult.content + } + } + const newResp = await fetchLLM(toolMessages, tools) + const hasToolCall = newResp.choices[0].message.tool_calls?.length > 0 + if (hasToolCall) { + await handleToolCall(newResp, tools, messages, toolMessages) + } else { + if (newResp.choices[0].message.content) { + currentMessage.messages.push({ + type: 'markdown', + content: newResp.choices[0].message.content + }) + } + } + } +} + +export const sendMcpRequest = async (messages: BubbleMessageProps[], options: RequestOptions = {}) => { + if (messages.length < 1) { + return + } + const tools = await useMcpServer().getLLMTools() + requestOptions = options + const res = await fetchLLM(messages.slice(0, -1), tools, options) + const hasToolCall = res.choices[0].message.tool_calls?.length > 0 + if (hasToolCall) { + await handleToolCall(res, tools, messages) + } else { + messages.at(-1)!.content = res.choices[0].message.content + } +} diff --git a/packages/register/src/constants.ts b/packages/register/src/constants.ts index d200a3d724..d85e3ff2be 100644 --- a/packages/register/src/constants.ts +++ b/packages/register/src/constants.ts @@ -18,7 +18,8 @@ export const META_SERVICE = { Property: 'engine.service.property', Properties: 'engine.service.properties', ThemeSwitch: 'engine.service.themeSwitch', - Style: 'engine.service.style' + Style: 'engine.service.style', + McpService: 'engine.service.mcpService' } export const META_APP = { diff --git a/packages/register/src/service.ts b/packages/register/src/service.ts index 0161c56652..54b5bcb8b3 100644 --- a/packages/register/src/service.ts +++ b/packages/register/src/service.ts @@ -8,7 +8,7 @@ interface Context { options: K } -interface ServiceOptions { +export interface ServiceOptions { id: string type: 'MetaService' initialState: T From 218df23c8420b866e845aeda100064fcc8537be0 Mon Sep 17 00:00:00 2001 From: hexqi Date: Fri, 25 Jul 2025 11:06:38 +0800 Subject: [PATCH 3/6] feat: add option customCompatibleAIModels --- packages/plugins/robot/src/Main.vue | 3 ++- packages/plugins/robot/src/RobotSettingPopover.vue | 3 ++- packages/plugins/robot/src/js/robotSetting.ts | 13 ++++++++----- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/plugins/robot/src/Main.vue b/packages/plugins/robot/src/Main.vue index 863fe62680..eeea94165e 100644 --- a/packages/plugins/robot/src/Main.vue +++ b/packages/plugins/robot/src/Main.vue @@ -80,7 +80,7 @@ import { TrContainer, TrWelcome, TrPrompts, TrBubbleList, TrSender, TrBubbleProv import type { BubbleRoleConfig, PromptProps } from '@opentiny/tiny-robot' import { IconNewSession } from '@opentiny/tiny-robot-svgs' import RobotSettingPopover from './RobotSettingPopover.vue' -import { getBlockContent, initBlockList, AIModelOptions } from './js/robotSetting' +import { getBlockContent, initBlockList, getAIModelOptions } from './js/robotSetting' import McpServer from './mcp/McpServer.vue' import useMcpServer from './mcp/useMcp' import MarkdownRenderer from './mcp/MarkdownRenderer.vue' @@ -102,6 +102,7 @@ export default { emits: ['close-chat'], setup() { const { initData, isBlock, isSaved, clearCurrentState } = useCanvas() + const AIModelOptions = getAIModelOptions() const robotVisible = ref(false) const avatarUrl = ref('') const chatWindowOpened = ref(true) diff --git a/packages/plugins/robot/src/RobotSettingPopover.vue b/packages/plugins/robot/src/RobotSettingPopover.vue index 8009f096bd..8dc621221b 100644 --- a/packages/plugins/robot/src/RobotSettingPopover.vue +++ b/packages/plugins/robot/src/RobotSettingPopover.vue @@ -30,7 +30,7 @@ /* metaService: engine.plugins.robot.RobotSettingPopover */ import { ref, reactive } from 'vue' import { TinyForm, TinyFormItem, TinyInput, TinyButton, TinySelect, TinyTooltip } from '@opentiny/vue' -import { AIModelOptions } from './js/robotSetting' +import { getAIModelOptions } from './js/robotSetting' export default { components: { @@ -52,6 +52,7 @@ export default { } }, setup(props, { emit }) { + const AIModelOptions = getAIModelOptions() const robotSettingForm = ref(null) const formData = reactive({ diff --git a/packages/plugins/robot/src/js/robotSetting.ts b/packages/plugins/robot/src/js/robotSetting.ts index 92cde1ddc0..ddbdf02343 100644 --- a/packages/plugins/robot/src/js/robotSetting.ts +++ b/packages/plugins/robot/src/js/robotSetting.ts @@ -12,12 +12,15 @@ /* metaService: engine.plugins.robot.js-robotSetting */ import { reactive } from 'vue' -import { getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register' +import { getOptions, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register' +import meta from '../../meta' -export const AIModelOptions = [ - { label: 'SiliconFlow:DeepSeek-V3', value: 'deepseek-ai/DeepSeek-V3', manufacturer: 'siliconflow' }, - { label: 'DeepSeek:DeepSeek-V3', value: 'deepseek-chat', manufacturer: 'deepseek' } -] +const DEFAULT_MODELS = [{ label: 'DeepSeek:DeepSeek-V3', value: 'deepseek-chat', manufacturer: 'deepseek' }] + +export const getAIModelOptions = () => { + const aiRobotOptions = getOptions(meta.id)?.customCompatibleAIModels || [] + return [...DEFAULT_MODELS, ...aiRobotOptions] +} // 这里存放的是aichat的响应式数据 const state = reactive({ From 4c4cdb71f5632e47a186fd7506d311761af89724 Mon Sep 17 00:00:00 2001 From: hexqi Date: Sun, 27 Jul 2025 17:28:10 +0800 Subject: [PATCH 4/6] feat: upgrade tinyrobot version and fix ui issue --- packages/plugins/robot/package.json | 7 +-- packages/plugins/robot/src/Main.vue | 25 +++++++--- .../robot/src/mcp/MarkdownRenderer.vue | 48 +++++++++++++++---- packages/plugins/robot/src/mcp/McpServer.vue | 10 ++++ packages/plugins/robot/src/mcp/types.ts | 16 ++++++- packages/plugins/robot/src/mcp/utils.ts | 40 +++++++--------- 6 files changed, 104 insertions(+), 42 deletions(-) diff --git a/packages/plugins/robot/package.json b/packages/plugins/robot/package.json index f365b71eac..8ef6b469ef 100644 --- a/packages/plugins/robot/package.json +++ b/packages/plugins/robot/package.json @@ -26,10 +26,11 @@ "homepage": "https://opentiny.design/tiny-engine", "dependencies": { "@opentiny/tiny-engine-meta-register": "workspace:*", - "@opentiny/tiny-robot": "0.3.0-alpha.8", - "@opentiny/tiny-robot-kit": "0.3.0-alpha.8", - "@opentiny/tiny-robot-svgs": "0.3.0-alpha.8", + "@opentiny/tiny-robot": "0.3.0-alpha.14", + "@opentiny/tiny-robot-kit": "0.3.0-alpha.14", + "@opentiny/tiny-robot-svgs": "0.3.0-alpha.14", "dompurify": "^3.0.1", + "highlight.js": "^11.11.1", "markdown-it": "^14.1.0" }, "devDependencies": { diff --git a/packages/plugins/robot/src/Main.vue b/packages/plugins/robot/src/Main.vue index eeea94165e..4e0a639590 100644 --- a/packages/plugins/robot/src/Main.vue +++ b/packages/plugins/robot/src/Main.vue @@ -56,6 +56,7 @@ mode="multiple" v-model="inputContent" :autoSize="{ minRows: 1, maxRows: 5 }" + :loading="requestLoading" placeholder="请输入您的问题..." @submit="sendContent(inputContent, false)" > @@ -191,10 +192,13 @@ export default { name: 'AI' }) - const sendRequest = () => { + const requestLoading = ref(false) + + const sendRequest = async () => { if (useMcpServer().isToolsEnabled) { try { - sendMcpRequest(messages.value, { + requestLoading.value = true + await sendMcpRequest(messages.value, { model: selectedModel.value.value, headers: { Authorization: `Bearer ${tokenValue.value || import.meta.env.VITE_API_TOKEN}` @@ -202,7 +206,9 @@ export default { }) } catch (error) { messages.value[messages.value.length - 1].content = '连接失败' + } finally { inProcesing.value = false + requestLoading.value = false } return } @@ -299,7 +305,7 @@ export default { // 根据localstorage初始化AI大模型 const initCurrentModel = (aiSession) => { const currentModelValue = JSON.parse(aiSession)?.foundationModel?.model - selectedModel.value = AIModelOptions.find((item) => item.value === currentModelValue) + selectedModel.value = AIModelOptions.find((item) => item.value === currentModelValue) || AIModelOptions[0] tokenValue.value = JSON.parse(aiSession)?.foundationModel?.token } @@ -412,8 +418,8 @@ export default { // 对话角色配置 const roles: Record = { - assistant: { placement: 'start', avatar: aiAvatar, maxWidth: '90%' }, - user: { placement: 'end', avatar: userAvatar, maxWidth: '90%' } + assistant: { placement: 'start', avatar: aiAvatar, maxWidth: '90%', contentRenderer: MarkdownRenderer }, + user: { placement: 'end', avatar: userAvatar, maxWidth: '90%', contentRenderer: MarkdownRenderer } } return { @@ -439,7 +445,8 @@ export default { handlePromptItemClick, welcomeIcon, roles, - MarkdownRenderer + MarkdownRenderer, + requestLoading } } } @@ -468,6 +475,7 @@ export default { .tiny-container { top: var(--base-top-panel-height) !important; container-type: inline-size; + font-size: 14px; :deep(button.icon-btn) { background-color: rgba(0, 0, 0, 0); @@ -485,11 +493,14 @@ export default { } .tiny-sender__container .tiny-textarea__inner { - font-size: 16px; + font-size: 14px; } .tr-bubble-list { flex: 1; + .tr-bubble { + word-break: break-word; + } } .robot-welcome > div { diff --git a/packages/plugins/robot/src/mcp/MarkdownRenderer.vue b/packages/plugins/robot/src/mcp/MarkdownRenderer.vue index e560b393da..6578667985 100644 --- a/packages/plugins/robot/src/mcp/MarkdownRenderer.vue +++ b/packages/plugins/robot/src/mcp/MarkdownRenderer.vue @@ -1,26 +1,58 @@ - + diff --git a/packages/plugins/robot/src/mcp/McpServer.vue b/packages/plugins/robot/src/mcp/McpServer.vue index 8196a12cec..25fad1185d 100644 --- a/packages/plugins/robot/src/mcp/McpServer.vue +++ b/packages/plugins/robot/src/mcp/McpServer.vue @@ -82,6 +82,16 @@ onMounted(() => { } } +:deep(.mcp-server-picker__content) { + .tiny-tabs.tiny-tabs .tiny-tabs__header .tiny-tabs__nav { + background-color: unset; + } + .tiny-tabs.tiny-tabs .tiny-tabs__header .tiny-tabs__item { + border: none; + background-color: unset; + } +} + :deep(.tiny-tabs__content) { .plugin-card__add-button { display: flex; diff --git a/packages/plugins/robot/src/mcp/types.ts b/packages/plugins/robot/src/mcp/types.ts index 652a2cf3a4..6304732af3 100644 --- a/packages/plugins/robot/src/mcp/types.ts +++ b/packages/plugins/robot/src/mcp/types.ts @@ -1,4 +1,4 @@ -import type { BubbleMessageProps } from '@opentiny/tiny-robot' +import type { BubbleContentItem } from '@opentiny/tiny-robot' export interface RequestOptions { url?: string @@ -26,10 +26,22 @@ export interface RequestTool { } } +export interface LLMMessage { + role: string + content: string + [prop: string]: unknown +} + +export interface RobotMessage { + role: string + content: string | BubbleContentItem[] + [prop: string]: unknown +} + export interface LLMRequestBody { model?: string stream: boolean - messages: BubbleMessageProps[] + messages: LLMMessage[] tools?: RequestTool[] } diff --git a/packages/plugins/robot/src/mcp/utils.ts b/packages/plugins/robot/src/mcp/utils.ts index 5acc7c4332..5ef759a8d2 100644 --- a/packages/plugins/robot/src/mcp/utils.ts +++ b/packages/plugins/robot/src/mcp/utils.ts @@ -1,17 +1,13 @@ import { toRaw } from 'vue' import useMcpServer from './useMcp' -import type { BubbleMessageProps } from '@opentiny/tiny-robot' +import type { LLMMessage, RobotMessage } from './types' import type { LLMRequestBody, LLMResponse, ReponseToolCall, RequestOptions, RequestTool } from './types' let requestOptions: RequestOptions = {} -const fetchLLM = async ( - messages: BubbleMessageProps[], - tools: RequestTool[], - options: RequestOptions = requestOptions -) => { +const fetchLLM = async (messages: LLMMessage[], tools: RequestTool[], options: RequestOptions = requestOptions) => { const bodyObj: LLMRequestBody = { - model: options?.model || 'deepseek-ai/DeepSeek-V3', + model: options?.model || 'deepseek-chat', stream: false, messages: toRaw(messages) } @@ -25,7 +21,7 @@ const fetchLLM = async ( ...options?.headers }, body: JSON.stringify(bodyObj) - }).then((res) => res.json()) + }) } const parseArgs = (args: string) => { @@ -39,18 +35,18 @@ const parseArgs = (args: string) => { const handleToolCall = async ( res: LLMResponse, tools: RequestTool[], - messages: BubbleMessageProps[], - contextMessages?: BubbleMessageProps[] + messages: RobotMessage[], + contextMessages?: RobotMessage[] ) => { if (messages.length < 1) { return } const currentMessage = messages.at(-1)! - if (!currentMessage.messages) { - currentMessage.messages = [] + if (typeof currentMessage.content === 'string' || !currentMessage.content) { + currentMessage.content = [] } if (res.choices[0].message.content) { - currentMessage.messages.push({ + currentMessage.content.push({ type: 'markdown', content: res.choices[0].message.content }) @@ -58,7 +54,7 @@ const handleToolCall = async ( const tool_calls: ReponseToolCall[] | undefined = res.choices[0].message.tool_calls if (tool_calls && tool_calls.length) { const historyMessages = contextMessages?.length ? contextMessages : toRaw(messages.slice(0, -1)) - const toolMessages = [...historyMessages, res.choices[0].message] as BubbleMessageProps[] + const toolMessages: LLMMessage[] = [...historyMessages, res.choices[0].message] as LLMMessage[] for (const tool of tool_calls) { const { name, arguments: args } = tool.function const parsedArgs = parseArgs(args) @@ -66,12 +62,12 @@ const handleToolCall = async ( type: 'tool', name, status: 'running', - params: { + content: { params: parsedArgs }, formatPretty: true } - currentMessage.messages.push(currentToolMessage) + currentMessage.content.push(currentToolMessage) const toolCallResult = await useMcpServer().callTool(name, parsedArgs) toolMessages.push({ type: 'text', @@ -80,19 +76,19 @@ const handleToolCall = async ( tool_call_id: tool.id }) - currentMessage.messages.at(-1).status = 'success' - currentMessage.messages.at(-1).params = { + currentMessage.content.at(-1)!.status = 'success' + currentMessage.content.at(-1)!.content = { params: parsedArgs, result: toolCallResult.content } } - const newResp = await fetchLLM(toolMessages, tools) + const newResp = await fetchLLM(toolMessages, tools).then((res) => res.json()) const hasToolCall = newResp.choices[0].message.tool_calls?.length > 0 if (hasToolCall) { await handleToolCall(newResp, tools, messages, toolMessages) } else { if (newResp.choices[0].message.content) { - currentMessage.messages.push({ + currentMessage.content.push({ type: 'markdown', content: newResp.choices[0].message.content }) @@ -101,13 +97,13 @@ const handleToolCall = async ( } } -export const sendMcpRequest = async (messages: BubbleMessageProps[], options: RequestOptions = {}) => { +export const sendMcpRequest = async (messages: LLMMessage[], options: RequestOptions = {}) => { if (messages.length < 1) { return } const tools = await useMcpServer().getLLMTools() requestOptions = options - const res = await fetchLLM(messages.slice(0, -1), tools, options) + const res = await fetchLLM(messages.slice(0, -1), tools, options).then((res) => res.json()) const hasToolCall = res.choices[0].message.tool_calls?.length > 0 if (hasToolCall) { await handleToolCall(res, tools, messages) From 85741ffcfffad029bd5bc0049af0f42e3603dccf Mon Sep 17 00:00:00 2001 From: hexqi Date: Mon, 28 Jul 2025 12:17:18 +0800 Subject: [PATCH 5/6] fix: review --- packages/plugins/robot/src/Main.vue | 28 +++++--------------- packages/plugins/robot/src/mcp/McpServer.vue | 16 +++++------ 2 files changed, 14 insertions(+), 30 deletions(-) diff --git a/packages/plugins/robot/src/Main.vue b/packages/plugins/robot/src/Main.vue index 4e0a639590..fb9326ccf2 100644 --- a/packages/plugins/robot/src/Main.vue +++ b/packages/plugins/robot/src/Main.vue @@ -388,18 +388,12 @@ export default { { label: '页面搭建场景', description: '如何生成表单嵌进我的网站?', - icon: h('span', { style: { fontSize: '18px' } as CSSProperties }, '✨'), - badge: 'NEW' + icon: h('span', { style: { fontSize: '18px' } as CSSProperties }, '✨') }, { label: '学习/知识型场景', - description: '有什么想了解的吗?可以是“Vue3 和 React 的区别”!', + description: 'Vue3 和 React 有什么区别?', icon: h('span', { style: { fontSize: '18px' } as CSSProperties }, '🤔') - }, - { - label: '创意生成场景', - description: '想写段文案、起个名字,还是来点灵感?', - icon: h('span', { style: { fontSize: '18px' } as CSSProperties }, '💡') } ] @@ -473,9 +467,12 @@ export default { } .tiny-container { - top: var(--base-top-panel-height) !important; container-type: inline-size; - font-size: 14px; + + &.tr-container.tr-container { + top: var(--base-top-panel-height); + --tr-container-width: 400px; + } :deep(button.icon-btn) { background-color: rgba(0, 0, 0, 0); @@ -485,17 +482,6 @@ export default { margin-left: 10px; } - .tr-bubble__content-messages { - font-size: 14px; - .tr-bubble__step-tool { - word-break: break-all !important; - } - } - - .tiny-sender__container .tiny-textarea__inner { - font-size: 14px; - } - .tr-bubble-list { flex: 1; .tr-bubble { diff --git a/packages/plugins/robot/src/mcp/McpServer.vue b/packages/plugins/robot/src/mcp/McpServer.vue index 25fad1185d..28d403a041 100644 --- a/packages/plugins/robot/src/mcp/McpServer.vue +++ b/packages/plugins/robot/src/mcp/McpServer.vue @@ -1,11 +1,9 @@