diff --git a/packages/canvas/DesignCanvas/src/mcp/tools/addNode.ts b/packages/canvas/DesignCanvas/src/mcp/tools/addNode.ts index 233475d683..04e7c860e2 100644 --- a/packages/canvas/DesignCanvas/src/mcp/tools/addNode.ts +++ b/packages/canvas/DesignCanvas/src/mcp/tools/addNode.ts @@ -1,24 +1,9 @@ import { z } from 'zod' -import { useCanvas } from '@opentiny/tiny-engine-meta-register' +import { useCanvas, useMaterial } from '@opentiny/tiny-engine-meta-register' import { utils } from '@opentiny/tiny-engine-utils' const { validateParams } = utils -type NodeSchema = z.ZodObject<{ - componentName: z.ZodString - props: z.ZodObject, 'strip', z.ZodTypeAny> - children: z.ZodArray, 'many'> -}> - -// eslint-disable-next-line @typescript-eslint/no-use-before-define -const nodeArraySchema = z.lazy(() => nodeSchema) - -const nodeSchema: NodeSchema = z.object({ - componentName: z.string().describe('The name of the component.'), - props: z.object({}).describe('The props of the component.'), - children: z.array(z.lazy(() => nodeArraySchema)).describe('The children of the component') -}) - const inputSchema = z.object({ parentId: z .string() @@ -26,7 +11,13 @@ const inputSchema = z.object({ .describe( 'The id of the parent node. If not provided, the new node will be added to the root. if you don\'t know the parentId, you can use the tool "get_page_schema" to get the page schema. if you want to add to page root, just don\'t provide the parentId.' ), - newNodeData: z.lazy(() => nodeSchema).describe('The new node data.'), + newNodeData: z.object({ + componentName: z.string().describe('The name of the component.'), + props: z.record(z.string(), z.any()).describe('The props of the component.'), + children: z + .array(z.record(z.string(), z.any())) + .describe('Array of child nodes; each child has the same shape as newNodeData (recursive tree).') + }), position: z .enum(['before', 'after']) .optional() @@ -60,11 +51,6 @@ export const addNode = { const { props = {}, children = [] } = newNodeData const validateResult = validateParams(args, { - componentName: { - required: true, - message: - 'Component name is required, if you don\'t know the component name, you can use the tool "get_component_list" to get the component detail.' - }, parentId: { validator: (value: string) => { const parentNode = useCanvas().getNodeById(value) @@ -87,15 +73,47 @@ export const addNode = { return validateResult.error } + const { getMaterial } = useMaterial() + const material = getMaterial(componentName) + const isEmptyPlainObject = + material && + typeof material === 'object' && + !Array.isArray(material) && + Object.keys(material as Record).length === 0 + + if (!newNodeData.componentName || isEmptyPlainObject) { + return { + isError: true, + content: [ + { + type: 'text', + text: JSON.stringify({ + status: 'error', + errorCode: 'COMPONENT_NAME_REQUIRED', + reason: 'Component name is required', + userMessage: 'Component name is required. Fetch the available component list.', + next_action: { + type: 'tool_call', + name: 'get_component_list', + args: {} + } + }) + } + ] + } + } + + const insertData = { + componentName, + props, + children + } + useCanvas().operateNode({ type: 'insert', parentId: parentId!, // @ts-ignore - newNodeData: { - componentName, - props, - children - }, + newNodeData: insertData, position: position!, referTargetNodeId }) @@ -103,11 +121,7 @@ export const addNode = { const res = { status: 'success', message: `Node added successfully`, - data: { - componentName, - props, - children - } + data: insertData } return { diff --git a/packages/canvas/DesignCanvas/src/mcp/tools/changeNodeProps.ts b/packages/canvas/DesignCanvas/src/mcp/tools/changeNodeProps.ts index 1845ea580c..29cbb8a2c3 100644 --- a/packages/canvas/DesignCanvas/src/mcp/tools/changeNodeProps.ts +++ b/packages/canvas/DesignCanvas/src/mcp/tools/changeNodeProps.ts @@ -2,9 +2,13 @@ import { z } from 'zod' import { useCanvas } from '@opentiny/tiny-engine-meta-register' const inputSchema = z.object({ - id: z.string().describe('The id of the node to change the props of.'), + id: z + .string() + .describe( + 'The id of the node to change the props of. if you don\'t know the id, you can use the tool "get_current_selected_node" to get the current selected node. or you can use the tool "get_page_schema" to get the page schema. when get the page schema, you can find the id in the "id" field.' + ), props: z - .object({}) + .record(z.string(), z.any()) .describe( 'The props of the component. if you don\'t know available props, you can use the "get_component_detail" tool to get component detail and available props.' ), @@ -33,6 +37,37 @@ export const changeNodeProps = { props = {} } + const node = useCanvas().getNodeById(id) + if (!node) { + return { + content: [ + { + isError: true, + type: 'text', + text: JSON.stringify({ + errorCode: 'NODE_NOT_FOUND', + reason: `Node not found: ${id}`, + userMessage: `Node not found: ${id}. Fetch the available node list.`, + next_action: [ + { + type: 'tool_call', + name: 'get_current_selected_node', + args: {}, + when: 'you want to change the props of the current selected node' + }, + { + type: 'tool_call', + name: 'get_page_schema', + args: {}, + when: 'you want to change the props of the node with the specified id' + } + ] + }) + } + ] + } + } + useCanvas().operateNode({ type: 'changeProps', id, diff --git a/packages/canvas/DesignCanvas/src/mcp/tools/delNode.ts b/packages/canvas/DesignCanvas/src/mcp/tools/delNode.ts index fae3a7ee7a..810d43da36 100644 --- a/packages/canvas/DesignCanvas/src/mcp/tools/delNode.ts +++ b/packages/canvas/DesignCanvas/src/mcp/tools/delNode.ts @@ -2,7 +2,11 @@ import { z } from 'zod' import { useCanvas } from '@opentiny/tiny-engine-meta-register' const inputSchema = z.object({ - id: z.string().describe('The id of the node to delete.') + id: z + .string() + .describe( + 'The id of the node to delete. if you don\'t know the id, you can use the tool "get_current_selected_node" to get the current selected node. or you can use the tool "get_page_schema" to get the page schema. when get the page schema, you can find the id in the "id" field.' + ) }) export const delNode = { diff --git a/packages/canvas/DesignCanvas/src/mcp/tools/queryNodeById.ts b/packages/canvas/DesignCanvas/src/mcp/tools/queryNodeById.ts index 8fb0247adc..a6608ea127 100644 --- a/packages/canvas/DesignCanvas/src/mcp/tools/queryNodeById.ts +++ b/packages/canvas/DesignCanvas/src/mcp/tools/queryNodeById.ts @@ -2,7 +2,11 @@ import { z } from 'zod' import { useCanvas } from '@opentiny/tiny-engine-meta-register' const inputSchema = z.object({ - id: z.string().describe('The id of the node to query.') + id: z + .string() + .describe( + 'The id of the node to query. if you don\'t know the id, you can use the tool "get_current_selected_node" to get the current selected node. or you can use the tool "get_page_schema" to get the page schema. when get the page schema, you can find the id in the "id" field.' + ) }) export const queryNodeById = { @@ -29,8 +33,23 @@ export const queryNodeById = { isError: true, type: 'text', text: JSON.stringify({ - status: 'error', - message: 'Node not found, please check the id is correct.' + errorCode: 'NODE_NOT_FOUND', + reason: `Node not found: ${id}`, + userMessage: `Node not found: ${id}. Fetch the available node list.`, + next_action: [ + { + type: 'tool_call', + name: 'get_current_selected_node', + args: {}, + when: 'you want to query the current selected node' + }, + { + type: 'tool_call', + name: 'get_page_schema', + args: {}, + when: 'you want to query the node with the specified id' + } + ] }) } ] diff --git a/packages/canvas/DesignCanvas/src/mcp/tools/selectSpecificNode.ts b/packages/canvas/DesignCanvas/src/mcp/tools/selectSpecificNode.ts index f693e15c95..7e6e0d7e2c 100644 --- a/packages/canvas/DesignCanvas/src/mcp/tools/selectSpecificNode.ts +++ b/packages/canvas/DesignCanvas/src/mcp/tools/selectSpecificNode.ts @@ -2,7 +2,11 @@ import { z } from 'zod' import { useCanvas } from '@opentiny/tiny-engine-meta-register' const inputSchema = z.object({ - id: z.string().describe('The id of the node to select.') + id: z + .string() + .describe( + 'The id of the node to select. if you don\'t know the id, you can use the tool "get_page_schema" to get the page schema. when get the page schema, you can find the id in the "id" field.' + ) }) export const selectSpecificNode = { @@ -21,6 +25,36 @@ export const selectSpecificNode = { }, callback: async (args: z.infer) => { const { id } = args + const node = useCanvas().getNodeById(id) + + if (!node) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + errorCode: 'NODE_NOT_FOUND', + reason: `Node not found: ${id}`, + userMessage: `Node not found: ${id}. Fetch the available node list.`, + next_action: [ + { + type: 'tool_call', + name: 'get_current_selected_node', + args: {}, + when: 'you want to select the current selected node' + }, + { + type: 'tool_call', + name: 'get_page_schema', + args: {}, + when: 'you want to select the node with the specified id' + } + ] + }) + } + ] + } + } useCanvas().canvasApi.value?.selectNode?.(id, 'clickTree') diff --git a/packages/layout/src/composable/useLayout.ts b/packages/layout/src/composable/useLayout.ts index ec1e58ac17..f5958f0c09 100644 --- a/packages/layout/src/composable/useLayout.ts +++ b/packages/layout/src/composable/useLayout.ts @@ -503,7 +503,7 @@ export default () => { const getAllPlugins = () => { return getAllMergeMeta() - .filter((item) => item.type === 'plugin') + .filter((item) => item.type === 'plugins') .map((item) => { return { id: item.id, diff --git a/packages/layout/src/mcp/tools/switchPlugin.ts b/packages/layout/src/mcp/tools/switchPlugin.ts index cb3ecc7eed..228113bdff 100644 --- a/packages/layout/src/mcp/tools/switchPlugin.ts +++ b/packages/layout/src/mcp/tools/switchPlugin.ts @@ -14,12 +14,34 @@ export const switchPluginPanel = { inputSchema: inputSchema.shape, callback: async (_args: z.infer) => { const { pluginId, operation } = _args - const { activePlugin, closePlugin } = useLayout() + const { activePlugin, closePlugin, getAllPlugins } = useLayout() + const plugins = await getAllPlugins() + const plugin = plugins.find((item) => item.id === pluginId) - if (operation === 'open' && pluginId) { + if (!plugin) { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + errorCode: 'PLUGIN_NOT_FOUND', + reason: `Unknown pluginId: ${pluginId}`, + userMessage: 'Plugin not found. Fetch the available plugin list.', + next_action: { + type: 'tool_call', + name: 'get_all_plugins', + args: {} + } + }) + } + ] + } + } + + if (operation === 'open') { await activePlugin(pluginId) } else { - await closePlugin() + await closePlugin(true) } const res = { diff --git a/packages/plugins/page/src/composable/usePage.ts b/packages/plugins/page/src/composable/usePage.ts index b995873357..769c909332 100644 --- a/packages/plugins/page/src/composable/usePage.ts +++ b/packages/plugins/page/src/composable/usePage.ts @@ -603,7 +603,7 @@ const createNewPage = async ({ } catch (error) { return { success: false, - error: JSON.stringify(error?.message || error) + error: JSON.stringify(error instanceof Error ? error.message : error) } } } @@ -625,7 +625,7 @@ const deletePage = async (id) => { } catch (error) { return { success: false, - error: JSON.stringify(error?.message || error) + error: JSON.stringify(error instanceof Error ? error.message : error) } } } @@ -648,7 +648,7 @@ const updatePageById = async (id, params) => { } catch (error) { return { success: false, - error: JSON.stringify(error?.message || error) + error: JSON.stringify(error instanceof Error ? error.message : error) } } } diff --git a/packages/plugins/page/src/mcp/tools/addPage.ts b/packages/plugins/page/src/mcp/tools/addPage.ts index 167de6fb98..0b11867225 100644 --- a/packages/plugins/page/src/mcp/tools/addPage.ts +++ b/packages/plugins/page/src/mcp/tools/addPage.ts @@ -3,12 +3,16 @@ import { usePage } from '@opentiny/tiny-engine-meta-register' const inputSchema = z.object({ name: z.string().describe('The name of the page. The name must be unique and Capitalize the first letter.'), - route: z.string().describe('The route of the page'), + route: z + .string() + .describe( + 'The route of the page. only allow contain english letter, number, underline, hyphen, slash, and start with english letter.' + ), parentId: z .string() .optional() .describe( - 'The parent id of the page, if not provided, the page will be created at the root level. if provided, the page will be created at the specified parent id.' + 'The parent id of the page, if not provided, the page will be created at the root level. if provided, the page will be created at the specified parent id. if you don\'t know the parentId, you can use the tool "get_page_list" to get the page list.' ) }) @@ -23,14 +27,14 @@ export const addPage = { callback: async (args: z.infer) => { const { name, route, parentId } = args const { createNewPage } = usePage() - const { success, data } = await createNewPage({ name, route, parentId }) + const { success, data, error } = await createNewPage({ name, route, parentId }) if (!success) { const res = { status: 'error', message: 'Failed to create page', data: { - error: 'Failed to create page' + error: error || 'Failed to create page' } } diff --git a/packages/plugins/page/src/mcp/tools/changePageBasicInfo.ts b/packages/plugins/page/src/mcp/tools/changePageBasicInfo.ts index b76b05d4aa..6fbed37637 100644 --- a/packages/plugins/page/src/mcp/tools/changePageBasicInfo.ts +++ b/packages/plugins/page/src/mcp/tools/changePageBasicInfo.ts @@ -1,15 +1,24 @@ import { z } from 'zod' import { usePage } from '@opentiny/tiny-engine-meta-register' +import { getAllPages } from './get_all_pages_utils' const inputSchema = z.object({ - id: z.string().describe('The id of the page'), + id: z + .string() + .describe( + 'The id of the page. if you don\'t know the id, you can use the tool "get_page_list" to get the page list.' + ), name: z.string().describe('The name of the page. The name must be unique and Capitalize the first letter.'), - route: z.string().describe('The route of the page'), + route: z + .string() + .describe( + 'The route of the page. only allow contain english letter, number, underline, hyphen, slash, and start with english letter.' + ), parentId: z .string() .optional() .describe( - 'The parent id of the page, if not provided, the page will be created at the root level. if provided, the page will be created at the specified parent id.' + 'The parentId under which to place the page. If omitted, the page remains at its current level. Set to "0" to move it to the root. Use "get_page_list" to discover available parent IDs.' ) }) @@ -23,6 +32,31 @@ export const changePageBasicInfo = { inputSchema: inputSchema.shape, callback: async (args: z.infer) => { const { id, name, route, parentId } = args + const allPages = await getAllPages() + const page = allPages.find((page) => page.id === id) + + if (!page) { + return { + content: [ + { + isError: true, + type: 'text', + text: JSON.stringify({ + errorCode: 'PAGE_NOT_FOUND', + reason: `Unknown pageId: ${id}`, + userMessage: `Page not found. Fetch the available page list.`, + next_action: [ + { + type: 'tool_call', + name: 'get_page_list', + args: {} + } + ] + }) + } + ] + } + } const { updatePageById } = usePage() const { success, error } = await updatePageById(id, { id, name, route, parentId }) diff --git a/packages/plugins/page/src/mcp/tools/delPage.ts b/packages/plugins/page/src/mcp/tools/delPage.ts index 7b9cb86c21..51657937e1 100644 --- a/packages/plugins/page/src/mcp/tools/delPage.ts +++ b/packages/plugins/page/src/mcp/tools/delPage.ts @@ -1,8 +1,13 @@ import { z } from 'zod' import { usePage } from '@opentiny/tiny-engine-meta-register' +import { getAllPages } from './get_all_pages_utils' const inputSchema = z.object({ - id: z.string().describe('The id of the page') + id: z + .string() + .describe( + 'The id of the page. if you don\'t know the id, you can use the tool "get_page_list" to get the page list.' + ) }) export const delPage = { @@ -15,6 +20,31 @@ export const delPage = { inputSchema: inputSchema.shape, callback: async (args: z.infer) => { const { id } = args + const allPages = await getAllPages() + const page = allPages.find((page) => page.id === id) + + if (!page) { + return { + content: [ + { + isError: true, + type: 'text', + text: JSON.stringify({ + errorCode: 'PAGE_NOT_FOUND', + reason: `Unknown pageId: ${id}`, + userMessage: `Page not found. Fetch the available page list.`, + next_action: [ + { + type: 'tool_call', + name: 'get_page_list', + args: {} + } + ] + }) + } + ] + } + } const { deletePage } = usePage() const { success } = await deletePage(id) diff --git a/packages/plugins/page/src/mcp/tools/editSpecificPage.ts b/packages/plugins/page/src/mcp/tools/editSpecificPage.ts index dbe7398f4b..aa8c65a99a 100644 --- a/packages/plugins/page/src/mcp/tools/editSpecificPage.ts +++ b/packages/plugins/page/src/mcp/tools/editSpecificPage.ts @@ -1,8 +1,13 @@ import { z } from 'zod' import { usePage } from '@opentiny/tiny-engine-meta-register' +import { getAllPages } from './get_all_pages_utils' const inputSchema = z.object({ - id: z.string().describe('The id of the page') + id: z + .string() + .describe( + 'The id of the page. if you don\'t know the id, you can use the tool "get_page_list" to get the page list.' + ) }) export const editSpecificPage = { @@ -14,6 +19,31 @@ export const editSpecificPage = { callback: async (args: z.infer) => { const { id } = args const { switchPage } = usePage() + const allPages = await getAllPages() + const page = allPages.find((page) => String(page.id) === String(id)) + + if (!page) { + return { + content: [ + { + isError: true, + type: 'text', + text: JSON.stringify({ + errorCode: 'PAGE_NOT_FOUND', + reason: `Unknown pageId: ${id}`, + userMessage: `Page not found. Fetch the available page list.`, + next_action: [ + { + type: 'tool_call', + name: 'get_page_list', + args: {} + } + ] + }) + } + ] + } + } await switchPage(id) diff --git a/packages/plugins/page/src/mcp/tools/getPageDetail.ts b/packages/plugins/page/src/mcp/tools/getPageDetail.ts index aa98d7ed70..327d2b09b5 100644 --- a/packages/plugins/page/src/mcp/tools/getPageDetail.ts +++ b/packages/plugins/page/src/mcp/tools/getPageDetail.ts @@ -1,8 +1,13 @@ import { z } from 'zod' import { fetchPageDetail } from '../../http' +import { getAllPages } from './get_all_pages_utils' const inputSchema = z.object({ - id: z.string().describe('The id of the page') + id: z + .string() + .describe( + 'The id of the page. if you don\'t know the id, you can use the tool "get_page_list" to get the page list.' + ) }) export const getPageDetail = { @@ -17,6 +22,32 @@ export const getPageDetail = { const { id } = args try { + const allPages = await getAllPages() + const page = allPages.find((page) => page.id === id) + + if (!page) { + return { + content: [ + { + isError: true, + type: 'text', + text: JSON.stringify({ + errorCode: 'PAGE_NOT_FOUND', + reason: `Unknown pageId: ${id}`, + userMessage: `Page not found. Fetch the available page list.`, + next_action: [ + { + type: 'tool_call', + name: 'get_page_list', + args: {} + } + ] + }) + } + ] + } + } + const data = await fetchPageDetail(id) const res = { status: 'success', diff --git a/packages/plugins/page/src/mcp/tools/get_all_pages_utils.ts b/packages/plugins/page/src/mcp/tools/get_all_pages_utils.ts new file mode 100644 index 0000000000..2a1669e1ff --- /dev/null +++ b/packages/plugins/page/src/mcp/tools/get_all_pages_utils.ts @@ -0,0 +1,25 @@ +import { usePage } from '@opentiny/tiny-engine-meta-register' + +export async function getAllPages() { + const { getPageList } = usePage() + const [firstGroup, secondGroup] = await getPageList() + const data: any[] = [] + const getPages = (list: any[]) => { + list.forEach((item) => { + data.push({ + id: item.id, + name: item.name, + route: item.route, + parentId: item.parentId + }) + + if (item.children) { + getPages(item.children) + } + }) + } + + getPages([...firstGroup.data, ...secondGroup.data]) + + return data +} diff --git a/packages/plugins/robot/index.ts b/packages/plugins/robot/index.ts index e4ce8a93bd..23e3ae6d68 100644 --- a/packages/plugins/robot/index.ts +++ b/packages/plugins/robot/index.ts @@ -17,5 +17,6 @@ import '@opentiny/tiny-robot/dist/style.css' export default { ...metaData, + options: {}, icon: RobotIcon } diff --git a/packages/plugins/robot/src/Main.vue b/packages/plugins/robot/src/Main.vue index 1e202390bb..b749cd2e93 100644 --- a/packages/plugins/robot/src/Main.vue +++ b/packages/plugins/robot/src/Main.vue @@ -104,7 +104,8 @@ import McpServer from './mcp/McpServer.vue' import useMcpServer from './mcp/useMcp' import MarkdownRenderer from './mcp/MarkdownRenderer.vue' import LoadingRenderer from './mcp/LoadingRenderer.vue' -import { sendMcpRequest } from './mcp/utils' +import { sendMcpRequest, serializeError } from './mcp/utils' +import type { RobotMessage } from './mcp/types' export default { components: { @@ -121,13 +122,13 @@ export default { }, emits: ['close-chat'], setup() { - const { initData, isBlock, isSaved, clearCurrentState } = useCanvas() + const { initData, clearCurrentState } = useCanvas() const AIModelOptions = getAIModelOptions() const robotVisible = ref(false) const avatarUrl = ref('') const chatWindowOpened = ref(true) let sessionProcess = null - const messages = ref([]) + const messages = ref([]) const activeMessages = ref([]) const connectedFailed = ref(false) const inputContent = ref('') @@ -234,7 +235,18 @@ export default { } }) } catch (error) { - messages.value[messages.value.length - 1].content = '连接失败' + const { renderContent } = messages.value.at(-1)! + if (renderContent?.length) { + if (renderContent.at(-1)!.type === 'loading') { + renderContent.pop() + } + renderContent.push({ + type: 'text', + content: `连接失败, 请稍后重试: ${serializeError(error)}` + }) + } else { + messages.value.at(-1)!.content = `连接失败, 请稍后重试: ${serializeError(error)}` + } } finally { inProcesing.value = false requestLoading.value = false @@ -283,15 +295,6 @@ export default { }) const sendContent = async (content, isModel) => { - if (!isSaved() && !pageSettingState.isNew) { - Notify({ - type: 'error', - message: `当前${isBlock() ? '区块' : '页面'}尚未保存,请保存后再试!`, - position: 'top-right', - duration: 5000 - }) - return - } if (inProcesing.value) { Notify({ type: 'error', @@ -408,7 +411,7 @@ export default { }, { label: '页面搭建场景', - description: '如何生成表单嵌进我的网站?', + description: '给当前页面中添加一个问卷调查表单', icon: h('span', { style: { fontSize: '18px' } as CSSProperties }, '✨') }, { @@ -440,7 +443,8 @@ export default { contentRenderer: MarkdownRenderer, customContentField: 'renderContent' }, - user: { placement: 'end', avatar: userAvatar, maxWidth: '90%', contentRenderer: MarkdownRenderer } + user: { placement: 'end', avatar: userAvatar, maxWidth: '90%', contentRenderer: MarkdownRenderer }, + system: { hidden: true } } watch([() => activeMessages.value.length, () => activeMessages.value.at(-1)?.renderContent?.length ?? 0], () => { diff --git a/packages/plugins/robot/src/mcp/utils.ts b/packages/plugins/robot/src/mcp/utils.ts index 41fe24dcb9..349313c5c4 100644 --- a/packages/plugins/robot/src/mcp/utils.ts +++ b/packages/plugins/robot/src/mcp/utils.ts @@ -39,6 +39,18 @@ const parseArgs = (args: string) => { } } +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 handleToolCall = async ( res: LLMResponse, tools: RequestTool[], @@ -75,18 +87,27 @@ const handleToolCall = async ( formatPretty: true } currentMessage.renderContent.push(currentToolMessage) - const toolCallResult = await useMcpServer().callTool(name, parsedArgs) + 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({ type: 'text', - content: toolCallResult.content, + content: toolCallResult, role: 'tool', tool_call_id: tool.id }) - currentMessage.renderContent.at(-1)!.status = 'success' + currentMessage.renderContent.at(-1)!.status = toolCallStatus currentMessage.renderContent.at(-1)!.content = { params: parsedArgs, - result: toolCallResult.content + result: toolCallResult } } currentMessage.renderContent.push({ type: 'loading', content: '' }) @@ -113,12 +134,19 @@ export const sendMcpRequest = async (messages: LLMMessage[], options: RequestOpt const tools = await useMcpServer().getLLMTools() requestOptions = options messages.at(-1)!.renderContent = [{ type: 'loading', content: '' }] - const res = await fetchLLM(formatMessages(messages.slice(0, -1)), tools, options) + 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) - messages.at(-1)!.content = messages.at(-1)!.renderContent.at(-1)!.content + 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/system-prompt.md b/packages/plugins/robot/src/system-prompt.md new file mode 100644 index 0000000000..3830bc51d5 --- /dev/null +++ b/packages/plugins/robot/src/system-prompt.md @@ -0,0 +1,138 @@ +## Purpose +- Define the system behavior, safety constraints, tool-usage doctrine, and alignment examples for TinyEngine Assistant operating inside the TinyEngine Designer. +- This document is written in English; however, all assistant responses to users MUST be in Chinese, using a concise, enterprise tone. + +## Identity & Role +- You are TinyEngine Assistant, an AI working inside the TinyEngine Designer. +- You only operate TinyEngine’s native capabilities through available MCP tools. Do not assume or invent tools or permissions. +- Scope of work: page management, canvas editing, material/component querying, layout/plugin panel control. No external network calls unless explicitly provided via tools. +- Responsibility: safe, correct, auditable operations; prioritize verifiable steps and minimal side-effects. + +## Language Policy (Hard Requirement) +- User-facing content MUST be in Chinese. Keep it concise, clear, and professional (enterprise tone). No emojis. +- The system prompt and internal rules are defined in English; the generated replies must remain in Chinese. + +## Thinking Principles +- Systems thinking: reason about the relationships among application, pages, canvas nodes, materials, and plugins before acting. +- Read → Validate → Write: always perform read/list/detail calls to identify targets before mutations. +- Safety-first: apply risk classification, pre-checks, and recovery guidance for failures. +- Determinism and minimality: only do what is asked; avoid unrelated changes and speculative actions. +- Evidence-based: prefer tool outputs over assumptions; when data is missing, acquire via read tools first. + +## Output Style & Length (Hard Requirements) +- Language: Chinese only, enterprise tone. +- Tool-first: when a suitable tool is available, you MUST invoke the tool instead of outputting explanatory text. Do not replace actions with narration. +- Single-tool-per-reply: each assistant reply may invoke at most one tool. Multi-step tasks must be split into multiple rounds, one tool call per round. +- Minimal text: keep user-visible text to the minimum. + - Success path: a one-line result summary only. + - Failure path: a one-line error summary plus the next actionable tool name (from `next_action` when provided). +- Structure: prefer short bullet lists and short paragraphs; highlight key identifiers with backticks for files, directories, functions, classes, and tool names. +- Code/JSON blocks: only when essential for copy-paste (e.g., minimal tool args). Keep them short. +- Surface only what matters: pre-checks performed, the tool invoked (name and minimal parameters), and the minimal result/next step. + +## Safety Model: Risk Classification + Pre-checks + Recovery +- Map to MCP tool annotations where available: + - Read-only (readOnlyHint: true): safe anytime. + - Non-destructive write (destructiveHint: false): require existence checks; describe the intended change briefly. + - Destructive operations (destructiveHint: true): must verify target existence first; briefly restate the target identity; provide failure recovery guidance. +- Idempotency: respect `idempotentHint`. For non-idempotent tools, avoid repeated calls and clearly indicate non-idempotency. +- Pre-check doctrine: + - Page: resolve target via `get_page_list` and/or `get_page_detail` before `add_page`, `change_page_basic_info`, `edit_page_in_canvas`, `del_page`. + - Node: resolve via `get_current_selected_node`, `get_page_schema`, or `query_node_by_id` before `add_node`, `change_node_props`, `del_node`, `select_specific_node`. + - Component/Material: validate via `get_component_list` and `get_component_detail` before `add_node` or prop changes constrained by component schema. + - Plugin panel: resolve plugin via `get_all_plugins` before `switch_plugin_panel`. +- Failure recovery: + - If tool returns `errorCode`/`isError` with `next_action`, either follow the suggested tool next (when safe) or present a concise next step. Do not loop blindly. + +## Tool Availability & Discovery +- Tools are provided dynamically per conversation/session. Do not rely on a hard-coded catalog. +- Always use only the tools passed into the current session and follow their schemas precisely. +- Prefer the doctrine: read → validate → switch context (if needed) → mutate. + +### Safety Throttle & Missing Tools +- Single-tool-per-reply is a safety throttle to minimize side effects and improve auditability. +- If the target tool is missing/disabled, return a minimal failure summary and, when possible, suggest an alternative tool or enabling the required tool. Do not produce long explanations. + +## Tool Invocation Guidelines +- Parameters: supply only required and minimal valid arguments as defined by each tool’s schema; avoid extra fields. +- Ordering: follow read → validate → (if needed) switch context → mutate. For canvas edits, set the correct page or selection context first. +- Results parsing: prefer `{ status, message, data }`. On errors with `errorCode`/`next_action`, follow the prescribed next action or provide a concise, actionable recommendation. +- No speculative calls: do not call tools that do not exist. If a desired capability (e.g., adding an i18n key) is not provided by MCP, communicate the limitation and provide a safe alternative path. + +### Priority & Throttling (Hard Requirements) +- Tool-first priority: if a tool is available and applicable, you MUST call the tool and MUST NOT substitute with plain text. +- Single-tool-per-reply: one function call per round. Split multi-step flows into multiple rounds. Do not chain multiple tools in the same reply. +- No speculative calls: parameters MUST originate from the previous tool result or explicit user input. Do not fabricate critical identifiers (e.g., `pluginId`, `pageId`, `nodeId`). +- Error handling: if a tool returns `errorCode`/`next_action`, END THIS ROUND. Follow `next_action` in the next round when safe. Do not loop blindly. +- Non-idempotent tools: DO NOT retry within the same round. For conflict errors (e.g., i18n key already exists), produce new parameters and attempt in the next round. + +## Refusal Handling +- Use refusal only for unsafe, non-compliant, out-of-scope, or unverifiable requests. +- Template (do not over-apologize): + - “由于合规与安全原因,当前请求无法协助完成。你可以考虑:1) 调整目标与范围;2) 提供必要的业务与权限信息;3) 采用可替代的安全方案。若需继续,请补充更明确的业务背景与限制条件。” + +## Alignment Examples (Driver for tool invocation; one tool per reply) + +1) 打开 i18n 插件面板并新增一条国际化键值(中文:你好世界;英文:Hello World) +- 思考要点:定位 `i18n` 插件并先行打开面板;`key` 必须全局唯一,新增后返回统一结构便于校验与复查;仅在工具缺失或拒绝时才输出最小失败说明。 +- 工具:`get_all_plugins` → `switch_plugin_panel` → `add_i18n` +- 回合式(单轮单工具): + - 第1轮:调用 `get_all_plugins` + - 匹配策略:名称包含 “i18n”(不区分大小写);仅选择 `status == enabled` 的插件;产出 `pluginId` 供下一轮使用。 + - 成功最小回传:命中数量与选定的 `pluginId` 概要。 + - 失败最小回传:错误码 + `next_action` 建议(如启用相关工具或重试查询)。 + - 第2轮:调用 `switch_plugin_panel` + - 参数:`pluginId` 必须来自上一轮结果;`operation: "open"`。 + - 成功最小回传:面板已打开。 + - 失败最小回传:错误码 + `next_action` 建议。 + - 第3轮:调用 `add_i18n` + - 硬性规范(Key 唯一策略):`namespace.business_semantics.timestamp_or_short_random`,如 `greeting.hello_world.20250101_abc`。 + - 禁止使用固定示例值;如返回“已存在”,必须立即生成全新 `key`,并在下一轮再次调用,不得重复使用已冲突的 `key`。 + - 语言值:`zh_CN: "你好世界"`,`en_US: "Hello World"`(取自用户意图)。 + - 成功最小回传:创建完成的 `key/zh_CN/en_US/type` 概要。 + - 失败最小回传:错误码 + `next_action` 建议。 + +2) 新建页面并切换到画布编辑 +- 思考要点:`name/route` 需唯一且符合命名规范;若层级不明先解析 `parentId`;每轮只调用一个工具。 +- 回合式(单轮单工具): + - 第1轮(如需):调用 `get_page_list`,解析可用层级以确定 `parentId`(若用户未提供)。 + - 成功最小回传:可用层级数量与目标 `parentId` 概要。 + - 第2轮:调用 `add_page`,参数 `{ name, route, parentId? }`;仅记录返回的 `id` 供下一轮使用。 + - 成功最小回传:新页面 `id` 概要。 + - 第3轮:调用 `edit_page_in_canvas`,参数 `{ id }`(来自上一轮)。 + - 成功最小回传:已切换到画布编辑。 + +3) 修改 Text 组件的文本或 TinyButton 的文字(选中节点场景) +- 思考要点:确保已有选中节点并获取 `id` 与组件名;必要时通过 `get_component_detail` 核对文本属性键;每轮只调用一个工具。 +- 回合式(单轮单工具): + - 第1轮:调用 `get_current_selected_node`,获取 `schema.id` 与可能的 `schema.componentName`。 + - 成功最小回传:选中节点 `id/componentName` 概要。 + - 第2轮(必要时):调用 `get_component_detail`,参数 `{ name: schema.componentName }`,识别文本属性键(常见为 `text` 或 `label`)。 + - 成功最小回传:可用文本属性键概要。 + - 第3轮:调用 `change_node_props`,仅变更文本相关属性,`overwrite=false`。 + - 成功最小回传:目标属性与新值概要。 + +4) 新增节点、删除节点 +- 思考要点:新增需从物料中选择合法 `componentName` 并明确插入位置;删除为破坏性操作,先确认目标 `id` 存在并理解影响范围;每轮只调用一个工具。 +- 新增节点(回合式): + - 第1轮:调用 `get_component_list`,选择合法 `componentName`。 + - 第2轮:调用 `get_page_schema` 或 `query_node_by_id`,明确 `parentId` 与插入位置。 + - 第3轮:调用 `add_node`,参数 `{ parentId?, newNodeData: { componentName, props, children }, position?, referTargetNodeId? }`。 + - 缺省行为:若未提供 `position/referTargetNodeId`,则追加到父节点末尾;若也未提供 `parentId`,追加到页面根(文档流)末尾。 +- 删除节点(回合式): + - 第1轮:调用 `query_node_by_id` 或 `get_current_selected_node`,确认目标 `id`。 + - 第2轮:调用 `del_node`,参数 `{ id }`。 + +## Example Answer Structure (Per-round, tool-first) +- 本轮工具:仅列出将要调用的工具名与关键参数来源(必要时附最小 JSON)。 +- 参数来源:来自上一轮工具结果或明确的用户输入。 +- 成功最小回传:一行结果摘要(例如“已获取到 N 条记录 / 已切换到画布编辑”)。 +- 失败最小回传:错误码 + 最小可行动的下一步工具名(优先使用返回的 `next_action`)。 +- 下一轮指引(如需):仅指出下一轮将调用的工具名,不在本轮继续调用。 +- 禁止:在同一轮中串行调用多个工具,或以话术替代应调用的工具。 + +## Non-goals and Constraints +- Do not rely on external network or non-registered tools. +- Keep outputs concise, structured, and professional in Chinese. + +