-
-
+
-
-
-
diff --git a/packages/plugins/robot/src/icon-prompt/page-icon.vue b/packages/plugins/robot/src/components/icons/page-icon.vue
similarity index 94%
rename from packages/plugins/robot/src/icon-prompt/page-icon.vue
rename to packages/plugins/robot/src/components/icons/page-icon.vue
index ae82278def..bd54f58f23 100644
--- a/packages/plugins/robot/src/icon-prompt/page-icon.vue
+++ b/packages/plugins/robot/src/components/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/components/icons/study-icon.vue
similarity index 93%
rename from packages/plugins/robot/src/icon-prompt/study-icon.vue
rename to packages/plugins/robot/src/components/icons/study-icon.vue
index b851005195..40a0d1245a 100644
--- a/packages/plugins/robot/src/icon-prompt/study-icon.vue
+++ b/packages/plugins/robot/src/components/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/components/renderers/AgentRenderer.vue b/packages/plugins/robot/src/components/renderers/AgentRenderer.vue
new file mode 100644
index 0000000000..6d10bd60ad
--- /dev/null
+++ b/packages/plugins/robot/src/components/renderers/AgentRenderer.vue
@@ -0,0 +1,109 @@
+
+
+
![]()
+
+
+
{{ statusData.content }}
+
+
+
+
+
+
+
diff --git a/packages/plugins/robot/src/components/renderers/ImgRenderer.vue b/packages/plugins/robot/src/components/renderers/ImgRenderer.vue
new file mode 100644
index 0000000000..76de912b25
--- /dev/null
+++ b/packages/plugins/robot/src/components/renderers/ImgRenderer.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
diff --git a/packages/plugins/robot/src/components/renderers/LoadingRenderer.vue b/packages/plugins/robot/src/components/renderers/LoadingRenderer.vue
new file mode 100644
index 0000000000..86f4891786
--- /dev/null
+++ b/packages/plugins/robot/src/components/renderers/LoadingRenderer.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/packages/plugins/robot/src/mcp/MarkdownRenderer.vue b/packages/plugins/robot/src/components/renderers/MarkdownRenderer.vue
similarity index 100%
rename from packages/plugins/robot/src/mcp/MarkdownRenderer.vue
rename to packages/plugins/robot/src/components/renderers/MarkdownRenderer.vue
diff --git a/packages/plugins/robot/src/composables/useAgent.ts b/packages/plugins/robot/src/composables/useAgent.ts
new file mode 100644
index 0000000000..c111de1236
--- /dev/null
+++ b/packages/plugins/robot/src/composables/useAgent.ts
@@ -0,0 +1,225 @@
+import { jsonrepair } from 'jsonrepair'
+import * as jsonpatch from 'fast-json-patch'
+import { utils } from '@opentiny/tiny-engine-utils'
+import { getMetaApi, META_SERVICE, useCanvas, useHistory } from '@opentiny/tiny-engine-meta-register'
+import SvgICons from '@opentiny/vue-icon'
+import { useThrottleFn } from '@vueuse/core'
+import useModelConfig from './useConfig'
+import { serializeError } from '../utils'
+
+const { deepClone } = utils
+
+const logger = console
+
+const setSchema = (schema: object) => {
+ const { importSchema, setSaved } = useCanvas()
+ importSchema(schema)
+ setSaved(false)
+}
+
+const fixIconComponent = (data: unknown) => {
+ if (data?.componentName === 'Icon' && data.props?.name && !SvgICons[data.props.name as keyof typeof SvgICons]) {
+ data.props.name = 'IconWarning'
+ logger.log('autofix icon to warning:', data)
+ }
+}
+
+const isPlainObject = (value: unknown) =>
+ typeof value === 'object' && value !== null && Object.prototype.toString.call(value) === '[object Object]'
+
+const fixComponentName = (data: object) => {
+ if (isPlainObject(data) && !data.componentName && !data.op && !data.path) {
+ data.componentName = 'div'
+ logger.log('autofix component to div:', data)
+ }
+}
+
+const fixMethods = (methods: Record
) => {
+ if (methods && Object.keys(methods).length) {
+ Object.entries(methods).forEach(([methodName, methodValue]: [string, any]) => {
+ if (
+ typeof methodValue !== 'object' ||
+ methodValue?.type !== 'JSFunction' ||
+ !methodValue?.value.startsWith('function')
+ ) {
+ methods[methodName] = {
+ type: 'JSFunction',
+ value: 'function ' + methodName + '() {\n console.log("' + methodName + '");\n}'
+ }
+ logger.log('autofix method to empty function:', methodName, methods[methodName])
+ }
+ })
+ }
+}
+
+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))
+ }
+}
+
+const isValidOperation = (operation: object) => {
+ const allowedOps = ['add', 'remove', 'replace', 'move', 'copy', 'test', '_get']
+
+ if (typeof operation !== 'object' || operation === null) {
+ return false
+ }
+ // 检查操作类型是否有效
+ if (!operation.op || !allowedOps.includes(operation.op)) {
+ return false
+ }
+ // 检查path字段是否存在且为字符串
+ if (!operation.path || typeof operation.path !== 'string') {
+ return false
+ }
+ // 根据操作类型检查其他必需字段
+ switch (operation.op) {
+ case 'add':
+ case 'replace':
+ case 'test':
+ if (!('value' in operation)) {
+ return false
+ }
+ break
+ case 'move':
+ case 'copy':
+ if (!operation.from || typeof operation.from !== 'string') {
+ return false
+ }
+ break
+ }
+
+ return true
+}
+
+const isValidFastJsonPatch = (patch) => {
+ if (Array.isArray(patch)) {
+ return patch.every(isValidOperation)
+ } else if (typeof patch === 'object' && patch !== null) {
+ return isValidOperation(patch)
+ }
+ return false
+}
+
+const jsonPatchAutoFix = (jsonPatches: any[], isFinial: boolean) => {
+ // 流式渲染过程中,画布只渲染完整的字段或流式的children字段,避免不完整的methods/states/css等字段导致解析报错
+ const childrenFilter = (patch, index, arr) =>
+ isFinial || index < arr.length - 1 || (index === arr.length - 1 && patch.path?.startsWith('/children'))
+ const validJsonPatches = jsonPatches.filter(childrenFilter).filter(isValidFastJsonPatch)
+
+ return validJsonPatches
+}
+
+export const getJsonObjectString = (streamContent: string) => {
+ const regex = /```(json|schema)?([\s\S]*?)```/
+ const match = streamContent.match(regex)
+ return (match && match[2]) || streamContent
+}
+
+export const isValidJsonPatchObjectString = (streamContent: string) => {
+ const jsonString = getJsonObjectString(streamContent)
+ try {
+ const data = JSON.parse(jsonString)
+ if (!isValidFastJsonPatch(data)) {
+ return {
+ isError: true,
+ error:
+ 'format error: not a valid json patch format(strictly `RFC 6902` compliant JSON Patch array. Format example: `[{ "op": "add", "path": "/children/0", "value": { ... } }, {"op":"add","path":"/methods/handleBtnClick","value": { ... }}, { "op": "replace", "path": "/css", "value": "..." }]`), please check and fix the json patch format.'
+ }
+ }
+ return { isError: false, data }
+ } catch (error) {
+ return { isError: true, error: serializeError(error) }
+ }
+}
+
+const _updatePageSchema = (streamContent: string, currentPageSchema: object, isFinial: boolean = false) => {
+ const { robotSettingState, CHAT_MODE } = useModelConfig()
+ if (robotSettingState.chatMode !== CHAT_MODE.Agent) {
+ return
+ }
+
+ // 解析流式返回的schema patch
+ let content = getJsonObjectString(streamContent)
+ let jsonPatches = []
+ try {
+ if (!isFinial) {
+ content = jsonrepair(content)
+ }
+ jsonPatches = JSON.parse(content)
+ } catch (error) {
+ if (isFinial) {
+ logger.error('parse json patch error:', error)
+ }
+ return { isError: true, error }
+ }
+
+ // 过滤有效的json patch
+ if (!isFinial && !isValidFastJsonPatch(jsonPatches)) {
+ return { isError: true, error: 'format error: not a valid json patch.' }
+ }
+ const validJsonPatches = jsonPatchAutoFix(jsonPatches, isFinial)
+
+ // 生成新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纠错
+ fixMethods(newSchema.methods)
+ schemaAutoFix(newSchema.children)
+
+ // 更新Schema
+ setSchema(newSchema)
+ if (isFinial) {
+ useHistory().addHistory()
+ }
+
+ return { schema: newSchema, isError: false }
+}
+
+export const updatePageSchema = useThrottleFn(_updatePageSchema, 200, true)
+
+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
+}
+
+export const fetchAssets = async () => {
+ try {
+ const res = (await getMetaApi(META_SERVICE.Http).get('/material-center/api/resource/find/1')) || []
+ return res
+ .filter((item) => item.description)
+ .map((item) => ({
+ url: item.resourceUrl,
+ describe: item.description
+ }))
+ } catch (error) {
+ return []
+ }
+}
diff --git a/packages/plugins/robot/src/composables/useChat.ts b/packages/plugins/robot/src/composables/useChat.ts
new file mode 100644
index 0000000000..9b260596cb
--- /dev/null
+++ b/packages/plugins/robot/src/composables/useChat.ts
@@ -0,0 +1,411 @@
+import { toRaw } from 'vue'
+import { getMetaApi, getOptions, META_SERVICE, useCanvas } from '@opentiny/tiny-engine-meta-register'
+import type { BubbleContentItem } from '@opentiny/tiny-robot'
+import {
+ STATUS,
+ useConversation,
+ type AIModelConfig,
+ type ChatCompletionStreamResponse,
+ type ChatCompletionStreamResponseChoice,
+ type ChatMessage,
+ type UseMessageOptions
+} from '@opentiny/tiny-robot-kit'
+import { utils } from '@opentiny/tiny-engine-utils'
+import { formatMessages, serializeError, mergeStringFields } from '../utils'
+import type { LLMMessage, ResponseToolCall, RobotMessage } from '../types'
+import { createClient } from '../client'
+import useMcpServer from './useMcp'
+import { updatePageSchema, fetchAssets, search, isValidJsonPatchObjectString } from './useAgent'
+import useModelConfig from './useConfig'
+import { getAgentSystemPrompt, getJsonFixPrompt } from '../prompts'
+import meta from '../../meta'
+
+const { deepClone } = utils
+
+type Message = ChatMessage & {
+ renderContent: BubbleContentItem[]
+ tool_calls: ResponseToolCall[]
+}
+
+const { robotSettingState, CHAT_MODE, saveRobotSettingState } = useModelConfig()
+
+const getApiUrl = () => {
+ return robotSettingState.chatMode === CHAT_MODE.Agent ? '/app-center/api/ai/chat' : '/app-center/api/chat/completions'
+}
+
+const config: Omit & {
+ axiosClient?: unknown
+ httpClientType?: 'axios' | 'fetch'
+} = {
+ apiKey: robotSettingState.selectedModel.apiKey || '',
+ apiUrl: getApiUrl(),
+ defaultModel: robotSettingState.selectedModel.model || 'deepseek-v3',
+ axiosClient: () => getMetaApi(META_SERVICE.Http)?.getHttp(),
+ httpClientType: 'axios'
+}
+
+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 beforeRequest = async (requestParams: any) => {
+ const { CHAT_MODE, robotSettingState, getModelCapabilities } = useModelConfig()
+ const pageSchema = deepClone(useCanvas().pageState.pageSchema)
+ const isAgentMode = robotSettingState.chatMode === CHAT_MODE.Agent
+ const tools = await useMcpServer().getLLMTools()
+ const modelCapabilities = getModelCapabilities(
+ robotSettingState.selectedModel.baseUrl,
+ robotSettingState.selectedModel.model
+ )
+ if (!requestParams.tools && tools?.length && !isAgentMode && modelCapabilities?.toolCalling !== false) {
+ Object.assign(requestParams, { tools })
+ }
+ if (isAgentMode) {
+ requestParams.apiKey = robotSettingState.selectedModel.apiKey
+ let referenceContext = ''
+ let imageAssets = []
+ if (requestParams.messages[0]?.role !== 'system') {
+ if (getOptions(meta.id)?.enableRagContext) {
+ referenceContext = await search(requestParams.messages?.at(-1)?.content)
+ }
+ if (getOptions(meta.id)?.enableResourceContext !== false) {
+ imageAssets = await fetchAssets()
+ }
+ addSystemPrompt(requestParams.messages, getAgentSystemPrompt(pageSchema, referenceContext, imageAssets))
+ }
+ if (!robotSettingState.enableThinking) {
+ Object.assign(requestParams, { response_format: { type: 'json_object' } })
+ }
+ }
+ requestParams.baseUrl = robotSettingState.selectedModel.baseUrl
+ requestParams.model = robotSettingState.selectedModel.model
+ if (modelCapabilities?.reasoning?.extraBody) {
+ Object.assign(
+ requestParams,
+ robotSettingState.enableThinking
+ ? modelCapabilities.reasoning.extraBody.enable
+ : modelCapabilities.reasoning.extraBody.disable
+ )
+ }
+ if (config.apiKey !== robotSettingState.selectedModel.apiKey) {
+ provider?.updateConfig({ apiKey: robotSettingState.selectedModel.apiKey }) // eslint-disable-line
+ config.apiKey = robotSettingState.selectedModel.apiKey
+ }
+ return requestParams
+}
+
+const { client, provider } = createClient({ config, beforeRequest })
+
+const updateLLMConfig = (newConfig: Omit) => {
+ provider?.updateConfig(newConfig)
+ Object.assign(config, newConfig)
+}
+
+const removeLoading = (messages: ChatMessage[], name?: string) => {
+ const renderContent = messages.at(-1)?.renderContent
+ if (!renderContent || !renderContent.length) return
+ const index = renderContent.findLastIndex((item) => item.type === 'loading' && (name ? item.content === name : true))
+ if (index !== -1) {
+ renderContent?.splice(index, 1)
+ }
+}
+
+let chatStatus = STATUS.INIT
+let pageSchema = null
+let chatAbortController: AbortController | null = null
+
+const events: UseMessageOptions['events'] = {
+ onReceiveData: (data: ChatCompletionStreamResponse, messages, preventDefault) => {
+ preventDefault()
+ const choice = data.choices?.[0]
+ if (!choice) {
+ return
+ }
+ if (chatStatus !== STATUS.STREAMING) {
+ chatStatus = STATUS.STREAMING
+ pageSchema = deepClone(useCanvas().pageState.pageSchema)
+ }
+ const lastMessage = messages.value.at(-1)
+ if (choice.delta.reasoning_content || choice.delta.content || choice.delta.tool_calls?.length) {
+ removeLoading(messages.value)
+ }
+ handleDeltaReasoning(choice, lastMessage) // eslint-disable-line
+ handleDeltaContent(choice, lastMessage) // eslint-disable-line
+ handleDeltaToolCalls(choice, lastMessage) // eslint-disable-line
+
+ updatePageSchema(lastMessage.content, pageSchema)
+ },
+ async onFinish(finishReason, { messages, messageState }, preventDefault) {
+ preventDefault()
+ const lastMessage = messages.value.at(-1)
+ if (finishReason === 'tool_calls') {
+ handleToolCall(lastMessage.tool_calls, messages.value) // eslint-disable-line
+ } else if (finishReason !== 'abort' && messageState.status !== STATUS.ABORTED) {
+ if (robotSettingState.chatMode === CHAT_MODE.Agent) {
+ const jsonValidResult = isValidJsonPatchObjectString(lastMessage.content)
+ if (jsonValidResult.isError) {
+ chatAbortController = new AbortController()
+ try {
+ const beforeRequest = (requestParams: any) => {
+ const { robotSettingState, getModelCapabilities } = useModelConfig()
+ const modelCapabilities = getModelCapabilities(
+ robotSettingState.selectedModel.baseUrl,
+ robotSettingState.selectedModel.model
+ )
+ if (modelCapabilities?.reasoning?.extraBody?.disable) {
+ Object.assign(requestParams, modelCapabilities.reasoning.extraBody.disable)
+ }
+ Object.assign(requestParams, {
+ response_format: { type: 'json_object' },
+ model: robotSettingState.selectedModel.model,
+ baseUrl: robotSettingState.selectedModel.baseUrl
+ })
+ return requestParams
+ }
+ updateLLMConfig({ apiUrl: '/app-center/api/chat/completions' })
+ messages.value.at(-1).renderContent.at(-1).status = 'fix'
+ const fixedResponse = await client.chat({
+ messages: [{ role: 'user', content: getJsonFixPrompt(lastMessage.content, jsonValidResult.error) }],
+ options: { signal: chatAbortController?.signal, beforeRequest: beforeRequest as any }
+ })
+ if (!isValidJsonPatchObjectString(fixedResponse.choices[0].message.content).isError) {
+ lastMessage.originContent = lastMessage.content
+ lastMessage.content = fixedResponse.choices[0].message.content
+ }
+ } catch (error) {
+ console.error('json fix failed', error) // eslint-disable-line
+ }
+ updateLLMConfig({ apiUrl: getApiUrl() })
+ }
+
+ const result = await updatePageSchema(lastMessage.content, pageSchema, true)
+ if (result.schema) {
+ messages.value.at(-1).renderContent.at(-1).status = 'success'
+ messages.value.at(-1).renderContent.at(-1).schema = result.schema
+ } else {
+ messages.value.at(-1).renderContent.at(-1).status = 'failed'
+ }
+ }
+ messageState.status = STATUS.FINISHED
+ }
+ chatStatus = messageState.status
+ pageSchema = null
+ }
+}
+
+const { messageManager, state: conversationState, createConversation, ...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: '',
+ status: 'reasoning',
+ 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)?.contentType === 'reasoning') {
+ lastMessage.renderContent.at(-1).status = 'finish'
+ }
+ 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
+ }
+}
+
+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
+ }
+}
+
+const handleToolCall = async (
+ tool_calls: ResponseToolCall[],
+ messages: ChatMessage[],
+ contextMessages?: RobotMessage[]
+) => {
+ const hasToolCall = tool_calls?.length > 0
+ if (!hasToolCall) {
+ return
+ }
+
+ chatAbortController = 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 (chatAbortController?.signal?.aborted) {
+ return
+ }
+ }
+ delete currentMessage.tool_calls
+ currentMessage.renderContent.push({ type: 'loading', content: '' })
+
+ await client.chatStream(
+ { messages: toolMessages, options: { signal: chatAbortController?.signal } },
+ {
+ onData: (data) => {
+ if (
+ data.choices[0].delta.reasoning_content ||
+ data.choices[0].delta.content ||
+ data.choices[0].delta.tool_calls?.length
+ ) {
+ removeLoading(messages)
+ }
+ 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)
+ const toolCalls = messages.at(-1)!.tool_calls
+ if (toolCalls?.length) {
+ await handleToolCall(toolCalls, messages, toolMessages)
+ } else {
+ getMessageManager().messageState.status = STATUS.FINISHED
+ }
+ }
+ }
+ )
+}
+
+const changeChatMode = (chatMode: string) => {
+ // 空会话更新metadata
+ const usedConversationId = conversationState.currentId
+ const newConversationId = createConversation('新会话', { chatMode })
+ if (usedConversationId === newConversationId) {
+ rest.updateMetadata(newConversationId, { chatMode })
+ rest.saveConversations()
+ }
+
+ robotSettingState.chatMode = chatMode
+ saveRobotSettingState({ chatMode })
+ updateLLMConfig({ apiUrl: getApiUrl() })
+}
+
+export default function () {
+ return {
+ updateLLMConfig,
+ conversationState,
+ ...messageManager,
+ changeChatMode,
+ abortRequest: () => {
+ chatAbortController?.abort()
+ messageManager.abortRequest()
+ messageManager.messageState.status = STATUS.ABORTED
+ removeLoading(messageManager.messages.value)
+ },
+ ...rest,
+ switchConversation: (conversationId: string) => {
+ const conversation = conversationState.conversations.find((conversation) => conversation.id === conversationId)
+ if (!conversation) return
+
+ rest.switchConversation(conversationId)
+ // 切换会话后跟随切换对话模式
+ if (conversation.metadata?.chatMode) {
+ robotSettingState.chatMode = robotSettingState.chatMode as string
+ } else {
+ robotSettingState.chatMode = CHAT_MODE.Agent
+ rest.updateMetadata(conversationId, { chatMode: CHAT_MODE.Agent })
+ rest.saveConversations()
+ }
+ if (robotSettingState.chatMode === CHAT_MODE.Agent) {
+ messageManager.messages.value.at(-1)?.renderContent?.forEach((item) => {
+ if (item.type === 'loading') {
+ item.status = 'failed'
+ }
+ })
+ } else {
+ removeLoading(messageManager.messages.value)
+ }
+ },
+ createConversation: (title?: string) => {
+ createConversation(title, { chatMode: robotSettingState.chatMode })
+ },
+ removeLoading
+ }
+}
diff --git a/packages/plugins/robot/src/composables/useConfig.ts b/packages/plugins/robot/src/composables/useConfig.ts
new file mode 100644
index 0000000000..51f89c965f
--- /dev/null
+++ b/packages/plugins/robot/src/composables/useConfig.ts
@@ -0,0 +1,155 @@
+/**
+ * Copyright (c) 2023 - present TinyEngine Authors.
+ * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
+ *
+ * Use of this source code is governed by an MIT-style license.
+ *
+ * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
+ * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
+ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
+ *
+ */
+
+/* metaService: engine.plugins.robot.useRobot */
+import { reactive } from 'vue'
+import { getOptions } from '@opentiny/tiny-engine-meta-register'
+import meta from '../../meta'
+import { DEFAULT_LLM_MODELS } from '../constants'
+
+const EXISTING_MODELS = 'existingModels'
+const CUSTOMIZE = 'customize'
+const CHAT_MODE = { Agent: 'agent', Chat: 'chat' }
+
+/**
+ * 合并 AI 模型配置
+ * 支持:
+ * 1. 通过 _remove: true 删除整个 provider(基于 baseUrl 匹配)
+ * 2. 通过在 model 中设置 _remove: true 删除特定 model
+ * 3. 相同 baseUrl 的 provider 会合并其 models
+ * 4. 相同 value 的 model 会被自定义配置覆盖
+ * @param defaults 默认配置
+ * @param customs 自定义配置
+ * @returns 合并后的配置
+ */
+const mergeAIModelOptions = (defaults: any[], customs: any[]): any[] => {
+ // 深拷贝默认配置作为基础
+ const result = JSON.parse(JSON.stringify(defaults))
+
+ customs.forEach((customProvider) => {
+ // 如果标记删除整个 provider(基于 baseUrl 匹配)
+ if (customProvider._remove) {
+ const index = result.findIndex((p: any) => p.baseUrl === customProvider.baseUrl)
+ if (index !== -1) {
+ result.splice(index, 1)
+ }
+ return
+ }
+
+ // 查找相同 baseUrl (value 字段) 的 provider
+ const existingProviderIndex = result.findIndex((p: any) => p.baseUrl === customProvider.baseUrl)
+
+ if (existingProviderIndex !== -1) {
+ // 找到相同 baseUrl 的 provider,合并 models
+ const existingProvider = result[existingProviderIndex]
+
+ customProvider.models?.forEach((customModel: any) => {
+ if (customModel._remove) {
+ // 移除指定的 model
+ const modelIndex = existingProvider.models.findIndex((m: any) => m.name === customModel.name)
+ if (modelIndex !== -1) {
+ existingProvider.models.splice(modelIndex, 1)
+ }
+ } else {
+ // 查找是否存在相同 value 的 model
+ const existingModelIndex = existingProvider.models.findIndex((m: any) => m.name === customModel.name)
+ if (existingModelIndex !== -1) {
+ // 替换已有 model(覆盖)
+ const { _remove, ...modelWithoutRemove } = customModel
+ existingProvider.models[existingModelIndex] = modelWithoutRemove
+ } else {
+ // 添加新 model
+ const { _remove, ...modelWithoutRemove } = customModel
+ existingProvider.models.push(modelWithoutRemove)
+ }
+ }
+ })
+
+ // 更新 provider 的其他属性(如果提供了)
+ if (customProvider.label) existingProvider.label = customProvider.label
+ if (customProvider.provider) existingProvider.provider = customProvider.provider
+ } else {
+ // 添加新的 provider
+ const { _remove, ...providerWithoutRemove } = customProvider
+ providerWithoutRemove.models = (providerWithoutRemove.models || [])
+ .filter((m: any) => !m._remove)
+ .map((m: any) => {
+ const { _remove, ...modelWithoutRemove } = m
+ return modelWithoutRemove
+ })
+ result.push(providerWithoutRemove)
+ }
+ })
+
+ return result
+}
+
+const getAIModelOptions = () => {
+ const customAIModels = getOptions(meta.id)?.customCompatibleAIModels || []
+ if (!customAIModels.length) {
+ return DEFAULT_LLM_MODELS
+ }
+ return mergeAIModelOptions(DEFAULT_LLM_MODELS, customAIModels)
+}
+
+const getModelCapabilities = (baseUrl: string, model: string) => {
+ return getAIModelOptions()
+ .find((option: any) => option.baseUrl === baseUrl)
+ ?.models.find((item: any) => item.name === model)?.capabilities
+}
+
+const SETTING_STORAGE_KEY = 'tiny-engine-robot-settings'
+
+const loadRobotSettingState = () => {
+ const items = localStorage.getItem(SETTING_STORAGE_KEY) || '{}'
+ try {
+ return JSON.parse(items)
+ } catch (error) {
+ return items
+ }
+}
+
+const saveRobotSettingState = (state: object) => {
+ const currentState = loadRobotSettingState() || {}
+ const newState = { ...currentState, ...state }
+ localStorage.setItem(SETTING_STORAGE_KEY, JSON.stringify(newState))
+}
+
+const { activeName, existModel, customizeModel, chatMode, enableThinking } = loadRobotSettingState() || {}
+
+const storageSettingState = (activeName === EXISTING_MODELS ? existModel : customizeModel) || {}
+
+const robotSettingState = reactive({
+ selectedModel: {
+ label: storageSettingState.label || getAIModelOptions()[0].label,
+ activeName: activeName || EXISTING_MODELS,
+ baseUrl: storageSettingState.baseUrl || getAIModelOptions()[0].value,
+ model: storageSettingState.model || getAIModelOptions()[0].models[0].value,
+ completeModel: storageSettingState.completeModel || getAIModelOptions()[0].models[0].value || '',
+ apiKey: storageSettingState.apiKey || ''
+ },
+ chatMode: chatMode || CHAT_MODE.Agent,
+ enableThinking: enableThinking || false
+})
+
+export default () => {
+ return {
+ saveRobotSettingState,
+ loadRobotSettingState,
+ EXISTING_MODELS,
+ CUSTOMIZE,
+ CHAT_MODE,
+ getAIModelOptions,
+ getModelCapabilities,
+ robotSettingState
+ }
+}
diff --git a/packages/plugins/robot/src/mcp/useMcp.ts b/packages/plugins/robot/src/composables/useMcp.ts
similarity index 83%
rename from packages/plugins/robot/src/mcp/useMcp.ts
rename to packages/plugins/robot/src/composables/useMcp.ts
index 26963e1cd9..adfa9af6e8 100644
--- a/packages/plugins/robot/src/mcp/useMcp.ts
+++ b/packages/plugins/robot/src/composables/useMcp.ts
@@ -1,7 +1,8 @@
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 { McpTool } from '../types/mcp-types'
+import type { RequestTool } from '../types/types'
const ENGINE_MCP_SERVER: PluginInfo = {
id: 'tiny-engine-mcp-server',
@@ -11,8 +12,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 +67,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 +84,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 +99,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
}
}
}
@@ -121,20 +107,24 @@ 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()
+ 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 || [])
+ const mcpTools = await getMetaApi(META_SERVICE.McpService)?.getMcpClient()?.listTools()
+ llmTools = convertMCPToOpenAITools(mcpTools?.tools || [])
+ return llmTools
}
export default function useMcpServer() {
return {
- mcpServers,
inUseMcpServers,
refreshMcpServerTools,
updateMcpServerStatus,
diff --git a/packages/plugins/robot/src/constants/index.ts b/packages/plugins/robot/src/constants/index.ts
new file mode 100644
index 0000000000..504e83352d
--- /dev/null
+++ b/packages/plugins/robot/src/constants/index.ts
@@ -0,0 +1 @@
+export * from './model-config'
diff --git a/packages/plugins/robot/src/constants/model-config.ts b/packages/plugins/robot/src/constants/model-config.ts
new file mode 100644
index 0000000000..84c970d6fc
--- /dev/null
+++ b/packages/plugins/robot/src/constants/model-config.ts
@@ -0,0 +1,92 @@
+const reasoningExtraBody = {
+ extraBody: {
+ enable: {
+ enable_thinking: true,
+ thinking_budget: 1000
+ },
+ disable: null
+ }
+}
+
+export const DEFAULT_LLM_MODELS = [
+ {
+ provider: 'bailian',
+ label: '阿里云百炼',
+ baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
+ allowEmptyApiKey: false,
+ models: [
+ // Agent/chat
+ {
+ label: 'Qwen 通用模型(Plus)',
+ name: 'qwen-plus',
+ capabilities: {
+ toolCalling: true,
+ reasoning: reasoningExtraBody
+ }
+ },
+ // 备注:千问多模态模型不支持工具调用;
+ {
+ label: 'Qwen VL视觉理解模型(PLUS)',
+ name: 'qwen3-vl-plus',
+ capabilities: {
+ vision: true,
+ reasoning: reasoningExtraBody
+ }
+ },
+ {
+ label: 'Qwen Coder编程模型(PLUS)',
+ name: 'qwen3-coder-plus',
+ capabilities: {
+ toolCalling: true,
+ reasoning: reasoningExtraBody
+ }
+ },
+ {
+ label: 'DeepSeek(v3.2)',
+ name: 'deepseek-v3.2-exp',
+ capabilities: {
+ toolCalling: true,
+ reasoning: reasoningExtraBody
+ }
+ },
+ // 小参数模型
+ {
+ label: 'Qwen 通用模型(Flash)',
+ name: 'qwen-flash',
+ capabilities: {
+ compact: true
+ }
+ },
+ {
+ label: 'Qwen Coder编程模型(Flash)',
+ name: 'qwen3-coder-flash',
+ capabilities: {
+ compact: true
+ }
+ },
+ { label: 'Qwen3(14b)', name: 'qwen3-14b', capabilities: { compact: true } },
+ { label: 'Qwen3(8b)', name: 'qwen3-8b', capabilities: { compact: true } }
+ ]
+ },
+ {
+ provider: 'deepseek',
+ label: 'DeepSeek',
+ baseUrl: 'https://api.deepseek.com/v1',
+ allowEmptyApiKey: false,
+ models: [
+ {
+ label: 'DeepSeek',
+ name: 'deepseek-chat',
+ capabilities: {
+ toolCalling: true,
+ reasoning: {
+ extraBody: {
+ enable: { model: 'deepseek-reasoner' },
+ disable: { model: 'deepseek-chat' }
+ }
+ }
+ }
+ }
+ ]
+ }
+]
diff --git a/packages/plugins/robot/src/js/prompts.ts b/packages/plugins/robot/src/js/prompts.ts
deleted file mode 100644
index 23f194d2ef..0000000000
--- a/packages/plugins/robot/src/js/prompts.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-export const PROMPTS = `
-# 静默JSON生成指令
-你是一个严格的JSON Patch生成器,必须且只能输出如下格式的内容:
-
-\`\`\`json
-/** 严格按照RFC 6902和IPageSchema规范的JSON Patch数组 */
-[
- {
- "op": "add",
- "path": "/children/-", // 根据当前schema去生成路径,新生成的模块从尾部添加。
- "value": {
- "componentName": "CanvasFlexBox",
- "id": "/* 随机生成8位数字符串(字母+数字) */",
- "props": {
- "className": "header-style",
- "justifyContent": "space-between",
- "alignItems": "center"
- },
- "children": [
- {
- "componentName": "img",
- "id": "/* 随机生成8位数字符串(字母+数字) */",
- "props": {
- "src": "https://res-static.hc-cdn.cn/cloudbu-site/intl/zh-cn/yunying/header-new/logo.png",
- "alt": "华为云Logo"
- }
- }
- ]
- }
- }
-]
-\`\`\`
-
-## 目标
-根据用户提供的图片/需求,生成value为IPageSchema规范数据的JSON Patch数据,在低代码中能够渲染出华为云官网的页面
-
-## 绝对规则
-1. 禁止输出任何非JSON内容,包括:
- - 解释性文字
- - 提示语(如"以下是...")
- - 未完成的标记(如...)
-2. 必须包含完整的JSON结构:
- - 始终以\`\`\`json开头和结尾
- - 确保数组闭合(所有括号匹配)
- - 包含所有必需的字段(componentName/id等)
- - 仅使用双引号,禁止单引号(如错误示例中的'autoplay')
-- 所有key必须加双引号(如"op"而非op)
-- 结尾不允许有多余逗号(如"children": [ {...}, ] ❌)
-- 布尔值必须小写(true/false,非'false'字符串)
-- 不要在json中添加注释,比如 "" 、 ""、 “// 添加顶部导航栏 (假设为一个容器)”、“ {/*首页大标题*/}”等
-- 不要有多余的空行和空格
-3. 错误处理:
- - 如果无法生成完整数据,返回空数组:\`\`\`json []\`\`\`
- - 不允许部分输出或占位注释
-4. 其中每个value值必须精确遵循IPageSchema规范
-5. 严格按照用户提供的图片在每个组件的props.style字段生成样式(值为字符串格式,与行内样式格式相同)
-6. 保留上一次生成的模块,不要把上一次生成的内容删除或者完全覆盖掉
-
-**错误修复示范**:
- - ❌ 'autoplay': 'false' → ✅ "autoplay": false
- - ❌ 'id': 'headerDiv' → ✅ "id": "headerDiv"
- - ❌ 'indicator-position ' → ✅ "indicatorPosition"(移除空格和连字符)
-
-## 修正模板(对照错误示例)
-错误示例:
-\`\`\`json
-{
- "componentName": "button",
- "props": {
- classNames: ["primary-btn"],
- clickHandler: function () {}
- }
-}
-\`\`\`
-
-修正后:
-\`\`\`json
-{
- "componentName": "TinyButton",
- "props": {
- "className": "primary-btn",
- "onClick": {
- "type": "JSFunction",
- "value": "function() { /* 处理逻辑 */ }"
- }
- }
-}
-\`\`\`
-
-# IPageSchema规范:
-## 1. 页面结构要求
-- 每个组件必须包含componentName,componentName: "Page" | "div" | "Text" | "TinyInput" | "TinyButton" | "img" | "video" | "a";可参考知识生成
-- 每个组件必须包含唯一ID:ID必须是8位随机字符串(示例:"k8jD3fG2");字符集:a-z, A-Z, 0-9;必须包含至少1个字母和1个数字;禁止连续模式(错误示例:"abc12345");使用强随机性组合(如"x7Y2pQ9r");同一JSON中所有组件的ID必须绝对唯一
-- 层级关系通过children数组嵌套,"children"的值不允许生成纯字符串数组、"children"的值不允许生成数组中混合对象和字符串的数据格式
-- 动态数据使用 this.state.xxx 绑定
-- 事件处理使用 this.methods.xxx 绑定
-- 样式通过每个组件的props.style字段定义(字符串格式,与行内样式格式相同),注意背景颜色、文字颜色、字体大小、字体系列、填充、边距、边框、布局等,严格按照图片样式还原,准确匹配颜色和尺寸。建议多用弹性布局。
-
-### 错误示例修正
-❌ 排序ID: "id": "12345678"
-✅ 乱序ID: "id": "8264a1c3"
-
-## 2. 组件转换规则
-├─ 容器元素 → { componentName: "div", id: "1aw73542" }
-├─ 表单元素 → { componentName: "TinyInput/TinySelect/TinyRadio", id: "162ee548" }
-├─ 按钮元素 → { componentName: "TinyButton", id: "16qw3541" }
-└─ 文本内容 → { componentName: "Text", id: "162731e8", props: { "text": "/** 文本内容 */" }}
-└─ 图片/图像元素 → { componentName: "img", id: "1qwe3548", props: { "src": "/** 图片链接 */", "alt": "/** 图片名称 */" }}
-└─ 视频元素 → { componentName: "video", id: "16173eq", props: { "src": "/** 视频链接 */", "autoPlay": true, "loop": true, "muted": true}}
-└─ 链接跳转元素 → { componentName: "a", id: "16273op9", props: {"href": "/** 跳转链接 */", "target": "_self"}}
-
-## 3. 特殊属性处理
-条件渲染: {
-"condition": {
-"type": "JSExpression",
-"value": "this.state.showSection"
-}
-}
-事件绑定: {
-"onClick": {
-"type": "JSFunction",
-"value": "function() { this.methods.handleSubmit() }"
-}
-}
-
-# 最终输出要求
-1. 必须通过以下校验:
- \`\`\`javascript
- JSON.parse(yourOutput) // 不能抛出语法错误
- \`\`\`
-2. 占位资源使用:
- - 图片: "src": "https://placehold.co/600x400"
- - 视频: "src": "https://placehold.co/640x360.mp4"
-3. 直接输出完整JSON,不要包含:
- - 注释(如)
- - 未实现的占位符(如...其他项目...)
- - 任何非JSON文本
-`
diff --git a/packages/plugins/robot/src/js/useRobot.ts b/packages/plugins/robot/src/js/useRobot.ts
deleted file mode 100644
index a67b9d3cf8..0000000000
--- a/packages/plugins/robot/src/js/useRobot.ts
+++ /dev/null
@@ -1,194 +0,0 @@
-/**
- * Copyright (c) 2023 - present TinyEngine Authors.
- * Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
- *
- * Use of this source code is governed by an MIT-style license.
- *
- * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
- * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
- * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
- *
- */
-
-/* metaService: engine.plugins.robot.useRobot */
-import { reactive } from 'vue'
-import { getOptions, getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register'
-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 AIModelOptions = [
- {
- label: '阿里云百炼',
- value: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
- model: [
- { label: 'qwen-vl-max', value: 'qwen-vl-max', ability: ['visual'] },
- { label: 'qwen-vl-plus', value: 'qwen-vl-plus', ability: ['visual'] },
- { label: 'qwen-plus', value: 'qwen-plus' },
- { label: 'qwen-max', value: 'qwen-max' },
- { label: 'qwen-turbo', value: 'qwen-turbo' },
- { label: 'qwen-long', value: 'qwen-long' },
- { label: 'deepseek-r1', value: 'deepseek-r1' },
- { label: 'deepseek-v3', value: 'deepseek-v3' },
- { label: 'qwen2.5-14b-instruct', value: 'qwen2.5-14b-instruct' },
- { label: 'qwen2.5-7b-instruct', value: 'qwen2.5-7b-instruct' },
- { label: 'qwen2.5-coder-7b-instruct', value: 'qwen2.5-coder-7b-instruct' },
- { label: 'qwen2.5-omni', value: 'qwen2.5-omni' },
- { label: 'qwen3-14b', value: 'qwen3-14b' },
- { label: 'qwen3-8b', value: 'qwen3-8b' },
- { label: 'deepseek-r1-distill-qwen-1.5b', value: 'deepseek-r1-distill-qwen-1.5b' },
- { label: 'deepseek-r1-distill-qwen-32b', value: 'deepseek-r1-distill-qwen-32b' }
- ]
- },
- {
- label: 'DeepSeek',
- value: 'https://api.deepseek.com/v1',
- model: [
- { label: 'deepseek-chat', value: 'deepseek-chat' },
- { label: 'deepseek-reasoner', value: 'deepseek-reasoner' }
- ]
- },
- {
- label: '月之暗面',
- value: 'https://api.moonshot.cn/v1',
- model: [
- { label: 'moonshot-v1-8k', value: 'moonshot-v1-8k' },
- { label: 'moonshot-v1-32k', value: 'moonshot-v1-32k' },
- { label: 'moonshot-v1-128k', value: 'moonshot-v1-128k' }
- ]
- }
-]
-
-const getAIModelOptions = () => {
- const aiRobotOptions = getOptions(meta.id)?.customCompatibleAIModels || []
- return aiRobotOptions.length ? aiRobotOptions : AIModelOptions
-}
-
-const robotSettingState = reactive({
- selectedModel: {
- label: getAIModelOptions()[0].label,
- activeName: EXISTING_MODELS,
- baseUrl: getAIModelOptions()[0].value,
- model: getAIModelOptions()[0].model[0].value,
- completeModel: getAIModelOptions()[0].model[0].value || '',
- apiKey: ''
- }
-})
-
-// 这里存放的是aichat的响应式数据
-const state = reactive({
- blockList: [],
- blockContent: ''
-})
-
-const getBlocks = () => state.blockList || []
-
-const setBlocks = (blocks) => {
- state.blockList = blocks
-}
-
-const getBlockContent = () => state.blockContent || ''
-
-const transformBlockNameToElement = (label) => {
- const elementName = label.replace(/[A-Z]/g, (letter, index) => {
- return index === 0 ? letter.toLowerCase() : `_${letter.toLowerCase()}`
- })
- return `<${elementName}>`
-}
-
-// 拼接blockContent,在ai初始时引入区块。
-const setBlockContent = (list = getBlocks()) => {
- const blockList = list.slice(0, 200) // 为了尽量避免每个请求的message内容过大,限制block的个数避免超出字节要求
- const blockMessages = blockList.map((item) => {
- const blockElementName = transformBlockNameToElement(item.label)
- return `${blockElementName}名称是${item.label}`
- })
- const content = blockMessages?.join(';')
- if (content) {
- state.blockContent = `在提问之前,我希望你记住以下自定义的前端组件:${content}。接下来我开始问出第一个问题:`
- } else {
- state.blockContent = ''
- }
-}
-
-const initBlockList = async () => {
- if (state.blockList?.length) {
- return
- }
- const appId = getMetaApi(META_SERVICE.GlobalService).getBaseInfo().id
- try {
- const list = await getMetaApi(META_SERVICE.Http).get('/material-center/api/blocks', { params: { appId } })
- setBlocks(list)
- setBlockContent(list)
- } catch (err) {
- // 捕获错误
- throw new Error('获取block列表失败', { cause: err })
- }
-}
-
-const isValidOperation = (operation) => {
- const allowedOps = ['add', 'remove', 'replace', 'move', 'copy', 'test', '_get']
-
- if (typeof operation !== 'object' || operation === null) {
- return false
- }
- // 检查操作类型是否有效
- if (!operation.op || !allowedOps.includes(operation.op)) {
- return false
- }
- // 检查path字段是否存在且为字符串
- if (!operation.path || typeof operation.path !== 'string') {
- return false
- }
- // 根据操作类型检查其他必需字段
- switch (operation.op) {
- case 'add':
- case 'replace':
- case 'test':
- if (!('value' in operation)) {
- return false
- }
- break
- case 'move':
- case 'copy':
- if (!operation.from || typeof operation.from !== 'string') {
- return false
- }
- break
- }
-
- return true
-}
-
-const isValidFastJsonPatch = (patch) => {
- if (Array.isArray(patch)) {
- return patch.every(isValidOperation)
- } else if (typeof patch === 'object' && patch !== null) {
- return isValidOperation(patch)
- }
- return false
-}
-
-export default () => {
- return {
- EXISTING_MODELS,
- CUSTOMIZE,
- VISUAL_MODEL,
- AI_MODES,
- AIModelOptions,
- getAIModelOptions,
- robotSettingState,
- state,
- getBlocks,
- setBlocks,
- getBlockContent,
- transformBlockNameToElement,
- setBlockContent,
- initBlockList,
- isValidOperation,
- isValidFastJsonPatch
- }
-}
diff --git a/packages/plugins/robot/src/js/utils.ts b/packages/plugins/robot/src/js/utils.ts
deleted file mode 100644
index 80b69a9332..0000000000
--- a/packages/plugins/robot/src/js/utils.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { handleSSEStream, type StreamHandler } from '@opentiny/tiny-robot-kit'
-
-export const chatStream = async (requestOpts: any, handler: StreamHandler, headers = {}) => {
- try {
- const { requestData, requestUrl } = requestOpts
-
- const requestOptions = {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Accept: 'text/event-stream',
- ...headers
- },
- body: JSON.stringify(requestData)
- }
- const response = await fetch(requestUrl, requestOptions)
-
- if (!response.ok) {
- const errorText = await response.text()
- throw new Error(`HTTP error! status: ${response.status}, details: ${errorText}`)
- }
-
- await handleSSEStream(response, handler)
- } catch (error: unknown) {
- const logger = console
- logger.error('Error in chatStream:', error)
- }
-}
-
-export const checkComponentNameExists = (data: any) => {
- if (!data.componentName) {
- return false
- }
-
- if (data.children && Array.isArray(data.children)) {
- for (const child of data.children) {
- if (!checkComponentNameExists(child)) {
- return false
- }
- }
- }
-
- return true
-}
-
-export const processSSEStream = (data,handler) => {
- const lines = data.split('\n')
-
- for (const line of lines) {
- if (line.startsWith('data: ')) {
- const dataStr = line.substring(6).trim()
-
- // 检查结束标记
- if (dataStr === '[DONE]') {
- handler.onDone()
-
- return
- }
-
- if (dataStr) {
- const data = JSON.parse(dataStr)
- handler.onData(data)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/packages/plugins/robot/src/mcp/LoadingRenderer.vue b/packages/plugins/robot/src/mcp/LoadingRenderer.vue
deleted file mode 100644
index d32def5b80..0000000000
--- a/packages/plugins/robot/src/mcp/LoadingRenderer.vue
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/packages/plugins/robot/src/mcp/utils.ts b/packages/plugins/robot/src/mcp/utils.ts
deleted file mode 100644
index aa9fd4887c..0000000000
--- a/packages/plugins/robot/src/mcp/utils.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-import { toRaw } from 'vue'
-import useMcpServer from './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'
-
-let requestOptions: RequestOptions = {}
-
-// 格式化LLM输入messages消息
-const formatMessages = (messages: LLMMessage[]) => {
- return messages.map((message) => ({
- role: message.role,
- content: message.content
- }))
-}
-
-const fetchLLM = async (messages: LLMMessage[], tools: RequestTool[], options: RequestOptions = requestOptions) => {
- const bodyObj: LLMRequestBody = {
- baseUrl: options.baseUrl,
- model: options?.model || 'deepseek-chat',
- stream: false,
- messages: toRaw(messages)
- }
- if (tools.length > 0) {
- bodyObj.tools = toRaw(tools)
- }
- return getMetaApi(META_SERVICE.Http).post(options?.url || '/app-center/api/chat/completions', bodyObj, {
- headers: {
- 'Content-Type': 'application/json',
- ...options?.headers
- }
- })
-}
-
-const parseArgs = (args: string) => {
- try {
- return JSON.parse(args)
- } catch (error) {
- return args
- }
-}
-
-export const serializeError = (err: unknown): string => {
- if (err instanceof Error) {
- return JSON.stringify({ name: err.name, message: err.message })
- }
- if (typeof err === 'string') return err
- try {
- return JSON.stringify(err)
- } catch {
- return String(err)
- }
-}
-
-const formatToolResult = (
- toolResult: string | { type: 'text'; text: string } | Array<{ type: 'text'; text: string }>
-) => {
- let result: any = toolResult
- if (Array.isArray(result) && result.length === 1) {
- result = result[0]
- }
-
- if (typeof result === 'object' && result.type === 'text' && result.text) {
- result = result.text
- }
-
- if (typeof result === 'string') {
- return result
- }
-
- return JSON.stringify(result)
-}
-
-const handleToolCall = async (
- res: LLMResponse,
- tools: RequestTool[],
- messages: RobotMessage[],
- contextMessages?: RobotMessage[]
-) => {
- if (messages.length < 1) {
- return
- }
- const currentMessage = messages.at(-1)!
- if (!currentMessage.renderContent) {
- currentMessage.renderContent = []
- }
- if (res.choices[0].message.content) {
- currentMessage.renderContent.push({
- type: 'markdown',
- content: res.choices[0].message.content
- })
- }
- const tool_calls: ReponseToolCall[] | undefined = res.choices[0].message.tool_calls
- if (tool_calls && tool_calls.length) {
- const historyMessages = contextMessages?.length ? contextMessages : toRaw(messages.slice(0, -1))
- const toolMessages: LLMMessage[] = [...historyMessages, res.choices[0].message] as LLMMessage[]
- 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: formatToolResult(toolCallResult),
- role: 'tool',
- tool_call_id: tool.id
- })
-
- currentMessage.renderContent.at(-1)!.status = toolCallStatus
- currentMessage.renderContent.at(-1)!.content = {
- params: parsedArgs,
- result: toolCallResult
- }
- }
- currentMessage.renderContent.push({ type: 'loading', content: '' })
- const newResp = await fetchLLM(toolMessages, tools)
- currentMessage.renderContent.pop()
- const hasToolCall = newResp.choices[0].message.tool_calls?.length > 0
- if (hasToolCall) {
- await handleToolCall(newResp, tools, messages, toolMessages)
- } else {
- if (newResp.choices[0].message.content) {
- currentMessage.renderContent.push({
- type: 'markdown',
- content: newResp.choices[0].message.content
- })
- }
- }
- }
-}
-
-export const sendMcpRequest = async (messages: LLMMessage[], options: RequestOptions = {}) => {
- if (messages.length < 1) {
- return
- }
- const tools = await useMcpServer().getLLMTools()
- requestOptions = options
- messages.at(-1)!.renderContent = [{ type: 'loading', content: '' }]
- const historyRaw = toRaw(messages.slice(0, -1)) as LLMMessage[]
- const res = await fetchLLM(formatMessages(historyRaw), tools, options)
- delete messages.at(-1)!.renderContent
- const hasToolCall = res.choices[0].message.tool_calls?.length > 0
- if (hasToolCall) {
- await handleToolCall(res, tools, messages)
- const lastMsg: any = messages.at(-1) as any
- const renderList: any[] | undefined = Array.isArray(lastMsg.renderContent)
- ? (lastMsg.renderContent as any[])
- : undefined
- const lastRendered: any = renderList && renderList.length > 0 ? renderList[renderList.length - 1] : undefined
- const renderedContent: unknown = lastRendered?.content
- lastMsg.content = typeof renderedContent === 'string' ? renderedContent : JSON.stringify(renderedContent ?? '')
- } else {
- messages.at(-1)!.content = res.choices[0].message.content
- }
-}
diff --git a/packages/plugins/robot/src/js/index.ts b/packages/plugins/robot/src/metas/index.ts
similarity index 51%
rename from packages/plugins/robot/src/js/index.ts
rename to packages/plugins/robot/src/metas/index.ts
index a17eb974d9..383d54ee2c 100644
--- a/packages/plugins/robot/src/js/index.ts
+++ b/packages/plugins/robot/src/metas/index.ts
@@ -1,10 +1,13 @@
import { HOOK_NAME } from '@opentiny/tiny-engine-meta-register'
-import useRobot from './useRobot'
+import useModelConfig from '../composables/useConfig'
export const RobotService = {
id: 'engine.service.robot',
type: 'MetaService',
- apis: useRobot(),
+ apis: {
+ robotSettingState: useModelConfig().robotSettingState,
+ getAIModelOptions: useModelConfig().getAIModelOptions
+ },
composable: {
name: HOOK_NAME.useRobot
}
diff --git a/packages/plugins/robot/src/prompts/data/components.json b/packages/plugins/robot/src/prompts/data/components.json
new file mode 100644
index 0000000000..878c2aa355
--- /dev/null
+++ b/packages/plugins/robot/src/prompts/data/components.json
@@ -0,0 +1,998 @@
+[
+ {
+ "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"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ }
+]
diff --git a/packages/plugins/robot/src/prompts/data/examples.json b/packages/plugins/robot/src/prompts/data/examples.json
new file mode 100644
index 0000000000..c5bbf1eb46
--- /dev/null
+++ b/packages/plugins/robot/src/prompts/data/examples.json
@@ -0,0 +1,148 @@
+{
+ "chatMessageList": {
+ "name": "Chat Message List Example",
+ "description": "A complete example of adding a chat message list with input and send functionality",
+ "note": "All JavaScript code uses string concatenation, not template literals",
+ "patch": [
+ {
+ "op": "add",
+ "path": "/state/messages",
+ "value": [
+ {
+ "content": "hello"
+ }
+ ]
+ },
+ {
+ "op": "add",
+ "path": "/state/inputMessage",
+ "value": ""
+ },
+ {
+ "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": "/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/prompts/index.ts b/packages/plugins/robot/src/prompts/index.ts
new file mode 100644
index 0000000000..970bd0d7db
--- /dev/null
+++ b/packages/plugins/robot/src/prompts/index.ts
@@ -0,0 +1,87 @@
+import agentPrompt from './templates/agent-prompt.md?raw'
+import chatPrompt from './templates/chat-prompt.md?raw'
+import componentsData from './data/components.json'
+import examplesData from './data/examples.json'
+
+/**
+ * Convert components array to JSONL format string
+ */
+const formatComponentsToJsonl = (components: any[]): string => {
+ return '```jsonl\n' + components.map((comp) => JSON.stringify(comp)).join('\n') + '\n```'
+}
+
+/**
+ * Format examples object to readable text
+ */
+const formatExamples = (examples: Record): string => {
+ return Object.entries(examples)
+ .map(([_key, example]) => {
+ const { name, description, note, patch } = example
+ const header = `### ${name}\n${description ? `${description}\n` : ''}${note ? `**Note**: ${note}\n` : ''}`
+ const patchContent = JSON.stringify(patch)
+ return `${header}\n${patchContent}`
+ })
+ .join('\n\n')
+}
+
+/**
+ * Generate agent system prompt with dynamic components and examples
+ */
+export const getAgentSystemPrompt = (currentPageSchema: object, referenceContext: string, imageAssets: any[]) => {
+ // Format components list
+ const ignoreComponents = ['TinyNumeric'] // 组件报错,先忽略
+ const componentsList = formatComponentsToJsonl(
+ componentsData.filter((component) => !ignoreComponents.includes(component.component))
+ )
+
+ // Format examples section
+ const examplesSection = formatExamples(examplesData)
+
+ // Format current page schema
+ const currentPageSchemaStr = JSON.stringify(currentPageSchema)
+
+ // Replace all placeholders in the prompt template
+ const prompt = agentPrompt
+ .replace('{{COMPONENTS_LIST}}', componentsList)
+ .replace('{{EXAMPLES_SECTION}}', examplesSection)
+ .replace('{{CURRENT_PAGE_SCHEMA}}', currentPageSchemaStr)
+ .replace('{{REFERENCE_KNOWLEDGE}}', referenceContext || '')
+ .replace('{{IMAGE_ASSETS}}', imageAssets.map((item) => `- `).join('\n'))
+
+ return prompt.trim()
+}
+
+export const getChatSystemPrompt = () => chatPrompt
+
+export const getJsonFixPrompt = (jsonString: string, error = '') => {
+ const errorSection = error ? `## Error Message\n${error}\n\n` : ''
+
+ return `
+You are a JSON repair specialist. Fix the following invalid JSON string to create a valid JSON Patch array (RFC 6902 standard).
+
+## JSON Patch Format Requirements:
+- Array of objects, each with required "op" and "path" properties
+- "op" must be one of: "add", "replace", "remove", "move", "copy", "test"
+- "path" must be a JSON Pointer string (e.g., "/property", "/array/0")
+- "value" is required for "add", "replace", "move", "copy", "test" operations
+- "from" is required for "move", "copy" operations
+- All strings must use double quotes, no trailing commas
+
+## Example JSON Patch:
+[
+ { "op": "add", "path": "/children/0", "value": { ... } },
+ { "op": "replace", "path": "/css", "value": "..." }
+]
+
+## Your Task:
+1. Parse and fix the invalid JSON string
+2. Ensure it conforms to JSON Patch format
+3. Output ONLY the corrected JSON string
+4. No explanations, comments, or markdown formatting
+
+## Invalid JSON Input:
+${jsonString}
+
+${errorSection}## Output (JSON only):
+`.trim()
+}
diff --git a/packages/plugins/robot/src/prompts/templates/agent-prompt.md b/packages/plugins/robot/src/prompts/templates/agent-prompt.md
new file mode 100644
index 0000000000..65b7dfbed0
--- /dev/null
+++ b/packages/plugins/robot/src/prompts/templates/agent-prompt.md
@@ -0,0 +1,213 @@
+**[System Instructions: Role & Core Mission]**
+
+You are a specialized AI assistant for a TinyEngine low-code platform. Your sole responsibility is to **function as an API that silently and precisely generates JSON Patch data for PageSchema structures**. You are not a conversational agent, but a functional service.
+
+**Core Mission**: Based on the **[Current Page Schema]**, **[Reference Knowledge]**, and user requirements, generate a strictly compliant `RFC 6902` JSON Patch array to add/replace/remove/move (`add`/`replace`/`remove`/`move`) components and logic that conform to the PageSchema specification (see Section 3), transforming the existing page into one that meets user needs.
+
+**⚠️ Critical Reminder**: Your output will be directly parsed by `JSON.parse()`. Any formatting errors will cause system crashes. You MUST:
+ 1. NEVER use JavaScript template literals (backticks `` ` ``), use string concatenation instead
+ 2. All newlines MUST be escaped as `\n`, no actual line breaks allowed
+ 3. Output pure JSON only, without any markers or comments
+
+-----
+
+## 1. Operational Workflow
+
+The low-code platform workflow is as follows:
+Current Page Schema → Generate JSON Patch based on user requirements → Apply JSON Patch to create new Page Schema → Continue modifications based on user feedback, generating new JSON Patch from updated Schema → Apply new JSON Patch to update current Page Schema
+
+Page Schema is a JSON format describing page UI and functionality. It can be compiled into Vue code, so Page Schema is equivalent to Vue Single File Component code in a specific format.
+
+Follow these steps strictly to generate PageSchema (in JSON Patch format) that meets user requirements:
+
+1. **Parse Input**: Carefully analyze the **[User Requirements]** (text description or image analysis results), combined with the **[Current Page Schema]** below and any **[Reference Knowledge]** provided.
+2. **Generate UI, Logic, Lifecycles, etc.**: Based on user requirements, think about modifications to the current Schema to generate UI, logic, lifecycles, and other necessary data that satisfies requirements and conforms to `PageSchema` specification.
+3. **Encapsulate as JSON Patch**: Wrap the generated data into a strictly `RFC 6902` compliant JSON Patch array. Format example: `[{ "op": "add", "path": "/children/0", "value": { ... } }, {"op":"add","path":"/methods/handleBtnClick","value": { ... }}, { "op": "replace", "path": "/css", "value": "..." }]`.
+4. **Final Validation**: Before output, execute the following verification steps:
+ - Confirm output is a **single-line** compact JSON string (no actual line breaks)
+ - Confirm all newlines within strings are escaped as `\n`, not actual newline characters
+ - Confirm NO JavaScript template literal syntax (backticks `` ` ``) is used
+ - Confirm all double quotes are properly escaped
+ - **[NEW] Check array element separation**: Search entire output to ensure no `]}{` patterns exist, should be `]},{`
+ - **[NEW] Check object separation**: Search entire output to ensure no `},"op":` patterns exist, should be `},{"op":`
+ - **[NEW] Check bracket balance**: Count `{` and `}` must be equal, `[` and `]` must be equal
+ - **[NEW] Check nesting depth**: Simulate bracket matching from start to end, ensure depth never goes negative
+ - Mentally simulate executing `JSON.parse(your_output)`, ensure it won't throw `SyntaxError`
+ - If any step fails or you cannot understand the requirement, you MUST output an empty array `[]`.
+
+-----
+
+## 2. Output Format & Absolute Constraints
+
+**Output in JSON format. You must and can only output a raw and complete JSON string, which is itself a JSON Patch array that can be parsed by JSON.parse into a JSON object.** For example, the following result adds a method named `handleBtnClick`, adds a page state variable named `name`, and removes a page element:
+[{"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"}]
+
+Constraint Rules:
+ * **Strictly Prohibited**:
+ * Any explanatory text, preamble, or closing remarks (e.g., "Here's the JSON you requested...")
+ * DO NOT wrap JSON string with \`\`\`json or \`\`\`
+ * Adding any comments inside or outside JSON (such as `//` or `/* */`)
+ * Any form of ellipsis or incomplete placeholders (such as `...`)
+ * **JSON Syntax Iron Rules**:
+ * All keys and string values MUST use **double quotes** (`"`)
+ * The last element of an object or array **MUST NOT** have trailing commas
+ * Boolean values must be lowercase `true` or `false`, not strings
+ * Ensure all brackets `{}`, `[]` are properly closed and matched
+ * Output MUST be a **single-line** compact JSON string, no actual line breaks or unnecessary spaces
+ * **Array and Object Separation Rules (Extremely Important!)**:
+ * **Array elements MUST have commas between them**:
+ * ❌ Fatal error: `[{...}{...}]` or `[{...}]{...}]` or `...]}{"componentName"...`
+ * ✅ Correct: `[{...},{...}]` or `...},{{"componentName"...`
+ * **Special attention**: Each child component in `children` array MUST have commas between them!
+ * **Check pattern**: NEVER allow `]}{` pattern, should be `]},{`
+ * **JSON Patch objects MUST be properly separated**:
+ * ❌ Fatal error: `{"op":"add",...},"op":"add"` (duplicate op field in same object)
+ * ✅ Correct: `{"op":"add",...},{"op":"add",...}`
+ * **Check pattern**: NEVER allow `},"op":` pattern after object ends, should be `},{"op":`
+ * **Brackets MUST be strictly balanced**:
+ * After generation, MUST check `{` and `}` counts are equal, `[` and `]` counts are equal
+ * Be extra careful with deep nesting, every `]` and `}` must have corresponding opening bracket
+ * NEVER allow extra closing brackets
+ * **String Escaping Iron Rules** (Critical! Avoid JSON.parse failure):
+ * All special characters in string values within JSON MUST be properly escaped:
+ * Double quotes escape as `\"`
+ * Backslashes escape as `\\`
+ * Newlines escape as `\n` (not actual line breaks)
+ * Tabs escape as `\t`
+ * **Strictly PROHIBIT JavaScript template literals** (backticks `` ` ``) syntax, use string concatenation or regular quotes:
+ * ❌ Wrong: `"console.log(\`hello ${name}\`)"`
+ * ✅ Correct: `"console.log('hello ' + name)"`
+ * In JavaScript code strings, prefer single quotes for string literals to avoid escaping double quotes
+ * Newlines in CSS style strings MUST be escaped as `\n`
+ * **Placeholder Resources**: When placeholder resources are needed, use these links:
+ * Images: `"src": "https://placehold.co/600x400"`
+ * Videos: `"src": "https://placehold.co/640x360.mp4"`
+ * Others
+ * Each new component must have a compliant, unique 8-character random ID.
+
+### 2.1 Common Error Examples (Absolutely Prohibited)
+
+To avoid JSON.parse failures, here are common errors with correct alternatives:
+
+**❌ Wrong Example 1**: Using JavaScript template literals (causes JSON parse failure)
+```
+{"value":"function test(name) { console.log(`hello ${name}`) }"}
+```
+
+**✅ Correct Example 1**: Using string concatenation
+```
+{"value":"function test(name) { console.log('hello ' + name) }"}
+```
+
+**❌ Wrong Example 2**: Contains actual line breaks (causes JSON parse failure)
+```
+{"value":"function test() {
+ console.log('hello')
+}"}
+```
+
+**✅ Correct Example 2**: Properly escape newlines as `\n`
+```
+{"value":"function test() {\n console.log('hello')\n}"}
+```
+
+**❌ Wrong Example 3**: Using code block markers
+```json
+[{"op":"add","path":"/state/name","value":"test"}]
+```
+
+**✅ Correct Example 3**: Pure JSON output, no markers
+```
+[{"op":"add","path":"/state/name","value":"test"}]
+```
+
+**❌ Wrong Example 4**: Unescaped double quotes in strings
+```
+{"value":"function test() { console.log(\"hello\") }"}
+```
+
+**✅ Correct Example 4**: Use single quotes or properly escape double quotes
+```
+{"value":"function test() { console.log('hello') }"}
+```
+
+-----
+
+## 3. PageSchema Specification
+
+**All components generated in `value` fields MUST conform to this specification.**
+
+### 3.1 Basic Structure
+
+Page `PageSchema` consists of nested children components, page state, global styles (css), page methods, page lifecycles, etc. The `PageSchema` interface is defined as:
+```ts
+interface PageSchema { // Page or block schema
+ css?: string; // Global page style class definitions, similar to in Vue, example: "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", referenced in components via props.class
+ props: {
+ className?: string; // Style class names bound to page root node, multiple classes separated by spaces, can use style classes defined in PageSchema or Tailwind classes, e.g.: "className": "page-base-style"
+ };
+ children?: Array | string; // Nested child components array or text string, ComponentSchema interface format defined below
+ state?: {
+ [name:string]: any; // State variables with initial values, e.g.: "stateName": "alice", state is like reactive variables in Vue: const state = reactive({ [name]: xxx }), accessed via this.state[name]
+ };
+ methods?: {
+ [name:string]: { type: 'JSFunction', value: string } // Define methods, e.g.: "modelChange": { "type": "JSFunction", "value": "function modelChange(value) {\n this.emit('change', value);\n}" }, accessed via this[methodName]
+ }
+ lifeCycles: {
+ [name:string]: { type: 'JSFunction', value: string } // Define page lifecycles, similar to Vue component lifecycles, lifecycle name values enum: ['setup', 'onBeforeMount', 'onMounted', 'onUnmounted', 'onUpdated', 'onBeforeUpdate'], example: { "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}" } }
+ }
+}
+```
+
+Page component `ComponentSchema` interface is defined as:
+```ts
+interface ComponentSchema { // Component schema
+ componentName?: string; // Component name, available component names reference Section 3.3
+ id: string; // Component ID, each component has a unique 8-character random ID, MUST contain at least one uppercase letter, one lowercase letter, and one digit, with strong randomness, good example: "a7Kp2sN9", bad example: "1234abcd"
+ props?: { // Component bound properties
+ condition?: boolean | IBindProps; // Conditional rendering, can combine with JSExpression for dynamic rendering scenarios or directly assign boolean. condition effect similar to v-if in Vue, e.g.: "condition": { "type": "JSExpression", "value": "this.state.visible" } equivalent to v-if="state.visible"
+ style?: string; // Component inline styles, e.g.: "style": "display: flex; align-items: center;"
+ className?: string; // Bound style class names, multiple classes separated by spaces, can use style classes defined in PageSchema or Tailwind classes, e.g.: "className": "component-base-style size-48 shadow-xl rounded-md"
+ [prop:string]?: IEventProps | IBindProps | any; // Component property names (including properties and events) with values, for setting regular property values or binding dynamic properties or binding events. Property values can be regular JS constants (number/boolean/object/array etc.), or { type,value} format to bind to variables/methods (starting with this.), example: { "total": 100, "fetch-data": { "type": "JSExpression", "value": "{api:this.getTableData}" }, "onClick": { "type": "JSExpression", "value": "this.fixedLayout" } }
+ };
+ children?: Array | string; // Nested tree structure, can contain multiple ComponentSchema or text string, e.g. {"componentName":"div","children":[{"componentName":"div","children":"hello"}]}
+}
+```
+
+### 3.2 Advanced Features
+
+- Dynamic expressions or methods: Represented by `{ type, value }` object format, type indicates type, possible values: "JSExpression" (value is expression string) or "JSFunction" (value is function body string). All dynamic content (involving this.xxx) needs `{ type, value }` format (such as condition, binding variables to component properties, binding events, etc.). Example 1, bind state to props.text: `"text": { "type": "JSExpression", "value": "this.state.text"}`, Example 2, bind method to click event: `"onClick": { "type": "JSExpression", "value": "this.handleButtonClick"}`
+- Event binding: Used to bind handler methods to component events, use dynamic expression `{ "type": "JSExpression", "value": "xxx" }` to bind, similar to event binding in Vue. Events automatically pass event parameter (first parameter), additional parameters passed via params(string[]) (second and subsequent parameters), e.g. `"onClick": { "type": "JSExpression", "value": "this.handleButtonClick"}`, equivalent to `@click="(...eventArgs) => handleButtonClick(eventArgs)"` in Vue. Example: `"onClick": { "type": "JSExpression", "value": "this.handleButtonClick", "params": ["item", "'pure string param'"]}`, equivalent to `@click="(...eventArgs) => sendMessage(eventArgs, item, 'pure string param')"` in Vue
+- Two-way binding: Used for input and other form scenarios, similar to two-way binding in Vue. Two-way binding enabled via model field (`model?: true | { prop: string }`). All form-type components with modelValue property support two-way binding and should prioritize it. Example 1: `{"value":{"type":"JSExpression","value":"item.selected", "model": true }}` equivalent to `v-model="item.selected"` in Vue. Example 2: `{"value":{"type":"JSExpression","value":"item.selected","model":{"prop":"visible"}}}` equivalent to `v-model:visible="item.selected"` in Vue
+- Dynamic class: When using dynamic class, set className type to JSExpression in props, set className value to dynamic class expression. Example: `{"className":{"type":"JSExpression","value":"['header-layout-icon left', {'active': this.state.fixedActive}]"}}`
+- Loop: When rendering multiple identical components, use loop feature, similar to v-for in Vue. loop property is the array to iterate, loopArgs property represents each array item, key property can represent each item's index. Example: `{ "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"] }`
+- Reactive watch: Used to watch variable values, similar to watch in Vue. When using watch, need to combine with setup passing watch. Example: `{ "lifeCycles": { "setup": { "type": "JSFunction", "value": "function setup({ props, state, watch }) {\n watch(() => props.list, (list) => { cloumnsVisibledSetting(list) }, { deep: true } )\n}" } } }`
+- Method invocation: When calling another method within a method, use `this.methodName()` invocation. Example: `{ "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 Component Rules
+
+Components (componentName) can use low-code platform components (TinyVue component library) or native HTML components (div, img, h1, a, span, etc.). All available low-code platform components are as follows:
+{{COMPONENTS_LIST}}
+
+Note:
+- All form components with the `modelValue` property support two-way binding. This approach should be prioritized. If two-way binding is used, there is no need to redundantly bind the `onChange` or `onUpdate:modelValue` events.
+
+-----
+
+## 4. Examples
+
+{{EXAMPLES_SECTION}}
+
+-----
+
+## 5. Current Context
+
+**[Current Page Schema]**
+{{CURRENT_PAGE_SCHEMA}}
+
+**[Reference Knowledge]**
+{{REFERENCE_KNOWLEDGE}}
+
+**[Image Assets]**
+Use the following image resources on demand:
+{{IMAGE_ASSETS}}
diff --git a/packages/plugins/robot/src/system-prompt.md b/packages/plugins/robot/src/prompts/templates/chat-prompt.md
similarity index 100%
rename from packages/plugins/robot/src/system-prompt.md
rename to packages/plugins/robot/src/prompts/templates/chat-prompt.md
diff --git a/packages/plugins/robot/src/mcp/types.ts b/packages/plugins/robot/src/types/chat.types.ts
similarity index 64%
rename from packages/plugins/robot/src/mcp/types.ts
rename to packages/plugins/robot/src/types/chat.types.ts
index 781523f3e0..eb8dd0a23b 100644
--- a/packages/plugins/robot/src/mcp/types.ts
+++ b/packages/plugins/robot/src/types/chat.types.ts
@@ -1,9 +1,11 @@
import type { BubbleContentItem } from '@opentiny/tiny-robot'
+import type { ResponseToolCall } from './mcp.types'
export interface RequestOptions {
url?: string
model?: string
headers?: Record
+ baseUrl?: string
}
export interface RequestTool {
@@ -35,7 +37,7 @@ export interface LLMMessage {
export interface RobotMessage {
role: string
- content: string
+ content: string | BubbleContentItem[]
renderContent?: Array
[prop: string]: unknown
}
@@ -48,44 +50,13 @@ export interface LLMRequestBody {
tools?: RequestTool[]
}
-export interface ReponseToolCall {
- id: string
- function: {
- name: string
- arguments: string
- }
-}
-
export interface LLMResponse {
choices: Array<{
message: {
role?: string
content: string
- tool_calls?: Array
+ tool_calls?: Array
[prop: string]: unknown
}
}>
}
-
-export interface McpTool {
- name: string
- title?: string
- description: string
- inputSchema?: {
- type: 'object'
- properties: Record<
- string,
- {
- type: string
- description: string
- [prop: string]: unknown
- }
- >
- [prop: string]: unknown
- }
- [prop: string]: unknown
-}
-
-export interface McpListToolsResponse {
- tools: Array
-}
diff --git a/packages/plugins/robot/assets/test.png b/packages/plugins/robot/src/types/common.types.ts
similarity index 100%
rename from packages/plugins/robot/assets/test.png
rename to packages/plugins/robot/src/types/common.types.ts
diff --git a/packages/plugins/robot/src/types/index.ts b/packages/plugins/robot/src/types/index.ts
new file mode 100644
index 0000000000..861b4d920d
--- /dev/null
+++ b/packages/plugins/robot/src/types/index.ts
@@ -0,0 +1,2 @@
+export * from './mcp.types'
+export * from './chat.types'
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..571d3c2b51
--- /dev/null
+++ b/packages/plugins/robot/src/types/mcp.types.ts
@@ -0,0 +1,38 @@
+export interface ResponseToolCall {
+ id: string
+ function: {
+ name: string
+ arguments: string
+ }
+}
+
+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/utils/chat.utils.ts b/packages/plugins/robot/src/utils/chat.utils.ts
new file mode 100644
index 0000000000..6a7cd768df
--- /dev/null
+++ b/packages/plugins/robot/src/utils/chat.utils.ts
@@ -0,0 +1,105 @@
+import { toRaw } from 'vue'
+import { META_SERVICE, getMetaApi } from '@opentiny/tiny-engine-meta-register'
+import type { StreamHandler } from '@opentiny/tiny-robot-kit'
+import type { LLMMessage, LLMRequestBody, RequestOptions, RequestTool } from '../types'
+
+// 格式化LLM输入messages消息
+export const formatMessages = (messages: LLMMessage[]) => {
+ 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 } : {})
+ }))
+}
+
+export const serializeError = (err: unknown): string => {
+ if (err instanceof Error) {
+ return JSON.stringify({ name: err.name, message: err.message })
+ }
+ if (typeof err === 'string') return err
+ try {
+ return JSON.stringify(err)
+ } catch {
+ return String(err)
+ }
+}
+
+/**
+ * 合并字符串字段。如果值是对象,则递归合并字符串字段
+ * @param target 目标对象
+ * @param source 源对象
+ * @returns 合并后的对象
+ */
+export 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
+}
+
+export const fetchLLM = async (messages: LLMMessage[], tools: RequestTool[], options: RequestOptions = {}) => {
+ const bodyObj: LLMRequestBody = {
+ baseUrl: options.baseUrl,
+ model: options?.model || 'deepseek-chat',
+ stream: false,
+ messages: toRaw(messages)
+ }
+ if (tools.length > 0) {
+ bodyObj.tools = toRaw(tools)
+ }
+ return getMetaApi(META_SERVICE.Http).post(options?.url || '/app-center/api/chat/completions', bodyObj, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options?.headers
+ }
+ })
+}
+
+export const processSSEStream = (data: string, handler: StreamHandler) => {
+ let finishReason: string | undefined
+ let latestFinishReason: string | undefined
+ const lines = data.split('\n\n')
+ lines.pop()
+
+ for (const line of lines) {
+ if (line.trim() === '') continue
+ if (line.trim() === 'data: [DONE]') {
+ if (latestFinishReason) {
+ finishReason = latestFinishReason
+ }
+ handler.onDone(finishReason)
+ continue
+ }
+
+ try {
+ // 解析SSE消息
+ const dataMatch = line.match(/^data: (.+)$/m)
+ if (!dataMatch) continue
+
+ const data = JSON.parse(dataMatch[1])
+ handler.onData(data)
+ latestFinishReason = data.choices?.[0]?.finish_reason || undefined
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error('Error parsing SSE message:', error, line)
+ }
+ }
+}
diff --git a/packages/plugins/robot/src/utils/index.ts b/packages/plugins/robot/src/utils/index.ts
new file mode 100644
index 0000000000..9763a670f3
--- /dev/null
+++ b/packages/plugins/robot/src/utils/index.ts
@@ -0,0 +1 @@
+export * from './chat.utils'
diff --git a/packages/plugins/robot/test/test.ts b/packages/plugins/robot/test/test.ts
deleted file mode 100644
index e69de29bb2..0000000000