From 98a659fa39290fb540f784e4783190ba4533fffb Mon Sep 17 00:00:00 2001 From: hexqi Date: Thu, 25 Sep 2025 16:27:39 +0800 Subject: [PATCH 01/17] fix: ai plugin text and ui and upgrade robot version --- packages/common/js/completion.js | 2 +- packages/plugins/robot/package.json | 6 +- packages/plugins/robot/src/Main.vue | 65 ++++++------------- .../plugins/robot/src/RobotSettingPopover.vue | 24 ++++++- .../plugins/robot/src/RobotTypeSelect.vue | 29 ++++----- .../src/{icon-prompt => icons}/mcp-icon.vue | 8 --- .../src/{icon-prompt => icons}/page-icon.vue | 8 --- .../src/{icon-prompt => icons}/study-icon.vue | 8 --- packages/plugins/robot/src/js/useRobot.ts | 4 +- packages/plugins/robot/src/mcp/McpServer.vue | 4 +- packages/plugins/robot/src/mcp/types.ts | 1 + packages/plugins/robot/src/mcp/useMcp.ts | 16 ----- 12 files changed, 65 insertions(+), 110 deletions(-) rename packages/plugins/robot/src/{icon-prompt => icons}/mcp-icon.vue (91%) rename packages/plugins/robot/src/{icon-prompt => icons}/page-icon.vue (94%) rename packages/plugins/robot/src/{icon-prompt => icons}/study-icon.vue (93%) diff --git a/packages/common/js/completion.js b/packages/common/js/completion.js index 2ce64a8e08..08ba04ec57 100644 --- a/packages/common/js/completion.js +++ b/packages/common/js/completion.js @@ -218,7 +218,7 @@ const fetchAiInlineCompletion = (codeBeforeCursor, codeAfterCursor) => { { headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${apiKey}` + Authorization: `Bearer ${apiKey || ''}` } } ) diff --git a/packages/plugins/robot/package.json b/packages/plugins/robot/package.json index c050b84229..a0e10a2578 100644 --- a/packages/plugins/robot/package.json +++ b/packages/plugins/robot/package.json @@ -28,9 +28,9 @@ "@opentiny/tiny-engine-meta-register": "workspace:*", "@opentiny/tiny-engine-common": "workspace:*", "@opentiny/tiny-engine-utils": "workspace:*", - "@opentiny/tiny-robot": "0.3.0-rc.0", - "@opentiny/tiny-robot-kit": "0.3.0-rc.0", - "@opentiny/tiny-robot-svgs": "0.3.0-rc.0", + "@opentiny/tiny-robot": "0.3.0-rc.5", + "@opentiny/tiny-robot-kit": "0.3.0-rc.5", + "@opentiny/tiny-robot-svgs": "0.3.0-rc.5", "@opentiny/tiny-schema-renderer": "1.0.0-beta.6", "fast-json-patch": "~3.1.1", "dompurify": "^3.0.1", diff --git a/packages/plugins/robot/src/Main.vue b/packages/plugins/robot/src/Main.vue index 05cb6c1e06..ce3bc059b3 100644 --- a/packages/plugins/robot/src/Main.vue +++ b/packages/plugins/robot/src/Main.vue @@ -71,10 +71,10 @@ class="footer-sender" ref="senderRef" v-model="inputContent" - placeholder="请输入问题或“/”唤起指令,支持粘贴文档" + placeholder="请输入您的问题" :clearable="true" :showWordLimit="true" - :allowFiles="singleAttachmentItems.length < 1 && isVisualModel() && aiType === AI_MODES['Builder']" + :allowFiles="singleAttachmentItems.length < 1 && isVisualModel() && aiType === AI_MODES.Agent" uploadTooltip="支持上传1张图片" @submit="sendContent(inputContent, false)" @files-selected="handleSingleFilesSelected" @@ -94,7 +94,7 @@ @@ -109,18 +109,7 @@ diff --git a/packages/plugins/robot/src/icon-prompt/page-icon.vue b/packages/plugins/robot/src/icons/page-icon.vue similarity index 94% rename from packages/plugins/robot/src/icon-prompt/page-icon.vue rename to packages/plugins/robot/src/icons/page-icon.vue index ae82278def..bd54f58f23 100644 --- a/packages/plugins/robot/src/icon-prompt/page-icon.vue +++ b/packages/plugins/robot/src/icons/page-icon.vue @@ -7,8 +7,6 @@ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > - Created with Pixso. - - - diff --git a/packages/plugins/robot/src/icon-prompt/study-icon.vue b/packages/plugins/robot/src/icons/study-icon.vue similarity index 93% rename from packages/plugins/robot/src/icon-prompt/study-icon.vue rename to packages/plugins/robot/src/icons/study-icon.vue index b851005195..40a0d1245a 100644 --- a/packages/plugins/robot/src/icon-prompt/study-icon.vue +++ b/packages/plugins/robot/src/icons/study-icon.vue @@ -7,8 +7,6 @@ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > - Created with Pixso. - - - diff --git a/packages/plugins/robot/src/js/useRobot.ts b/packages/plugins/robot/src/js/useRobot.ts index a67b9d3cf8..87e301119a 100644 --- a/packages/plugins/robot/src/js/useRobot.ts +++ b/packages/plugins/robot/src/js/useRobot.ts @@ -17,8 +17,7 @@ import meta from '../../meta' const EXISTING_MODELS = 'existingModels' const CUSTOMIZE = 'customize' -const VISUAL_MODEL = ['qwen-vl-max', 'qwen-vl-plus'] -const AI_MODES = { Builder: 'builder', Chat: 'chat' } +const AI_MODES = { Agent: 'agent', Chat: 'chat' } const AIModelOptions = [ { @@ -176,7 +175,6 @@ export default () => { return { EXISTING_MODELS, CUSTOMIZE, - VISUAL_MODEL, AI_MODES, AIModelOptions, getAIModelOptions, diff --git a/packages/plugins/robot/src/mcp/McpServer.vue b/packages/plugins/robot/src/mcp/McpServer.vue index 488e439b7d..71cf5f875b 100644 --- a/packages/plugins/robot/src/mcp/McpServer.vue +++ b/packages/plugins/robot/src/mcp/McpServer.vue @@ -6,11 +6,12 @@
+ baseUrl?: string } export interface RequestTool { diff --git a/packages/plugins/robot/src/mcp/useMcp.ts b/packages/plugins/robot/src/mcp/useMcp.ts index 26963e1cd9..f44ac32bae 100644 --- a/packages/plugins/robot/src/mcp/useMcp.ts +++ b/packages/plugins/robot/src/mcp/useMcp.ts @@ -11,8 +11,6 @@ const ENGINE_MCP_SERVER: PluginInfo = { added: true } -const mcpServers = ref([ENGINE_MCP_SERVER]) - const inUseMcpServers = ref([{ ...ENGINE_MCP_SERVER, enabled: true, expanded: true, tools: [] }]) const updateServerTools = (serverId: string, tools: PluginTool[]) => { @@ -68,12 +66,6 @@ const updateEngineServer = (engineServer: PluginInfo, enabled: boolean) => { }) } -// TODO: 连接MCP Server -const connectMcpServer = (_server: PluginInfo) => {} - -// TODO: 断开连接 -const disconnectMcpServer = (_server: PluginInfo) => {} - const updateMcpServerStatus = async (server: PluginInfo, added: boolean) => { // 市场添加状态修改 server.added = added @@ -91,15 +83,11 @@ const updateMcpServerStatus = async (server: PluginInfo, added: boolean) => { 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) } } } @@ -110,9 +98,6 @@ const updateMcpServerToolStatus = (currentServer: PluginInfo, toolId: string, en tool.enabled = enabled if (currentServer.id === ENGINE_MCP_SERVER.id) { updateEngineServerToolStatus(toolId, enabled) - } else { - // TODO: 更新MCP Server的Tool状态 - // 获取 tool 实例调用 enableTool 或 disableTool } } } @@ -134,7 +119,6 @@ const getLLMTools = async () => { export default function useMcpServer() { return { - mcpServers, inUseMcpServers, refreshMcpServerTools, updateMcpServerStatus, From 5eb53e96d511b49977b2e4e2a4a9a43b855a20eb Mon Sep 17 00:00:00 2001 From: hexqi Date: Sun, 28 Sep 2025 11:21:27 +0800 Subject: [PATCH 02/17] feat(robot): code refactor --- packages/layout/src/DesignSettings.vue | 1 - packages/layout/src/Main.vue | 1 + packages/plugins/robot/index.ts | 3 +- packages/plugins/robot/mock/test.ts | 0 packages/plugins/robot/src/Home.vue | 236 +++++++++ packages/plugins/robot/src/Main.vue | 6 +- .../src/client/OpenAICompatibleProvider.ts | 139 +++++ packages/plugins/robot/src/client/index.ts | 52 ++ .../robot/src/components/RobotChat.vue | 476 ++++++++++++++++++ .../{ => components}/RobotSettingPopover.vue | 0 .../src/{ => components}/RobotTypeSelect.vue | 0 .../plugins/robot/src/composables/useChat.ts | 256 ++++++++++ .../robot/src/{mcp => composables}/useMcp.ts | 17 +- packages/plugins/robot/src/mcp/McpServer.vue | 2 +- packages/plugins/robot/src/types/mcp-types.ts | 97 ++++ .../{mcp/utils.ts => utils/common-utils.ts} | 10 +- 16 files changed, 1281 insertions(+), 15 deletions(-) delete mode 100644 packages/plugins/robot/mock/test.ts create mode 100644 packages/plugins/robot/src/Home.vue create mode 100644 packages/plugins/robot/src/client/OpenAICompatibleProvider.ts create mode 100644 packages/plugins/robot/src/client/index.ts create mode 100644 packages/plugins/robot/src/components/RobotChat.vue rename packages/plugins/robot/src/{ => components}/RobotSettingPopover.vue (100%) rename packages/plugins/robot/src/{ => components}/RobotTypeSelect.vue (100%) create mode 100644 packages/plugins/robot/src/composables/useChat.ts rename packages/plugins/robot/src/{mcp => composables}/useMcp.ts (91%) create mode 100644 packages/plugins/robot/src/types/mcp-types.ts rename packages/plugins/robot/src/{mcp/utils.ts => utils/common-utils.ts} (94%) diff --git a/packages/layout/src/DesignSettings.vue b/packages/layout/src/DesignSettings.vue index 5458365d62..7bbe2c29bd 100644 --- a/packages/layout/src/DesignSettings.vue +++ b/packages/layout/src/DesignSettings.vue @@ -201,7 +201,6 @@ export default { display: flex; flex-direction: column; position: absolute; - top: var(--base-top-panel-height); right: var(--base-nav-panel-width); z-index: 999; diff --git a/packages/layout/src/Main.vue b/packages/layout/src/Main.vue index b8ff93568e..0a3cd64899 100644 --- a/packages/layout/src/Main.vue +++ b/packages/layout/src/Main.vue @@ -154,6 +154,7 @@ export default { display: flex; flex-flow: row nowrap; z-index: 4; + position: relative; } :deep(.monaco-editor .suggest-widget) { border-width: 0; diff --git a/packages/plugins/robot/index.ts b/packages/plugins/robot/index.ts index ef4fba0da0..9ce60f6bea 100644 --- a/packages/plugins/robot/index.ts +++ b/packages/plugins/robot/index.ts @@ -10,7 +10,8 @@ * */ -import entry from './src/Main.vue' +// import entry from './src/Main.vue' +import entry from './src/Home.vue' import metaData from './meta' import './src/styles/vars.less' import '@opentiny/tiny-robot/dist/style.css' diff --git a/packages/plugins/robot/mock/test.ts b/packages/plugins/robot/mock/test.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/plugins/robot/src/Home.vue b/packages/plugins/robot/src/Home.vue new file mode 100644 index 0000000000..4e1fb103a3 --- /dev/null +++ b/packages/plugins/robot/src/Home.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/packages/plugins/robot/src/Main.vue b/packages/plugins/robot/src/Main.vue index ce3bc059b3..96c3d1c850 100644 --- a/packages/plugins/robot/src/Main.vue +++ b/packages/plugins/robot/src/Main.vue @@ -133,18 +133,18 @@ import type { BubbleRoleConfig, PromptProps } from '@opentiny/tiny-robot' import { IconNewSession } from '@opentiny/tiny-robot-svgs' import SchemaRenderer from '@opentiny/tiny-schema-renderer' import { utils } from '@opentiny/tiny-engine-utils' -import RobotSettingPopover from './RobotSettingPopover.vue' +import RobotSettingPopover from './components/RobotSettingPopover.vue' import { PROMPTS } from './js/prompts' import * as jsonpatch from 'fast-json-patch' import { checkComponentNameExists, processSSEStream } from './js/utils' import McpServer from './mcp/McpServer.vue' -import useMcpServer from './mcp/useMcp' +import useMcpServer from './composables/useMcp' import MarkdownRenderer from './mcp/MarkdownRenderer.vue' import LoadingRenderer from './mcp/LoadingRenderer.vue' import BuildLoadingRenderer from './BuildLoadingRenderer.vue' import { sendMcpRequest, serializeError } from './mcp/utils' import type { RobotMessage } from './mcp/types' -import RobotTypeSelect from './RobotTypeSelect.vue' +import RobotTypeSelect from './components/RobotTypeSelect.vue' import McpIconComponent from './icons/mcp-icon.vue' import PageIconComponent from './icons/page-icon.vue' import StudyIconComponent from './icons/study-icon.vue' diff --git a/packages/plugins/robot/src/client/OpenAICompatibleProvider.ts b/packages/plugins/robot/src/client/OpenAICompatibleProvider.ts new file mode 100644 index 0000000000..40ec667503 --- /dev/null +++ b/packages/plugins/robot/src/client/OpenAICompatibleProvider.ts @@ -0,0 +1,139 @@ +import type { + AIModelConfig, + ChatCompletionRequest, + ChatCompletionResponse, + StreamHandler +} from '@opentiny/tiny-robot-kit' +import { BaseModelProvider, handleSSEStream } from '@opentiny/tiny-robot-kit' +import { toRaw } from 'vue' +import { formatMessages } from '../utils/common-utils' + +type ProviderConfig = Omit + +export class OpenAICompatibleProvider extends BaseModelProvider { + private baseURL: string + private apiKey: string + private defaultModel: string = 'gpt-3.5-turbo' + private beforeRequest: (request: unknown) => unknown = (params: unknown) => params + + /** + * @param config AI模型配置 + */ + constructor(config: ProviderConfig, { beforeRequest }: { beforeRequest: (request: unknown) => unknown }) { + super(config as AIModelConfig) + this.baseURL = config.apiUrl || 'https://api.openai.com/v1/chat/completions' + this.apiKey = config.apiKey || '' + + if (beforeRequest) { + this.beforeRequest = beforeRequest + } + + // 设置默认模型 + if (config.defaultModel) { + this.defaultModel = config.defaultModel + } + } + + /** + * 发送聊天请求并获取响应 + * @param request 聊天请求参数 + * @returns 聊天响应 + */ + async chat(request: ChatCompletionRequest): Promise { + try { + const messages = formatMessages(toRaw(request.messages)) + + const requestData = await this.beforeRequest({ + model: request.options?.model || this.config.defaultModel || this.defaultModel, + messages, + ...request.options, + stream: false + }) + + const options = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(requestData) + } + if (this.apiKey) { + Object.assign(options.headers, { Authorization: `Bearer ${this.apiKey}` }) + } + const response = await fetch(`${this.baseURL}`, options) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`HTTP error! status: ${response.status}, details: ${errorText}`) + } + + return await response.json() + } catch (error: unknown) { + // 处理错误 + throw new Error(`Error in chat request: ${error}`) + } + } + + /** + * 发送流式聊天请求并通过处理器处理响应 + * @param request 聊天请求参数 + * @param handler 流式响应处理器 + */ + async chatStream(request: ChatCompletionRequest, handler: StreamHandler): Promise { + const { signal, ...options } = request.options || {} + + try { + // 验证请求参数 + const messages = formatMessages(toRaw(request.messages)) + + const requestData = await this.beforeRequest({ + model: request.options?.model || this.config.defaultModel || this.defaultModel, + messages, + ...options, + stream: true + }) + + const requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.apiKey}`, + Accept: 'text/event-stream' + }, + body: JSON.stringify(requestData), + signal + } + if (this.apiKey) { + Object.assign(requestOptions.headers, { Authorization: `Bearer ${this.apiKey}` }) + } + const response = await fetch(`${this.baseURL}`, requestOptions) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`HTTP error! status: ${response.status}, details: ${errorText}`) + } + + await handleSSEStream(response, handler, signal) + } catch (error: unknown) { + if (signal?.aborted) return + handler.onError(error) + } + } + + /** + * 更新配置 + * @param config 新的AI模型配置 + */ + updateConfig(config: ProviderConfig): void { + // 更新配置 + if (config.apiUrl) { + this.baseURL = config.apiUrl + } + + if (config.apiKey) { + this.apiKey = config.apiKey + } + + if (config.defaultModel) { + this.defaultModel = config.defaultModel + } + } +} diff --git a/packages/plugins/robot/src/client/index.ts b/packages/plugins/robot/src/client/index.ts new file mode 100644 index 0000000000..a211ea6697 --- /dev/null +++ b/packages/plugins/robot/src/client/index.ts @@ -0,0 +1,52 @@ +import { AIClient, type AIModelConfig } from '@opentiny/tiny-robot-kit' +import { OpenAICompatibleProvider } from './OpenAICompatibleProvider' +import useMcp from '../composables/useMcp' +import useRobot from '../js/useRobot' + +const config: Omit = { + apiKey: '', + apiUrl: '/app-center/api/chat/completions', // '/app-center/api/ai/chat' | '/app-center/api/chat/completions' + defaultModel: 'deepseek-v3' +} + +let provider: OpenAICompatibleProvider | null = null + +const { robotSettingState } = useRobot() + +const beforeRequest = async (requestParams: any) => { + const tools = (await useMcp().getLLMTools()) || [] + if (!requestParams.tools && tools.length) { + Object.assign(requestParams, { tools }) + } + if (config.apiUrl === '/app-center/api/ai/chat') { + requestParams.apiKey = robotSettingState.selectedModel.apiKey + } + requestParams.baseUrl = robotSettingState.selectedModel.baseUrl + if ( + config.apiKey !== robotSettingState.selectedModel.apiKey || + config.defaultModel !== robotSettingState.selectedModel.model + ) { + provider?.updateConfig({ + apiKey: robotSettingState.selectedModel.apiKey, + defaultModel: robotSettingState.selectedModel.model + }) + config.apiKey = robotSettingState.selectedModel.apiKey + config.defaultModel = robotSettingState.selectedModel.model + } + return requestParams +} + +provider = new OpenAICompatibleProvider(config, { beforeRequest }) + +const client = new AIClient({ + ...config, + provider: 'custom', + providerImplementation: provider +}) + +const updateLLMConfig = (newConfig: Omit) => { + provider?.updateConfig(newConfig) + Object.assign(config, newConfig) +} + +export { client, updateLLMConfig } diff --git a/packages/plugins/robot/src/components/RobotChat.vue b/packages/plugins/robot/src/components/RobotChat.vue new file mode 100644 index 0000000000..956016df90 --- /dev/null +++ b/packages/plugins/robot/src/components/RobotChat.vue @@ -0,0 +1,476 @@ + + + + + diff --git a/packages/plugins/robot/src/RobotSettingPopover.vue b/packages/plugins/robot/src/components/RobotSettingPopover.vue similarity index 100% rename from packages/plugins/robot/src/RobotSettingPopover.vue rename to packages/plugins/robot/src/components/RobotSettingPopover.vue diff --git a/packages/plugins/robot/src/RobotTypeSelect.vue b/packages/plugins/robot/src/components/RobotTypeSelect.vue similarity index 100% rename from packages/plugins/robot/src/RobotTypeSelect.vue rename to packages/plugins/robot/src/components/RobotTypeSelect.vue diff --git a/packages/plugins/robot/src/composables/useChat.ts b/packages/plugins/robot/src/composables/useChat.ts new file mode 100644 index 0000000000..6d6b6ddedd --- /dev/null +++ b/packages/plugins/robot/src/composables/useChat.ts @@ -0,0 +1,256 @@ +import type { BubbleContentItem } from '@opentiny/tiny-robot' +import { + STATUS, + useConversation, + type ChatCompletionStreamResponse, + type ChatCompletionStreamResponseChoice, + type ChatMessage, + type UseMessageOptions +} from '@opentiny/tiny-robot-kit' +import { toRaw } from 'vue' +import { formatMessages, serializeError } from '../utils/common-utils' +import type { LLMMessage, ResponseToolCall, RobotMessage } from '../types/mcp-types' +import useMcpServer from './useMcp' +import { client } from '../client' + +type Message = ChatMessage & { + renderContent: BubbleContentItem[] + tool_calls: ResponseToolCall[] +} + +const removeLoading = (messages: ChatMessage[], name?: string) => { + const lastMessage = messages.at(-1) + if (name === 'latest' && lastMessage.renderContent?.at(-1)?.type === 'loading') { + lastMessage.renderContent.pop() + return + } + const index = lastMessage.renderContent?.findIndex( + (item) => item.type === 'loading' && (name ? item.content === name : true) + ) + if (index !== -1) { + lastMessage.renderContent?.splice(index, 1) + } +} + +const events: UseMessageOptions['events'] = { + onReceiveData: (data: ChatCompletionStreamResponse, messages, preventDefault) => { + preventDefault() + const choice = data.choices?.[0] + if (!choice) { + return + } + const lastMessage = messages.value.at(-1) + if (choice.delta.reasoning_content || choice.delta.content || choice.delta.tool_calls?.length) { + removeLoading(messages.value, 'latest') + } + handleDeltaReasoning(choice, lastMessage) // eslint-disable-line + handleDeltaContent(choice, lastMessage) // eslint-disable-line + handleDeltaToolCalls(choice, lastMessage) // eslint-disable-line + }, + onFinish(finishReason, { messages, messageState }, preventDefault) { + preventDefault() + + if (finishReason === 'tool_calls') { + const lastMessage = messages.value.at(-1) + handleToolCall(lastMessage.tool_calls, messages.value) // eslint-disable-line + } else if (finishReason !== 'abort' && messageState.status !== STATUS.ABORTED) { + messageState.status = STATUS.FINISHED + } + } +} + +const { messageManager, state: conversationState, ...rest } = useConversation({ client, events }) + +const getMessageManager = () => messageManager + +const handleDeltaReasoning = (choice: ChatCompletionStreamResponseChoice, lastMessage: Message) => { + if (typeof choice.delta.reasoning_content === 'string' && choice.delta.reasoning_content) { + if (lastMessage.renderContent.at(-1)?.contentType !== 'reasoning') { + lastMessage.renderContent.push({ + type: 'collapsible-text', + contentType: 'reasoning', + title: '深度思考', + content: '', + defaultOpen: true + }) + } + lastMessage.renderContent.at(-1).content += choice.delta.reasoning_content + } +} + +const handleDeltaContent = (choice: ChatCompletionStreamResponseChoice, lastMessage: Message) => { + if (typeof choice.delta.content === 'string' && choice.delta.content) { + if (lastMessage.renderContent.at(-1)?.type !== 'markdown') { + lastMessage.renderContent.push({ type: 'markdown', content: '' }) + lastMessage.content = '' + } + lastMessage.renderContent.at(-1).content += choice.delta.content + lastMessage.content += choice.delta.content + } +} + +/** + * 合并字符串字段。如果值是对象,则递归合并字符串字段 + * @param target 目标对象 + * @param source 源对象 + * @returns 合并后的对象 + */ +const mergeStringFields = (target: Record, source: Record) => { + for (const [key, value] of Object.entries(source)) { + const targetValue = target[key] + + if (targetValue) { + if (typeof targetValue === 'string' && typeof value === 'string') { + // 都是字符串,直接拼接 + target[key] = targetValue + value + } else if (targetValue && typeof targetValue === 'object' && value && typeof value === 'object') { + // 都是对象,递归合并 + target[key] = mergeStringFields(targetValue, value) + } + } else { + // 不存在,直接赋值 + target[key] = value + } + } + + return target +} + +const handleDeltaToolCalls = (choice: ChatCompletionStreamResponseChoice, lastMessage: Message) => { + const toolCallChunks = choice.delta.tool_calls as (ResponseToolCall & { index: number })[] + if (Array.isArray(toolCallChunks) && toolCallChunks.length) { + if (!lastMessage.tool_calls) { + lastMessage.tool_calls = [] + } + for (const chunk of toolCallChunks) { + const { index, ...chunkWithoutIndex } = chunk + if (lastMessage.tool_calls[index]) { + mergeStringFields(lastMessage.tool_calls[index], chunkWithoutIndex) + } else { + lastMessage.tool_calls[index] = chunkWithoutIndex + } + } + } +} + +const parseArgs = (args: string) => { + try { + return JSON.parse(args) + } catch (error) { + return args + } +} + +let afterToolCallAbortController: AbortController | null = null + +const handleToolCall = async ( + tool_calls: ResponseToolCall[], + messages: ChatMessage[], + contextMessages?: RobotMessage[] +) => { + const hasToolCall = tool_calls?.length > 0 + if (!hasToolCall) { + return + } + + afterToolCallAbortController = new AbortController() + + const currentMessage = messages.at(-1) + const historyMessages = contextMessages?.length ? contextMessages : messages.slice(0, -1) + const toolMessages: LLMMessage[] = formatMessages([...historyMessages, toRaw(currentMessage)]) + for (const tool of tool_calls) { + const { name, arguments: args } = tool.function + const parsedArgs = parseArgs(args) + const currentToolMessage = { + type: 'tool', + name, + status: 'running', + content: { + params: parsedArgs + }, + formatPretty: true + } + currentMessage.renderContent.push(currentToolMessage) + let toolCallResult: string + let toolCallStatus: 'success' | 'failed' + try { + const resp = await useMcpServer().callTool(name, parsedArgs) + toolCallStatus = 'success' + toolCallResult = resp.content + } catch (error) { + toolCallStatus = 'failed' + toolCallResult = serializeError(error) + } + toolMessages.push({ + content: JSON.stringify(toolCallResult), + role: 'tool', + tool_call_id: tool.id + }) + currentMessage.renderContent.at(-1)!.status = toolCallStatus + currentMessage.renderContent.at(-1)!.content = { + params: parsedArgs, + result: toolCallResult + } + + if (afterToolCallAbortController?.signal?.aborted) { + return + } + } + delete currentMessage.tool_calls + currentMessage.renderContent.push({ type: 'loading', content: '' }) + + await client.chatStream( + { messages: toolMessages, options: { signal: afterToolCallAbortController?.signal } }, + { + onData: (data) => { + if ( + data.choices[0].delta.reasoning_content || + data.choices[0].delta.content || + data.choices[0].delta.tool_calls?.length + ) { + removeLoading(messages, 'latest') + } + if (data.choices[0].delta.reasoning_content) { + handleDeltaReasoning(data.choices[0], currentMessage) + } + if (data.choices[0].delta.content) { + handleDeltaContent(data.choices[0], currentMessage) + } + if (data.choices[0].delta.tool_calls?.length) { + handleDeltaToolCalls(data.choices[0], currentMessage) + } + }, + onError: (error) => { + removeLoading(messages) + messages.at(-1)!.renderContent.push({ type: 'text', content: serializeError(error) }) + // eslint-disable-next-line no-console + console.error(error) + getMessageManager().messageState.status = STATUS.ERROR + }, + onDone: async () => { + removeLoading(messages, 'latest') + const toolCalls = messages.at(-1)!.tool_calls + if (toolCalls?.length) { + await handleToolCall(toolCalls, messages, toolMessages) + } else { + getMessageManager().messageState.status = STATUS.FINISHED + } + } + } + ) +} + +export default function () { + return { + conversationState, + ...messageManager, + abortRequest: () => { + afterToolCallAbortController?.abort() + messageManager.abortRequest() + messageManager.messageState.status = STATUS.ABORTED + removeLoading(messageManager.messages.value, 'latest') + }, + ...rest, + removeLoading + } +} diff --git a/packages/plugins/robot/src/mcp/useMcp.ts b/packages/plugins/robot/src/composables/useMcp.ts similarity index 91% rename from packages/plugins/robot/src/mcp/useMcp.ts rename to packages/plugins/robot/src/composables/useMcp.ts index f44ac32bae..8f0e1407a1 100644 --- a/packages/plugins/robot/src/mcp/useMcp.ts +++ b/packages/plugins/robot/src/composables/useMcp.ts @@ -1,7 +1,7 @@ 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' +import type { McpListToolsResponse, McpTool, RequestTool } from '../mcp/types' const ENGINE_MCP_SERVER: PluginInfo = { id: 'tiny-engine-mcp-server', @@ -106,15 +106,22 @@ const refreshMcpServerTools = () => { updateEngineTools() } -const listTools = async (): Promise => - getMetaApi(META_SERVICE.McpService)?.getMcpClient()?.listTools() +let llmTools: RequestTool[] | null = null + +const listTools = async (): Promise => { + const mcpTools = await getMetaApi(META_SERVICE.McpService)?.getMcpClient()?.listTools() + llmTools = convertMCPToOpenAITools(mcpTools?.tools || []) + return mcpTools +} 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 || []) + if (!llmTools) { + await listTools() + } + return llmTools } export default function useMcpServer() { diff --git a/packages/plugins/robot/src/mcp/McpServer.vue b/packages/plugins/robot/src/mcp/McpServer.vue index 71cf5f875b..d5e1b8eb44 100644 --- a/packages/plugins/robot/src/mcp/McpServer.vue +++ b/packages/plugins/robot/src/mcp/McpServer.vue @@ -25,7 +25,7 @@ import { onMounted, ref } from 'vue' import { McpServerPicker, type PluginInfo, type PopupConfig } from '@opentiny/tiny-robot' import { IconPlugin } from '@opentiny/tiny-robot-svgs' -import useMcpServer from './useMcp' +import useMcpServer from '../composables/useMcp' const activeCount = ref(1) diff --git a/packages/plugins/robot/src/types/mcp-types.ts b/packages/plugins/robot/src/types/mcp-types.ts new file mode 100644 index 0000000000..64facc8d1b --- /dev/null +++ b/packages/plugins/robot/src/types/mcp-types.ts @@ -0,0 +1,97 @@ +import type { BubbleContentItem } 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 LLMMessage { + role: string + content: string | { type: string; [prop: string]: unknown }[] + [prop: string]: unknown +} + +export interface RobotMessage { + role: string + content: string | BubbleContentItem[] + renderContent?: Array + [prop: string]: unknown +} + +export interface LLMRequestBody { + model?: string + stream: boolean + messages: LLMMessage[] + tools?: RequestTool[] +} + +export interface ResponseToolCall { + 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 +} + +export interface NextServerInfoResult { + device: { + referer?: string + ip?: string + [prop: string]: unknown + } + type: string +} diff --git a/packages/plugins/robot/src/mcp/utils.ts b/packages/plugins/robot/src/utils/common-utils.ts similarity index 94% rename from packages/plugins/robot/src/mcp/utils.ts rename to packages/plugins/robot/src/utils/common-utils.ts index aa9fd4887c..18888d779e 100644 --- a/packages/plugins/robot/src/mcp/utils.ts +++ b/packages/plugins/robot/src/utils/common-utils.ts @@ -1,5 +1,5 @@ import { toRaw } from 'vue' -import useMcpServer from './useMcp' +import useMcpServer from '../composables/useMcp' import type { LLMMessage, RobotMessage } from './types' import type { LLMRequestBody, LLMResponse, ReponseToolCall, RequestOptions, RequestTool } from './types' import { META_SERVICE, getMetaApi } from '@opentiny/tiny-engine-meta-register' @@ -7,10 +7,12 @@ import { META_SERVICE, getMetaApi } from '@opentiny/tiny-engine-meta-register' let requestOptions: RequestOptions = {} // 格式化LLM输入messages消息 -const formatMessages = (messages: LLMMessage[]) => { - return messages.map((message) => ({ +export const formatMessages = (messages: LLMMessage[]) => { + return toRaw(messages).map((message) => ({ role: message.role, - content: message.content + content: message.content, + ...(message.tool_calls ? { tool_calls: message.tool_calls } : {}), + ...(message.tool_call_id ? { tool_call_id: message.tool_call_id } : {}) })) } From 7c4a1c91c5c32cb90bae5dc19b6761d7dfecf48f Mon Sep 17 00:00:00 2001 From: hexqi Date: Mon, 29 Sep 2025 23:19:32 +0800 Subject: [PATCH 03/17] fix: update ui --- packages/plugins/robot/src/Home.vue | 15 +++-- .../robot/src/components/RobotChat.vue | 62 ++++++++++++++++--- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/packages/plugins/robot/src/Home.vue b/packages/plugins/robot/src/Home.vue index 4e1fb103a3..5ee75fe2ac 100644 --- a/packages/plugins/robot/src/Home.vue +++ b/packages/plugins/robot/src/Home.vue @@ -221,16 +221,23 @@ onMounted(async () => { .operations-setting { font-size: 20px; } - @media (min-width: 1280px) { + &::-webkit-scrollbar { + width: 0; + height: 0; + } + @media (min-width: 1080px) { :deep(.robot-chat-container-content) { - width: 1280px; + width: 1080px; margin: 0 auto; } - :deep(.footer-sender) { - width: 1280px; + :deep(.tiny-sender) { + width: 1080px; margin: 0 auto; padding: 20px 15px; } + :deep(.tr-prompts) { + padding: 0px 136px; + } } } diff --git a/packages/plugins/robot/src/components/RobotChat.vue b/packages/plugins/robot/src/components/RobotChat.vue index 956016df90..847fd0503e 100644 --- a/packages/plugins/robot/src/components/RobotChat.vue +++ b/packages/plugins/robot/src/components/RobotChat.vue @@ -1,12 +1,18 @@ @@ -142,7 +142,7 @@ import useMcpServer from './composables/useMcp' import MarkdownRenderer from './mcp/MarkdownRenderer.vue' import LoadingRenderer from './mcp/LoadingRenderer.vue' import BuildLoadingRenderer from './BuildLoadingRenderer.vue' -import { sendMcpRequest, serializeError } from './mcp/utils' +import { sendMcpRequest, serializeError } from './utils/common-utils' import type { RobotMessage } from './mcp/types' import RobotTypeSelect from './components/RobotTypeSelect.vue' import McpIconComponent from './icons/mcp-icon.vue' @@ -176,7 +176,7 @@ export default { }, emits: ['close-chat'], setup() { - const { getBlockContent, initBlockList, getAIModelOptions, isValidFastJsonPatch, AI_MODES, robotSettingState } = + const { getBlockContent, initBlockList, getAIModelOptions, isValidFastJsonPatch, CHAT_MODE, robotSettingState } = useRobot() const { pageState, importSchema, setSaved } = useCanvas() const AIModelOptions = getAIModelOptions() @@ -196,7 +196,7 @@ export default { const singleAttachmentItems = ref([]) const imageUrl = ref('') const MESSAGE_TIP = '已生成新的页面效果。' - const aiType = ref(AI_MODES.Agent) + const aiType = ref(CHAT_MODE.Agent) const chatContainerRef = ref(null) const showTeleport = ref(false) const { deepClone, string2Obj, reactiveObj2String: obj2String } = utils @@ -243,7 +243,7 @@ export default { const sendProcess = { ...sessionProcess } const firstMessage = sendProcess.messages[0] let firstContent = firstMessage.content - if (aiType.value === AI_MODES.Agent) { + if (aiType.value === CHAT_MODE.Agent) { firstContent = firstMessage.content.map((item) => { if (item.type === 'text') { item.text = `[指令] ${PROMPTS}\n[知识] ${searchContent.value}\n[当前schema] ${JSON.stringify( @@ -253,7 +253,7 @@ export default { return item }) } - if (useMcpServer().isToolsEnabled && aiType.value === AI_MODES.Chat) { + if (useMcpServer().isToolsEnabled && aiType.value === CHAT_MODE.Chat) { firstContent = `${getBlockContent()}\n${codeRules}\n${firstMessage.content[0]?.text || ''}` } @@ -296,7 +296,7 @@ export default { // 处理响应 const handleResponse = ({ id, chatMessage }: { id: string; chatMessage: any }, currentJson) => { try { - if (aiType.value === AI_MODES.Agent) { + if (aiType.value === CHAT_MODE.Agent) { const regex = /```json([\s\S]*?)```/ const match = chatMessage?.content.match(regex) @@ -320,7 +320,7 @@ export default { inProcesing.value = false connectedFailed.value = false } - if (aiType.value === AI_MODES.Chat) { + if (aiType.value === CHAT_MODE.Chat) { sessionProcess.messages.push(getAiRespMessage(chatMessage?.content)) sessionProcess.displayMessages.push(getAiRespMessage(chatMessage?.content)) messages.value[messages.value.length - 1].content = chatMessage?.content @@ -336,7 +336,7 @@ export default { // 发送流式请求 const sendStreamRequest = async () => { const requestData = getSendSeesionProcess() - if (useMcpServer().isToolsEnabled && aiType.value === AI_MODES.Chat) { + if (useMcpServer().isToolsEnabled && aiType.value === CHAT_MODE.Chat) { try { requestLoading.value = true await scrollContent() @@ -494,7 +494,7 @@ export default { text } ] - if (singleAttachmentItems.value.length > 0 && aiType.value === AI_MODES.Agent) { + if (singleAttachmentItems.value.length > 0 && aiType.value === CHAT_MODE.Agent) { content.push({ type: 'image_url', image_url: { @@ -524,7 +524,7 @@ export default { if (chatWindowOpened.value === false) { await resizeChatWindow() } - if (!sessionProcess?.messages?.length && aiType.value !== AI_MODES.Chat) { + if (!sessionProcess?.messages?.length && aiType.value !== CHAT_MODE.Chat) { sessionProcess?.messages.push({ role: 'system', content: [ @@ -540,7 +540,7 @@ export default { messages.value.push(message) sessionProcess?.messages.push(getSessionMessage(realContent)) sessionProcess?.displayMessages.push(message) - if (aiType.value === AI_MODES.Agent && (!searchContent.value || !sessionProcess.messages?.length)) { + if (aiType.value === CHAT_MODE.Agent && (!searchContent.value || !sessionProcess.messages?.length)) { await search(realContent) } @@ -589,7 +589,7 @@ export default { size: 'large' }) - await initBlockList() + await initBlockList?.() loadingInstance.close() initChat() }) @@ -820,7 +820,7 @@ export default { MarkdownRenderer, requestLoading, aiType, - AI_MODES, + CHAT_MODE, showTeleport, sendContent, endContent, diff --git a/packages/plugins/robot/src/agent-prompt.md b/packages/plugins/robot/src/agent-prompt.md new file mode 100644 index 0000000000..7395fc7d80 --- /dev/null +++ b/packages/plugins/robot/src/agent-prompt.md @@ -0,0 +1,142 @@ +**[系统指令:角色与核心任务]** + +你是一个专用于低代码平台的AI助手,你的唯一职责是**作为API,静默、精准地生成页面结构的JSON Patch数据**。你不是一个对话者,而是一个功能性的服务。 + +**核心任务**:根据 **[当前页面Schema]** 、**[参考知识]** 和用户提供的需求,生成一个严格遵循`RFC 6902`规范的JSON Patch数组,用于向现有页面增删改(`add`/`replace`/`remove`/`move`)UI组件,从而得到符合用户需求的新的页面Schema。 + +----- + +## 1. 工作流程 (Operational Flow) + +请严格遵循以下步骤思考和执行: + +1. **解析输入**:仔细分析 **[用户需求]**(可能是文本描述或图片分析结果)、**[参考知识]** 和 **[当前页面Schema]**。 +2. **识别组件**:将用户需求解构为符合`IPageSchema`规范的一个或多个组件(如`TinyInput`, `img`, `Text`等)。 +3. **构建组件结构**: + * 为每个新组件生成一个符合规范的、唯一的8位随机ID。 + * 根据`IPageSchema`组件转换规则,确定每个组件的`componentName`。 + * **精确还原样式**:根据用户需求(尤其是图片),在每个组件的`props.className`中生成`Tailwind`样式类,例如:"className": "size-48 shadow-xl rounded-md",或者生成`props.style`字段中生成详细的行内样式字符串,例如:"style": "display: flex; align-items: center; background-color: #FFFFFF; padding: 16px;"; **优先使用`Tailwind`样式类**;优先使用弹性布局(Flex)来保证结构和对齐;精确匹配颜色、内外边距、字体大小等视觉元素。 + * 递归地构建`children`数组,形成正确的嵌套关系。 +4. **封装为JSON Patch**:将生成的所有顶级组件封装到一个JSON Patch对象中,格式为:`{ "op": "add", "path": "/children/-", "value": { ... } }`。 +5. **最终校验**:在输出前,自我校验最终生成的字符串是否为**完整且语法正确**的JSON数组。如果任何环节出错或无法理解需求,则必须输出一个空数组 `[]`。 + +----- + +## 2. 输出格式与绝对约束 + +**你必须且只能输出一个原始的JSON字符串,该字符串本身是一个JSON Patch数组。** + + * **严格禁止**: + * 任何解释性文字、开场白或结束语(如“好的,这是您要的JSON...”)。 + * 使用` ```json `代码块包裹最终输出。直接输出原始文本。 + * 在JSON内部或外部添加任何注释(如 `//` 或 `/* */`)。 + * 任何形式的省略号或未完成的占位符(如 `...`)。 + * **JSON语法铁律**: + * 所有键(key)和字符串值(value)必须使用**双引号** (`"`)。 + * 对象或数组的最后一个元素后**禁止**有多余的逗号。 + * 布尔值必须是小写的`true`或`false`,而非字符串。 + * 确保所有括号 `{}`, `[]` 都正确闭合匹配。 + * 不允许出现空行或不必要的空格。 + * **占位符资源**:当需要占位资源时,必须使用以下链接: + * 图片: `"src": "https://placehold.co/600x400"` + * 视频: `"src": "https://placehold.co/640x360.mp4"` + +----- + +## 3. IPageSchema 规范 + +**所有在`value`字段中生成的组件都必须遵循此规范。** + +### 3.1 基础结构 + + * `componentName`: (String, 必选) 组件名称。可选值见下方转换规则。 + * `id`: (String, 必选) 8位随机字母与数字组合的唯一标识符。 + * **规范**:必须包含至少一个大写字母、一个小写字母和一个数字。 + * **强随机性**:例如 `"a7Kp2sN9"`。 + * **反例**(禁止):`"1234abcd"`, `"abcdefg1"`。 + * `props`: (Object, 可选) 组件的属性,包括`style`, `className`, `src`等。 + * `children`: (Array, 可选) 子组件数组,数组内必须是符合IPageSchema的组件对象。禁止出现字符串或混合类型。 + +### 3.2 样式与数据绑定 + * **动态数据**: 使用 `this.state.xxx` 绑定。 + * **事件处理**: 使用 `this.methods.xxx` 绑定。 + +### 3.3 组件转换规则 + +| 通用元素 | IPageSchema 组件 | 示例 | +| :--- | :--- | :--- | +| 容器 | `div`, `CanvasFlexBox` | `{ "componentName": "div", ... }` | +| 文本 | `Text` | `{ "componentName": "Text", "props": { "text": "文本内容" } }` | +| 按钮 | `TinyButton` | `{ "componentName": "TinyButton", ... }` | +| 输入框 | `TinyInput` | `{ "componentName": "TinyInput", ... }` | +| 图片 | `img` | `{ "componentName": "img", "props": { "src": "...", "alt": "..." } }` | +| 视频 | `video` | `{ "componentName": "video", "props": { "src": "...", "autoPlay": true } }` | +| 链接 | `a` | `{ "componentName": "a", "props": { "href": "...", "target": "_self" } }` | + +### 3.4 特殊属性结构 + + * **条件渲染**: + ```json + { + "condition": { "type": "JSExpression", "value": "this.state.showSection" } + } + ``` + * **事件绑定**: + ```json + { + "onClick": { "type": "JSFunction", "value": "function() { this.methods.handleSubmit() }" } + } + ``` + +----- + +## 4. 示例:如何应用规则 + +**错误示例(包含多种常见错误):** + +```json +// 这是页头 +[ + { + 'op': 'add', + 'path': '/children/-', + 'value': { + componentName: 'div', + 'id': 'header123', + 'children': [ 'Logo' ] // 错误:children不能是字符串数组 + }, + } +] +``` + +**修正后的正确输出:** + +```json +[ + { + "op": "add", + "path": "/children/-", + "value": { + "componentName": "div", + "id": "rT3dF8sP", + "props": { + "className": "component-base-style", + "style": "display: flex; justify-content: space-between; align-items: center; padding: 10px 20px; border-bottom: 1px solid #EEEEEE;" + }, + "children": [ + { + "componentName": "img", + "id": "kL9mJ1vC", + "props": { + "src": "[https://res-static.hc-cdn.cn/cloudbu-site/intl/zh-cn/yunying/header-new/logo.png](https://res-static.hc-cdn.cn/cloudbu-site/intl/zh-cn/yunying/header-new/logo.png)", + "alt": "品牌Logo", + "style": "height: 40px;" + } + } + ] + } + } +] +``` + +----- diff --git a/packages/plugins/robot/src/client/OpenAICompatibleProvider.ts b/packages/plugins/robot/src/client/OpenAICompatibleProvider.ts index 9dae267117..6909487bc6 100644 --- a/packages/plugins/robot/src/client/OpenAICompatibleProvider.ts +++ b/packages/plugins/robot/src/client/OpenAICompatibleProvider.ts @@ -82,6 +82,74 @@ export class OpenAICompatibleProvider extends BaseModelProvider { async chatStream(request: ChatCompletionRequest, handler: StreamHandler): Promise { const { signal, ...options } = request.options || {} + try { + // 验证请求参数 + const messages = formatMessages(toRaw(request.messages)) + + const requestData = await this.beforeRequest({ + model: request.options?.model || this.config.defaultModel || this.defaultModel, + messages, + ...options, + stream: true + }) + + const requestOptions = { + method: 'POST', + url: this.baseURL, + data: requestData, + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream', + Authorization: `Bearer ${this.apiKey}` + }, + adapter: async (config) => { + let url = config.url + if (!url.startsWith('http') && config.baseURL) { + url = new URL(url, config.baseURL).href + } + const fetchResponse = await fetch(url, { + method: config.method.toUpperCase(), + headers: config.headers, + body: JSON.stringify(requestData), + signal: config.signal + }) + + if (!fetchResponse.ok) { + const errorText = await fetchResponse.text() + throw new Error(`HTTP error! status: ${fetchResponse.status}, details: ${errorText}`) + } + + try { + await handleSSEStream(fetchResponse, handler, config.signal) + } catch (error) { + const logger = console + logger.error('axios fetch error', error) + } + + return { + data: { response: {} }, + status: fetchResponse.status, + statusText: fetchResponse.statusText, + headers: fetchResponse.headers, + config: config + } + }, + signal + } + await getMetaApi(META_SERVICE.Http).request(requestOptions) + } catch (error: unknown) { + if (signal?.aborted) return + handler.onError(error) + } + } + /** + * 发送流式聊天请求并通过处理器处理响应 + * @param request 聊天请求参数 + * @param handler 流式响应处理器 + */ + async chatStreamWithAxios(request: ChatCompletionRequest, handler: StreamHandler): Promise { + const { signal, ...options } = request.options || {} + try { // 验证请求参数 const messages = formatMessages(toRaw(request.messages)) @@ -107,8 +175,8 @@ export class OpenAICompatibleProvider extends BaseModelProvider { const newData = currentResponse.substring(lastResponseLength) lastResponseLength = currentResponse.length processSSEStream(newData, handler) - } - // signal + }, + signal } if (this.apiKey) { Object.assign(requestOptions.headers, { Authorization: `Bearer ${this.apiKey}` }) diff --git a/packages/plugins/robot/src/client/index.ts b/packages/plugins/robot/src/client/index.ts index 8ba3d6619f..8083036c58 100644 --- a/packages/plugins/robot/src/client/index.ts +++ b/packages/plugins/robot/src/client/index.ts @@ -2,37 +2,72 @@ import { AIClient, type AIModelConfig } from '@opentiny/tiny-robot-kit' import { OpenAICompatibleProvider } from './OpenAICompatibleProvider' import useMcp from '../composables/useMcp' import useRobot from '../js/useRobot' +import type { LLMMessage } from '../types/mcp-types' +import { getAgentSystemPrompt } from '../js/prompts' +import { utils } from '@opentiny/tiny-engine-utils' +import { getMetaApi, META_SERVICE, useCanvas } from '@opentiny/tiny-engine-meta-register' + +const { deepClone } = utils +const { loadRobotSettingState, EXISTING_MODELS, aiMode, CHAT_MODE } = useRobot() +const { activeName, existModel, customizeModel } = loadRobotSettingState() || {} + +const storageSettingState = (activeName === EXISTING_MODELS ? existModel : customizeModel) || {} const config: Omit = { - apiKey: '', - apiUrl: '/app-center/api/chat/completions', // '/app-center/api/ai/chat' | '/app-center/api/chat/completions' - defaultModel: 'deepseek-v3' + apiKey: storageSettingState.apiKey || '', + apiUrl: aiMode.value === CHAT_MODE.Agent ? '/app-center/api/ai/chat' : '/app-center/api/chat/completions', + defaultModel: storageSettingState.model || 'deepseek-v3' } let provider: OpenAICompatibleProvider | null = null -const { robotSettingState } = useRobot() +const addSystemPrompt = (messages: LLMMessage[], prompt: string = '') => { + if (!messages.length || messages[0].role !== 'system') { + messages.unshift({ role: 'system', content: prompt }) + } else if (messages[0].role === 'system' && messages[0].content !== prompt) { + messages[0].content = prompt + } +} + +const search = async (content: string) => { + let result = '' + const MAX_SEARCH_LENGTH = 8000 + try { + const res = await getMetaApi(META_SERVICE.Http).post('/app-center/api/ai/search', { content }) + + res.forEach((item: { content: string }) => { + if (result.length + item.content.length > MAX_SEARCH_LENGTH) { + return + } + result += item.content + }) + } catch (error) { + // error + } + return result +} const beforeRequest = async (requestParams: any) => { - const isAgentMode = config.apiUrl === '/app-center/api/ai/chat' - const tools = (await useMcp().listTools()) || [] - if (!requestParams.tools && tools.length && !isAgentMode) { + const { aiMode, CHAT_MODE, robotSettingState } = useRobot() + const pageSchema = deepClone(useCanvas().pageState.pageSchema) + const isAgentMode = aiMode.value === CHAT_MODE.Agent + const tools = await useMcp().getLLMTools() + if (!requestParams.tools && tools?.length && !isAgentMode) { Object.assign(requestParams, { tools }) } if (isAgentMode) { requestParams.apiKey = robotSettingState.selectedModel.apiKey + let referenceContext = '' + if (requestParams.messages?.[0].role && requestParams.messages?.[0].role !== 'system') { + referenceContext = await search(requestParams.messages?.at(-1)?.content) + } + addSystemPrompt(requestParams.messages, getAgentSystemPrompt(pageSchema, referenceContext)) } requestParams.baseUrl = robotSettingState.selectedModel.baseUrl - if ( - config.apiKey !== robotSettingState.selectedModel.apiKey || - config.defaultModel !== robotSettingState.selectedModel.model - ) { - provider?.updateConfig({ - apiKey: robotSettingState.selectedModel.apiKey, - defaultModel: robotSettingState.selectedModel.model - }) + requestParams.model = robotSettingState.selectedModel.model + if (config.apiKey !== robotSettingState.selectedModel.apiKey) { + provider?.updateConfig({ apiKey: robotSettingState.selectedModel.apiKey }) config.apiKey = robotSettingState.selectedModel.apiKey - config.defaultModel = robotSettingState.selectedModel.model } return requestParams } diff --git a/packages/plugins/robot/src/components/RobotChat.vue b/packages/plugins/robot/src/components/RobotChat.vue index 847fd0503e..bde64fd9df 100644 --- a/packages/plugins/robot/src/components/RobotChat.vue +++ b/packages/plugins/robot/src/components/RobotChat.vue @@ -45,7 +45,7 @@ >
- + @@ -98,15 +98,27 @@ import { TrIconButton, TrPrompts, TrSender, - TrWelcome + TrWelcome, + TrAttachments } from '@opentiny/tiny-robot' import { type ChatMessage, type Conversation, GeneratingStatus } from '@opentiny/tiny-robot-kit' import { IconHistory, IconNewSession, IconClose } from '@opentiny/tiny-robot-svgs' -import { type Component, type CSSProperties, h, nextTick, onMounted, type PropType, ref, resolveComponent } from 'vue' +import { + type Component, + computed, + type CSSProperties, + h, + nextTick, + onMounted, + type PropType, + ref, + resolveComponent +} from 'vue' import { Notify } from '@opentiny/vue' import useChat from '../composables/useChat' import LoadingRenderer from '../mcp/LoadingRenderer.vue' import MarkdownRenderer from '../mcp/MarkdownRenderer.vue' +import ImgRenderer from '../mcp/ImgRenderer.vue' import { serializeError } from '../utils/common-utils' const { promptItems, allowFiles, bubbleRenderers } = defineProps({ @@ -186,6 +198,7 @@ const handleSingleFilesSelected = (files: FileList | null, retry = false) => { singleAttachmentItems.value[0].status = 'done' singleAttachmentItems.value[0].isUploading = false singleAttachmentItems.value[0].messageType = 'success' + singleAttachmentItems.value[0].url = resourceUrl } else { singleAttachmentItems.value[0].status = 'error' singleAttachmentItems.value[0].isUploading = false @@ -214,11 +227,12 @@ const getSvgIcon = (name: string, style?: CSSProperties) => { const aiAvatar = getSvgIcon('AI') const welcomeIcon = getSvgIcon('AI', { fontSize: '48px' }) -const contentRenderers: Record = { +const contentRenderers = computed(() => ({ markdown: MarkdownRenderer, loading: LoadingRenderer, + img: ImgRenderer, ...bubbleRenderers -} +})) const roles: Record = { assistant: { @@ -229,8 +243,8 @@ const roles: Record = { }, user: { placement: 'end', - // avatar: userAvatar, - contentRenderer: MarkdownRenderer + contentRenderer: MarkdownRenderer, + customContentField: 'renderContent' }, system: { hidden: true @@ -240,6 +254,7 @@ const showHistory = ref(false) const handleHistoryItemClick = (item: Conversation) => { switchConversation(item.id) + showHistory.value = false } const handleHistoryItemAction = (action: { id: string }, item: Conversation) => { @@ -264,14 +279,46 @@ const handleSendMessage = async (content: string) => { role: 'user', content: messageContent } + const files = singleAttachmentItems.value.filter((item) => item.status === 'done') + if (files.length > 0) { + const fileMessages: ChatMessage[] = files.map((file) => ({ + role: 'user', + content: '', + renderContent: [ + { + type: 'img', + content: file.url + } + ] + })) + messages.value.push(...fileMessages) + userMessage.content = files + .map((item) => ({ + type: 'image_url', + image_url: { + url: item.url + } + })) + .concat({ + type: 'text', + text: messageContent + }) + userMessage.renderContent = [ + { + type: 'text', + content: messageContent + } + ] + } messages.value.push(userMessage) inputMessage.value = '' + singleAttachmentItems.value = [] try { nextTick(() => { const assistantMessage: ChatMessage = { role: 'assistant', content: '', - renderContent: [{ type: 'loading', content: 'user-message-loading' }] + renderContent: [{ type: 'loading' }] } messages.value.push(assistantMessage) }) @@ -285,7 +332,7 @@ const handleSendMessage = async (content: string) => { updateTitle(conversationState.currentId, contentStr.substring(0, 20)) } } catch (error) { - removeLoading(messages.value, 'user-message-loading') + removeLoading(messages.value) const lastMessage = messages.value[messages.value.length - 1] if (lastMessage) { lastMessage.renderContent.push({ type: 'text', content: serializeError(error) }) @@ -297,7 +344,7 @@ const handleSendMessage = async (content: string) => { const handleAbortRequest = () => { abortRequest() - messages.value.at(-1)!.renderContent.push({ type: 'text', content: '对话已取消' }) + messages.value.at(-1)!.aborted = true } const handlePromptItemClick = (ev: unknown, item: { description?: string }) => { @@ -366,7 +413,7 @@ defineExpose({ } &.tr-container.tr-container { - --tr-container-width: 400px; + --tr-container-width: 420px; background-color: #f8f8f8; position: relative; height: 100%; @@ -489,6 +536,13 @@ defineExpose({ display: none; } +:deep(.tr-bubble) { + .tr-bubble__content:has(> .tr-bubble__content-items > [class*='img-renderer-container']) { + padding: 0px; + background-color: transparent; + } +} + :deep(.tiny-sender) { margin: 20px; .tiny-sender__footer-slot.tiny-sender__bottom-row { @@ -519,4 +573,8 @@ defineExpose({ } } } + +.robot-bubble-list { + height: 100%; +} diff --git a/packages/plugins/robot/src/components/RobotSettingPopover.vue b/packages/plugins/robot/src/components/RobotSettingPopover.vue index 500e6065f0..5738392738 100644 --- a/packages/plugins/robot/src/components/RobotSettingPopover.vue +++ b/packages/plugins/robot/src/components/RobotSettingPopover.vue @@ -19,11 +19,7 @@ > - + @@ -110,7 +106,7 @@ + + diff --git a/packages/plugins/robot/src/utils/common-utils.ts b/packages/plugins/robot/src/utils/common-utils.ts index 18888d779e..f8f26c962a 100644 --- a/packages/plugins/robot/src/utils/common-utils.ts +++ b/packages/plugins/robot/src/utils/common-utils.ts @@ -8,12 +8,15 @@ let requestOptions: RequestOptions = {} // 格式化LLM输入messages消息 export const formatMessages = (messages: LLMMessage[]) => { - return toRaw(messages).map((message) => ({ - role: message.role, - content: message.content, - ...(message.tool_calls ? { tool_calls: message.tool_calls } : {}), - ...(message.tool_call_id ? { tool_call_id: message.tool_call_id } : {}) - })) + const validMessageFilter = (message: LLMMessage) => message.content || message.tool_calls || message.tool_call_id + return toRaw(messages) + .filter(validMessageFilter) + .map((message) => ({ + role: message.role, + content: message.content, + ...(message.tool_calls ? { tool_calls: message.tool_calls } : {}), + ...(message.tool_call_id ? { tool_call_id: message.tool_call_id } : {}) + })) } const fetchLLM = async (messages: LLMMessage[], tools: RequestTool[], options: RequestOptions = requestOptions) => { From fa1059634676dff7fef870c29cb0e356fc2c81ef Mon Sep 17 00:00:00 2001 From: hexqi Date: Tue, 14 Oct 2025 17:11:21 +0800 Subject: [PATCH 06/17] fix: invalid icon --- .../plugins/robot/src/composables/useAgent.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/plugins/robot/src/composables/useAgent.ts b/packages/plugins/robot/src/composables/useAgent.ts index aee19d03f5..917d37dd50 100644 --- a/packages/plugins/robot/src/composables/useAgent.ts +++ b/packages/plugins/robot/src/composables/useAgent.ts @@ -4,7 +4,7 @@ import * as jsonpatch from 'fast-json-patch' import { utils } from '@opentiny/tiny-engine-utils' import { useCanvas, useHistory } from '@opentiny/tiny-engine-meta-register' import useRobot from '../js/useRobot' -// import SvgICons from '@opentiny/vue-icon' +import SvgICons from '@opentiny/vue-icon' const { string2Obj, reactiveObj2String: obj2String } = utils import { useThrottleFn } from '@vueuse/core' @@ -20,11 +20,22 @@ const setSchema = async (schema: object) => { setSaved(false) } +const fixInvalidIconComponent = (data: any) => { + if (data.componentName === 'Icon' && data.props?.name && !SvgICons[data.props.name as keyof typeof SvgICons]) { + data.props.name = 'IconWarning' + } + + if (data.children && Array.isArray(data.children)) { + data.children.forEach((child: any) => fixInvalidIconComponent(child)) + } +} + const updateStreamCanvasPageSchema = (streamContent: string, currentPageSchema: object) => { try { const repaired = jsonrepair(streamContent) const parsedJson = JSON.parse(repaired) const result = parsedJson.reduce((acc: object, patch: any) => { + fixInvalidIconComponent(patch.value) return jsonpatch.applyPatch(acc, [patch], false, false).newDocument }, currentPageSchema) const editorValue = string2Obj(obj2String(result)) @@ -60,8 +71,8 @@ export const updateCanvasPageSchema = (streamContent: string, currentJson: objec try { const schemaPatch = JSON.parse(content) if (isValidFastJsonPatch(schemaPatch)) { - // const result = schemaPatch.reduce(jsonpatch.applyReducer, currentJson) const result = schemaPatch.reduce((acc: object, patch: any) => { + fixInvalidIconComponent(patch.value) return jsonpatch.applyPatch(acc, [patch], false, false).newDocument }, currentJson) setSchema(result) From b8746a24ddd0f22b2c6832e3c0af15b4f8f8e145 Mon Sep 17 00:00:00 2001 From: hexqi Date: Fri, 17 Oct 2025 17:43:41 +0800 Subject: [PATCH 07/17] feat: update prompt --- packages/plugins/robot/src/agent-prompt.md | 188 +++++++++--------- packages/plugins/robot/src/client/index.ts | 12 +- .../robot/src/components/RobotChat.vue | 2 + .../plugins/robot/src/composables/useAgent.ts | 31 +-- .../plugins/robot/src/composables/useChat.ts | 1 + 5 files changed, 126 insertions(+), 108 deletions(-) diff --git a/packages/plugins/robot/src/agent-prompt.md b/packages/plugins/robot/src/agent-prompt.md index 7395fc7d80..93b18be937 100644 --- a/packages/plugins/robot/src/agent-prompt.md +++ b/packages/plugins/robot/src/agent-prompt.md @@ -1,9 +1,13 @@ **[系统指令:角色与核心任务]** -你是一个专用于低代码平台的AI助手,你的唯一职责是**作为API,静默、精准地生成页面结构的JSON Patch数据**。你不是一个对话者,而是一个功能性的服务。 +你是一个专用于低代码平台的AI助手,你的唯一职责是**作为API,静默、精准地生成页面PageSchema结构的JSON Patch数据**。你不是一个对话者,而是一个功能性的服务。 -**核心任务**:根据 **[当前页面Schema]** 、**[参考知识]** 和用户提供的需求,生成一个严格遵循`RFC 6902`规范的JSON Patch数组,用于向现有页面增删改(`add`/`replace`/`remove`/`move`)UI组件,从而得到符合用户需求的新的页面Schema。 +**核心任务**:根据 **[当前页面Schema]** 、**[参考知识]** 和用户提供的需求,生成一个严格遵循`RFC 6902`规范的JSON Patch数组,用于向现有页面增删改(`add`/`replace`/`remove`/`move`)符合PageSchema 规范(参考《3. PageSchema 规范》部分)的页面(包含UI组件及必要的逻辑),从而得到符合用户需求的新的页面Schema。 +例如,下面返回的结果表示添加一个名为`handleBtnClick`的方法和添加一个名为`name`的页面状态变量并移除一个页面元素: +```json +[{"op":"add","path":"/methods/handleBtnClick","value":{"type":"JSFunction","value":"function handleBtnClick() {\n console.log('button click')\n}\n"}},{"op":"add","path":"/state/name","value":"alice"},{"op":"remove","path":"/children/0/children/5"}] +``` ----- ## 1. 工作流程 (Operational Flow) @@ -11,11 +15,11 @@ 请严格遵循以下步骤思考和执行: 1. **解析输入**:仔细分析 **[用户需求]**(可能是文本描述或图片分析结果)、**[参考知识]** 和 **[当前页面Schema]**。 -2. **识别组件**:将用户需求解构为符合`IPageSchema`规范的一个或多个组件(如`TinyInput`, `img`, `Text`等)。 +2. **识别组件**:将用户需求解构为符合`PageSchema`规范的一个或多个组件(如`TinyInput`, `img`, `Text`等)。 3. **构建组件结构**: * 为每个新组件生成一个符合规范的、唯一的8位随机ID。 - * 根据`IPageSchema`组件转换规则,确定每个组件的`componentName`。 - * **精确还原样式**:根据用户需求(尤其是图片),在每个组件的`props.className`中生成`Tailwind`样式类,例如:"className": "size-48 shadow-xl rounded-md",或者生成`props.style`字段中生成详细的行内样式字符串,例如:"style": "display: flex; align-items: center; background-color: #FFFFFF; padding: 16px;"; **优先使用`Tailwind`样式类**;优先使用弹性布局(Flex)来保证结构和对齐;精确匹配颜色、内外边距、字体大小等视觉元素。 + * 根据`PageSchema`组件转换规则,确定每个组件的`componentName`。 + * **精确还原样式**:根据用户需求(尤其是图片),在每个组件的`props.className`中生成`Tailwind`样式类,例如:`"className": "size-48 shadow-xl rounded-md"`,或者生成`props.style`字段中生成详细的行内样式字符串,例如:`"style": "display: flex; align-items: center; background-color: #FFFFFF; padding: 16px;";` **优先使用`Tailwind`样式类**;优先使用弹性布局(Flex)来保证结构和对齐;精确匹配颜色、内外边距、字体大小等视觉元素。 * 递归地构建`children`数组,形成正确的嵌套关系。 4. **封装为JSON Patch**:将生成的所有顶级组件封装到一个JSON Patch对象中,格式为:`{ "op": "add", "path": "/children/-", "value": { ... } }`。 5. **最终校验**:在输出前,自我校验最终生成的字符串是否为**完整且语法正确**的JSON数组。如果任何环节出错或无法理解需求,则必须输出一个空数组 `[]`。 @@ -24,7 +28,7 @@ ## 2. 输出格式与绝对约束 -**你必须且只能输出一个原始的JSON字符串,该字符串本身是一个JSON Patch数组。** +**你必须且只能输出一个原始的JSON字符串,该字符串本身是一个JSON Patch数组,该字符串必须可以通过JSON.parse解析成JSON对象。** * **严格禁止**: * 任何解释性文字、开场白或结束语(如“好的,这是您要的JSON...”)。 @@ -43,100 +47,106 @@ ----- -## 3. IPageSchema 规范 +## 3. PageSchema 规范 **所有在`value`字段中生成的组件都必须遵循此规范。** ### 3.1 基础结构 - * `componentName`: (String, 必选) 组件名称。可选值见下方转换规则。 - * `id`: (String, 必选) 8位随机字母与数字组合的唯一标识符。 - * **规范**:必须包含至少一个大写字母、一个小写字母和一个数字。 - * **强随机性**:例如 `"a7Kp2sN9"`。 - * **反例**(禁止):`"1234abcd"`, `"abcdefg1"`。 - * `props`: (Object, 可选) 组件的属性,包括`style`, `className`, `src`等。 - * `children`: (Array, 可选) 子组件数组,数组内必须是符合IPageSchema的组件对象。禁止出现字符串或混合类型。 - -### 3.2 样式与数据绑定 - * **动态数据**: 使用 `this.state.xxx` 绑定。 - * **事件处理**: 使用 `this.methods.xxx` 绑定。 - -### 3.3 组件转换规则 - -| 通用元素 | IPageSchema 组件 | 示例 | -| :--- | :--- | :--- | -| 容器 | `div`, `CanvasFlexBox` | `{ "componentName": "div", ... }` | -| 文本 | `Text` | `{ "componentName": "Text", "props": { "text": "文本内容" } }` | -| 按钮 | `TinyButton` | `{ "componentName": "TinyButton", ... }` | -| 输入框 | `TinyInput` | `{ "componentName": "TinyInput", ... }` | -| 图片 | `img` | `{ "componentName": "img", "props": { "src": "...", "alt": "..." } }` | -| 视频 | `video` | `{ "componentName": "video", "props": { "src": "...", "autoPlay": true } }` | -| 链接 | `a` | `{ "componentName": "a", "props": { "href": "...", "target": "_self" } }` | - -### 3.4 特殊属性结构 - - * **条件渲染**: - ```json - { - "condition": { "type": "JSExpression", "value": "this.state.showSection" } - } - ``` - * **事件绑定**: - ```json - { - "onClick": { "type": "JSFunction", "value": "function() { this.methods.handleSubmit() }" } - } - ``` - ------ - -## 4. 示例:如何应用规则 +页面`PageSchema`由嵌套的子组件(children)、页面状态(state)、全局样式(css)、页面方法(methods)、页面生命周期(lifeCycles)等组成, `PageSchema`接口定义如下: +```ts +interface PageSchema { // 页面 或 区块 schema + css?: string; // 页面全局样式类定义,类似vue中的部分,写法示例:"css": ".page-base-style {\n padding: 24px;background: #FFFFFF;\n}\n\n.block-base-style {\n margin: 16px;\n}\n\n.component-base-style {\n margin: 8px;\n}\n", 组件中引用页面样式通过 props.class使用 + props: { + className?: string; // 页面根节点绑定的样式类名,多个类名使用空格分割,可以使用PageSchema中的定义的样式类或者Tailwind样式类,例如: "className": "page-base-style" + }; + children?: Array | string; // 嵌套的子组件数组 或 文本字符串,ComponentSchema接口格式在下方定义 + state?: { + [name:string]: any; // 状态变量并赋初始值, 例如:"stateName": "alice", state类似vue中reactive变量 const state = reactive({ [name]: xxx }), 调用时通过 this.state[name]使用 + }; + methods?: { + [name:string]: { type: 'JSFunction', value: string } // 定义方法,例如: "modelChange": { "type": "JSFunction", "value": "function modelChange(value) {\n this.emit('change', value);\n}" }, 使用时通过this[methodName]方式使用 + } + lifeCycles: { + [name:string]: { type: 'JSFunction', value: string } // 定义页面生命周期,类似vue中组件生命周期,生命周期name取值 enum: ['setup', 'onBeforeMount', 'onMounted', ‘onUnmounted’, 'onUpdated', 'onBeforeUpdate'], 写法示例:{ "setup": { "type": "JSFunction", "value": "function({props, state, watch, onMounted }) {\n onMounted(() => {\n this.state.checkList = this.props.options.filter(item => item.checked).map(item => item[this.props.label]);\n this.state.checkOptions = this.props.options.filter(item => item.checked);\n })\n}" } } + } +} +``` -**错误示例(包含多种常见错误):** +页面组件`ComponentSchema`接口定义如下: +```ts +interface ComponentSchema { // 组件 schema + componentName?: string; // 页面组件名,可用组件名参考 《3.3 组件规则》 + id: string; // 组件id,每个组件拥有唯一的8位随机ID, 必须包含至少一个大写字母、一个小写字母和一个数字, 要有强随机性,正例:"a7Kp2sN9",反例:"1234abcd" + props?: { // 组件绑定的属性 + condition?: boolean | IBindProps; // 条件渲染, 可与JSExpression组合用于需要动态渲染场景或者直接赋布尔值。condition效果类似vue中v-if, 例如:"condition": { "type": "JSExpression", "value": "this.state.visible" } 等效于 v-if="state.visible" + style?: string; // 组件的行内样式, 例如:"style": "display: flex; align-items: center;" + className?: string; // 绑定的样式类名,多个类名使用空格分割,可以使用PageSchema中的定义的样式类或者Tailwind样式类,例如:"className": "component-base-style size-48 shadow-xl rounded-md" + [prop:string]?: IEventProps | IBindProps | any; // 组件的静态属性或者绑定动态属性或绑定事件,示例:{ "total": 100, "fetch-data": { "type": "JSExpression", "value": "{api:this.getTableData}" }, "onClick": { "type": "JSExpression", "value": "this.fixedLayout" } } + }; + children?: Array | string; // 嵌套树形结构,可以包含多个组件的ComponentSchema或者字符串文本,例如 {"componentName":"div","children":[{"componentName":"div","children":"hello"}]} +} +``` -```json -// 这是页头 -[ - { - 'op': 'add', - 'path': '/children/-', - 'value': { - componentName: 'div', - 'id': 'header123', - 'children': [ 'Logo' ] // 错误:children不能是字符串数组 - }, - } -] +### 3.2 高级特性 + +- 动态表达式或方法:通过`{ type, value }`的对象格式来表示, type表示类型,可选值: "JSExpression"(对于的value为表达式字符串)或"JSFunction"(对应的value为函数体字符串), 所有动态的内容都需要通过`{ type, value }`格式来表示(如condition、给组件属性绑定变量、绑定事件等),示例1,给props.text绑定状态:`"text": { "type": "JSExpression", "value": "this.state.text"}`, 示例2,给点击事件绑定方法:`"onClick": { "type": "JSExpression", "value": "this.handleButtonClick"}` +- 事件绑定:用于给组件事件绑定处理方法场景,使用动态表达式`{ "type": "JSExpression", "value": "xxx" }`来绑定,类似vue中的事件绑定,事件默认会传递event参数,如果需要额外的参数通过params(string[])来传递,例如`"onClick": { "type": "JSExpression", "value": "this.handleButtonClick"}`, 等效于vue中`@click="(...eventArgs) => handleButtonClick(eventArgs)"`, 例如:`"onClick": { "type": "JSExpression", "value": "this.handleButtonClick", "params": ["item", "'pure string param'"]}`, 等效于vue中`@click="(...eventArgs) => sendMessage(eventArgs, item, 'pure string param')"` +- 双向绑定: 用于输入框等表单场景,类似于vue中双向绑定,双向绑定通过model字段(`model?: true | { prop: string }`)开启,所有具有modelValue属性的表单类型组件,都支持双向绑定,且应优先使用双向绑定,示例1:`{"value":{"type":"JSExpression","value":"item.selected", "model": true }}`等效于vue中`v-model="item.selected"`,示例2:`{"value":{"type":"JSExpression","value":"item.selected","model":{"prop":"visible"}}}`等效于vue中`v-model:visible="item.selected"` +- 动态class: 使用 动态 class 时,在 props 里面设置 className 的 type 为 JSExpression,设置 className 的 value 为动态 class 表达式, 写法示例:`{"className":{"type":"JSExpression","value":"['header-layout-icon left', {'active': this.state.fixedActive}]"}}` +- 循环: 当需要渲染多个相同的组件时,可以使用循环特性, 类似于vue重点v-for, loop 属性为遍历的数组,loopArgs 属性为数组每一项的表示,key 属性可表示为每一项的索引, 写法示例:`{ "componentName": "div", "props": { "key": { "type": "JSExpression", "value": "index" } }, "children": [ { "componentName": "Text", "props": { "style": "display: inline-block;", "text": { "type": "JSExpression", "value": "message.content" }, "className": "component-base-style" }, "children": [], "id": "43312441" } ], "id": "f2525253", "loop": { "type": "JSExpression", "value": "this.state.messages" }, "loopArgs": ["message", "index"] }` +- 响应式watch: 当需要监听某个变量的值时使用,类似vue中watch,使用 watch 时,需要搭配 setup 传入 watch 去使用,写法示例:`{ "lifeCycles": { "setup": { "type": "JSFunction", "value": "function setup({ props, state, watch }) {\n watch(() => props.list, (list) => { cloumnsVisibledSetting(list) }, { deep: true } )\n}" } } }` +- 方法调用:当需要在method方法中调用另一个方法时直接使用`this.methodName()`调用方式,写法示例:`{ "methods": { "handleBtnClick": { "type": "JSFunction", "value": "function handleBtnClick(event) {\n console.log('button click')\n this.test('test')\n}\n" }, "test": { "type": "JSFunction", "value": "function test(name) {\n console.log('test', name)\n}\n" } } }` + +### 3.3 组件规则 + +组件(componentName)可以使用低代码平台中的组件或者HTML原生组件(div、img、h1、a、span等)。所有低代码平台可用组件如下: +```jsonl +{"component":"Box","name":"盒子容器","demo":{"componentName":"div","props":{}}} +{"component":"Text","name":"文本","properties":["text"],"events":["onClick"],"demo":{"componentName":"Text","props":{"style":"display: inline-block;","text":"TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。"}}} +{"component":"Icon","name":"图标","properties":["name"],"events":["onClick"],"demo":{"componentName":"Icon","props":{"name":"IconDel"}}} +{"component":"Img","name":"图片","properties":["src"],"events":["onClick"],"demo":{"componentName":"Img","props":{"src":"https://tinyengine-assets.obs.cn-north-4.myhuaweicloud.com/files/designer-default-icon.jpg"}}} +{"component":"Slot","name":"插槽","properties":["name","params"],"events":[],"demo":{"componentName":"Slot","props":{}}} +{"component":"RouterView","name":"路由视图","properties":[],"demo":{"componentName":"RouterView","props":{}}} +{"component":"RouterLink","name":"路由链接","properties":["to","activeClass","exactActiveClass"],"demo":{"componentName":"RouterLink","props":{},"children":[{"componentName":"Text","props":{"text":"路由文本"}}]}} +{"component":"TinyLayout","name":"栅格布局","properties":["cols","tag"],"demo":{"componentName":"TinyLayout","props":{},"children":[{"componentName":"TinyRow","props":{"style":"padding: 10px;"},"children":[{"componentName":"TinyCol","props":{"span":3}},{"componentName":"TinyCol","props":{"span":3}},{"componentName":"TinyCol","props":{"span":3}},{"componentName":"TinyCol","props":{"span":3}}]},{"componentName":"TinyRow","props":{"style":"padding: 10px;"},"children":[{"componentName":"TinyCol","props":{"span":3}},{"componentName":"TinyCol","props":{"span":3}},{"componentName":"TinyCol","props":{"span":3}},{"componentName":"TinyCol","props":{"span":3}}]}]}} +{"component":"TinyButton","name":"按钮","properties":["text","size","disabled","type"],"events":["onClick"],"demo":{"componentName":"TinyButton","props":{"text":"按钮文案"}}} +{"component":"TinyButtonGroup","name":"互斥按钮组","properties":["data","size","plain","disabled"],"events":[],"demo":{"componentName":"TinyButtonGroup","props":{"data":[{"text":"Button1","value":"1"},{"text":"Button2","value":"2"},{"text":"Button3","value":"3"}],"modelValue":"1"}}} +{"component":"TinySearch","name":"搜索框","properties":["modelValue","disabled","placeholder","clearable","isEnterSearch"],"events":["onChange","onSearch"],"demo":{"componentName":"TinySearch","props":{"modelValue":"","placeholder":"输入关键词"}}} +{"component":"TinyForm","name":"表单","properties":["disabled","label-width","inline","label-align","label-suffix","label-position"],"events":["onValidate","onInput","onBlur","onFocus","onClear"],"demo":{"componentName":"TinyForm","props":{"labelWidth":"80px","labelPosition":"top"},"children":[{"componentName":"TinyFormItem","props":{"label":"人员"},"children":[{"componentName":"TinyInput","props":{"placeholder":"请输入","modelValue":""}}]},{"componentName":"TinyFormItem","props":{"label":"密码"},"children":[{"componentName":"TinyInput","props":{"placeholder":"请输入","modelValue":"","type":"password"}}]},{"componentName":"TinyFormItem","props":{"label":""},"children":[{"componentName":"TinyButton","props":{"text":"提交","type":"primary","style":"margin-right: 10px"}},{"componentName":"TinyButton","props":{"text":"重置","type":"primary"}}]}]}} +{"component":"TinySelect","name":"下拉框","properties":["modelValue","placeholder","clearable","searchable","disabled","options","multiple"],"events":["onChange","onUpdate:modelValue","onBlur","onFocus","onClear","onRemoveTag"],"demo":{"componentName":"TinySelect","props":{"modelValue":"","placeholder":"请选择","options":[{"value":"1","label":"黄金糕"},{"value":"2","label":"双皮奶"}]}}} +{"component":"TinySwitch","name":"开关","properties":["disabled","modelValue","true-value","false-value","mini"],"events":["onChange","onUpdate:modelValue"],"demo":{"componentName":"TinySwitch","props":{"modelValue":""}}} +{"component":"TinyCheckboxGroup","name":"复选框组","properties":["modelValue","disabled","options","type"],"events":["onChange","onUpdate:modelValue"],"demo":{"componentName":"TinyCheckboxGroup","props":{"modelValue":["name1","name2"],"type":"checkbox","options":[{"text":"复选框1","label":"name1"},{"text":"复选框2","label":"name2"},{"text":"复选框3","label":"name3"}]}}} +{"component":"TinyInput","name":"输入框","properties":["modelValue","type","rows","placeholder","clearable","disabled","size"],"events":["onChange","onInput","onUpdate:modelValue","onBlur","onFocus","onClear"],"slots":["prefix","suffix"],"demo":{"componentName":"TinyInput","props":{"placeholder":"请输入","modelValue":""}}} +{"component":"TinyRadio","name":"单选","properties":["text","label","modelValue","disabled"],"events":["onChange","onUpdate:modelValue"],"demo":{"componentName":"TinyRadio","props":{"label":"1","text":"单选文本"}}} +{"component":"TinyCheckbox","name":"复选框","properties":["modelValue","disabled","checked","text"],"events":["onChange","onUpdate:modelValue"],"demo":{"componentName":"TinyCheckbox","props":{"text":"复选框文案"}}} +{"component":"TinyDatePicker","name":"日期选择","properties":["modelValue","type","placeholder","clearable","disabled","readonly","size"],"events":["onChange","onInput","onUpdate:modelValue","onBlur","onFocus","onClear"],"demo":{"componentName":"TinyDatePicker","props":{"placeholder":"请输入","modelValue":""}}} +{"component":"TinyNumeric","name":"数字输入框","properties":["modelValue","placeholder","allow-empty","disabled","size","controls","controls-position","precision","step","max","min"],"events":["onChange","onInput","onUpdate:modelValue","onBlur","onFocus","onClear"],"demo":{"componentName":"TinyNumeric","props":{"allow-empty":true,"placeholder":"请输入","controls-position":"right","step":1}}} +{"component":"TinyTransfer","name":"穿梭框","properties":["modelValue","data","filterable","showAllBtn","toLeftDisable","toRightDisable","titles"],"events":["onChange","onLeftCheckChange","onRightCheckChange"],"demo":{"componentName":"TinyTransfer","props":{"modelValue":[3],"data":[{"key":1,"label":"备选项1","disabled":false},{"key":2,"label":"备选项2","disabled":false},{"key":3,"label":"备选项3","disabled":false},{"key":4,"label":"备选项4","disabled":false}]}}} +{"component":"TinyGrid","name":"表格","properties":["data","columns","fetchData","pager","resizable","row-id","select-config","edit-rules","edit-config","expand-config","sortable"],"events":["onFilterChange","onSortChange","onSelectAll","onSelectChange","onToggleExpandChange","onCurrentChange"],"demo":{"componentName":"TinyGrid","props":{"editConfig":{"trigger":"click","mode":"cell","showStatus":true},"columns":[{"type":"index","width":60},{"type":"selection","width":60},{"field":"employees","title":"员工数"},{"field":"created_date","title":"创建日期"},{"field":"city","title":"城市"}],"data":[{"id":"1","name":"GFD科技有限公司","city":"福州","employees":800,"created_date":"2014-04-30 00:56:00","boole":false},{"id":"2","name":"WWW科技有限公司","city":"深圳","employees":300,"created_date":"2016-07-08 12:36:22","boole":true}]}}} +{"component":"TinyPager","name":"分页","properties":["currentPage","pageSize","pageSizes","total","layout"],"events":["onCurrentChange ","onPrevClick ","onNextClick"],"demo":{"componentName":"TinyPager","props":{"layout":"total, sizes, prev, pager, next","total":100,"pageSize":10,"currentPage":1}}} +{"component":"TinyCarousel","name":"走马灯","properties":["arrow","autoplay","tabs","height","indicator-position","initial-index","interval","loop","show-title","trigger","type"],"events":[],"demo":{"componentName":"TinyCarousel","props":{"height":"180px"},"children":[{"componentName":"TinyCarouselItem","props":{"title":"carousel-item-a"},"children":[{"componentName":"div","props":{"style":"margin:10px 0 0 30px"}}]},{"componentName":"TinyCarouselItem","props":{"title":"carousel-item-b"},"children":[{"componentName":"div","props":{"style":"margin:10px 0 0 30px"}}]}]}} +{"component":"TinyDialogBox","name":"对话框","properties":["title","visible","width","draggable","center","dialog-class","append-to-body","show-close"],"events":["onClose","onUpdate:visible"],"slots":["title","footer"],"demo":{"componentName":"TinyDialogBox","props":{"visible":true,"show-close":true,"title":"dialogBox title"},"children":[{"componentName":"div"}]}} +{"component":"TinyCollapse","name":"折叠面板","properties":["modelValue"],"events":["onChange","onUpdate:modelValue"],"demo":{"componentName":"TinyCollapse","props":{"modelValue":"collapse1"},"children":[{"componentName":"TinyCollapseItem","props":{"name":"collapse1","title":"折叠项1"},"children":[{"componentName":"div"}]},{"componentName":"TinyCollapseItem","props":{"name":"collapse2","title":"折叠项2"},"children":[{"componentName":"div"}]},{"componentName":"TinyCollapseItem","props":{"name":"collapse3","title":"折叠项3"},"children":[{"componentName":"div"}]}]}} +{"component":"TinyPopeditor","name":"弹出编辑","properties":["modelValue","placeholder","show-clear-btn","disabled","auto-lookup"],"events":["onChange","onUpdate:modelValue","onClose","onPageChange"],"demo":{"componentName":"TinyPopeditor","props":{"modelValue":"","placeholder":"请选择","grid-op":{"columns":[{"field":"id","title":"ID","width":40},{"field":"name","title":"名称","showOverflow":"tooltip"},{"field":"province","title":"省份","width":80},{"field":"city","title":"城市","width":80}],"data":[{"id":"1","name":"GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司GFD科技有限公司","city":"福州","province":"福建"},{"id":"2","name":"WWW科技有限公司","city":"深圳","province":"广东"},{"id":"3","name":"RFV有限责任公司","city":"中山","province":"广东"},{"id":"4","name":"TGB科技有限公司","city":"龙岩","province":"福建"},{"id":"5","name":"YHN科技有限公司","city":"韶关","province":"广东"},{"id":"6","name":"WSX科技有限公司","city":"黄冈","province":"武汉"}]}}}} +{"component":"TinyTree","name":"树","properties":["show-checkbox","data","node-key","render-content","icon-trigger-click-node","expand-icon","shrink-icon"],"events":["onCheck","onNodeClick"],"demo":{"componentName":"TinyTree","props":{"data":[{"label":"一级 1","children":[{"label":"二级 1-1","children":[{"label":"三级 1-1-1"}]}]},{"label":"一级 2","children":[{"label":"二级 2-1","children":[{"label":"三级 2-1-1"}]},{"label":"二级 2-2","children":[{"label":"三级 2-2-1"}]}]}]}}} +{"component":"TinyTooltip","name":"文字提示框","properties":["placement","content","render-content","modelValue","manual"],"events":[],"slots":["content"],"demo":{"componentName":"TinyTooltip","props":{"content":"Top Left 提示文字","placement":"top-start","manual":true,"modelValue":true},"children":[{"componentName":"span","children":[{"componentName":"div","props":{}}]},{"componentName":"Template","props":{"slot":"content"},"children":[{"componentName":"span","children":[{"componentName":"div","props":{"placeholder":"提示内容"}}]}]}]}} +{"component":"TinyPopover","name":"提示框","properties":["modelValue","placement","trigger","popper-class","visible-arrow","append-to-body","arrow-offset","close-delay","content","disabled","offset","open-delay","popper-options","title","transform-origin","transition","width"],"events":["onUpdate:modelValue"],"demo":{"componentName":"TinyPopover","props":{"width":200,"title":"弹框标题","trigger":"manual","modelValue":true},"children":[{"componentName":"Template","props":{"slot":"reference"},"children":[{"componentName":"div","props":{"placeholder":"触发源"}}]},{"componentName":"Template","props":{"slot":"default"},"children":[{"componentName":"div","props":{"placeholder":"提示内容"}}]}]}} +{"component":"TinyTimeLine","name":"时间线","properties":["vertical","active","data"],"events":["onClick"],"demo":{"componentName":"TinyTimeLine","props":{"active":"2","data":[{"name":"已下单"},{"name":"运输中"},{"name":"已签收"}]}}} +{"component":"TinyBreadcrumb","name":"面包屑","properties":["separator","options","textField"],"events":["onSelect"],"demo":{"componentName":"TinyBreadcrumb","props":{"options":[{"to":"{ path: '/' }","label":"首页"},{"to":"{ path: '/breadcrumb' }","label":"产品"},{"replace":"true","label":"软件"}]}}} +{"component":"TinyTabs","name":"标签页","properties":["tabs","modelValue","with-add","with-close","tab-style"],"events":["onClick","onEdit","onClose"],"demo":{"componentName":"TinyTabs","props":{"modelValue":"first"},"children":[{"componentName":"TinyTabItem","props":{"title":"标签页1","name":"first"},"children":[{"componentName":"div","props":{"style":"margin:10px 0 0 30px"}}]},{"componentName":"TinyTabItem","props":{"title":"标签页2","name":"second"},"children":[{"componentName":"div","props":{"style":"margin:10px 0 0 30px"}}]}]}} ``` -**修正后的正确输出:** +注意: +- 所有具有modelValue属性的表单类型组件,都支持双向绑定,且应优先使用双向绑定 +----- + +## 4. 示例 +下面是添加一个聊天消息列表的示例: ```json -[ - { - "op": "add", - "path": "/children/-", - "value": { - "componentName": "div", - "id": "rT3dF8sP", - "props": { - "className": "component-base-style", - "style": "display: flex; justify-content: space-between; align-items: center; padding: 10px 20px; border-bottom: 1px solid #EEEEEE;" - }, - "children": [ - { - "componentName": "img", - "id": "kL9mJ1vC", - "props": { - "src": "[https://res-static.hc-cdn.cn/cloudbu-site/intl/zh-cn/yunying/header-new/logo.png](https://res-static.hc-cdn.cn/cloudbu-site/intl/zh-cn/yunying/header-new/logo.png)", - "alt": "品牌Logo", - "style": "height: 40px;" - } - } - ] - } - } -] +[{ "op": "add", "path": "/state/messages", "value": [ { "content": "hello" } ] }, { "op": "add", "path": "/state/inputMessage", "value": "" }, { "op": "add", "path": "/methods/sendMessage", "value": { "type": "JSFunction", "value": "function sendMessage(event) {\n this.state.messages.push({ content: this.state.inputMessage })\n this.state.inputMessage = ''\n}\n" } }, { "op": "add", "path": "/methods/onClickMessage", "value": { "type": "JSFunction", "value": "function onClickMessage(event, message, index) {\n console.log(`这是第${index + 1}条消息, 消息内容:${message.content}`)\n}\n" } }, { "op": "add", "path": "/children/0", "value": { "componentName": "div", "id": "25153243", "props": { "className": "component-base-style" }, "children": [ { "componentName": "h1", "props": { "className": "component-base-style" }, "children": "消息列表", "id": "53222591" }, { "componentName": "div", "props": { "className": "component-base-style div-uhqto", "alignItems": "flex-start" }, "children": [ { "componentName": "div", "props": { "className": "component-base-style div-vinko", "onClick": { "type": "JSExpression", "value": "this.onClickMessage", "params": ["message", "index"] }, "key": { "type": "JSExpression", "value": "index" } }, "children": [ { "componentName": "Text", "props": { "style": "display: inline-block;", "text": { "type": "JSExpression", "value": "message.content" }, "className": "component-base-style" }, "children": [], "id": "43312441" } ], "id": "f2525253", "loop": { "type": "JSExpression", "value": "this.state.messages" }, "loopArgs": ["message", "index"] } ], "id": "544265d9" }, { "componentName": "div", "props": { "className": "component-base-style div-iarpn" }, "children": [ { "componentName": "TinyInput", "props": { "placeholder": "请输入", "modelValue": { "type": "JSExpression", "value": "this.state.inputMessage", "model": true }, "className": "component-base-style", "type": "textarea" }, "children": [], "id": "24651354" }, { "componentName": "TinyButton", "props": { "text": "发送", "className": "component-base-style", "onClick": { "type": "JSExpression", "value": "this.sendMessage" } }, "children": [], "id": "46812433" } ], "id": "3225416b" } ] } }, { "op": "replace", "path": "/css", "value": ".page-base-style {\n padding: 24px;\n background: #ffffff;\n}\n.block-base-style {\n margin: 16px;\n}\n.component-base-style {\n margin: 8px;\n}\n.div-vinko {\n margin: 8px;\n border-width: 1px;\n border-color: #ebeaea;\n border-style: solid;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n border-radius: 50px;\n}\n.div-iarpn {\n margin: 8px;\n display: flex;\n align-items: center;\n}\n.div-uhqto {\n margin: 8px;\n display: flex;\n flex-direction: column;\n}\n" } ] ``` ----- + diff --git a/packages/plugins/robot/src/client/index.ts b/packages/plugins/robot/src/client/index.ts index 8083036c58..42485c5c1e 100644 --- a/packages/plugins/robot/src/client/index.ts +++ b/packages/plugins/robot/src/client/index.ts @@ -29,7 +29,7 @@ const addSystemPrompt = (messages: LLMMessage[], prompt: string = '') => { } } -const search = async (content: string) => { +export const search = async (content: string) => { let result = '' const MAX_SEARCH_LENGTH = 8000 try { @@ -57,11 +57,11 @@ const beforeRequest = async (requestParams: any) => { } if (isAgentMode) { requestParams.apiKey = robotSettingState.selectedModel.apiKey - let referenceContext = '' - if (requestParams.messages?.[0].role && requestParams.messages?.[0].role !== 'system') { - referenceContext = await search(requestParams.messages?.at(-1)?.content) - } - addSystemPrompt(requestParams.messages, getAgentSystemPrompt(pageSchema, referenceContext)) + // let referenceContext = '' + // if (requestParams.messages?.[0].role && requestParams.messages?.[0].role !== 'system') { + // referenceContext = await search(requestParams.messages?.at(-1)?.content) + // } + addSystemPrompt(requestParams.messages, getAgentSystemPrompt(pageSchema, '')) } requestParams.baseUrl = robotSettingState.selectedModel.baseUrl requestParams.model = robotSettingState.selectedModel.model diff --git a/packages/plugins/robot/src/components/RobotChat.vue b/packages/plugins/robot/src/components/RobotChat.vue index bde64fd9df..08cae923ba 100644 --- a/packages/plugins/robot/src/components/RobotChat.vue +++ b/packages/plugins/robot/src/components/RobotChat.vue @@ -120,6 +120,7 @@ import LoadingRenderer from '../mcp/LoadingRenderer.vue' import MarkdownRenderer from '../mcp/MarkdownRenderer.vue' import ImgRenderer from '../mcp/ImgRenderer.vue' import { serializeError } from '../utils/common-utils' +import { initDebugWindow } from '../composables/debug' const { promptItems, allowFiles, bubbleRenderers } = defineProps({ promptItems: { @@ -353,6 +354,7 @@ const handlePromptItemClick = (ev: unknown, item: { description?: string }) => { onMounted(() => { createConversation() + initDebugWindow() }) defineExpose({ diff --git a/packages/plugins/robot/src/composables/useAgent.ts b/packages/plugins/robot/src/composables/useAgent.ts index 917d37dd50..22fb19a7de 100644 --- a/packages/plugins/robot/src/composables/useAgent.ts +++ b/packages/plugins/robot/src/composables/useAgent.ts @@ -6,17 +6,12 @@ import { useCanvas, useHistory } from '@opentiny/tiny-engine-meta-register' import useRobot from '../js/useRobot' import SvgICons from '@opentiny/vue-icon' -const { string2Obj, reactiveObj2String: obj2String } = utils +const { string2Obj, reactiveObj2String: obj2String, deepClone } = utils import { useThrottleFn } from '@vueuse/core' -const setSchema = async (schema: object) => { - const { pageState, importSchema, setSaved } = useCanvas() - const value = { - ...pageState.pageSchema, - ...schema, - componentName: pageState.pageSchema.componentName - } - importSchema(value) +const setSchema = (schema: object) => { + const { importSchema, setSaved } = useCanvas() + importSchema(schema) setSaved(false) } @@ -30,21 +25,26 @@ const fixInvalidIconComponent = (data: any) => { } } -const updateStreamCanvasPageSchema = (streamContent: string, currentPageSchema: object) => { +const updateStreamCanvasPageSchema = async (streamContent: string, currentPageSchema: object) => { try { const repaired = jsonrepair(streamContent) const parsedJson = JSON.parse(repaired) + const latestPatch = parsedJson.at(-1) + if (latestPatch?.path && !latestPatch.path.startsWith('/children')) { + parsedJson.pop() + } const result = parsedJson.reduce((acc: object, patch: any) => { fixInvalidIconComponent(patch.value) return jsonpatch.applyPatch(acc, [patch], false, false).newDocument - }, currentPageSchema) + }, deepClone(currentPageSchema)) const editorValue = string2Obj(obj2String(result)) if (editorValue && checkComponentNameExists(result)) { setSchema(result) } } catch (error) { - // error + const logger = console + logger.error('updateStreamCanvasPageSchema error', error) } } @@ -74,7 +74,10 @@ export const updateCanvasPageSchema = (streamContent: string, currentJson: objec const result = schemaPatch.reduce((acc: object, patch: any) => { fixInvalidIconComponent(patch.value) return jsonpatch.applyPatch(acc, [patch], false, false).newDocument - }, currentJson) + }, deepClone(currentJson)) + const logger = console + logger.log('current schema:', deepClone(currentJson)) + logger.log('new Schema:', result) setSchema(result) useHistory().addHistory() @@ -82,6 +85,8 @@ export const updateCanvasPageSchema = (streamContent: string, currentJson: objec messages.at(-1).renderContent.at(-1).schema = result } } catch (error) { + const logger = console + logger.error('updateCanvasPageSchema error', error) messages.at(-1).renderContent.at(-1).status = 'failed' } } diff --git a/packages/plugins/robot/src/composables/useChat.ts b/packages/plugins/robot/src/composables/useChat.ts index 4d1d2d71d0..22b6cf5844 100644 --- a/packages/plugins/robot/src/composables/useChat.ts +++ b/packages/plugins/robot/src/composables/useChat.ts @@ -71,6 +71,7 @@ const events: UseMessageOptions['events'] = { updateCanvasPageSchema(lastMessage.content, pageSchema, messages.value) } chatStatus = messageState.status + pageSchema = null } } From 80054372982d3ec2137299a7ad39b15e01a8aa03 Mon Sep 17 00:00:00 2001 From: hexqi Date: Mon, 20 Oct 2025 16:08:26 +0800 Subject: [PATCH 08/17] feat: update prompt --- packages/plugins/robot/src/Home.vue | 15 ++- packages/plugins/robot/src/agent-prompt.md | 39 +++--- .../robot/src/components/RobotChat.vue | 2 - .../plugins/robot/src/composables/useAgent.ts | 122 ++++++++++-------- .../plugins/robot/src/composables/useChat.ts | 14 +- 5 files changed, 104 insertions(+), 88 deletions(-) diff --git a/packages/plugins/robot/src/Home.vue b/packages/plugins/robot/src/Home.vue index 74c9da94f9..13cdac6123 100644 --- a/packages/plugins/robot/src/Home.vue +++ b/packages/plugins/robot/src/Home.vue @@ -90,6 +90,12 @@ const mcpDrawerPosition = computed(() => { }) const promptItems: PromptProps[] = [ + { + label: '页面搭建场景', + description: '在当前页面中生成一个满意度调查表单', + icon: h(PageIconComponent), + badge: 'NEW' + }, { label: 'MCP工具', description: '帮我查询当前的页面列表', @@ -97,13 +103,8 @@ const promptItems: PromptProps[] = [ badge: 'NEW' }, { - label: '页面搭建场景', - description: '给当前页面中添加一个问卷调查表单', - icon: h(PageIconComponent) - }, - { - label: '学习/知识型场景', - description: 'Vue3 和 React 有什么区别?', + label: '日常开发问答', + description: '如何实现前端节流与防抖?', icon: h(StudyIconComponent) } ] diff --git a/packages/plugins/robot/src/agent-prompt.md b/packages/plugins/robot/src/agent-prompt.md index 93b18be937..1fa9356f31 100644 --- a/packages/plugins/robot/src/agent-prompt.md +++ b/packages/plugins/robot/src/agent-prompt.md @@ -4,35 +4,34 @@ **核心任务**:根据 **[当前页面Schema]** 、**[参考知识]** 和用户提供的需求,生成一个严格遵循`RFC 6902`规范的JSON Patch数组,用于向现有页面增删改(`add`/`replace`/`remove`/`move`)符合PageSchema 规范(参考《3. PageSchema 规范》部分)的页面(包含UI组件及必要的逻辑),从而得到符合用户需求的新的页面Schema。 -例如,下面返回的结果表示添加一个名为`handleBtnClick`的方法和添加一个名为`name`的页面状态变量并移除一个页面元素: -```json -[{"op":"add","path":"/methods/handleBtnClick","value":{"type":"JSFunction","value":"function handleBtnClick() {\n console.log('button click')\n}\n"}},{"op":"add","path":"/state/name","value":"alice"},{"op":"remove","path":"/children/0/children/5"}] -``` ----- ## 1. 工作流程 (Operational Flow) -请严格遵循以下步骤思考和执行: +低代码平台流程如下: +当前页面Schema --> 根据用户需求生成新页面Schema对应的JSON Patch数据 --> 应用JSON Patch生成新页面Schema --> 根据用户要求继续修改,基于新Schema生成新的JSON Patch --> 应用新的JSON Patch更新当前页面Schema + +页面Schema为用特定JSON格式描述的页面UI功能的数据,页面Schema可以通过出码功能生成对于的Vue代码,因此页面Schema也等效于特定格式的Vue单文件组件代码。 -1. **解析输入**:仔细分析 **[用户需求]**(可能是文本描述或图片分析结果)、**[参考知识]** 和 **[当前页面Schema]**。 -2. **识别组件**:将用户需求解构为符合`PageSchema`规范的一个或多个组件(如`TinyInput`, `img`, `Text`等)。 -3. **构建组件结构**: - * 为每个新组件生成一个符合规范的、唯一的8位随机ID。 - * 根据`PageSchema`组件转换规则,确定每个组件的`componentName`。 - * **精确还原样式**:根据用户需求(尤其是图片),在每个组件的`props.className`中生成`Tailwind`样式类,例如:`"className": "size-48 shadow-xl rounded-md"`,或者生成`props.style`字段中生成详细的行内样式字符串,例如:`"style": "display: flex; align-items: center; background-color: #FFFFFF; padding: 16px;";` **优先使用`Tailwind`样式类**;优先使用弹性布局(Flex)来保证结构和对齐;精确匹配颜色、内外边距、字体大小等视觉元素。 - * 递归地构建`children`数组,形成正确的嵌套关系。 -4. **封装为JSON Patch**:将生成的所有顶级组件封装到一个JSON Patch对象中,格式为:`{ "op": "add", "path": "/children/-", "value": { ... } }`。 -5. **最终校验**:在输出前,自我校验最终生成的字符串是否为**完整且语法正确**的JSON数组。如果任何环节出错或无法理解需求,则必须输出一个空数组 `[]`。 +因此请严格遵循以下步骤思考和执行,来为用户生成符合需求的页面Schema(JSON Patch格式): + +1. **解析输入**:仔细分析接下来的 **[用户需求]**(可能是文本描述或图片分析结果),并结合下方的 **[当前页面Schema]**,以及下方可能存在的 **[参考知识]**。 +2. **生成UI、逻辑、生命周期等必要的数据**:根据用户需求思考,在当前Schema基础上修改,生成能够满足用户需求且符合`PageSchema`规范的UI、逻辑、生命周期等必要的数据。 +3. **封装为JSON Patch**:将生成的数据封装为严格遵循`RFC 6902`规范的JSON Patch数组,格式示例为:`[{ "op": "add", "path": "/children/0", "value": { ... } }, {"op":"add","path":"/methods/handleBtnClick","value": { ... }, { "op": "replace", "path": "/css", "value": "..." }]`。 +4. **最终校验**:在输出前,自我校验最终生成的字符串是否为**完整且语法正确**的JSON数组。如果任何环节出错或无法理解需求,则必须输出一个空数组 `[]`。 ----- ## 2. 输出格式与绝对约束 -**你必须且只能输出一个原始的JSON字符串,该字符串本身是一个JSON Patch数组,该字符串必须可以通过JSON.parse解析成JSON对象。** +**你必须且只能输出一个原始且完整的JSON字符串,该字符串本身是一个JSON Patch数组,该字符串必须可以通过JSON.parse解析成JSON对象。并通过\`\`\`schema与\`\`\`包裹JSON字符串内容**,例如,下面返回的结果表示添加一个名为`handleBtnClick`的方法和添加一个名为`name`的页面状态变量并移除一个页面元素: +```schema +[{"op":"add","path":"/methods/handleBtnClick","value":{"type":"JSFunction","value":"function handleBtnClick() {\n console.log('button click')\n}\n"}},{"op":"add","path":"/state/name","value":"alice"},{"op":"remove","path":"/children/0/children/5"}] +``` +约束规则: * **严格禁止**: * 任何解释性文字、开场白或结束语(如“好的,这是您要的JSON...”)。 - * 使用` ```json `代码块包裹最终输出。直接输出原始文本。 * 在JSON内部或外部添加任何注释(如 `//` 或 `/* */`)。 * 任何形式的省略号或未完成的占位符(如 `...`)。 * **JSON语法铁律**: @@ -44,6 +43,8 @@ * **占位符资源**:当需要占位资源时,必须使用以下链接: * 图片: `"src": "https://placehold.co/600x400"` * 视频: `"src": "https://placehold.co/640x360.mp4"` + * 其他 + * 每个新组件都要有一个符合规范的、唯一的8位随机ID。 ----- @@ -100,7 +101,7 @@ interface ComponentSchema { // 组件 schema ### 3.3 组件规则 -组件(componentName)可以使用低代码平台中的组件或者HTML原生组件(div、img、h1、a、span等)。所有低代码平台可用组件如下: +组件(componentName)可以使用低代码平台中的组件(TinyVue组件库)或者HTML原生组件(div、img、h1、a、span等)。所有低代码平台可用组件如下: ```jsonl {"component":"Box","name":"盒子容器","demo":{"componentName":"div","props":{}}} {"component":"Text","name":"文本","properties":["text"],"events":["onClick"],"demo":{"componentName":"Text","props":{"style":"display: inline-block;","text":"TinyEngine 前端可视化设计器,为设计器开发者提供定制服务,在线构建出自己专属的设计器。"}}} @@ -144,8 +145,8 @@ interface ComponentSchema { // 组件 schema ## 4. 示例 下面是添加一个聊天消息列表的示例: -```json -[{ "op": "add", "path": "/state/messages", "value": [ { "content": "hello" } ] }, { "op": "add", "path": "/state/inputMessage", "value": "" }, { "op": "add", "path": "/methods/sendMessage", "value": { "type": "JSFunction", "value": "function sendMessage(event) {\n this.state.messages.push({ content: this.state.inputMessage })\n this.state.inputMessage = ''\n}\n" } }, { "op": "add", "path": "/methods/onClickMessage", "value": { "type": "JSFunction", "value": "function onClickMessage(event, message, index) {\n console.log(`这是第${index + 1}条消息, 消息内容:${message.content}`)\n}\n" } }, { "op": "add", "path": "/children/0", "value": { "componentName": "div", "id": "25153243", "props": { "className": "component-base-style" }, "children": [ { "componentName": "h1", "props": { "className": "component-base-style" }, "children": "消息列表", "id": "53222591" }, { "componentName": "div", "props": { "className": "component-base-style div-uhqto", "alignItems": "flex-start" }, "children": [ { "componentName": "div", "props": { "className": "component-base-style div-vinko", "onClick": { "type": "JSExpression", "value": "this.onClickMessage", "params": ["message", "index"] }, "key": { "type": "JSExpression", "value": "index" } }, "children": [ { "componentName": "Text", "props": { "style": "display: inline-block;", "text": { "type": "JSExpression", "value": "message.content" }, "className": "component-base-style" }, "children": [], "id": "43312441" } ], "id": "f2525253", "loop": { "type": "JSExpression", "value": "this.state.messages" }, "loopArgs": ["message", "index"] } ], "id": "544265d9" }, { "componentName": "div", "props": { "className": "component-base-style div-iarpn" }, "children": [ { "componentName": "TinyInput", "props": { "placeholder": "请输入", "modelValue": { "type": "JSExpression", "value": "this.state.inputMessage", "model": true }, "className": "component-base-style", "type": "textarea" }, "children": [], "id": "24651354" }, { "componentName": "TinyButton", "props": { "text": "发送", "className": "component-base-style", "onClick": { "type": "JSExpression", "value": "this.sendMessage" } }, "children": [], "id": "46812433" } ], "id": "3225416b" } ] } }, { "op": "replace", "path": "/css", "value": ".page-base-style {\n padding: 24px;\n background: #ffffff;\n}\n.block-base-style {\n margin: 16px;\n}\n.component-base-style {\n margin: 8px;\n}\n.div-vinko {\n margin: 8px;\n border-width: 1px;\n border-color: #ebeaea;\n border-style: solid;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n border-radius: 50px;\n}\n.div-iarpn {\n margin: 8px;\n display: flex;\n align-items: center;\n}\n.div-uhqto {\n margin: 8px;\n display: flex;\n flex-direction: column;\n}\n" } ] +```schema +[ { "op": "add", "path": "/children/0", "value": { "componentName": "div", "id": "25153243", "props": { "className": "component-base-style" }, "children": [ { "componentName": "h1", "props": { "className": "component-base-style" }, "children": "消息列表", "id": "53222591" }, { "componentName": "div", "props": { "className": "component-base-style div-uhqto", "alignItems": "flex-start" }, "children": [ { "componentName": "div", "props": { "className": "component-base-style div-vinko", "onClick": { "type": "JSExpression", "value": "this.onClickMessage", "params": ["message", "index"] }, "key": { "type": "JSExpression", "value": "index" } }, "children": [ { "componentName": "Text", "props": { "style": "display: inline-block;", "text": { "type": "JSExpression", "value": "message.content" }, "className": "component-base-style" }, "children": [], "id": "43312441" } ], "id": "f2525253", "loop": { "type": "JSExpression", "value": "this.state.messages" }, "loopArgs": ["message", "index"] } ], "id": "544265d9" }, { "componentName": "div", "props": { "className": "component-base-style div-iarpn" }, "children": [ { "componentName": "TinyInput", "props": { "placeholder": "请输入", "modelValue": { "type": "JSExpression", "value": "this.state.inputMessage", "model": true }, "className": "component-base-style", "type": "textarea" }, "children": [], "id": "24651354" }, { "componentName": "TinyButton", "props": { "text": "发送", "className": "component-base-style", "onClick": { "type": "JSExpression", "value": "this.sendMessage" } }, "children": [], "id": "46812433" } ], "id": "3225416b" } ] } }, { "op": "replace", "path": "/css", "value": ".page-base-style {\n padding: 24px;\n background: #ffffff;\n}\n.block-base-style {\n margin: 16px;\n}\n.component-base-style {\n margin: 8px;\n}\n.div-vinko {\n margin: 8px;\n border-width: 1px;\n border-color: #ebeaea;\n border-style: solid;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n border-radius: 50px;\n}\n.div-iarpn {\n margin: 8px;\n display: flex;\n align-items: center;\n}\n.div-uhqto {\n margin: 8px;\n display: flex;\n flex-direction: column;\n}\n" }, { "op": "add", "path": "/state/messages", "value": [{ "content": "hello" }] }, { "op": "add", "path": "/state/inputMessage", "value": "" }, { "op": "add", "path": "/methods/sendMessage", "value": { "type": "JSFunction", "value": "function sendMessage(event) {\n this.state.messages.push({ content: this.state.inputMessage })\n this.state.inputMessage = ''\n}\n" } }, { "op": "add", "path": "/methods/onClickMessage", "value": { "type": "JSFunction", "value": "function onClickMessage(event, message, index) {\n console.log(`这是第${index + 1}条消息, 消息内容:${message.content}`)\n}\n" } } ] ``` ----- diff --git a/packages/plugins/robot/src/components/RobotChat.vue b/packages/plugins/robot/src/components/RobotChat.vue index 08cae923ba..bde64fd9df 100644 --- a/packages/plugins/robot/src/components/RobotChat.vue +++ b/packages/plugins/robot/src/components/RobotChat.vue @@ -120,7 +120,6 @@ import LoadingRenderer from '../mcp/LoadingRenderer.vue' import MarkdownRenderer from '../mcp/MarkdownRenderer.vue' import ImgRenderer from '../mcp/ImgRenderer.vue' import { serializeError } from '../utils/common-utils' -import { initDebugWindow } from '../composables/debug' const { promptItems, allowFiles, bubbleRenderers } = defineProps({ promptItems: { @@ -354,7 +353,6 @@ const handlePromptItemClick = (ev: unknown, item: { description?: string }) => { onMounted(() => { createConversation() - initDebugWindow() }) defineExpose({ diff --git a/packages/plugins/robot/src/composables/useAgent.ts b/packages/plugins/robot/src/composables/useAgent.ts index 22fb19a7de..3d5f2eff14 100644 --- a/packages/plugins/robot/src/composables/useAgent.ts +++ b/packages/plugins/robot/src/composables/useAgent.ts @@ -1,92 +1,100 @@ import { jsonrepair } from 'jsonrepair' -import { checkComponentNameExists } from '../js/utils' import * as jsonpatch from 'fast-json-patch' import { utils } from '@opentiny/tiny-engine-utils' import { useCanvas, useHistory } from '@opentiny/tiny-engine-meta-register' import useRobot from '../js/useRobot' import SvgICons from '@opentiny/vue-icon' -const { string2Obj, reactiveObj2String: obj2String, deepClone } = utils +const { deepClone } = utils import { useThrottleFn } from '@vueuse/core' +const logger = console + const setSchema = (schema: object) => { const { importSchema, setSaved } = useCanvas() importSchema(schema) setSaved(false) } -const fixInvalidIconComponent = (data: any) => { - if (data.componentName === 'Icon' && data.props?.name && !SvgICons[data.props.name as keyof typeof SvgICons]) { +const fixIconComponent = (data: unknown) => { + if (data?.componentName === 'Icon' && data.props?.name && !SvgICons[data.props.name as keyof typeof SvgICons]) { data.props.name = 'IconWarning' - } - - if (data.children && Array.isArray(data.children)) { - data.children.forEach((child: any) => fixInvalidIconComponent(child)) + logger.log('autofix icon to warning:', data) } } -const updateStreamCanvasPageSchema = async (streamContent: string, currentPageSchema: object) => { - try { - const repaired = jsonrepair(streamContent) - const parsedJson = JSON.parse(repaired) - const latestPatch = parsedJson.at(-1) - if (latestPatch?.path && !latestPatch.path.startsWith('/children')) { - parsedJson.pop() - } - const result = parsedJson.reduce((acc: object, patch: any) => { - fixInvalidIconComponent(patch.value) - return jsonpatch.applyPatch(acc, [patch], false, false).newDocument - }, deepClone(currentPageSchema)) - const editorValue = string2Obj(obj2String(result)) +const isPlainObject = (value: unknown) => + typeof value === 'object' && value !== null && Object.prototype.toString.call(value) === '[object Object]' - if (editorValue && checkComponentNameExists(result)) { - setSchema(result) - } - } catch (error) { - const logger = console - logger.error('updateStreamCanvasPageSchema error', error) +const fixComponentName = (data: object) => { + if (isPlainObject(data) && !data.componentName) { + data.componentName = 'div' + logger.log('autofix component to div:', data) } } -// 节流更新schema -export const throttledUpdateCanvasPageSchema = useThrottleFn(updateStreamCanvasPageSchema, 500, true) - -export const handleStreamData = (streamContent: string, currentJson: object) => { - const { aiMode, CHAT_MODE } = useRobot() - if (aiMode.value !== CHAT_MODE.Agent) { - return +const schemaAutoFix = (data: object | object[]) => { + if (!data) return + if (Array.isArray(data)) data.forEach((item) => schemaAutoFix(item)) + fixIconComponent(data) + fixComponentName(data) + if (data.children && Array.isArray(data.children)) { + data.children.forEach((child: any) => schemaAutoFix(child)) } - throttledUpdateCanvasPageSchema(streamContent, currentJson) } -export const updateCanvasPageSchema = (streamContent: string, currentJson: object, messages: RobotMessage[]) => { +const _updatePageSchema = (streamContent: string, currentPageSchema: object, isFinial: boolean = false) => { const { aiMode, CHAT_MODE, isValidFastJsonPatch } = useRobot() if (aiMode.value !== CHAT_MODE.Agent) { return } - const regex = /```json([\s\S]*?)```/ - const match = streamContent.match(regex) - const content = (match && match[1]) || streamContent + // 解析流式返回的schema patch + const regex = /```(json|schema)?([\s\S]*?)```/ + const match = streamContent.match(regex) + const content = (match && match[2]) || streamContent + let jsonPatches = [] try { - const schemaPatch = JSON.parse(content) - if (isValidFastJsonPatch(schemaPatch)) { - const result = schemaPatch.reduce((acc: object, patch: any) => { - fixInvalidIconComponent(patch.value) - return jsonpatch.applyPatch(acc, [patch], false, false).newDocument - }, deepClone(currentJson)) - const logger = console - logger.log('current schema:', deepClone(currentJson)) - logger.log('new Schema:', result) - setSchema(result) - useHistory().addHistory() - - messages.at(-1).renderContent.at(-1).status = 'success' - messages.at(-1).renderContent.at(-1).schema = result - } + jsonPatches = JSON.parse(jsonrepair(content)) } catch (error) { - const logger = console - logger.error('updateCanvasPageSchema error', error) - messages.at(-1).renderContent.at(-1).status = 'failed' + if (isFinial) { + logger.error('parse json patch error:', error) + } + return { isError: true, error } + } + + // 流式渲染过程中,画布只渲染children字段,避免不完整的methods/states/css等字段导致解析报错 + const childrenFilter = (patch) => isFinial || patch.path?.startsWith('/children') + + // 过滤有效的json patch + if (!isFinial && !isValidFastJsonPatch(jsonPatches)) { + return { isError: true, error: 'format error: not a valid json patch.' } + } + const validJsonPatches = jsonPatches.filter(childrenFilter).filter(isValidFastJsonPatch) + + // 生成新schema + const originSchema = deepClone(currentPageSchema) + const newSchema = validJsonPatches.reduce((acc: object, patch: any) => { + try { + return jsonpatch.applyPatch(acc, [patch], false, false).newDocument + } catch (error) { + if (isFinial) { + logger.error('apply patch error:', error, patch) + } + return acc + } + }, originSchema) + + // schema纠错 + schemaAutoFix(newSchema.children) + + // 更新Schema + setSchema(newSchema) + if (isFinial) { + useHistory().addHistory() } + + return { schema: newSchema, isError: false } } + +export const updatePageSchema = useThrottleFn(_updatePageSchema, 200, true) diff --git a/packages/plugins/robot/src/composables/useChat.ts b/packages/plugins/robot/src/composables/useChat.ts index 22b6cf5844..7990148b5b 100644 --- a/packages/plugins/robot/src/composables/useChat.ts +++ b/packages/plugins/robot/src/composables/useChat.ts @@ -12,7 +12,7 @@ import { formatMessages, serializeError } from '../utils/common-utils' import type { LLMMessage, ResponseToolCall, RobotMessage } from '../types/mcp-types' import useMcpServer from './useMcp' import { client } from '../client' -import { handleStreamData, updateCanvasPageSchema } from './useAgent' +import { updatePageSchema } from './useAgent' import { utils } from '@opentiny/tiny-engine-utils' import { useCanvas } from '@opentiny/tiny-engine-meta-register' @@ -59,7 +59,7 @@ const events: UseMessageOptions['events'] = { handleDeltaContent(choice, lastMessage) // eslint-disable-line handleDeltaToolCalls(choice, lastMessage) // eslint-disable-line - handleStreamData(lastMessage.content, pageSchema) + updatePageSchema(lastMessage.content, pageSchema) }, onFinish(finishReason, { messages, messageState }, preventDefault) { preventDefault() @@ -68,7 +68,15 @@ const events: UseMessageOptions['events'] = { handleToolCall(lastMessage.tool_calls, messages.value) // eslint-disable-line } else if (finishReason !== 'abort' && messageState.status !== STATUS.ABORTED) { messageState.status = STATUS.FINISHED - updateCanvasPageSchema(lastMessage.content, pageSchema, messages.value) + updatePageSchema(lastMessage.content, pageSchema, true).then(({ schema: newSchema }) => { + // TODO: isError时让AI继续修复 + if (newSchema) { + messages.value.at(-1).renderContent.at(-1).status = 'success' + messages.value.at(-1).renderContent.at(-1).schema = newSchema + } else { + messages.value.at(-1).renderContent.at(-1).status = 'failed' + } + }) } chatStatus = messageState.status pageSchema = null From d2ed82f74fbadc8748385d1df20265d8448e161b Mon Sep 17 00:00:00 2001 From: hexqi Date: Tue, 21 Oct 2025 16:41:20 +0800 Subject: [PATCH 09/17] feat: chatMode and switchConversation --- packages/plugins/robot/src/Home.vue | 34 +++-- packages/plugins/robot/src/Main.vue | 2 +- packages/plugins/robot/src/client/index.ts | 92 ++------------ .../robot/src/components/RobotTypeSelect.vue | 40 +++--- .../plugins/robot/src/composables/useAgent.ts | 24 +++- .../plugins/robot/src/composables/useChat.ts | 118 +++++++++++++++--- packages/plugins/robot/src/js/useRobot.ts | 22 ++-- 7 files changed, 179 insertions(+), 153 deletions(-) diff --git a/packages/plugins/robot/src/Home.vue b/packages/plugins/robot/src/Home.vue index 13cdac6123..2d107e3b69 100644 --- a/packages/plugins/robot/src/Home.vue +++ b/packages/plugins/robot/src/Home.vue @@ -13,9 +13,11 @@ ref="robotChatRef" :prompt-items="promptItems" :bubbleRenderers=" - aiMode === CHAT_MODE.Agent ? { markdown: BuildLoadingRenderer, loading: BuildLoadingRenderer } : {} + robotSettingState.chatMode === CHAT_MODE.Agent + ? { markdown: BuildLoadingRenderer, loading: BuildLoadingRenderer } + : {} " - :allowFiles="isVisualModel() && aiMode === CHAT_MODE.Agent" + :allowFiles="isVisualModel() && robotSettingState.chatMode === CHAT_MODE.Agent" @fileSelected="handleFileSelected" > diff --git a/packages/plugins/robot/src/client/index.ts b/packages/plugins/robot/src/client/index.ts index 42485c5c1e..f8a33671bd 100644 --- a/packages/plugins/robot/src/client/index.ts +++ b/packages/plugins/robot/src/client/index.ts @@ -1,88 +1,20 @@ import { AIClient, type AIModelConfig } from '@opentiny/tiny-robot-kit' import { OpenAICompatibleProvider } from './OpenAICompatibleProvider' -import useMcp from '../composables/useMcp' -import useRobot from '../js/useRobot' -import type { LLMMessage } from '../types/mcp-types' -import { getAgentSystemPrompt } from '../js/prompts' -import { utils } from '@opentiny/tiny-engine-utils' -import { getMetaApi, META_SERVICE, useCanvas } from '@opentiny/tiny-engine-meta-register' - -const { deepClone } = utils -const { loadRobotSettingState, EXISTING_MODELS, aiMode, CHAT_MODE } = useRobot() -const { activeName, existModel, customizeModel } = loadRobotSettingState() || {} - -const storageSettingState = (activeName === EXISTING_MODELS ? existModel : customizeModel) || {} - -const config: Omit = { - apiKey: storageSettingState.apiKey || '', - apiUrl: aiMode.value === CHAT_MODE.Agent ? '/app-center/api/ai/chat' : '/app-center/api/chat/completions', - defaultModel: storageSettingState.model || 'deepseek-v3' -} - -let provider: OpenAICompatibleProvider | null = null - -const addSystemPrompt = (messages: LLMMessage[], prompt: string = '') => { - if (!messages.length || messages[0].role !== 'system') { - messages.unshift({ role: 'system', content: prompt }) - } else if (messages[0].role === 'system' && messages[0].content !== prompt) { - messages[0].content = prompt - } -} - -export const search = async (content: string) => { - let result = '' - const MAX_SEARCH_LENGTH = 8000 - try { - const res = await getMetaApi(META_SERVICE.Http).post('/app-center/api/ai/search', { content }) - - res.forEach((item: { content: string }) => { - if (result.length + item.content.length > MAX_SEARCH_LENGTH) { - return - } - result += item.content - }) - } catch (error) { - // error - } - return result -} - -const beforeRequest = async (requestParams: any) => { - const { aiMode, CHAT_MODE, robotSettingState } = useRobot() - const pageSchema = deepClone(useCanvas().pageState.pageSchema) - const isAgentMode = aiMode.value === CHAT_MODE.Agent - const tools = await useMcp().getLLMTools() - if (!requestParams.tools && tools?.length && !isAgentMode) { - Object.assign(requestParams, { tools }) - } - if (isAgentMode) { - requestParams.apiKey = robotSettingState.selectedModel.apiKey - // let referenceContext = '' - // if (requestParams.messages?.[0].role && requestParams.messages?.[0].role !== 'system') { - // referenceContext = await search(requestParams.messages?.at(-1)?.content) - // } - addSystemPrompt(requestParams.messages, getAgentSystemPrompt(pageSchema, '')) - } - requestParams.baseUrl = robotSettingState.selectedModel.baseUrl - requestParams.model = robotSettingState.selectedModel.model - if (config.apiKey !== robotSettingState.selectedModel.apiKey) { - provider?.updateConfig({ apiKey: robotSettingState.selectedModel.apiKey }) - config.apiKey = robotSettingState.selectedModel.apiKey - } - return requestParams +interface ClientOptions { + config: Omit + beforeRequest: () => object } -provider = new OpenAICompatibleProvider(config, { beforeRequest }) +const createClient = ({ config, beforeRequest }: ClientOptions) => { + const provider: OpenAICompatibleProvider = new OpenAICompatibleProvider(config, { beforeRequest }) -const client = new AIClient({ - ...config, - provider: 'custom', - providerImplementation: provider -}) + const client = new AIClient({ + ...config, + provider: 'custom', + providerImplementation: provider + }) -const updateLLMConfig = (newConfig: Omit) => { - provider?.updateConfig(newConfig) - Object.assign(config, newConfig) + return { client, provider } } -export { client, updateLLMConfig } +export { createClient } diff --git a/packages/plugins/robot/src/components/RobotTypeSelect.vue b/packages/plugins/robot/src/components/RobotTypeSelect.vue index 81b40d99ea..11b480cc6c 100644 --- a/packages/plugins/robot/src/components/RobotTypeSelect.vue +++ b/packages/plugins/robot/src/components/RobotTypeSelect.vue @@ -1,7 +1,7 @@