From 59e8548715805b139fbbccfa2d4b74885c4cbdfe Mon Sep 17 00:00:00 2001 From: lichunn <46984087+lichunn@users.noreply.github.com> Date: Thu, 30 Apr 2026 18:10:03 +0800 Subject: [PATCH] =?UTF-8?q?Feat=EF=BC=9AAdd=20AI=20assistant=20to=20canvas?= =?UTF-8?q?=20node=20(#1800)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat:画布点击节点实现AI对话修改 * feat:添加节点AI助手交互 * feat:添加AI返回JSON处理 --- packages/canvas/DesignCanvas/src/api/types.ts | 25 +- .../canvas/DesignCanvas/src/api/useCanvas.ts | 149 +++- packages/canvas/container/assets/loading.webp | Bin 0 -> 72222 bytes .../src/components/AIConfirmDialog.vue | 115 +++ .../src/components/AILoadingDialog.vue | 93 +++ .../container/src/components/CanvasAIChat.vue | 71 ++ .../container/src/components/CanvasAction.vue | 353 ++++++++- .../container/src/composables/useAIChat.ts | 701 ++++++++++++++++++ .../container/src/services/agentServices.ts | 73 ++ packages/canvas/package.json | 5 + .../robot/src/composables/core/useConfig.ts | 29 +- packages/toolbars/save/src/Main.vue | 15 +- .../toolbars/save/src/js/aiSaveValidation.ts | 36 + packages/toolbars/save/src/js/index.ts | 14 + 14 files changed, 1664 insertions(+), 15 deletions(-) create mode 100644 packages/canvas/container/assets/loading.webp create mode 100644 packages/canvas/container/src/components/AIConfirmDialog.vue create mode 100644 packages/canvas/container/src/components/AILoadingDialog.vue create mode 100644 packages/canvas/container/src/components/CanvasAIChat.vue create mode 100644 packages/canvas/container/src/composables/useAIChat.ts create mode 100644 packages/canvas/container/src/services/agentServices.ts create mode 100644 packages/toolbars/save/src/js/aiSaveValidation.ts diff --git a/packages/canvas/DesignCanvas/src/api/types.ts b/packages/canvas/DesignCanvas/src/api/types.ts index bb2a8266..4ba9f5be 100644 --- a/packages/canvas/DesignCanvas/src/api/types.ts +++ b/packages/canvas/DesignCanvas/src/api/types.ts @@ -2,6 +2,28 @@ import type { Node, RootNode } from '../../../types' export type PageSchema = RootNode +export type AIHelperState = 'hidden' | 'chat' | 'loading' | 'confirm' | 'completed' + +export interface NodeAIStatus { + state: AIHelperState + collapsed?: boolean // 面板是否收起(收起时保留原状态,重新打开可恢复) + aiContext?: any + lastAIAction?: string + aiHistory?: Array<{ + timestamp: number + action: string + content: any + }> + chatContent?: string // 聊天内容 + // AI采纳状态相关字段 + originalNodeData?: any // AI修改前的节点数据备份 + aiModifiedNodeData?: any // AI修改后的节点数据 +} + +export interface NodeStatus { + [key: string]: any +} + export interface PageState { currentVm?: unknown currentSchema?: { [x: string]: any; id: string } @@ -17,7 +39,8 @@ export interface PageState { isSaved: boolean isLock: boolean isBlock: boolean - nodesStatus: Record + nodesStatus: Record + aiNodesStatus: Record // AI状态独立存储,避免与nodesStatus的可见性(false)冲突 loading: boolean } diff --git a/packages/canvas/DesignCanvas/src/api/useCanvas.ts b/packages/canvas/DesignCanvas/src/api/useCanvas.ts index 1c64aefc..30d2c430 100644 --- a/packages/canvas/DesignCanvas/src/api/useCanvas.ts +++ b/packages/canvas/DesignCanvas/src/api/useCanvas.ts @@ -21,6 +21,7 @@ import type { ChangePropsOperation, DeleteOperation, InsertOperation, + NodeAIStatus, NodeOperation, PageSchema, PageState, @@ -43,6 +44,7 @@ const defaultPageState: PageState = { isLock: false, isBlock: false, nodesStatus: {}, + aiNodesStatus: {}, loading: false } @@ -84,6 +86,39 @@ const rootSchema = ref([ } ]) +// 初始化单个节点的AI状态(使用独立的aiNodesStatus,避免与nodesStatus可见性冲突) +const initializeNodeAIStatus = (node: Node, initialStatus: Partial = {}) => { + pageState.aiNodesStatus[node.id] = { + state: 'hidden', + originalNodeData: deepClone(node), + aiModifiedNodeData: undefined, + aiContext: null, + lastAIAction: '', + aiHistory: [], + ...initialStatus + } +} + +// 初始化所有现有节点的AI状态 +const initializeAllNodesAIStatus = () => { + // 递归遍历 pageSchema 的 children 来初始化所有节点的AI状态 + const traverseNodes = (nodes: any[]) => { + if (!nodes) return + nodes.forEach((node) => { + if (node.id && !pageState.aiNodesStatus[node.id]) { + initializeNodeAIStatus(node) + } + if (Array.isArray(node.children) && node.children.length) { + traverseNodes(node.children) + } + }) + } + + if (pageState.pageSchema?.children) { + traverseNodes(pageState.pageSchema.children) + } +} + const handleTinyGridColumnsSlots = (node: Node) => { const columns = Array.isArray(node.props?.columns) ? node.props.columns : [] for (const columnItem of columns) { @@ -176,13 +211,26 @@ const jsonDiffPatchInstance = jsonDiffPatch.create({ const { publish } = useMessage() // 重置画布数据 -const resetCanvasState = async (state: Partial = {}) => { +// preserveAINodeStatus: 为true时保留aiNodesStatus并为新增节点补初始化(适用于AI/robot等schema热更新场景) +const resetCanvasState = async (state: Partial = {}, options?: { preserveAINodeStatus?: boolean }) => { const previousSchema = JSON.parse(JSON.stringify(pageState.pageSchema)) + const preserveAINodeStatus = options?.preserveAINodeStatus ?? false + + // 保留旧aiNodesStatus快照,用于后续diff补初始化 + const oldAINodesStatus: Record = preserveAINodeStatus ? { ...pageState.aiNodesStatus } : {} Object.assign(pageState, defaultPageState, state) nodesMap.value.clear() + if (preserveAINodeStatus) { + // 保留aiNodesStatus,后续只为新增节点补初始化 + pageState.aiNodesStatus = oldAINodesStatus + } else { + // 切换页面时清空所有节点的AI状态,避免旧页面的AI状态残留 + pageState.aiNodesStatus = {} + } + if (pageState.pageSchema) { if (!pageState.pageSchema.children) { pageState.pageSchema.children = [] @@ -200,14 +248,34 @@ const resetCanvasState = async (state: Partial = {}) => { nodesMap.value.set(0, { node: rootSchema.value, parent: pageState.pageSchema }) generateNodesMap(pageState.pageSchema.children, pageState.pageSchema) + + if (preserveAINodeStatus) { + // 为新增的节点初始化AI状态(已存在的不覆盖) + nodesMap.value.forEach(({ node }) => { + if (node.id && !pageState.aiNodesStatus[node.id]) { + initializeNodeAIStatus(node) + } + }) + } else { + // 初始化所有节点的AI状态 + initializeAllNodesAIStatus() + } } const diffPatch = jsonDiffPatchInstance.diff(previousSchema, pageState.pageSchema) - canvasApi.value?.clearSelect?.() + if (!preserveAINodeStatus) { + canvasApi.value?.clearSelect?.() + } + publish({ topic: 'schemaImport', data: { current: pageState.pageSchema, previous: previousSchema, diffPatch } }) } +// 更新页面schema,保留AI状态(委托resetCanvasState + preserveAINodeStatus) +const updatePageSchema = (newPageSchema: any) => { + resetCanvasState({ ...pageState, pageSchema: newPageSchema }, { preserveAINodeStatus: true }) +} + // 页面重置画布数据 const resetPageCanvasState = (state: Partial = {}) => { state.isBlock = false @@ -383,10 +451,28 @@ const operationTypeMap = { setNode(newNodeData, parentNode) + // 初始化新节点的AI状态 + if (newNodeData.id) { + initializeNodeAIStatus(newNodeData) + } + // 6. 如果新节点有子节点,递归构建 nodeMap if (Array.isArray(newNodeData?.children) && newNodeData.children.length > 0) { const newNode = getNode(newNodeData.id) generateNodesMap(newNodeData.children, newNode) + + // 递归初始化所有子节点的AI状态 + const initChildrenAIStatus = (children: Node[]) => { + children.forEach((child) => { + if (child.id) { + initializeNodeAIStatus(child) + } + if (Array.isArray(child?.children) && child.children.length > 0) { + initChildrenAIStatus(child.children) + } + }) + } + initChildrenAIStatus(newNodeData.children) } // 7. 返回插入结果 @@ -410,16 +496,18 @@ const operationTypeMap = { if (index > -1) { parent.children.splice(index, 1) nodesMap.value.delete(node.id) + delete pageState.aiNodesStatus[node.id] } let children = [...(node.children || [])] - // 递归清理 nodesMap + // 递归清理 nodesMap 和 aiNodesStatus while (children?.length) { const len = children.length children.forEach((item) => { const nodeItem = getNode(item.id) nodesMap.value.delete(item.id) + delete pageState.aiNodesStatus[item.id] if (Array.isArray(nodeItem?.children) && nodeItem?.children.length) { children.push(...nodeItem.children) @@ -597,7 +685,7 @@ const patchLatestSchema = (schema: unknown) => { } } -const importSchema = (data: any) => { +const importSchema = (data: any, options?: { preserveAINodeStatus?: boolean }) => { let importData = data if (typeof data === 'string') { @@ -609,11 +697,7 @@ const importSchema = (data: any) => { } } - // JSON 格式校验 - resetCanvasState({ - ...pageState, - pageSchema: importData - }) + resetCanvasState({ ...pageState, pageSchema: importData }, options) } const exportSchema = () => { @@ -650,6 +734,49 @@ const updateSchema = (data: Partial) => { publish({ topic: 'schemaChange', data: {} }) } +/** + * 恢复节点子树数据并重建nodesMap + * 用于AI回滚场景:恢复originalNodeData后需要同步清理/重建nodesMap + * @param nodeId 要恢复的节点ID + * @param restoredData 恢复后的节点数据(deepClone后的originalNodeData) + */ +const restoreNodeSubtree = (nodeId: string, restoredData: any) => { + // 1. 收集恢复前该节点子树中的所有ID(这些是需要从nodesMap中清理的) + const collectSubtreeIds = (node: any): string[] => { + const ids: string[] = [] + if (node?.id) ids.push(node.id) + if (Array.isArray(node?.children)) { + node.children.forEach((child: any) => ids.push(...collectSubtreeIds(child))) + } + return ids + } + + const currentNode = getNode(nodeId) + const oldIds = currentNode ? collectSubtreeIds(currentNode) : [] + // 获取当前节点的parent信息(在清理前保存) + const parentEntry = nodesMap.value.get(nodeId) + const parentNode = parentEntry?.parent + + // 2. 清理旧子树的nodesMap + oldIds.forEach((id) => nodesMap.value.delete(id)) + + // 3. 用恢复后的数据覆盖当前节点 + if (currentNode) { + Object.keys(currentNode).forEach((key) => delete currentNode[key]) + Object.assign(currentNode, restoredData) + } + + // 4. 重建该节点自身的nodesMap条目 + if (currentNode && parentNode) { + nodesMap.value.set(nodeId, { node: currentNode, parent: parentNode }) + } + + // 5. 重建子节点的nodesMap + if (Array.isArray(restoredData?.children) && restoredData.children.length && currentNode) { + generateNodesMap(restoredData.children, currentNode) + } +} + export default function () { return { pageState, @@ -684,6 +811,8 @@ export default function () { exportSchema, getSchema, getNodePath, - updateSchema + updateSchema, + updatePageSchema, + restoreNodeSubtree } } diff --git a/packages/canvas/container/assets/loading.webp b/packages/canvas/container/assets/loading.webp new file mode 100644 index 0000000000000000000000000000000000000000..ed71b43597d5ff58ea5aee8200723040e5004a3f GIT binary patch literal 72222 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�Rz7Ri6B%5H@76N_7tR8aU(Sjto0?`4 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 RA%Uv{b%|xFq1w#K000&5hS2~3 literal 0 HcmV?d00001 diff --git a/packages/canvas/container/src/components/AIConfirmDialog.vue b/packages/canvas/container/src/components/AIConfirmDialog.vue new file mode 100644 index 00000000..f3af12e3 --- /dev/null +++ b/packages/canvas/container/src/components/AIConfirmDialog.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/packages/canvas/container/src/components/AILoadingDialog.vue b/packages/canvas/container/src/components/AILoadingDialog.vue new file mode 100644 index 00000000..192d0054 --- /dev/null +++ b/packages/canvas/container/src/components/AILoadingDialog.vue @@ -0,0 +1,93 @@ + + + + diff --git a/packages/canvas/container/src/components/CanvasAIChat.vue b/packages/canvas/container/src/components/CanvasAIChat.vue new file mode 100644 index 00000000..75bce350 --- /dev/null +++ b/packages/canvas/container/src/components/CanvasAIChat.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/packages/canvas/container/src/components/CanvasAction.vue b/packages/canvas/container/src/components/CanvasAction.vue index 9df3bc0c..f3cd4a50 100644 --- a/packages/canvas/container/src/components/CanvasAction.vue +++ b/packages/canvas/container/src/components/CanvasAction.vue @@ -91,6 +91,39 @@

+ +
+ + + + + + +
@@ -136,6 +169,14 @@ import { import { useLayout, useMaterial, useCanvas, useMessage } from '@opentiny/tiny-engine-meta-register' import { Popover } from '@opentiny/vue' import shortCutPopover from './shortCutPopover.vue' +import CanvasAIChat from './CanvasAIChat.vue' +import AIConfirmDialog from './AIConfirmDialog.vue' +import AILoadingDialog from './AILoadingDialog.vue' +import { chat } from '../services/agentServices' +import { utils } from '@opentiny/tiny-engine-utils' +import useAIChat from '../composables/useAIChat' + +const { deepClone } = utils // 工具操作条高度 const OPTION_BAR_HEIGHT = 24 @@ -155,6 +196,9 @@ const STYLE_UNSET = 'unset' export default { components: { + AILoadingDialog, + CanvasAIChat, + AIConfirmDialog, IconDel: IconDel(), IconSetting: IconSetting(), IconChevronLeft: IconChevronLeft(), @@ -198,6 +242,29 @@ export default { }, emits: ['remove', 'selectSlot', 'setting'], setup(props) { + const { pageState: _pageState, getNode } = useCanvas() + + const { + getNodeAIStatus, + openNodeAIChat, + closeNodeAIHelper, + startNodeAILoading, + cancelNodeAILoading, + shouldShowNodeAIChat, + shouldShowNodeAILoading, + shouldShowNodeAIConfirm, + confirmNodeAIAction, + updateNodeAIStatus, + cancelNodeAIAction, + applyAIPatches, + buildAIChatRequest + } = useAIChat() + + // AI请求的AbortController,用于取消正在进行的请求 + let aiChatAbortController = null + // 递增token,用于防止并发请求的响应竞争 + let aiChatRequestToken = 0 + const remove = () => { removeNodeById(getCurrent().schema?.id) } @@ -283,6 +350,89 @@ export default { return config?.configure?.isModal }) + // 是否显示AI聊天界面 + const shouldShowAIChat = computed(() => { + const currentSchema = getCurrent().schema + if (!currentSchema?.id) { + return false + } + + return shouldShowNodeAIChat(currentSchema.id) + }) + + // 是否显示确认弹窗 + const shouldShowAIConfirm = computed(() => { + const currentSchema = getCurrent().schema + if (!currentSchema?.id) { + return false + } + + return shouldShowNodeAIConfirm(currentSchema.id) + }) + + // 是否显示AI加载状态 + const shouldShowAILoading = computed(() => { + const currentSchema = getCurrent().schema + if (!currentSchema?.id) { + return false + } + + return shouldShowNodeAILoading(currentSchema.id) + }) + + const showAIPopover = ref(false) + watch( + () => { + const currentSchema = getCurrent().schema + if (!currentSchema?.id) { + return false + } + const status = getNodeAIStatus(currentSchema?.id) + return status?.state !== 'hidden' && !status?.collapsed + }, + (val) => { + showAIPopover.value = val + } + ) + + // 切换AI助手显示/隐藏 + const openAIHelper = () => { + const currentSchema = getCurrent().schema + + if (!currentSchema?.id) { + return + } + + const currentStatus = getNodeAIStatus(currentSchema.id) + + if (currentStatus && currentStatus.collapsed) { + // 面板已收起,重新展开恢复原状态 + updateNodeAIStatus(currentSchema.id, { collapsed: false }) + } else if (currentStatus && currentStatus.state !== 'hidden') { + // 面板当前可见,收起面板但保留业务状态 + updateNodeAIStatus(currentSchema.id, { collapsed: true }) + } else { + // hidden状态,进入chat + openNodeAIChat(currentSchema.id) + } + } + + // 关闭AI助手(由其他组件调用,如AI聊天界面的关闭按钮) + const closeAIHelper = () => { + const currentSchema = getCurrent().schema + if (!currentSchema?.id) { + return + } + + const currentStatus = getNodeAIStatus(currentSchema.id) + // loading/confirm状态下只收起面板,保留状态以便重新打开恢复 + if (currentStatus && (currentStatus.state === 'loading' || currentStatus.state === 'confirm')) { + updateNodeAIStatus(currentSchema.id, { collapsed: true }) + } else { + closeNodeAIHelper(currentSchema.id) + } + } + const optionRef = ref(null) const fixStyle = ref('') @@ -633,6 +783,166 @@ export default { fixStyle.value = optionStyleValue }) + // AI聊天完成处理 + const handleAIChatComplete = async (content) => { + const currentSchema = getCurrent().schema + if (!currentSchema?.id) { + return + } + + // 先进入加载状态 + startNodeAILoading(currentSchema.id, 'AI正在处理您的请求...') + + // 在AI修改节点前,先刷新originalNodeData为当前最新节点数据 + // 避免取消/重新生成时回滚到过期的快照,丢失用户之前的普通编辑 + const currentNode = getNode(currentSchema.id) + if (currentNode) { + const currentStatus = getNodeAIStatus(currentSchema.id) + if (currentStatus) { + currentStatus.originalNodeData = deepClone(currentNode) + } + } + + // 创建新的AbortController用于取消请求,递增token防止响应竞争 + aiChatAbortController = new AbortController() + const currentToken = ++aiChatRequestToken + + try { + const params = await buildAIChatRequest(content) + const response = await chat(params, aiChatAbortController.signal) + + // 响应到达后校验token:如果有更新的请求已经发出,丢弃本次响应 + if (currentToken !== aiChatRequestToken) { + return + } + + // 响应到达后再次检查:如果用户已经取消,不应用AI补丁 + const status = getNodeAIStatus(currentSchema.id) + if (!status || status.state !== 'loading') { + return + } + + // AI运行完:设置chatContent、aiModifiedNodeData,修改画布schema为AI的schema + // 应用失败则取消loading,避免UI永久转圈 + if (!applyAIPatches(currentSchema.id, response, content)) { + cancelNodeAILoading(currentSchema.id) + } + } catch (error) { + // 请求被取消时不再应用补丁 + if (error.name === 'AbortError' || error.name === 'CanceledError') { + return + } + // 其他错误:取消loading状态,避免UI永久转圈 + cancelNodeAILoading(currentSchema.id) + } + } + + // AI加载取消处理 + const handleAILoadingCancel = () => { + const currentSchema = getCurrent().schema + if (!currentSchema?.id) { + return + } + + // 中止正在进行的AI请求 + if (aiChatAbortController) { + aiChatAbortController.abort() + aiChatAbortController = null + // 递增token,使任何未完成的请求响应失效 + aiChatRequestToken++ + } + + // 取消加载状态 + cancelNodeAILoading(currentSchema.id) + } + + // 刷新AI操作(重新生成) + // 逻辑:修改画布节点schema为originalNodeData,设置aiModifiedNodeData为空,重新发起请求 + const handleAIRefresh = async () => { + const currentSchema = getCurrent().schema + if (!currentSchema?.id) { + return + } + + const nodeId = currentSchema.id + const currentAIStatus = getNodeAIStatus(nodeId) + if (!currentAIStatus) { + return + } + + // 恢复画布节点schema为originalNodeData,同步重建nodesMap + if (currentAIStatus.originalNodeData) { + const { restoreNodeSubtree } = useCanvas() + restoreNodeSubtree(nodeId, deepClone(currentAIStatus.originalNodeData)) + useMessage().publish({ topic: 'schemaChange', data: { nodeId } }) + } + + // 设置aiModifiedNodeData为空 + updateNodeAIStatus(nodeId, { + aiModifiedNodeData: undefined + }) + + // 重新进入加载状态 + startNodeAILoading(nodeId, 'AI正在重新生成...') + + // 使用上次的聊天消息重新发起请求 + const chatContent = currentAIStatus.chatContent + if (!chatContent) { + cancelNodeAILoading(nodeId) + return + } + + try { + const params = await buildAIChatRequest(chatContent) + // 创建新的AbortController用于重新生成的请求,递增token防止响应竞争 + const refreshToken = ++aiChatRequestToken + aiChatAbortController = new AbortController() + const response = await chat(params, aiChatAbortController.signal) + + // 响应到达后校验token:如果有更新的请求已经发出,丢弃本次响应 + if (refreshToken !== aiChatRequestToken) { + return + } + + // 响应到达后检查是否已被取消 + const status = getNodeAIStatus(nodeId) + if (!status || status.state !== 'loading') { + return + } + + // AI运行完操作和 handleAIChatComplete 一样,应用失败则取消loading + if (!applyAIPatches(nodeId, response, chatContent)) { + cancelNodeAILoading(nodeId) + } + } catch (error) { + // 请求被取消时不做额外处理 + if (error.name === 'AbortError' || error.name === 'CanceledError') { + return + } + cancelNodeAILoading(nodeId) + } + } + + // 确认AI操作 + const handleAIConfirm = () => { + const currentSchema = getCurrent().schema + if (!currentSchema?.id) { + return + } + + confirmNodeAIAction(currentSchema.id) + } + + // 取消AI操作 + const handleAICancel = () => { + const currentSchema = getCurrent().schema + if (!currentSchema?.id) { + return + } + + cancelNodeAIAction(currentSchema.id) + } + return { remove, moveUp, @@ -646,11 +956,22 @@ export default { showQuickAction, showPopover, showToParent, + showAIPopover, activeSetting, isModal, onMousedown, labelStyle, - labelRef + labelRef, + openAIHelper, + shouldShowAIChat, + shouldShowAILoading, + shouldShowAIConfirm, + closeAIHelper, + handleAIChatComplete, + handleAILoadingCancel, + handleAIConfirm, + handleAICancel, + handleAIRefresh } } } @@ -942,4 +1263,34 @@ export default { cursor: se-resize; } } + +.ai-helper { + position: relative; + .ai-component, + .ai-component-loading { + position: absolute; + right: 0; + width: 360px; + opacity: 0; + transform: translateY(-10px); + animation: slideIn 0.2s ease-in-out forwards; + } + .ai-component-loading { + width: 270px; + } +} +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(10px); + } +} +.ai-popper.ai-popper.tiny-popper.tiny-popover { + padding: 0; + border-radius: 40px; +} diff --git a/packages/canvas/container/src/composables/useAIChat.ts b/packages/canvas/container/src/composables/useAIChat.ts new file mode 100644 index 00000000..f3cac5d2 --- /dev/null +++ b/packages/canvas/container/src/composables/useAIChat.ts @@ -0,0 +1,701 @@ +/** + * 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. + * + */ + +import { utils } from '@opentiny/tiny-engine-utils' +import * as jsonpatch from 'fast-json-patch' +import { jsonrepair } from 'jsonrepair' +import { + useCanvas, + useMaterial, + getMetaApi, + META_SERVICE, + useMessage, + useHistory +} from '@opentiny/tiny-engine-meta-register' +import type { NodeAIStatus } from '../../../DesignCanvas/src/api/types' +import { search, fetchAssets } from '../services/agentServices' +import { getCurrent } from '../container' + +const { deepClone } = utils + +// ==================== AI助手状态管理 ==================== + +const updateNodeAIStatus = (nodeId: string, aiStatus: Partial) => { + const { pageState } = useCanvas() + const { publish } = useMessage() + + if (!pageState.aiNodesStatus[nodeId]) { + pageState.aiNodesStatus[nodeId] = { + state: 'hidden', // 默认隐藏 + aiContext: null, + lastAIAction: '', + aiHistory: [] + } + } + + Object.assign(pageState.aiNodesStatus[nodeId], aiStatus) + + // 发布状态更新事件 + publish({ topic: 'nodeAIStatusUpdate', data: { nodeId, aiStatus: pageState.aiNodesStatus[nodeId] } }) +} + +const getNodeAIStatus = (nodeId: string): NodeAIStatus | null => { + const { pageState } = useCanvas() + return pageState.aiNodesStatus[nodeId] || null +} + +// 添加AI操作历史记录 +const addNodeAIActionHistory = (nodeId: string, action: string, content: any) => { + const currentStatus = getNodeAIStatus(nodeId) + if (!currentStatus) { + updateNodeAIStatus(nodeId, { + state: 'hidden', + aiHistory: [{ timestamp: Date.now(), action, content }] + }) + return + } + + const aiHistory = currentStatus.aiHistory || [] + aiHistory.push({ timestamp: Date.now(), action, content }) + + updateNodeAIStatus(nodeId, { + aiHistory, + lastAIAction: action + }) +} + +// 打开AI助手聊天界面 +const openNodeAIChat = (nodeId: string, initialContent: string = '') => { + updateNodeAIStatus(nodeId, { + state: 'chat', + collapsed: false, + chatContent: initialContent, + lastAIAction: 'open_chat' + }) + + addNodeAIActionHistory(nodeId, 'open_chat', { + timestamp: Date.now(), + initialContent + }) +} + +// 关闭AI助手 +const closeNodeAIHelper = (nodeId: string) => { + updateNodeAIStatus(nodeId, { + state: 'hidden', + lastAIAction: 'close' + }) + + addNodeAIActionHistory(nodeId, 'close', { + timestamp: Date.now() + }) +} + +// 确认AI助手操作(采纳) +// 逻辑:设置originalNodeData为AI修改后的schema,设置aiModifiedNodeData为空 +const confirmNodeAIAction = (nodeId: string) => { + const currentStatus = getNodeAIStatus(nodeId) + if (!currentStatus || currentStatus.state !== 'confirm') { + return + } + + // 采纳后直接回到hidden,completed无对应UI,避免需要点击两次才能重新打开 + updateNodeAIStatus(nodeId, { + state: 'hidden', + collapsed: false, + originalNodeData: deepClone(currentStatus.aiModifiedNodeData), + aiModifiedNodeData: undefined, + lastAIAction: 'confirm' + }) +} + +// 取消AI助手操作 +// 逻辑:设置aiModifiedNodeData为空,修改画布节点schema为originalNodeData +const cancelNodeAIAction = (nodeId: string) => { + const currentStatus = getNodeAIStatus(nodeId) + if (!currentStatus) { + return + } + + // 恢复画布节点schema为originalNodeData,同步重建nodesMap + if (currentStatus.originalNodeData && currentStatus.state === 'confirm') { + const { restoreNodeSubtree } = useCanvas() + const { publish } = useMessage() + restoreNodeSubtree(nodeId, deepClone(currentStatus.originalNodeData)) + publish({ topic: 'schemaChange', data: { nodeId } }) + } + + const newState = currentStatus.chatContent ? 'chat' : 'hidden' + + updateNodeAIStatus(nodeId, { + state: newState, + collapsed: false, + aiModifiedNodeData: undefined, + lastAIAction: 'cancel' + }) +} + +/** + * 设置AI修改后的节点数据(AI运行完调用) + * @param nodeId 节点ID + * @param aiModifiedNodeData AI修改后的节点数据 + */ +const setNodeAIModifiedData = (nodeId: string, aiModifiedNodeData: any) => { + const currentAIStatus = getNodeAIStatus(nodeId) + if (!currentAIStatus) { + return + } + + updateNodeAIStatus(nodeId, { + aiModifiedNodeData: deepClone(aiModifiedNodeData) + }) +} + +/** + * 采纳AI修改 + * 逻辑:设置originalNodeData为AI修改后的schema,设置aiModifiedNodeData为空 + * @param nodeId 节点ID + */ +const adoptNodeAIModification = (nodeId: string) => { + const currentAIStatus = getNodeAIStatus(nodeId) + if (!currentAIStatus || currentAIStatus.state !== 'confirm') { + return false + } + + updateNodeAIStatus(nodeId, { + state: 'completed', + originalNodeData: deepClone(currentAIStatus.aiModifiedNodeData), + aiModifiedNodeData: undefined + }) + + return true +} + +/** + * 拒绝AI修改 + * 逻辑:设置aiModifiedNodeData为空,修改画布节点schema为originalNodeData + * @param nodeId 节点ID + */ +const rejectNodeAIModification = (nodeId: string) => { + const currentAIStatus = getNodeAIStatus(nodeId) + if (!currentAIStatus || currentAIStatus.state !== 'confirm') { + return false + } + + // 恢复画布节点schema为originalNodeData,同步重建nodesMap + if (currentAIStatus.originalNodeData) { + const { restoreNodeSubtree } = useCanvas() + const { publish } = useMessage() + restoreNodeSubtree(nodeId, deepClone(currentAIStatus.originalNodeData)) + publish({ topic: 'schemaChange', data: { nodeId } }) + } + + const newState = currentAIStatus.chatContent ? 'chat' : 'hidden' + + updateNodeAIStatus(nodeId, { + state: newState, + aiModifiedNodeData: undefined + }) + + return true +} + +/** + * 重置节点AI采纳状态 + * @param nodeId 节点ID + */ +const resetNodeAIAdoptionStatus = (nodeId: string) => { + const currentAIStatus = getNodeAIStatus(nodeId) + if (currentAIStatus) { + updateNodeAIStatus(nodeId, { + originalNodeData: undefined, + aiModifiedNodeData: undefined + }) + } +} + +/** + * 检查节点是否有待处理的AI修改 + * @param nodeId 节点ID + */ +const hasNodePendingAIModification = (nodeId: string): boolean => { + const aiStatus = getNodeAIStatus(nodeId) + return aiStatus?.state === 'confirm' +} +// ==================== AI助手状态函数 ==================== + +// 获取当前节点是否应该显示AI聊天界面 +const shouldShowNodeAIChat = (nodeId: string): boolean => { + const status = getNodeAIStatus(nodeId) + return status?.state === 'chat' && !status?.collapsed +} + +// 获取当前节点是否应该显示确认弹窗 +const shouldShowNodeAIConfirm = (nodeId: string): boolean => { + const status = getNodeAIStatus(nodeId) + return status?.state === 'confirm' && !status?.collapsed +} + +// 开始AI加载状态 +const startNodeAILoading = (nodeId: string, loadingMessage: string = 'AI处理中...') => { + updateNodeAIStatus(nodeId, { + state: 'loading', + collapsed: false, + lastAIAction: 'start_loading', + aiContext: { + loadingMessage, + startTime: Date.now() + } + }) + + addNodeAIActionHistory(nodeId, 'start_loading', { + timestamp: Date.now(), + loadingMessage + }) +} + +// 完成AI加载,进入确认状态 +const completeNodeAILoading = (nodeId: string) => { + updateNodeAIStatus(nodeId, { + state: 'confirm', + collapsed: false, + lastAIAction: 'complete_loading' + }) + + addNodeAIActionHistory(nodeId, 'complete_loading', { + timestamp: Date.now() + }) +} + +// 取消AI加载 +const cancelNodeAILoading = (nodeId: string) => { + const currentStatus = getNodeAIStatus(nodeId) + if (!currentStatus || currentStatus.state !== 'loading') { + return + } + + // 返回到聊天状态或隐藏状态 + const newState = currentStatus.chatContent ? 'chat' : 'hidden' + + updateNodeAIStatus(nodeId, { + state: newState, + collapsed: false, + aiContext: undefined, + lastAIAction: 'cancel_loading' + }) + + addNodeAIActionHistory(nodeId, 'cancel_loading', { + timestamp: Date.now(), + previousState: currentStatus.state, + loadingDuration: Date.now() - (currentStatus.aiContext?.startTime || Date.now()) + }) +} + +// 获取当前节点是否应该显示AI加载状态 +const shouldShowNodeAILoading = (nodeId: string): boolean => { + const status = getNodeAIStatus(nodeId) + return status?.state === 'loading' && !status?.collapsed +} + +/** + * 逐个解析 JSON Patch 数组中的顶级 patch 对象 + * 当整个数组因括号不匹配等原因无法解析时,通过顶层 `{"op":` 模式分割后分别解析 + * 对每个单独的对象尝试 JSON.parse → jsonrepair → 修复多余括号 + */ +const parseIndividualPatches = (jsonStr: string): any[] => { + const patches: any[] = [] + + // 移除外层数组的 [ ] 包裹 + let str = jsonStr.trim() + if (str.startsWith('[')) str = str.slice(1) + if (str.endsWith(']')) str = str.slice(0, -1) + + // 通过顶层 {"op": 模式分割各个 patch 对象 + const segments: string[] = [] + let depth = 0 + let inString = false + let escape = false + let segStart = -1 + + for (let i = 0; i < str.length; i++) { + const ch = str[i] + + if (escape) { + escape = false + continue + } + if (ch === '\\' && inString) { + escape = true + continue + } + if (ch === '"') { + inString = !inString + continue + } + if (inString) continue + + if (ch === '{') { + if (depth === 0) segStart = i + depth++ + } else if (ch === '}') { + depth-- + if (depth === 0 && segStart >= 0) { + segments.push(str.slice(segStart, i + 1)) + segStart = -1 + } + } + } + + // 对每个 segment 尝试解析,逐步增强容错 + for (const seg of segments) { + let patch = null + try { + patch = JSON.parse(seg) + } catch { + try { + patch = JSON.parse(jsonrepair(seg)) + } catch { + // 尝试修复多余的尾部 } + // 找到最后一个合法的 } 位置(通过从后往前逐步移除多余的 }) + let fixed = seg + for (let trim = 0; trim < 5; trim++) { + fixed = fixed.replace(/\}(\}*)$/, '$1') // 移除最末尾的一个 } + try { + patch = JSON.parse(fixed) + break + } catch { + // 继续尝试 + } + } + } + } + if (patch && patch.op && patch.path) { + patches.push(patch) + } + } + + return patches +} + +const findJsonPatchPath = (node, targetId, path = []) => { + if (!node || typeof node !== 'object') return null + + if (node.id === targetId) { + return '/' + path.join('/') + } + + if (Array.isArray(node.children)) { + for (let i = 0; i < node.children.length; i++) { + const result = findJsonPatchPath(node.children[i], targetId, [...path, 'children', i]) + if (result) return result + } + } + + // 如果不是数组也不是目标,继续搜索其他属性 + for (const key in node) { + if (key !== 'children' && Object.hasOwn(node, key)) { + const value = node[key] + if (value && typeof value === 'object') { + const result = findJsonPatchPath(value, targetId, [...path, key]) + if (result) return result + } + } + } + + return null +} + +/** + * 解析 AI 返回的 JSON Patch 字符串,逐步增强容错: + * 1. 剥离 Markdown 代码块包裹 + * 2. 直接 JSON.parse + * 3. 修复未转义换行符后再 parse + * 4. jsonrepair 修复 + * 5. 逐个 patch 解析(跳过损坏的单个 patch) + */ +const parseJsonPatches = (content: string): any[] | null => { + let jsonStr = content + // 剥离 Markdown 代码块 + const codeBlockMatch = jsonStr.match(/```(?:json|schema)?([\s\S]*?)```/) + if (codeBlockMatch) { + jsonStr = codeBlockMatch[1].trim() + } + + let patches: any[] = [] + try { + // 策略1:直接解析 + try { + patches = JSON.parse(jsonStr) + } catch { + // 策略2:修复 JSON 字符串值中未转义的换行符 + try { + const fixedStr = jsonStr.replace( + /"((?:[^"\\]|\\.)*)"/g, + (match, inner) => '"' + inner.replace(/\n/g, '\\n').replace(/\r/g, '\\r') + '"' + ) + patches = JSON.parse(fixedStr) + } catch { + // 策略3:使用 jsonrepair 修复 + try { + patches = JSON.parse(jsonrepair(jsonStr)) + } catch { + // 策略4:逐个 patch 解析 — AI 常在深层嵌套的 children 中产生多余的括号, + // 导致整个数组解析失败。逐个提取顶级 patch 对象分别解析,跳过损坏的。 + patches = parseIndividualPatches(jsonStr) + } + } + } + } catch (error) { + return null + } + + if (!Array.isArray(patches)) { + patches = [patches] + } + return patches +} + +/** + * 辅助函数:根据路径段从对象中安全取值 + */ +const getValueBySegments = (obj: any, segments: string[]): any => { + return segments.reduce((o, key) => (o !== null ? o[key] : undefined), obj) +} + +/** + * 辅助函数:将路径段拼接为 JSON Pointer 格式(以 / 开头) + */ +const toPointer = (segments: string[]): string => '/' + segments.filter(Boolean).join('/') + +/** + * 将 JSON Patch 数组应用到页面 schema 上 + * 按操作类型分步应用:先 replace/remove,再 add,最后其他 + * add 操作会自动处理:目标数组不存在时初始化、索引越界时追加到末尾 + * @param patches JSON Patch 数组 + * @param pageSchema 当前页面 schema + * @param parentPath 当前节点在 schema 中的路径 + * @returns 应用后的新 schema + */ +const applyPatchesToSchema = (patches: any[], pageSchema: object, parentPath: string): object => { + // 分离操作类型 + const replacePatches = patches.filter((p) => p.op === 'replace' || p.op === 'remove') + const addPatches = patches.filter((p) => p.op === 'add') + const otherPatches = patches.filter((p) => p.op !== 'replace' && p.op !== 'remove' && p.op !== 'add') + + // 先应用 replace/remove 操作 + let newSchema = replacePatches.reduce((acc, patch) => { + try { + const fullPatch = { + ...patch, + path: parentPath + patch.path + } + return jsonpatch.applyPatch(acc, [fullPatch], false, false).newDocument + } catch (error) { + return acc + } + }, pageSchema) + + // 再应用 add 操作 + // 需要处理的情况: + // 1. 目标数组不存在 → 先初始化空数组,再追加 + // 2. 索引超出数组长度 → 降级为追加到末尾(/-) + // 3. 最后一段不是数字索引且目标是数组 → 追加到末尾(/-) + newSchema = addPatches.reduce((acc, patch) => { + try { + const pathSegments = patch.path.split('/').filter(Boolean) + const lastSegment = pathSegments[pathSegments.length - 1] + + if (!lastSegment) { + return acc + } + + const parentSegments = pathSegments.slice(0, -1) + const fullParentSegments = (parentPath + '/' + parentSegments.join('/')).split('/').filter(Boolean) + let parentValue = getValueBySegments(acc, fullParentSegments) + let fixedPath = patch.path + + // 父路径对应的值不是数组但路径暗示要往数组中插入(如 /children/0), + // 需要先初始化空数组 + if (!Array.isArray(parentValue) && /^\d+$/.test(lastSegment)) { + const arrayPath = toPointer(parentPath.split('/').filter(Boolean).concat(parentSegments)) + try { + const patched = jsonpatch.applyPatch( + acc, + [{ op: 'add', path: arrayPath, value: [] }], + false, + false + ).newDocument + acc = patched + parentValue = getValueBySegments(acc, fullParentSegments) + } catch { + // 路径已存在或初始化失败,尝试继续 + } + } + + // 根据目标数组的状态修正路径 + if (Array.isArray(parentValue)) { + const index = Number(lastSegment) + if (Number.isNaN(index)) { + fixedPath = toPointer(pathSegments) + '/-' + } else if (index >= parentValue.length) { + fixedPath = toPointer(parentSegments) + '/-' + } + } + + const fullPatch = { + ...patch, + path: parentPath + fixedPath + } + return jsonpatch.applyPatch(acc, [fullPatch], false, false).newDocument + } catch (error) { + return acc + } + }, newSchema) + + // 最后应用其他操作(move, copy, test 等) + newSchema = otherPatches.reduce((acc, patch) => { + try { + const fullPatch = { + ...patch, + path: parentPath + patch.path + } + return jsonpatch.applyPatch(acc, [fullPatch], false, false).newDocument + } catch (error) { + return acc + } + }, newSchema) + + return newSchema +} + +/** + * 应用AI返回的JSON Patch到页面schema,完成画布更新 + * 逻辑:设置chatContent、设置aiModifiedNodeData为AI修改后的节点schema、修改画布节点schema为AI的schema + * @param nodeId 当前节点ID + * @param chatResponse AI聊天接口返回的响应对象 + * @param chatContent 用户发送的聊天消息 + * @returns 应用成功返回 true,失败返回 false + */ +const applyAIPatches = (nodeId: string, chatResponse: any, chatContent?: string): boolean => { + if (!chatResponse?.choices?.[0]?.message?.content) { + return false + } + + const { fixMethods, schemaAutoFix } = getMetaApi('engine.service.robot') + const { getPageSchema, getNode, updatePageSchema, setSaved } = useCanvas() + + const content = chatResponse.choices[0].message.content + const validJsonPatches = parseJsonPatches(content) + if (!validJsonPatches) { + return false + } + + const parentPath = findJsonPatchPath(getPageSchema(), nodeId) + const newSchema = applyPatchesToSchema(validJsonPatches, getPageSchema(), parentPath) + + fixMethods(newSchema.methods) + schemaAutoFix(newSchema.children) + + // 使用 updatePageSchema 更新画布(保留 nodesStatus 不清空) + updatePageSchema(newSchema) + + // 设置 AI 状态:chatContent、aiModifiedNodeData + const modifiedNode = getNode(nodeId) + const modifiedNodeData = modifiedNode ? deepClone(modifiedNode) : validJsonPatches[0]?.value + + updateNodeAIStatus(nodeId, { + state: 'confirm', + collapsed: false, + chatContent, + aiModifiedNodeData: modifiedNodeData + }) + + setSaved(false) + useHistory().addHistory() + completeNodeAILoading(nodeId) + return true +} + +// ==================== AI聊天请求构建 ==================== + +/** + * 构建AI聊天请求参数 + * @param content 用户输入的消息文本 + * @returns 请求参数对象 + */ +const buildAIChatRequest = async (content: string) => { + const { getRobotServiceOptions, formatComponents, getAgentSystemPrompt, getSelectedModelInfo } = + getMetaApi('engine.service.robot') + + const currentSchema = getCurrent().schema + const modelInfo = getSelectedModelInfo() + let referenceContext = '' + let imageAssets: any[] = [] + + if (getRobotServiceOptions()?.enableRagContext) { + referenceContext = await search(content) + } + if (getRobotServiceOptions()?.enableResourceContext) { + const appId = getMetaApi(META_SERVICE.GlobalService).getBaseInfo().id + imageAssets = await fetchAssets(appId) + } + + const { materialState, getComponentDetail } = useMaterial() + const components = formatComponents(materialState.components, getComponentDetail) + const messages = [ + { role: 'system', content: getAgentSystemPrompt(components, currentSchema, referenceContext, imageAssets) }, + { role: 'user', content: [{ type: 'text', text: content }] } + ] + + return { + body: { + baseUrl: modelInfo.baseUrl, + model: modelInfo.model, + apiKey: modelInfo.apiKey, + messages + }, + headers: { + Authorization: `Bearer ${modelInfo.apiKey}`, + 'Content-Type': 'application/json' + } + } +} + +export default function () { + return { + // AI助手状态管理 + updateNodeAIStatus, + getNodeAIStatus, + addNodeAIActionHistory, + // AI助手状态机函数 + openNodeAIChat, + closeNodeAIHelper, + startNodeAILoading, + completeNodeAILoading, + cancelNodeAILoading, + confirmNodeAIAction, + cancelNodeAIAction, + shouldShowNodeAIChat, + shouldShowNodeAILoading, + shouldShowNodeAIConfirm, + // 节点级AI采纳状态管理 + setNodeAIModifiedData, + adoptNodeAIModification, + rejectNodeAIModification, + resetNodeAIAdoptionStatus, + hasNodePendingAIModification, + findJsonPatchPath, + applyAIPatches, + // AI聊天请求构建 + buildAIChatRequest + } +} diff --git a/packages/canvas/container/src/services/agentServices.ts b/packages/canvas/container/src/services/agentServices.ts new file mode 100644 index 00000000..3f9361cb --- /dev/null +++ b/packages/canvas/container/src/services/agentServices.ts @@ -0,0 +1,73 @@ +/** + * 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. + * + */ + +import { getMetaApi, META_SERVICE } from '@opentiny/tiny-engine-meta-register' + +/** + * AI搜索功能 + * @param content 搜索内容 + * @returns 搜索结果字符串 + */ +export const search = async (content: string): Promise => { + 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) { + return '' + } + + return result +} + +/** + * 获取资源列表 + * @returns 格式化的资源列表 + */ +export const fetchAssets = async (appId: string) => { + try { + const res = (await getMetaApi(META_SERVICE.Http).get(`material-center/api/resource/find/${appId}`)) || [] + return res + .map((group: any) => group.resources) + .flat() + .filter((item: any) => item.description) + .map((item: any) => ({ + url: item.resourceUrl, + describe: item.description + })) + } catch (error) { + return [] + } +} + +export const chat = async (params, signal?: AbortSignal) => { + const response = await getMetaApi(META_SERVICE.Http).request({ + url: 'app-center/api/ai/chat', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...params.headers + }, + data: params.body, + signal + }) + + return response +} diff --git a/packages/canvas/package.json b/packages/canvas/package.json index 047d6297..d3ed66ef 100644 --- a/packages/canvas/package.json +++ b/packages/canvas/package.json @@ -42,6 +42,11 @@ "@opentiny/tiny-engine-i18n-host": "workspace:*", "@opentiny/tiny-engine-meta-register": "workspace:*", "@opentiny/tiny-engine-utils": "workspace:*", + "@opentiny/tiny-robot": "0.4.0", + "@opentiny/tiny-robot-kit": "0.4.0", + "@opentiny/tiny-robot-svgs": "0.4.0", + "fast-json-patch": "~3.1.1", + "jsonrepair": "3.13.0", "@vue/babel-plugin-jsx": "^1.2.5", "@vue/shared": "^3.3.4", "@vueuse/core": "^9.6.0", diff --git a/packages/plugins/robot/src/composables/core/useConfig.ts b/packages/plugins/robot/src/composables/core/useConfig.ts index 01908a89..b81683f5 100644 --- a/packages/plugins/robot/src/composables/core/useConfig.ts +++ b/packages/plugins/robot/src/composables/core/useConfig.ts @@ -13,10 +13,21 @@ /* metaService: engine.plugins.robot.useRobot */ import { reactive, readonly } from 'vue' import { DEFAULT_LLM_MODELS } from '../../constants' -import { getRobotServiceOptions } from '../../utils' +import { formatComponents, getAgentSystemPrompt, getJsonFixPrompt } from '../../constants/prompts' +import { + isValidJsonPatchObjectString, + getRobotServiceOptions, + addSystemPrompt, + jsonPatchAutoFix, + isValidFastJsonPatch, + getJsonObjectString, + fixMethods, + schemaAutoFix +} from '../../utils' import { ChatMode } from '../../types/mode.types' import type { ModelConfig, ModelService, RobotSettings, SelectedModelInfo } from '../../types/setting.types' import apiService from '../../services/api' +import { updatePageSchema } from '../core/pageUpdater' const SETTING_STORAGE_KEY = 'tiny-engine-robot-settings' const SETTING_VERSION = 2 // 新版本号 @@ -463,6 +474,20 @@ export default () => { addCustomService, updateService, deleteService, - getServiceById + getServiceById, + + // 公共方法 + formatComponents, + getAgentSystemPrompt, + getJsonFixPrompt, + isValidJsonPatchObjectString, + getRobotServiceOptions, + addSystemPrompt, + jsonPatchAutoFix, + isValidFastJsonPatch, + getJsonObjectString, + fixMethods, + schemaAutoFix, + updatePageSchema } } diff --git a/packages/toolbars/save/src/Main.vue b/packages/toolbars/save/src/Main.vue index c3a16c6b..8b95c2e3 100644 --- a/packages/toolbars/save/src/Main.vue +++ b/packages/toolbars/save/src/Main.vue @@ -70,11 +70,12 @@ import type { Component } from 'vue' import { VueMonaco } from '@opentiny/tiny-engine-common' import { Button, Popover, DialogBox, Checkbox, Select } from '@opentiny/vue' import { iconUpWard, iconDownWard } from '@opentiny/vue-icon' -import { useCanvas, useMessage } from '@opentiny/tiny-engine-meta-register' +import { useCanvas, useMessage, useNotify } from '@opentiny/tiny-engine-meta-register' import { ToolbarBase } from '@opentiny/tiny-engine-common' import { openCommon, saveCommon } from './js/index' import { isLoading, setAutoSaveStatus, getAutoSaveStatus } from './js/index' import { constants } from '@opentiny/tiny-engine-utils' +import useSaveValidation from './js/aiSaveValidation' const { OPEN_DELAY } = constants export const api = { @@ -175,6 +176,18 @@ export default { state.originalCode = '' } const openApi = () => { + if (useSaveValidation().hasAnyPendingAIModification()) { + const pendingNodeIds = useSaveValidation().getAllNodesWithPendingAIModification() + + useNotify({ + type: 'warning', + title: '保存被阻止', + message: `以下节点的AI修改还未处理:${pendingNodeIds.join('、')},请先采纳或取消`, + duration: 5000, + customClass: 'ai-save-warning' + }) + return + } if (!isLoading.value) { openCommon() } diff --git a/packages/toolbars/save/src/js/aiSaveValidation.ts b/packages/toolbars/save/src/js/aiSaveValidation.ts new file mode 100644 index 00000000..202c3489 --- /dev/null +++ b/packages/toolbars/save/src/js/aiSaveValidation.ts @@ -0,0 +1,36 @@ +/** + * 保存页面时的验证逻辑 + * 检查是否有未完成的AI修改,如果有则提示用户 + */ +import { useCanvas } from '@opentiny/tiny-engine-meta-register' + +/** + * 获取所有有待处理AI修改的节点ID + */ +const getAllNodesWithPendingAIModification = (): string[] => { + const { pageState, getNode } = useCanvas() + const pendingNodes: string[] = [] + + Object.entries(pageState.aiNodesStatus).forEach(([nodeId, status]) => { + // 只统计仍存在于schema中的节点,已删除节点不再阻塞保存 + if (status?.state === 'confirm' && getNode(nodeId)) { + pendingNodes.push(nodeId) + } + }) + + return pendingNodes +} + +/** + * 检查页面是否有任何待处理的AI修改 + */ +const hasAnyPendingAIModification = (): boolean => { + return getAllNodesWithPendingAIModification().length > 0 +} + +export default function () { + return { + getAllNodesWithPendingAIModification, + hasAnyPendingAIModification + } +} diff --git a/packages/toolbars/save/src/js/index.ts b/packages/toolbars/save/src/js/index.ts index 9434e689..9000592d 100644 --- a/packages/toolbars/save/src/js/index.ts +++ b/packages/toolbars/save/src/js/index.ts @@ -26,6 +26,7 @@ import { import { constants } from '@opentiny/tiny-engine-utils' import { handlePageUpdate } from '@opentiny/tiny-engine-common/js/http' import meta from '../../meta' +import useSaveValidation from './aiSaveValidation' const { publish } = useMessage() @@ -153,6 +154,19 @@ export const openCommon = async () => { return } + // 检查是否有未处理的AI修改 + if (useSaveValidation().hasAnyPendingAIModification()) { + const pendingNodeIds = useSaveValidation().getAllNodesWithPendingAIModification() + + useNotify({ + type: 'warning', + title: '保存被阻止', + message: `以下节点的AI修改还未处理:${pendingNodeIds.join('、')},请先采纳或取消`, + duration: 5000 + }) + return + } + state.disabled = true const pageSchema = getSchema()