Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/alibaba_nacos.json
T

1 line
147 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "alibaba/nacos", "files": {"console-ui/src/pages/AI/NewSkill/NewSkill.js": {"315faf6c5a4d198c5eabeafd8a115914a98e73bb": {"t": 1785768937.6538086, "n": 142086, "h": "27ade4c34b59d01f", "c": "/*\n * Copyright 1999-2025 Alibaba Group Holding Ltd.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport {\n Button,\n Card,\n Collapse,\n ConfigProvider,\n Field,\n Form,\n Input,\n Message,\n Grid,\n Icon,\n Dialog,\n Select,\n Checkbox,\n Loading,\n Balloon,\n Tag,\n} from '@alifd/next';\nimport PageTitle from 'components/PageTitle';\nimport SkillOptimizeDialog from '../SkillManagement/SkillOptimizeDialog';\nimport MonacoEditor from '../../../components/MonacoEditor/MonacoEditor';\nimport MarkdownRenderer from '../../../components/MarkdownRenderer/MarkdownRenderer';\nimport MagicWandIcon from '../../../components/MagicWandIcon/MagicWandIcon';\nimport { getLanguageFromFileName } from '../../../utils/languageDetector';\nimport { getParams, request } from '@/globalLib';\nimport { COPILOT_ENABLED } from '@/constants';\nimport './NewSkill.scss';\n\nconst { Row, Col } = Grid;\nconst { Panel } = Collapse;\n\n@ConfigProvider.config\nclass NewSkill extends React.Component {\n static displayName = 'SkillManagement';\n\n static propTypes = {\n locale: PropTypes.object,\n history: PropTypes.object,\n };\n\n constructor(props) {\n super(props);\n this.field = new Field(this);\n\n const skillName = getParams('name');\n const mode = getParams('mode');\n const isEdit = mode === 'edit' && !!skillName;\n const isDraft = getParams('isDraft') === 'true';\n\n this.state = {\n loading: false,\n generating: false,\n streaming: false,\n streamContent: '',\n thinkingContent: '',\n thinkingCollapsed: false,\n showInputCollapsed: false,\n inputSectionCollapsed: false,\n resultContentCollapsed: false,\n inputTooltipVisible: false,\n inputTooltipPosition: { x: 0, y: 0 },\n thinkingTooltipVisible: false,\n thinkingTooltipPosition: { x: 0, y: 0 },\n parsingResult: false,\n isEdit: isEdit,\n isDraft: isDraft,\n skillName,\n backgroundInfo: '',\n conversationHistory: null, // 对话历史\n conversationHistoryJson: '', // 对话历史 JSON 字符串(用于编辑)\n showConversationHistory: false, // 是否显示对话历史输入\n showAiGenerateDialog: false, // 控制是否显示AI生成弹窗\n generatedSkill: null, // 存储生成的Skill数据\n mcpServers: [], // MCP服务器列表\n selectedMcpServer: null, // 选中的MCP服务器\n mcpTools: [], // MCP工具列表\n selectedMcpTools: [], // 选中的MCP工具\n loadingMcpServers: false, // 加载MCP服务器列表\n loadingMcpTools: false, // 加载MCP工具列表\n mcpToolSearchKeyword: '', // MCP工具搜索关键词\n optimizeDialogVisible: false, // 控制是否显示AI优化弹窗\n currentSkillData: null, // 存储当前Skill数据用于优化\n editModeFileTree: null, // 编辑模式下的文件树\n editModeSelectedFile: null, // 编辑模式下选中的文件\n editingFileName: null, // 正在编辑的文件名(格式:{nodeKey, oldName, type})\n editingFileNameValue: '', // 正在编辑的文件名的临时值\n draggingFile: null, // 正在拖拽的文件(格式:{resourceKey, name, type})\n dragOverFolder: null, // 当前拖拽悬停的文件夹名称\n prevFormValues: {}, // 上一次的表单值,用于检测变化\n originalFormData: null, // 原始表单数据(用于比较是否有修改)\n originalResources: [], // 原始资源数据(用于比较是否有修改)\n hasChanges: false, // 是否有数据变化\n // 创建模式下默认添加一个空的资源项\n resources: isEdit\n ? []\n : [\n {\n name: '',\n type: '',\n content: '',\n metadata: null,\n },\n ],\n // 资源面板展开状态,创建模式下默认展开第一个\n expandedKeys: isEdit ? [] : ['0'],\n // 正在编辑的资源索引(用于标题编辑)\n editingResourceIndex: null,\n // 鼠标位置和tooltip显示状态\n tooltipVisible: false,\n tooltipPosition: { x: 0, y: 0 },\n tooltipResourceIndex: null,\n };\n this.streamReader = null;\n }\n\n componentDidMount() {\n if (this.state.isEdit) {\n // Check if there's optimized skill data from optimization dialog\n const optimizedParam = getParams('optimized');\n if (optimizedParam === 'true') {\n this.loadOptimizedSkillData();\n } else {\n this.loadSkillData();\n }\n }\n // Add click listener to handle clicking outside edit area\n // Use capture phase to handle before other click handlers\n document.addEventListener('click', this.handleDocumentClick, true);\n }\n\n componentWillUnmount() {\n // Remove click listener\n document.removeEventListener('click', this.handleDocumentClick, true);\n }\n\n componentDidUpdate(prevProps, prevState) {\n // 在编辑模式下,当表单值或资源变化时更新文件树和变化状态\n if (this.state.isEdit) {\n const values = this.field.getValues();\n const prevValues = prevState.prevFormValues || {};\n\n // 检查表单值是否变化\n const formChanged =\n values.name !== prevValues.name ||\n values.description !== prevValues.description ||\n values.instruction !== prevValues.instruction;\n\n // 检查资源是否变化\n const resourcesChanged =\n JSON.stringify(this.state.resources) !== JSON.stringify(prevState.resources);\n\n if (formChanged || resourcesChanged) {\n this.updateEditModeFileTree();\n // 保存当前表单值用于下次比较\n this.setState({ prevFormValues: values });\n }\n\n // 检查是否有数据变化并更新状态\n const hasChanged = this.hasDataChanged();\n if (hasChanged !== this.state.hasChanges) {\n this.setState({ hasChanges: hasChanged });\n }\n }\n }\n\n handleDocumentClick = e => {\n // Skip if clicking on resource title text (which should enter edit mode)\n if (e.target.closest('.resource-title-text')) {\n return;\n }\n\n // If editing resource title, check if click is outside the editor\n if (this.state.editingResourceIndex !== null) {\n // Find the editor element for the current editing resource\n const editorElements = document.querySelectorAll('.resource-title-editor');\n const currentEditor = editorElements[this.state.editingResourceIndex];\n\n if (currentEditor && !currentEditor.contains(e.target)) {\n // Click is outside the editor, exit edit mode\n this.setState({ editingResourceIndex: null });\n }\n }\n };\n\n loadOptimizedSkillData = () => {\n try {\n const optimizedSkillStr = localStorage.getItem('nacos_optimized_skill');\n if (optimizedSkillStr) {\n const optimizedSkill = JSON.parse(optimizedSkillStr);\n\n // 先加载原始数据作为基准,用于比较变化\n const { skillName } = this.state;\n const namespaceId = optimizedSkill.namespaceId || getParams('namespace') || '';\n\n this.setState({ loading: true });\n\n const params = new URLSearchParams();\n params.append('skillName', skillName);\n params.append('namespaceId', namespaceId);\n\n request({\n url: `v3/console/ai/skills?${params.toString()}`,\n success: data => {\n this.setState({ loading: false });\n if (data && (data.code === 0 || data.code === 200) && data.data) {\n const originalSkillData = data.data;\n const originalResources = originalSkillData.resource\n ? Object.values(originalSkillData.resource)\n : [];\n\n // 保存原始数据用于比较\n const originalFormData = {\n name: originalSkillData.name,\n description: originalSkillData.description || '',\n instruction: originalSkillData.skillMd || '',\n };\n\n // Fill form with optimized skill data\n // Important: Keep original skill name, don't use optimized name\n const originalName = this.state.skillName || '';\n this.field.setValues({\n name: originalName, // Always use original skill name\n description: optimizedSkill.description || '',\n instruction: optimizedSkill.instruction || '',\n });\n\n // Fill resources if any\n let resources = [];\n let resourceMap = {};\n if (optimizedSkill.resource && Object.keys(optimizedSkill.resource).length > 0) {\n resources = Object.values(optimizedSkill.resource).map(resource => ({\n name: resource.name || '',\n type: resource.type || '',\n content: resource.content || '',\n metadata: resource.metadata || null,\n }));\n // Build resource map for currentSkillData\n Object.entries(optimizedSkill.resource).forEach(([key, resource]) => {\n resourceMap[key] = resource;\n });\n }\n\n // Build currentSkillData for AI optimization feature\n const currentSkillData = {\n name: originalName,\n namespaceId: namespaceId,\n description: optimizedSkill.description || '',\n instruction: optimizedSkill.instruction || '',\n resource: resourceMap,\n };\n\n // 获取被优化的文件名\n const targetFileName = optimizedSkill.targetFileName;\n\n // 比较优化后的数据与原始数据,判断是否有变化\n const formChanged =\n (optimizedSkill.description || '') !== (originalSkillData.description || '') ||\n (optimizedSkill.instruction || '') !== (originalSkillData.skillMd || '');\n const resourcesChanged =\n JSON.stringify(resources) !== JSON.stringify(originalResources);\n const hasChanges = formChanged || resourcesChanged;\n\n this.setState(\n {\n resources,\n expandedKeys: resources.map((_, index) => String(index)),\n currentSkillData, // Set currentSkillData so AI optimization can work\n originalFormData, // 保存原始表单数据用于比较\n originalResources: JSON.parse(JSON.stringify(originalResources)), // 深拷贝原始资源\n hasChanges, // 设置变化状态\n },\n () => {\n // 在 resources 更新后构建文件树\n const previewData = this.buildPreviewData();\n const fileTree = this.buildFileTree(previewData);\n\n // 根据 targetFileName 查找并选中对应的文件\n let selectedFile = null;\n if (targetFileName && fileTree && Array.isArray(fileTree)) {\n // 递归查找文件\n const findFileInTree = tree => {\n if (Array.isArray(tree)) {\n for (const node of tree) {\n if (node.type === 'file') {\n // 检查是否匹配\n let fileIdentifier = node.name;\n if (node.fileType === 'resource' && node.resourceKey) {\n fileIdentifier = node.resourceKey;\n }\n if (fileIdentifier === targetFileName) {\n return node;\n }\n }\n if (node.type === 'folder' && node.children) {\n const found = findFileInTree(node.children);\n if (found) {\n return found;\n }\n }\n }\n } else if (tree.type === 'file') {\n let fileIdentifier = tree.name;\n if (tree.fileType === 'resource' && tree.resourceKey) {\n fileIdentifier = tree.resourceKey;\n }\n if (fileIdentifier === targetFileName) {\n return tree;\n }\n } else if (tree.type === 'folder' && tree.children) {\n return findFileInTree(tree.children);\n }\n return null;\n };\n\n selectedFile = findFileInTree(fileTree);\n }\n\n // 如果没有找到目标文件,默认选中 SKILL.md 或第一个文件\n if (!selectedFile && fileTree && Array.isArray(fileTree)) {\n selectedFile =\n fileTree.find(\n file => file.name === 'SKILL.md' && file.fileType === 'skill-md'\n ) || (fileTree.length > 0 ? fileTree[0] : null);\n }\n\n this.setState({\n editModeFileTree: fileTree,\n editModeSelectedFile: selectedFile,\n });\n\n // Clear the stored data\n localStorage.removeItem('nacos_optimized_skill');\n\n const { locale = {} } = this.props;\n Message.success(\n this.getLocaleValue(\n 'optimizedSkillLoaded',\n 'Optimized skill data loaded successfully'\n )\n );\n }\n );\n } else {\n // 如果加载原始数据失败,fallback 到正常加载\n this.loadSkillData();\n }\n },\n error: () => {\n this.setState({ loading: false });\n // 如果加载原始数据失败,fallback 到正常加载\n this.loadSkillData();\n },\n });\n } else {\n // Fallback to normal load\n this.loadSkillData();\n }\n } catch (e) {\n // eslint-disable-next-line no-console\n console.error('Failed to load optimized skill data', e);\n // Fallback to normal load\n this.loadSkillData();\n }\n };\n\n loadSkillData = callback => {\n const { skillName, isDraft } = this.state;\n const namespaceId = getParams('namespace') || '';\n\n this.setState({ loading: true });\n\n const params = new URLSearchParams();\n params.append('skillName', skillName);\n params.append('namespaceId', namespaceId);\n\n request({\n url: `v3/console/ai/skills?${params.toString()}`,\n success: data => {\n if (data && (data.code === 0 || data.code === 200) && data.data) {\n const skillData = data.data;\n // SkillAdminDetail may not include name; fill from URL params\n if (!skillData.name) {\n skillData.name = skillName;\n }\n const editingVersion = skillData.editingVersion || skillData.editingVersionStr;\n\n // If isDraft and there is an editingVersion, load draft version content\n if (isDraft && editingVersion) {\n const versionParams = new URLSearchParams();\n versionParams.append('skillName', skillName);\n versionParams.append('version', editingVersion);\n if (namespaceId) {\n versionParams.append('namespaceId', namespaceId);\n }\n\n request({\n url: `v3/console/ai/skills/version?${versionParams.toString()}`,\n success: versionData => {\n this.setState({ loading: false });\n if (\n versionData &&\n (versionData.code === 0 || versionData.code === 200) &&\n versionData.data\n ) {\n // Use draft version content instead of default content\n const draftContent = versionData.data;\n const mergedSkillData = {\n ...skillData,\n name: draftContent.name || skillData.name || skillName,\n instruction: draftContent.skillMd || '',\n description: draftContent.description || '',\n resource: draftContent.resource || skillData.resource || {},\n };\n this._applySkillData(mergedSkillData, callback);\n } else {\n // Fallback: use base skillData if version load fails\n this._applySkillData(skillData, callback);\n }\n },\n error: () => {\n this.setState({ loading: false });\n // Fallback: use base skillData if version load fails\n this._applySkillData(skillData, callback);\n },\n });\n } else {\n this.setState({ loading: false });\n this._applySkillData(skillData, callback);\n }\n } else {\n this.setState({ loading: false });\n Message.error(\n data?.message ||\n this.getLocaleValue('getSkillInfoFailed', 'Failed to get Skill information')\n );\n }\n },\n error: () => {\n this.setState({ loading: false });\n Message.error(this.getLocaleValue('getSkillInfoFailed', 'Failed to get Skill information'));\n },\n });\n };\n\n _applySkillData = (skillData, callback) => {\n const resources = skillData.resource ? Object.values(skillData.resource) : [];\n\n this.field.setValues({\n name: skillData.name,\n description: skillData.description || '',\n instruction: skillData.skillMd || skillData.instruction || '',\n });\n\n // 保存原始数据用于比较\n const originalFormData = {\n name: skillData.name,\n description: skillData.description || '',\n instruction: skillData.skillMd || skillData.instruction || '',\n };\n const originalResources = JSON.parse(JSON.stringify(resources)); // 深拷贝\n\n // 先更新 resources,然后再构建文件树,确保使用最新的数据\n this.setState(\n {\n resources,\n expandedKeys: resources.map((_, index) => String(index)),\n currentSkillData: skillData,\n prevFormValues: originalFormData,\n originalFormData,\n originalResources,\n hasChanges: false, // 初始状态没有变化\n },\n () => {\n // 在 resources 更新后构建文件树\n const previewData = this.buildPreviewData();\n const fileTree = this.buildFileTree(previewData);\n // Find SKILL.md file in the file list\n const skillMdFile =\n fileTree && Array.isArray(fileTree)\n ? fileTree.find(file => file.name === 'SKILL.md' && file.fileType === 'skill-md')\n : null;\n\n // 更新文件树和选中的文件\n this.setState(\n {\n editModeFileTree: fileTree,\n editModeSelectedFile:\n skillMdFile || (fileTree && fileTree.length > 0 ? fileTree[0] : null),\n },\n () => {\n // 执行回调,允许调用方在数据加载完成后执行额外操作\n if (callback && typeof callback === 'function') {\n callback();\n }\n }\n );\n }\n );\n };\n\n handleSubmit = () => {\n const { locale = {} } = this.props;\n\n // 如果有正在编辑的文件名,先保存它\n const { editingFileName, editingFileNameValue, resources } = this.state;\n if (editingFileName && editingFileNameValue) {\n const nameToSave = editingFileNameValue.trim();\n if (nameToSave && nameToSave !== editingFileName.oldName) {\n // 同步保存文件名\n const filteredName = nameToSave.replace(/[^a-zA-Z0-9._-]/g, '');\n\n if (filteredName && filteredName !== editingFileName.oldName) {\n // 检查是否重名\n const isDuplicate = resources.some(\n r =>\n r.name === filteredName &&\n r.type === editingFileName.type &&\n r.name !== editingFileName.oldName\n );\n\n if (isDuplicate) {\n Message.warning(locale.fileNameDuplicate || 'File name already exists');\n return;\n }\n\n const resourceIndex = resources.findIndex(\n r => r.name === editingFileName.oldName && r.type === editingFileName.type\n );\n\n if (resourceIndex !== -1) {\n const newResources = [...resources];\n newResources[resourceIndex] = {\n ...newResources[resourceIndex],\n name: filteredName,\n };\n // 更新 resources,然后继续提交\n this.setState(\n {\n resources: newResources,\n editingFileName: null,\n editingFileNameValue: '',\n },\n () => {\n // 继续提交\n this.doSubmit(newResources);\n }\n );\n return;\n }\n }\n }\n // 如果文件名没有变化或无效,取消编辑状态后继续提交\n this.setState({ editingFileName: null, editingFileNameValue: '' }, () => {\n this.doSubmit();\n });\n return;\n }\n\n this.doSubmit();\n };\n\n doSubmit = (resourcesToUse = null) => {\n const { locale = {} } = this.props;\n\n // 在 validate 之前获取 resources,避免闭包问题\n const resourcesSnapshot = resourcesToUse || [...this.state.resources];\n console.log('[doSubmit] resources snapshot:', JSON.stringify(resourcesSnapshot));\n\n this.field.validate((errors, values) => {\n if (errors) {\n return;\n }\n\n this.setState({ loading: true });\n\n // 使用快照的 resources\n const resources = resourcesSnapshot;\n\n const namespaceId = getParams('namespace') || '';\n const { isEdit } = this.state;\n\n // 构建 skillCard 对象\n const skillCard = {\n name: values.name,\n description: values.description || '',\n skillMd: values.instruction || '',\n };\n\n // 构建 resource Map,过滤掉无效的资源(没有 name 或 name 为空的资源)\n if (resources && resources.length > 0) {\n const resourceMap = {};\n const duplicateNames = new Set();\n resources.forEach((resource, index) => {\n // 只包含有效的资源(有 name 且 name 不为空)\n if (resource.name && resource.name.trim() !== '') {\n const key = resource.name.trim();\n // 检查是否有重复的文件名\n if (resourceMap[key]) {\n duplicateNames.add(key);\n console.warn(\n `Duplicate resource name detected: ${key}. Previous content will be overwritten.`\n );\n }\n resourceMap[key] = {\n name: resource.name.trim(),\n type: resource.type || '',\n content: resource.content || '',\n metadata: resource.metadata || null,\n };\n } else {\n console.warn(\n `Skipping resource at index ${index} because name is empty or invalid:`,\n resource\n );\n }\n });\n if (duplicateNames.size > 0) {\n console.warn(\n `Found ${duplicateNames.size} duplicate resource name(s):`,\n Array.from(duplicateNames)\n );\n }\n skillCard.resource = resourceMap;\n console.log('[doSubmit] Built resourceMap:', JSON.stringify(resourceMap));\n } else {\n skillCard.resource = {};\n }\n\n console.log('[doSubmit] Final skillCard:', JSON.stringify(skillCard));\n\n // 准备请求数据\n const requestData = {\n namespaceId: namespaceId,\n skillName: values.name,\n skillCard: JSON.stringify(skillCard),\n };\n\n const { isDraft } = this.state;\n\n if (isEdit && isDraft) {\n // Edit draft mode: PUT /draft\n request({\n url: 'v3/console/ai/skills/draft',\n method: 'PUT',\n data: requestData,\n contentType: 'application/x-www-form-urlencoded',\n success: data => {\n this.setState({ loading: false });\n if (\n data &&\n (data.code === 0 ||\n data.code === 200 ||\n data.data === 'ok' ||\n data.message === 'success')\n ) {\n Message.success(\n this.getLocaleValue('updateDraftSuccess', 'Draft updated successfully')\n );\n // Reload data and restore selected file\n const { editModeSelectedFile } = this.state;\n const selectedFileName = editModeSelectedFile?.name;\n const selectedFileType = editModeSelectedFile?.fileType;\n const selectedResourceKey = editModeSelectedFile?.resourceKey;\n\n this.loadSkillData(() => {\n if (selectedFileName && selectedFileType === 'resource') {\n const { editModeFileTree } = this.state;\n if (editModeFileTree && Array.isArray(editModeFileTree)) {\n const findFileInTree = tree => {\n if (Array.isArray(tree)) {\n for (const node of tree) {\n if (\n node.fileType === 'resource' &&\n (node.resourceKey === selectedResourceKey ||\n node.name === selectedFileName)\n ) {\n return node;\n }\n if (node.children) {\n const found = findFileInTree(node.children);\n if (found) return found;\n }\n }\n }\n return null;\n };\n const restoredFile = findFileInTree(editModeFileTree);\n if (restoredFile) {\n this.setState({ editModeSelectedFile: restoredFile });\n }\n }\n }\n });\n } else {\n Message.error(\n data?.message || this.getLocaleValue('updateDraftFailed', 'Failed to update draft')\n );\n }\n },\n error: () => {\n this.setState({ loading: false });\n Message.error(this.getLocaleValue('updateDraftFailed', 'Failed to update draft'));\n },\n });\n } else if (!isEdit) {\n // Create mode: POST /draft to create, then PUT /draft to write content\n request({\n url: 'v3/console/ai/skills/draft',\n method: 'POST',\n data: {\n skillName: values.name,\n namespaceId: namespaceId,\n basedOnVersion: '',\n },\n contentType: 'application/x-www-form-urlencoded',\n success: createData => {\n if (\n createData &&\n (createData.code === 0 ||\n createData.code === 200 ||\n createData.data === 'ok' ||\n createData.message === 'success')\n ) {\n // Draft created, now update with content\n request({\n url: 'v3/console/ai/skills/draft',\n method: 'PUT',\n data: requestData,\n contentType: 'application/x-www-form-urlencoded',\n success: updateData => {\n this.setState({ loading: false });\n if (\n updateData &&\n (updateData.code === 0 ||\n updateData.code === 200 ||\n updateData.data === 'ok' ||\n updateData.message === 'success')\n ) {\n Message.success(this.getLocaleValue('createSuccess', 'Create successful'));\n setTimeout(() => {\n // Navigate to detail page\n this.props.history.push(\n `/skillDetail?namespace=${namespaceId}&name=${values.name}`\n );\n }, 1000);\n } else {\n Message.error(\n updateData?.message || this.getLocaleValue('createFailed', 'Create failed')\n );\n }\n },\n error: () => {\n this.setState({ loading: false });\n Message.error(this.getLocaleValue('createFailed', 'Create failed'));\n },\n });\n } else {\n this.setState({ loading: false });\n Message.error(\n createData?.message ||\n this.getLocaleValue('createDraftFailed', 'Failed to create draft')\n );\n }\n },\n error: () => {\n this.setState({ loading: false });\n Message.error(this.getLocaleValue('createDraftFailed', 'Failed to create draft'));\n },\n });\n } else {\n // Legacy edit mode (e.g., after ZIP upload): PUT /skills (backward compatible)\n const url = 'v3/console/ai/skills';\n\n request({\n url: url,\n method: 'PUT',\n data: requestData,\n contentType: 'application/x-www-form-urlencoded',\n success: data => {\n this.setState({ loading: false });\n if (\n data &&\n (data.code === 0 ||\n data.code === 200 ||\n data.data === 'ok' ||\n data.message === 'success')\n ) {\n Message.success(this.getLocaleValue('updateSuccess', 'Update successful'));\n\n const { editModeSelectedFile } = this.state;\n const selectedFileName = editModeSelectedFile?.name;\n const selectedFileType = editModeSelectedFile?.fileType;\n const selectedResourceKey = editModeSelectedFile?.resourceKey;\n\n this.loadSkillData(() => {\n if (selectedFileName && selectedFileType === 'resource') {\n const { editModeFileTree } = this.state;\n if (editModeFileTree && Array.isArray(editModeFileTree)) {\n const findFileInTree = tree => {\n if (Array.isArray(tree)) {\n for (const node of tree) {\n if (\n node.fileType === 'resource' &&\n (node.resourceKey === selectedResourceKey ||\n node.name === selectedFileName)\n ) {\n return node;\n }\n if (node.children) {\n const found = findFileInTree(node.children);\n if (found) return found;\n }\n }\n }\n return null;\n };\n const restoredFile = findFileInTree(editModeFileTree);\n if (restoredFile) {\n this.setState({ editModeSelectedFile: restoredFile });\n }\n }\n }\n });\n } else {\n Message.error(data?.message || this.getLocaleValue('updateFailed', 'Update failed'));\n }\n },\n error: () => {\n this.setState({ loading: false });\n Message.error(this.getLocaleValue('updateFailed', 'Update failed'));\n },\n });\n }\n });\n };\n\n handleGoBack = () => {\n const namespaceId = getParams('namespace') || '';\n this.props.history.push(`/skillManagement?namespace=${namespaceId}`);\n };\n\n handleAddResource = () => {\n const { resources } = this.state;\n const newResource = {\n name: '',\n type: '',\n content: '',\n metadata: null,\n };\n const newIndex = resources.length;\n this.setState(\n {\n resources: [...resources, newResource],\n expandedKeys: [String(newIndex)], // 只展开新添加的资源\n },\n () => {\n // 更新文件树和变化状态\n if (this.state.isEdit) {\n this.updateEditModeFileTree();\n // 检查是否有变化\n const hasChanged = this.hasDataChanged();\n if (hasChanged !== this.state.hasChanges) {\n this.setState({ hasChanges: hasChanged });\n }\n }\n }\n );\n };\n\n handleExpandChange = expandedKeys => {\n this.setState({ expandedKeys });\n };\n\n handleRemoveResource = index => {\n Dialog.confirm({\n title: this.getLocaleValue('deleteConfirm', 'Delete Confirmation'),\n content: this.getLocaleValue(\n 'deleteResourceConfirm',\n 'Are you sure you want to delete this resource?'\n ),\n onOk: () => {\n const { resources, expandedKeys } = this.state;\n const newResources = resources.filter((_, i) => i !== index);\n // 更新 expandedKeys,移除被删除的索引并重新计算后续索引\n const newExpandedKeys = expandedKeys\n .filter(key => key !== String(index))\n .map(key => {\n const keyNum = parseInt(key, 10);\n return keyNum > index ? String(keyNum - 1) : key;\n });\n this.setState({ resources: newResources, expandedKeys: newExpandedKeys }, () => {\n // 更新文件树和变化状态\n if (this.state.isEdit) {\n this.updateEditModeFileTree();\n // 检查是否有变化\n const hasChanged = this.hasDataChanged();\n if (hasChanged !== this.state.hasChanges) {\n this.setState({ hasChanges: hasChanged });\n }\n }\n });\n },\n });\n };\n\n handleResourceChange = (index, field, value) => {\n let filteredValue = value;\n\n // 资源名称:支持英文大小写、数字、点号、下划线、横杠,不能有空格\n if (field === 'name') {\n filteredValue = value.replace(/[^a-zA-Z0-9._-]/g, '');\n }\n // 资源类型:只支持英文大小写、横杠、点号\n if (field === 'type') {\n filteredValue = value.replace(/[^a-zA-Z.-]/g, '');\n }\n\n const { resources } = this.state;\n const newResources = [...resources];\n newResources[index] = {\n ...newResources[index],\n [field]: filteredValue,\n };\n\n this.setState({ resources: newResources }, () => {\n // 更新文件树和变化状态\n if (this.state.isEdit) {\n this.updateEditModeFileTree();\n // 检查是否有变化\n const hasChanged = this.hasDataChanged();\n if (hasChanged !== this.state.hasChanges) {\n this.setState({ hasChanges: hasChanged });\n }\n }\n });\n };\n\n handleResourceTitleClick = (index, e) => {\n e.stopPropagation();\n this.setState({ editingResourceIndex: index });\n };\n\n handleResourceTitleMouseEnter = (index, e) => {\n this.setState({\n tooltipVisible: true,\n tooltipPosition: { x: e.clientX, y: e.clientY },\n tooltipResourceIndex: index,\n });\n };\n\n handleResourceTitleMouseMove = e => {\n if (this.state.tooltipVisible) {\n this.setState({\n tooltipPosition: { x: e.clientX, y: e.clientY },\n });\n }\n };\n\n handleResourceTitleMouseLeave = () => {\n this.setState({\n tooltipVisible: false,\n tooltipResourceIndex: null,\n });\n };\n\n handleResourceTitleCancel = () => {\n this.setState({ editingResourceIndex: null });\n };\n\n handleSkillNameChange = value => {\n // Skill名称:只允许英文、下划线、横杠,输入非法字符时实时提醒\n const filteredValue = value.replace(/[^a-zA-Z_-]/g, '');\n if (filteredValue.length !== value.length) {\n Message.warning(\n this.getLocaleValue(\n 'skillNameInvalidChars',\n 'Name can only contain English letters, underscore and hyphen'\n )\n );\n }\n this.field.setValue('name', filteredValue);\n // 实时校验(双下划线、必填等),在表单项下展示错误\n this.field.validate('name');\n // 检查是否有变化\n if (this.state.isEdit) {\n setTimeout(() => {\n const hasChanged = this.hasDataChanged();\n if (hasChanged !== this.state.hasChanges) {\n this.setState({ hasChanges: hasChanged });\n }\n }, 0);\n }\n };\n\n validateSkillName = (rule, value, callback) => {\n if (!value || value.trim() === '') {\n callback(this.getLocaleValue('requiredField', 'This field is required'));\n return;\n }\n if (!/^[a-zA-Z_-]+$/.test(value)) {\n callback(\n this.getLocaleValue(\n 'skillNameInvalidChars',\n 'Name can only contain English letters, underscore and hyphen'\n )\n );\n return;\n }\n if (value.includes('__')) {\n callback(\n this.getLocaleValue(\n 'skillNameNoDoubleUnderscore',\n 'Name cannot contain double underscores (__)'\n )\n );\n return;\n }\n callback();\n };\n\n validateRequired = (rule, value, callback) => {\n if (!value || value.trim() === '') {\n callback(this.getLocaleValue('requiredField', 'This field is required'));\n } else {\n callback();\n }\n };\n\n handleGenerateSkill = () => {\n const { backgroundInfo } = this.state;\n\n if (!backgroundInfo || backgroundInfo.trim() === '') {\n Message.warning(\n this.getLocaleValue('backgroundInfoRequired', 'Please enter background information')\n );\n return;\n }\n\n this.setState({\n generating: true,\n streaming: true,\n streamContent: '',\n thinkingContent: '',\n generatedSkill: null,\n showInputCollapsed: true, // 折叠用户输入\n inputSectionCollapsed: true, // 折叠输入区域\n thinkingCollapsed: false, // 展开思考内容\n resultContentCollapsed: false, // 展开结果内容\n parsingResult: false,\n });\n\n // Parse conversation history if provided\n let conversationHistory = null;\n if (this.state.conversationHistoryJson && this.state.conversationHistoryJson.trim()) {\n try {\n conversationHistory = JSON.parse(this.state.conversationHistoryJson);\n } catch (e) {\n Message.error(\n this.getLocaleValue(\n 'conversationHistoryInvalid',\n 'Invalid conversation history JSON format'\n )\n );\n this.setState({ generating: false, streaming: false });\n return;\n }\n } else if (this.state.conversationHistory) {\n conversationHistory = this.state.conversationHistory;\n }\n\n // Build request payload\n const payload = {\n backgroundInfo: backgroundInfo.trim(),\n selectedMcpTools: this.state.selectedMcpTools.map(tool => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n })),\n };\n\n if (conversationHistory) {\n payload.conversationHistory = conversationHistory;\n }\n\n // Use SSE stream\n const ctxPath = window.location.pathname.replace(/\\/(next|legacy)(\\/.*)?$/, '/') || '/';\n const url = `${window.location.origin}${ctxPath}v3/console/copilot/skill/generate`;\n const token = localStorage.getItem('token');\n\n this.startSSEStream(url, payload, token);\n };\n\n startSSEStream = (url, payload, token) => {\n let accessToken = '';\n try {\n const tokenObj = JSON.parse(token);\n accessToken = tokenObj.accessToken || '';\n } catch (e) {\n // eslint-disable-next-line no-console\n console.error('Failed to parse token:', e);\n }\n\n // Use fetch API for POST request with SSE\n fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'text/event-stream',\n ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),\n ...(accessToken ? { AccessToken: accessToken } : {}),\n },\n body: JSON.stringify(payload),\n })\n .then(response => {\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n this.streamReader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n let currentEventType = 'message'; // Default event type\n let pendingData = null; // Store data when event type comes after data\n\n const readStream = () => {\n this.streamReader\n .read()\n .then(({ done, value }) => {\n if (done) {\n this.setState({ streaming: false, generating: false });\n return;\n }\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || ''; // Keep incomplete line in buffer\n\n lines.forEach(line => {\n if (line.startsWith('event:')) {\n const eventType = line.substring(6).trim();\n currentEventType = eventType;\n // If we have pending data and this is an error event, handle it now\n if (eventType === 'error' && pendingData) {\n const errorMessage =\n pendingData.explanation ||\n pendingData.message ||\n this.getLocaleValue('generateFailed', '生成失败');\n Message.error(errorMessage);\n this.setState({\n streaming: false,\n generating: false,\n error: errorMessage,\n });\n pendingData = null;\n }\n } else if (line.startsWith('data:')) {\n const dataStr = line.substring(5).trim();\n if (dataStr) {\n try {\n const data = JSON.parse(dataStr);\n // Handle error event - check if current event type is error\n if (currentEventType === 'error') {\n const errorMessage =\n data.explanation ||\n data.message ||\n this.getLocaleValue('generateFailed', '生成失败');\n Message.error(errorMessage);\n this.setState({\n streaming: false,\n generating: false,\n error: errorMessage,\n });\n } else if (data.done && data.explanation && !data.skill) {\n // Handle case where error comes in data with done=true but no skill\n // This might be an error response\n const errorMessage =\n data.explanation ||\n data.message ||\n this.getLocaleValue('generateFailed', '生成失败');\n Message.error(errorMessage);\n this.setState({\n streaming: false,\n generating: false,\n error: errorMessage,\n });\n pendingData = null; // Clear pending data since we've handled the error\n } else {\n // Store data temporarily in case event:error comes after\n pendingData = data;\n this.handleSSEMessage(data);\n }\n } catch (e) {\n // Failed to parse SSE data\n console.error('Failed to parse SSE data:', e, dataStr);\n }\n }\n }\n });\n\n readStream();\n })\n .catch(error => {\n this.setState({\n streaming: false,\n generating: false,\n error: error.message || 'Stream read failed',\n });\n Message.error(this.getLocaleValue('generateFailed', 'Failed to generate skill'));\n });\n };\n\n readStream();\n })\n .catch(error => {\n this.setState({\n streaming: false,\n generating: false,\n });\n Message.error(this.getLocaleValue('generateFailed', 'Failed to generate skill'));\n });\n };\n\n handleSSEMessage = data => {\n if (!data) {\n return;\n }\n\n const typeStr = data.type?.code || data.type || 'CONTENT';\n\n // Handle different response types\n if (typeStr === 'THINKING' || data.type === 'THINKING') {\n // Accumulate thinking content separately\n this.setState(prevState => ({\n thinkingContent: (prevState.thinkingContent || '') + (data.chunk || ''),\n streamType: 'THINKING',\n }));\n } else if (\n typeStr === 'TOOL_CALL' ||\n data.type === 'TOOL_CALL' ||\n typeStr === 'CONTENT' ||\n data.type === 'CONTENT'\n ) {\n // Accumulate stream content\n const currentContent = this.state.streamContent || '';\n this.setState({\n streamContent: currentContent + (data.chunk || ''),\n streamType: typeStr === 'TOOL_CALL' ? 'TOOL_CALL' : 'CONTENT',\n thinkingCollapsed: true, // 折叠思考内容,开始显示结果\n resultContentCollapsed: false, // 展开结果内容\n });\n } else if (typeStr === 'DONE' || data.done) {\n // Final response received - start parsing\n this.setState({\n streaming: false,\n thinkingCollapsed: true, // 折叠思考内容\n resultContentCollapsed: true, // 折叠结果内容,显示生成的Skill\n parsingResult: true, // 开始解析结果\n });\n\n // Parse result after a short delay\n setTimeout(() => {\n // DONE event doesn't contain full content, must parse from accumulated streamContent\n let skillData = null;\n\n // Always try to parse from accumulated streamContent first\n if (this.state.streamContent) {\n try {\n // Try to extract JSON from streamContent\n const content = this.state.streamContent;\n let jsonContent = content;\n\n // Try to extract JSON from markdown code blocks\n if (content.includes('```json')) {\n const start = content.indexOf('```json') + 7;\n const end = content.indexOf('```', start);\n if (end > start) {\n jsonContent = content.substring(start, end).trim();\n }\n } else if (content.includes('```')) {\n const start = content.indexOf('```') + 3;\n const end = content.indexOf('```', start);\n if (end > start) {\n jsonContent = content.substring(start, end).trim();\n }\n }\n\n // Try to find JSON object by matching braces\n if (!jsonContent.startsWith('{') && !jsonContent.startsWith('[')) {\n const jsonMatch = jsonContent.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n jsonContent = jsonMatch[0];\n }\n }\n\n // Parse JSON\n const parsed = JSON.parse(jsonContent);\n // Support multiple formats: {skill: {...}} or direct skill object\n skillData = parsed.skill || parsed;\n\n // eslint-disable-next-line no-console\n console.log('Parsed skill from streamContent:', skillData);\n } catch (e) {\n // eslint-disable-next-line no-console\n console.error(\n 'Failed to parse skill from streamContent:',\n e,\n 'Content:',\n this.state.streamContent\n );\n }\n }\n\n // Fallback: check if skill is directly in DONE event (unlikely but possible)\n if (!skillData && data.skill) {\n skillData = data.skill;\n // eslint-disable-next-line no-console\n console.log('Using skill from DONE event:', skillData);\n }\n\n if (skillData) {\n this.setState({\n generating: false,\n parsingResult: false,\n generatedSkill: {\n skill: skillData,\n explanation: this.getLocaleValue('generateSuccess', 'Skill generated successfully'),\n },\n });\n } else {\n // eslint-disable-next-line no-console\n console.error('No skill found in DONE event or streamContent:', {\n data,\n streamContent: this.state.streamContent,\n });\n Message.error(\n this.getLocaleValue(\n 'generateFailed',\n 'Failed to generate skill: no skill data returned'\n )\n );\n this.setState({ generating: false, parsingResult: false });\n }\n }, 500);\n } else if (data.explanation && data.explanation.includes('失败')) {\n // Error response\n Message.error(data.explanation);\n this.setState({ generating: false, streaming: false, parsingResult: false });\n }\n };\n\n handleApplyGeneratedSkill = () => {\n const { generatedSkill } = this.state;\n\n if (!generatedSkill || !generatedSkill.skill) {\n return;\n }\n\n const skill = generatedSkill.skill;\n\n // Fill form with generated skill\n this.field.setValues({\n name: skill.name || '',\n description: skill.description || '',\n instruction: skill.instruction || skill.skillMd || '',\n });\n\n // Fill resources if any\n if (skill.resource && Object.keys(skill.resource).length > 0) {\n const resources = Object.values(skill.resource).map(resource => ({\n name: resource.name || '',\n type: resource.type || '',\n content: resource.content || '',\n metadata: resource.metadata || null,\n }));\n this.setState({\n resources,\n expandedKeys: resources.map((_, index) => String(index)),\n });\n }\n\n Message.success(\n generatedSkill.explanation ||\n this.getLocaleValue('generateSuccess', 'Skill generated successfully')\n );\n\n // Close dialog and reset\n this.setState({\n showAiGenerateDialog: false,\n backgroundInfo: '',\n generatedSkill: null,\n });\n };\n\n handleBackgroundInfoChange = value => {\n this.setState({ backgroundInfo: value });\n };\n\n handleShowAiGenerate = () => {\n this.setState({\n showAiGenerateDialog: true,\n backgroundInfo: '',\n generatedSkill: null,\n selectedMcpServer: null,\n mcpTools: [],\n selectedMcpTools: [],\n mcpToolSearchKeyword: '',\n thinkingContent: '',\n thinkingCollapsed: false,\n showInputCollapsed: false,\n inputTooltipVisible: false,\n thinkingTooltipVisible: false,\n parsingResult: false,\n streaming: false,\n streamContent: '',\n });\n this.loadMcpServers();\n };\n\n handleCloseAiGenerateDialog = () => {\n this.setState({\n showAiGenerateDialog: false,\n backgroundInfo: '',\n generatedSkill: null,\n selectedMcpServer: null,\n mcpTools: [],\n selectedMcpTools: [],\n mcpToolSearchKeyword: '',\n thinkingContent: '',\n thinkingCollapsed: false,\n showInputCollapsed: false,\n inputTooltipVisible: false,\n thinkingTooltipVisible: false,\n parsingResult: false,\n });\n };\n\n handleInputMouseEnter = e => {\n this.setState({\n inputTooltipVisible: true,\n inputTooltipPosition: { x: e.clientX, y: e.clientY },\n });\n };\n\n handleInputMouseMove = e => {\n if (this.state.inputTooltipVisible) {\n this.setState({\n inputTooltipPosition: { x: e.clientX, y: e.clientY },\n });\n }\n };\n\n handleInputMouseLeave = () => {\n this.setState({\n inputTooltipVisible: false,\n });\n };\n\n handleThinkingMouseEnter = e => {\n this.setState({\n thinkingTooltipVisible: true,\n thinkingTooltipPosition: { x: e.clientX, y: e.clientY },\n });\n };\n\n handleThinkingMouseMove = e => {\n if (this.state.thinkingTooltipVisible) {\n this.setState({\n thinkingTooltipPosition: { x: e.clientX, y: e.clientY },\n });\n }\n };\n\n handleThinkingMouseLeave = () => {\n this.setState({\n thinkingTooltipVisible: false,\n });\n };\n\n renderUserInput = () => {\n const { backgroundInfo, selectedMcpTools, showInputCollapsed } = this.state;\n\n if (!showInputCollapsed) {\n return null;\n }\n\n const selectedToolsText =\n selectedMcpTools.length > 0\n ? selectedMcpTools.map(t => t.name).join(', ')\n : this.getLocaleValue('noToolsSelected', 'No tools selected');\n\n const fullContent = `${this.getLocaleValue(\n 'backgroundInfo',\n 'Background Information'\n )}: ${backgroundInfo}\\n\\n${this.getLocaleValue(\n 'selectedTools',\n 'Selected Tools'\n )}: ${selectedToolsText}`;\n\n return (\n <div\n style={{\n marginBottom: 16,\n padding: '12px 16px',\n background: '#fafafa',\n borderRadius: 4,\n border: '1px solid #e6e6e6',\n cursor: 'pointer',\n }}\n onMouseEnter={this.handleInputMouseEnter}\n onMouseMove={this.handleInputMouseMove}\n onMouseLeave={this.handleInputMouseLeave}\n >\n <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>\n <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>\n <Icon type=\"info\" size=\"small\" style={{ color: '#1890ff' }} />\n <span style={{ fontSize: '13px', color: '#666' }}>\n {this.getLocaleValue('userInput', 'User Input')}\n </span>\n </div>\n <Icon type=\"arrow-up\" size=\"small\" style={{ color: '#999' }} />\n </div>\n <div\n style={{\n marginTop: 8,\n fontSize: '12px',\n color: '#999',\n maxHeight: 40,\n overflow: 'hidden',\n }}\n >\n {backgroundInfo.substring(0, 100)}\n {backgroundInfo.length > 100 ? '...' : ''}\n </div>\n </div>\n );\n };\n\n renderThinkingContent = () => {\n const { thinkingContent, thinkingCollapsed, streaming } = this.state;\n\n if (!streaming && !thinkingContent) {\n return null;\n }\n\n if (thinkingCollapsed && thinkingContent) {\n return (\n <div\n style={{\n marginBottom: 16,\n padding: '12px 16px',\n background: '#fafafa',\n borderRadius: 4,\n border: '1px solid #e6e6e6',\n cursor: 'pointer',\n }}\n onMouseEnter={this.handleThinkingMouseEnter}\n onMouseMove={this.handleThinkingMouseMove}\n onMouseLeave={this.handleThinkingMouseLeave}\n >\n <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>\n <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>\n <Tag type=\"normal\" size=\"small\">\n {this.getLocaleValue('thinking', 'Thinking')}\n </Tag>\n <span style={{ fontSize: '13px', color: '#666' }}>\n {this.getLocaleValue('thinkingProcess', 'Thinking Process')}\n </span>\n </div>\n <Icon type=\"arrow-up\" size=\"small\" style={{ color: '#999' }} />\n </div>\n <div\n style={{\n marginTop: 8,\n fontSize: '12px',\n color: '#999',\n maxHeight: 40,\n overflow: 'hidden',\n }}\n >\n {thinkingContent.substring(0, 100)}\n {thinkingContent.length > 100 ? '...' : ''}\n </div>\n </div>\n );\n }\n\n if (!thinkingCollapsed && thinkingContent) {\n return (\n <div style={{ marginBottom: 16 }}>\n <Collapse\n defaultExpandedKeys={['0']}\n style={{ border: '1px solid #e6e6e6', borderRadius: '4px' }}\n >\n <Panel\n key=\"0\"\n title={\n <div style={{ display: 'flex', alignItems: 'center' }}>\n <Tag type=\"normal\" size=\"small\" style={{ marginRight: 8 }}>\n {this.getLocaleValue('thinking', 'Thinking')}\n </Tag>\n <span>{this.getLocaleValue('thinkingProcess', 'Thinking Process')}</span>\n </div>\n }\n >\n <div\n style={{\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n margin: 0,\n fontSize: '13px',\n }}\n >\n {thinkingContent}\n </div>\n </Panel>\n </Collapse>\n </div>\n );\n }\n\n return null;\n };\n\n // 获取文本的前N行预览\n getPreviewLines = (text, lines = 2) => {\n if (!text) return '';\n const textLines = text.split('\\n');\n if (textLines.length <= lines) return text;\n return `${textLines.slice(0, lines).join('\\n')}\\n...`;\n };\n\n // 渲染可折叠内容区域(参考 SkillOptimizeDialog)\n renderCollapsibleSection = (\n title,\n content,\n isCollapsed,\n onToggle,\n icon = null,\n loading = false,\n thinkingContentForIcon = null\n ) => {\n if (!content && !loading) {\n return null;\n }\n\n const contentLines = content ? content.split('\\n') : [];\n const previewContent = isCollapsed && content ? this.getPreviewLines(content, 2) : content;\n const hasMore = isCollapsed && contentLines.length > 2;\n\n return (\n <div\n style={{\n border: '1px solid #e6e6e6',\n borderRadius: '4px',\n marginBottom: 16,\n overflow: 'hidden',\n }}\n >\n <div\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '12px 16px',\n background: '#fafafa',\n cursor: 'pointer',\n borderBottom: content || loading ? '1px solid #e6e6e6' : 'none',\n }}\n onClick={onToggle}\n >\n <div style={{ display: 'flex', alignItems: 'center', flex: 1 }}>\n {icon && (\n <div style={{ marginRight: 8, display: 'flex', alignItems: 'center' }}>{icon}</div>\n )}\n <span style={{ fontWeight: 500 }}>{title}</span>\n {thinkingContentForIcon && thinkingContentForIcon.trim() && (\n <span\n style={{ marginLeft: 8, display: 'inline-flex', alignItems: 'center' }}\n onClick={e => {\n e.stopPropagation();\n }}\n >\n <Balloon\n trigger={\n <Icon\n type=\"info\"\n size=\"small\"\n style={{\n color: '#1890ff',\n cursor: 'pointer',\n fontSize: '14px',\n }}\n />\n }\n triggerType=\"hover\"\n align=\"t\"\n style={{ maxWidth: '500px' }}\n >\n <div style={{ maxHeight: '300px', overflowY: 'auto' }}>\n <div style={{ fontWeight: 500, marginBottom: 8 }}>\n {this.getLocaleValue('thinking', 'Thinking')}\n </div>\n <pre\n style={{\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n margin: 0,\n fontSize: '12px',\n lineHeight: '1.6',\n }}\n >\n {thinkingContentForIcon}\n </pre>\n </div>\n </Balloon>\n </span>\n )}\n {loading && <Loading size=\"medium\" style={{ marginLeft: 8 }} />}\n </div>\n <Icon\n type={isCollapsed ? 'arrow-down' : 'arrow-up'}\n style={{ fontSize: 12, color: '#666' }}\n />\n </div>\n {(content || loading) && (\n <div\n style={{\n padding: '12px 16px',\n maxHeight: isCollapsed ? '60px' : 'none',\n overflow: isCollapsed ? 'hidden' : 'auto',\n background: '#fff',\n position: 'relative',\n }}\n >\n {loading && !content ? (\n <div style={{ display: 'flex', alignItems: 'center', color: '#999' }}>\n <Loading size=\"medium\" style={{ marginRight: 8 }} />\n <span>{title}</span>\n </div>\n ) : (\n <pre\n style={{\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n margin: 0,\n fontSize: '13px',\n lineHeight: '1.6',\n color: '#333',\n }}\n >\n {previewContent}\n </pre>\n )}\n {isCollapsed && hasMore && (\n <div\n style={{\n position: 'absolute',\n bottom: 0,\n left: 0,\n right: 0,\n height: '20px',\n background:\n 'linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%)',\n pointerEvents: 'none',\n }}\n />\n )}\n </div>\n )}\n </div>\n );\n };\n\n // 渲染流式内容(参考 SkillOptimizeDialog)\n renderStreamContent = () => {\n const {\n streamContent,\n thinkingContent,\n streamType,\n thinkingCollapsed,\n resultContentCollapsed,\n streaming,\n } = this.state;\n\n return (\n <div>\n {/* 思考内容区域 - 仅在 THINKING 阶段显示 */}\n {thinkingContent &&\n streamType === 'THINKING' &&\n this.renderCollapsibleSection(\n this.getLocaleValue('thinking', '思考') || '思考',\n thinkingContent,\n thinkingCollapsed,\n () => this.setState({ thinkingCollapsed: !thinkingCollapsed }),\n <Tag type=\"normal\" size=\"small\">\n {this.getLocaleValue('thinking', '思考') || '思考'}\n </Tag>,\n streaming && streamType === 'THINKING' && !thinkingContent\n )}\n\n {/* 结果内容区域 - 当开始接收模型回复时显示,包含思考内容图标 */}\n {streamContent &&\n streamType !== 'THINKING' &&\n this.renderCollapsibleSection(\n this.getLocaleValue('generatingContent', '生成内容') || '生成内容',\n streamContent,\n resultContentCollapsed,\n () => this.setState({ resultContentCollapsed: !resultContentCollapsed }),\n <Tag type=\"primary\" size=\"small\">\n {this.getLocaleValue('generatingContent', '生成内容') || '生成内容'}\n </Tag>,\n streaming && !streamContent,\n this.state.thinkingContent\n )}\n </div>\n );\n };\n\n renderGeneratedSkill = () => {\n const {\n generatedSkill,\n backgroundInfo,\n selectedMcpTools,\n thinkingContent,\n streamContent,\n inputSectionCollapsed,\n thinkingCollapsed,\n resultContentCollapsed,\n } = this.state;\n if (!generatedSkill || !generatedSkill.skill) {\n return null;\n }\n\n try {\n const skill = generatedSkill.skill;\n const resources = skill.resource ? Object.values(skill.resource) : [];\n\n // 构建用户输入内容\n const selectedToolsText =\n selectedMcpTools.length > 0\n ? selectedMcpTools.map(t => t.name).join(', ')\n : this.getLocaleValue('noToolsSelected', 'No tools selected');\n const userInputContent = `${this.getLocaleValue(\n 'backgroundInfo',\n 'Background Information'\n )}: ${backgroundInfo}\\n\\n${this.getLocaleValue(\n 'selectedTools',\n 'Selected Tools'\n )}: ${selectedToolsText}`;\n const userInputPreview = backgroundInfo\n ? backgroundInfo.substring(0, 100) + (backgroundInfo.length > 100 ? '...' : '')\n : '';\n\n return (\n <div>\n {/* 显示用户输入 - 折叠状态,可展开查看 */}\n {this.renderCollapsibleSection(\n this.getLocaleValue('userInput', 'User Input') || '用户输入',\n inputSectionCollapsed ? userInputPreview : userInputContent,\n inputSectionCollapsed,\n () => this.setState({ inputSectionCollapsed: !this.state.inputSectionCollapsed }),\n null,\n false\n )}\n\n {/* 思考内容 - 始终显示(如果存在) */}\n {thinkingContent &&\n this.renderCollapsibleSection(\n this.getLocaleValue('thinking', '思考') || '思考',\n thinkingContent,\n thinkingCollapsed,\n () => this.setState({ thinkingCollapsed: !thinkingCollapsed }),\n <Tag type=\"normal\" size=\"small\">\n {this.getLocaleValue('thinking', '思考') || '思考'}\n </Tag>,\n false\n )}\n\n {/* 结果内容 - 始终显示(如果存在) */}\n {streamContent &&\n this.renderCollapsibleSection(\n this.getLocaleValue('generatingContent', '生成内容') || '生成内容',\n streamContent,\n resultContentCollapsed,\n () => this.setState({ resultContentCollapsed: !resultContentCollapsed }),\n <Tag type=\"primary\" size=\"small\">\n {this.getLocaleValue('generatingContent', '生成内容') || '生成内容'}\n </Tag>,\n false,\n thinkingContent\n )}\n\n {/* 生成的Skill预览 */}\n <div style={{ marginTop: 24, paddingTop: 16, borderTop: '2px solid #e6e6e6' }}>\n <div\n style={{\n background: '#fff',\n padding: '20px',\n borderRadius: '4px',\n border: '1px solid #e6e6e6',\n }}\n >\n <Row gutter={16} style={{ marginBottom: 16 }}>\n <Col span={12}>\n <div style={{ marginBottom: 16 }}>\n <label\n style={{\n display: 'block',\n marginBottom: 8,\n fontSize: '14px',\n fontWeight: 500,\n color: '#333',\n }}\n >\n {this.getLocaleValue('skillName', 'Skill Name')}:\n </label>\n <span style={{ fontSize: '14px', color: '#666' }}>{skill.name || '--'}</span>\n </div>\n </Col>\n </Row>\n <Row gutter={16} style={{ marginBottom: 24 }}>\n <Col span={24}>\n <div style={{ marginBottom: 16 }}>\n <label\n style={{\n display: 'block',\n marginBottom: 8,\n fontSize: '14px',\n fontWeight: 500,\n color: '#333',\n }}\n >\n {this.getLocaleValue('description', 'Description')}:\n </label>\n <span style={{ fontSize: '14px', color: '#666' }}>\n {skill.description || '--'}\n </span>\n </div>\n </Col>\n </Row>\n\n <div style={{ marginBottom: 24 }}>\n <div\n style={{ fontSize: '14px', fontWeight: 500, marginBottom: '12px', color: '#333' }}\n >\n {this.getLocaleValue('instruction', 'Instruction')}\n </div>\n <div\n style={{\n minHeight: 200,\n padding: '12px',\n background: '#fafafa',\n borderRadius: 4,\n border: '1px solid #e6e6e6',\n }}\n >\n <MarkdownRenderer content={skill.instruction || ''} />\n </div>\n </div>\n\n <div>\n <div\n style={{ fontSize: '14px', fontWeight: 500, marginBottom: '12px', color: '#333' }}\n >\n {this.getLocaleValue('resources', 'Resources')}\n </div>\n {resources.length > 0 ? (\n <div>\n <Collapse>\n {resources.map((resource, index) => (\n <Panel\n key={String(index)}\n title={\n <div>\n <span>\n {resource.type && resource.name\n ? `${resource.type}/${resource.name}`\n : `${this.getLocaleValue('resource', 'Resource')} ${index + 1}`}\n </span>\n </div>\n }\n >\n <Row gutter={16}>\n <Col span={12}>\n <div style={{ marginBottom: 16 }}>\n <label\n style={{\n display: 'block',\n marginBottom: 8,\n fontSize: '13px',\n color: '#666',\n }}\n >\n {this.getLocaleValue('resourceName', 'Resource Name')}:\n </label>\n <span style={{ fontSize: '13px', color: '#333' }}>\n {resource.name || '--'}\n </span>\n </div>\n </Col>\n <Col span={12}>\n <div style={{ marginBottom: 16 }}>\n <label\n style={{\n display: 'block',\n marginBottom: 8,\n fontSize: '13px',\n color: '#666',\n }}\n >\n {this.getLocaleValue('resourceType', 'Resource Type')}:\n </label>\n <span>\n {resource.type ? (\n <Tag type=\"primary\" size=\"small\">\n {resource.type}\n </Tag>\n ) : (\n '--'\n )}\n </span>\n </div>\n </Col>\n </Row>\n <Row gutter={16}>\n <Col span={24}>\n <div>\n <label\n style={{\n display: 'block',\n marginBottom: 8,\n fontSize: '13px',\n color: '#666',\n }}\n >\n {this.getLocaleValue('resourceContent', 'Resource Content')}:\n </label>\n <div\n style={{\n border: '1px solid #e6e6e6',\n borderRadius: '4px',\n marginTop: '8px',\n }}\n >\n {resource.content ? (\n <MonacoEditor\n language={getLanguageFromFileName(resource.name || '')}\n width=\"100%\"\n height={300}\n value={resource.content}\n options={{\n readOnly: true,\n wordWrap: 'on',\n minimap: { enabled: false },\n lineNumbers: 'on',\n scrollBeyondLastLine: false,\n }}\n />\n ) : (\n <div\n style={{ padding: '12px', color: '#999', marginTop: '8px' }}\n >\n {this.getLocaleValue('noContent', 'No content')}\n </div>\n )}\n </div>\n </div>\n </Col>\n </Row>\n </Panel>\n ))}\n </Collapse>\n </div>\n ) : (\n <div\n style={{\n padding: '20px',\n textAlign: 'center',\n color: '#999',\n fontSize: '13px',\n }}\n >\n {this.getLocaleValue('noResources', 'No resources')}\n </div>\n )}\n </div>\n </div>\n </div>\n\n <div style={{ color: '#999', fontSize: '12px', marginTop: 16 }}>\n {this.getLocaleValue(\n 'applyGeneratedSkillHint',\n 'Click \"Apply\" to fill the form with the generated Skill'\n )}\n </div>\n </div>\n );\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error('Error rendering generated skill:', error, generatedSkill);\n return (\n <div\n style={{\n padding: '20px',\n color: '#f5222d',\n background: '#fff2f0',\n border: '1px solid #ffccc7',\n borderRadius: '4px',\n }}\n >\n <div style={{ marginBottom: 8, fontWeight: 500 }}>渲染生成的 Skill 时出错</div>\n <div style={{ fontSize: '12px' }}>{error.message || 'Unknown error'}</div>\n </div>\n );\n }\n };\n\n loadMcpServers = async () => {\n this.setState({ loadingMcpServers: true });\n try {\n const namespaceId = getParams('namespace') || 'public';\n const result = await request({\n url: 'v3/console/ai/mcp/list',\n method: 'get',\n data: {\n namespaceId,\n pageNo: 1,\n pageSize: 100,\n },\n });\n if (result.code === 0 && result.data) {\n // Map items to ensure id field exists (use name as id if id is not available)\n const servers = (result.data.pageItems || []).map(item => ({\n ...item,\n id: item.id || item.name,\n }));\n this.setState({\n mcpServers: servers,\n loadingMcpServers: false,\n });\n } else {\n this.setState({ loadingMcpServers: false });\n }\n } catch (error) {\n console.error('Failed to load MCP servers:', error);\n this.setState({ loadingMcpServers: false });\n }\n };\n\n handleMcpServerChange = async value => {\n this.setState({\n selectedMcpServer: value,\n mcpTools: [],\n selectedMcpTools: [],\n mcpToolSearchKeyword: '', // 重置搜索关键词\n });\n\n if (value) {\n await this.loadMcpTools(value);\n }\n };\n\n handleMcpToolSearchChange = value => {\n this.setState({\n mcpToolSearchKeyword: value,\n });\n };\n\n getFilteredMcpTools = () => {\n const { mcpTools, mcpToolSearchKeyword } = this.state;\n if (!mcpToolSearchKeyword.trim()) {\n return mcpTools;\n }\n const keyword = mcpToolSearchKeyword.toLowerCase();\n return mcpTools.filter(tool => tool.name && tool.name.toLowerCase().includes(keyword));\n };\n\n loadMcpTools = async mcpServerId => {\n this.setState({ loadingMcpTools: true });\n try {\n const namespaceId = getParams('namespace') || 'public';\n const result = await request({\n url: 'v3/console/ai/mcp',\n method: 'get',\n data: {\n namespaceId,\n mcpId: mcpServerId,\n },\n });\n if (result.code === 0 && result.data && result.data.toolSpec) {\n const tools = result.data.toolSpec.tools || [];\n this.setState({\n mcpTools: tools,\n loadingMcpTools: false,\n });\n } else {\n this.setState({ loadingMcpTools: false });\n }\n } catch (error) {\n console.error('Failed to load MCP tools:', error);\n this.setState({ loadingMcpTools: false });\n }\n };\n\n handleMcpToolChange = (checked, tool) => {\n const { selectedMcpTools } = this.state;\n if (checked) {\n this.setState({\n selectedMcpTools: [...selectedMcpTools, tool],\n });\n } else {\n this.setState({\n selectedMcpTools: selectedMcpTools.filter(t => t.name !== tool.name),\n });\n }\n };\n\n handleShowOptimizeDialog = () => {\n this.setState({ optimizeDialogVisible: true });\n };\n\n handleOptimizeDialogClose = () => {\n this.setState({ optimizeDialogVisible: false });\n };\n\n handleOptimizeSuccess = optimizedSkill => {\n const { locale = {} } = this.props;\n console.log('handleOptimizeSuccess called with:', optimizedSkill);\n\n // 优化成功后,将优化后的skill填充到表单中\n if (optimizedSkill) {\n // 处理资源数据,确保格式正确\n let resources = [];\n const resourceData = optimizedSkill.resource || optimizedSkill.resources || {};\n console.log('Resource data:', resourceData);\n\n if (resourceData && Object.keys(resourceData).length > 0) {\n // 使用Object.entries来同时获取key和value,因为Map的key可能是资源名称\n resources = Object.entries(resourceData).map(([resourceKey, resource]) => {\n // 确保资源对象包含所有必要字段\n // 如果resource对象没有name,使用resourceKey作为name\n const resourceObj = {\n name: resource.name || resourceKey || '',\n type: resource.type || '',\n content: resource.content || '',\n metadata: resource.metadata || null,\n };\n return resourceObj;\n });\n }\n\n console.log('Processed resources:', resources);\n\n // Important: Keep original skill name, don't use optimized name\n const originalName = this.field.getValue('name') || this.state.skillName;\n\n // 获取当前的原始数据用于比较(不要更新原始数据)\n const { originalFormData, originalResources } = this.state;\n\n // Update form fields with optimized values\n // Use optimized values directly, even if they're empty (to allow clearing fields)\n const optimizedDescription =\n optimizedSkill.description !== undefined\n ? optimizedSkill.description\n : this.field.getValue('description');\n const optimizedInstruction =\n optimizedSkill.instruction !== undefined\n ? optimizedSkill.instruction\n : this.field.getValue('instruction');\n\n this.field.setValues({\n name: originalName, // Always use original skill name\n description: optimizedDescription,\n instruction: optimizedInstruction,\n });\n\n // 比较优化后的数据与原始数据,判断是否有变化\n // 如果 originalFormData 或 originalResources 不存在,说明是新建模式,不需要比较\n let hasChanges = false;\n if (originalFormData && originalResources) {\n const formChanged =\n optimizedDescription !== (originalFormData.description || '') ||\n optimizedInstruction !== (originalFormData.instruction || '');\n const resourcesChanged = JSON.stringify(resources) !== JSON.stringify(originalResources);\n hasChanges = formChanged || resourcesChanged;\n } else {\n // 新建模式:如果有任何内容,就认为有变化\n hasChanges =\n optimizedDescription !== '' || optimizedInstruction !== '' || resources.length > 0;\n }\n\n // Force form to re-render by updating state\n // 重要:不要更新 originalFormData 和 originalResources,保持它们作为比较基准\n this.setState(\n {\n resources,\n expandedKeys: resources.map((_, index) => String(index)),\n currentSkillData: optimizedSkill,\n // 保持 originalFormData 和 originalResources 不变,用于后续比较\n hasChanges, // 根据比较结果设置变化状态\n },\n () => {\n // After state update, ensure form fields are refreshed\n // Force a re-render by calling setValue again if needed\n if (optimizedSkill.description !== undefined) {\n this.field.setValue('description', optimizedSkill.description);\n }\n if (optimizedSkill.instruction !== undefined) {\n this.field.setValue('instruction', optimizedSkill.instruction);\n }\n }\n );\n\n // 如果添加了新资源,显示提示信息\n if (resources.length > 0) {\n // All resources are considered new when applied from optimization\n const newResourcesCount = resources.length;\n if (newResourcesCount > 0) {\n Message.success(\n (locale.optimizeSuccess || 'Optimization applied successfully') +\n ` (${newResourcesCount} new resource(s) added)`\n );\n } else {\n Message.success(locale.optimizeSuccess || 'Optimization applied successfully');\n }\n } else {\n Message.success(locale.optimizeSuccess || 'Optimization applied successfully');\n }\n } else {\n Message.success(locale.optimizeSuccess || 'Optimization applied successfully');\n }\n\n this.handleOptimizeDialogClose();\n };\n\n // Helper function to get locale value with fallback\n getLocaleValue = (key, fallback) => {\n const { locale = {} } = this.props;\n return locale[key] || fallback;\n };\n\n // Generate resource unique identifier\n // Format: \"type::name\" if type is not blank, otherwise \"name\"\n // The separator \"::\" is used because it's not in the allowed character set for type and name\n getResourceIdentifier = resource => {\n if (resource.type && resource.type.trim() !== '') {\n return `${resource.type}::${resource.name || ''}`;\n }\n return resource.name || '';\n };\n\n // Build preview data from current form values\n buildPreviewData = () => {\n const values = this.field.getValues();\n const { resources } = this.state;\n\n // 构建 skillCard 对象(与 handleSubmit 中的逻辑一致)\n const skillCard = {\n name: values.name || '',\n description: values.description || '',\n instruction: values.instruction || '',\n };\n\n // 构建 resource Map,过滤掉无效的资源(没有 name 或 name 为空的资源)\n if (resources && resources.length > 0) {\n const resourceMap = {};\n resources.forEach((resource, index) => {\n // 只包含有效的资源(有 name 且 name 不为空)\n if (resource.name && resource.name.trim() !== '') {\n const key = resource.name.trim();\n resourceMap[key] = {\n name: resource.name.trim(),\n type: resource.type || '',\n content: resource.content || '',\n metadata: resource.metadata || null,\n };\n }\n });\n skillCard.resource = resourceMap;\n } else {\n skillCard.resource = {};\n }\n\n return skillCard;\n };\n\n // Update file tree when resources change\n updateEditModeFileTree = callback => {\n const previewData = this.buildPreviewData();\n const fileTree = this.buildFileTree(previewData);\n this.setState({ editModeFileTree: fileTree }, () => {\n if (callback && typeof callback === 'function') {\n callback(fileTree);\n }\n });\n };\n\n // Check if data has been modified\n hasDataChanged = () => {\n const { originalFormData, originalResources } = this.state;\n if (!originalFormData || !originalResources) {\n return false;\n }\n\n const currentValues = this.field.getValues();\n\n // Check form data changes\n const formChanged =\n currentValues.name !== originalFormData.name ||\n currentValues.description !== originalFormData.description ||\n currentValues.instruction !== originalFormData.instruction;\n\n // Check resources changes\n const currentResources = this.state.resources || [];\n const resourcesChanged = JSON.stringify(currentResources) !== JSON.stringify(originalResources);\n\n return formChanged || resourcesChanged;\n };\n\n // Build file tree structure; type may contain \"/\" for multi-level folders (e.g. folder1/folder2)\n buildFileTree = previewData => {\n if (!previewData || !previewData.name) {\n return null;\n }\n\n const fileList = [\n {\n name: 'SKILL.md',\n type: 'file',\n fileType: 'skill-md',\n },\n ];\n\n const rootChildren = [];\n const resourcesWithoutType = [];\n\n const getOrCreateFolder = (children, folderName, level = 0) => {\n let folder = children.find(n => n.type === 'folder' && n.name === folderName);\n if (!folder) {\n // Default expand folders up to level 2 (0-indexed: 0, 1, 2 = 3 levels)\n folder = { name: folderName, type: 'folder', children: [], expanded: level < 3 };\n children.push(folder);\n }\n return { children: folder.children, level: level + 1 };\n };\n\n const sortNodeChildren = nodes => {\n nodes.sort((a, b) =>\n (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' })\n );\n nodes.forEach(n => {\n if (n.type === 'folder' && n.children && n.children.length) {\n sortNodeChildren(n.children);\n }\n });\n };\n\n if (previewData.resource && Object.keys(previewData.resource).length > 0) {\n Object.entries(previewData.resource).forEach(([key, resource]) => {\n const fileNode = {\n name: resource.name || key,\n type: 'file',\n fileType: 'resource',\n resourceKey: key,\n resource: resource,\n };\n if (!resource.type || resource.type.trim() === '') {\n resourcesWithoutType.push(fileNode);\n } else {\n const pathSegments = resource.type\n .trim()\n .split('/')\n .filter(Boolean);\n let target = { children: rootChildren, level: 0 };\n for (const seg of pathSegments) {\n target = getOrCreateFolder(target.children, seg, target.level);\n }\n target.children.push(fileNode);\n }\n });\n }\n\n sortNodeChildren(rootChildren);\n const sortedRootFiles = resourcesWithoutType.sort((a, b) =>\n (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' })\n );\n const afterSkill = [...rootChildren, ...sortedRootFiles].sort((a, b) =>\n (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' })\n );\n fileList.push(...afterSkill);\n\n return fileList;\n };\n\n // Escape YAML value (handle special characters)\n escapeYamlValue = value => {\n if (!value) {\n return '';\n }\n // If value contains special characters, wrap in quotes\n if (value.includes(':') || value.includes('\"') || value.includes(\"'\") || value.includes('\\n')) {\n return `\"${value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n }\n return value;\n };\n\n // Build SKILL.md content\n buildSkillMarkdown = previewData => {\n if (!previewData) {\n return '';\n }\n\n let markdown = '---\\n';\n markdown += `name: ${this.escapeYamlValue(previewData.name || '')}\\n`;\n markdown += `description: ${this.escapeYamlValue(previewData.description || '')}\\n`;\n markdown += '---\\n\\n';\n\n // Instructions section - directly show instruction content without \"## Instructions\" header\n if (previewData.instruction && previewData.instruction.trim() !== '') {\n markdown += `${previewData.instruction}\\n`;\n }\n\n return markdown;\n };\n\n handleEditModeFileClick = (file, e) => {\n if (e) {\n e.preventDefault();\n e.stopPropagation();\n }\n // Clear editModeSelectedFile first to force MonacoEditor to unmount, then set new file\n this.setState({ editModeSelectedFile: null }, () => {\n setTimeout(() => {\n this.setState({ editModeSelectedFile: file });\n }, 0);\n });\n };\n\n // 开始编辑文件名\n handleStartEditFileName = (node, e) => {\n if (e) {\n e.preventDefault();\n e.stopPropagation();\n }\n // SKILL.md 不能被编辑\n if (node.fileType === 'skill-md' || node.name === 'SKILL.md') {\n return;\n }\n // 只有资源文件可以编辑\n if (node.fileType === 'resource') {\n this.setState({\n editingFileName: {\n nodeKey: node.resourceKey || node.name,\n oldName: node.name,\n type: node.resource?.type || '',\n },\n editingFileNameValue: node.name, // 初始化编辑值\n });\n }\n };\n\n // 更新正在编辑的文件名临时值\n handleEditingFileNameChange = value => {\n // 过滤文件名:只允许英文大小写、数字、点号、下划线、横杠\n const filteredValue = value.replace(/[^a-zA-Z0-9._-]/g, '');\n this.setState({ editingFileNameValue: filteredValue });\n };\n\n // 保存文件名修改\n handleSaveFileName = newName => {\n const { editingFileName, resources } = this.state;\n if (!editingFileName) {\n this.setState({ editingFileName: null, editingFileNameValue: '' });\n return;\n }\n\n // 使用传入的 newName 或当前编辑值\n const nameToSave = newName || this.state.editingFileNameValue || editingFileName.oldName;\n if (!nameToSave || nameToSave.trim() === '') {\n this.setState({ editingFileName: null, editingFileNameValue: '' });\n return;\n }\n\n // 过滤文件名:只允许英文大小写、数字、点号、下划线、横杠\n const filteredName = nameToSave.replace(/[^a-zA-Z0-9._-]/g, '');\n\n if (filteredName === editingFileName.oldName) {\n // 没有变化,取消编辑\n this.setState({ editingFileName: null, editingFileNameValue: '' });\n return;\n }\n\n // 检查是否重名\n const isDuplicate = resources.some(\n r =>\n r.name === filteredName &&\n r.type === editingFileName.type &&\n r.name !== editingFileName.oldName\n );\n\n if (isDuplicate) {\n const { locale = {} } = this.props;\n Message.warning(locale.fileNameDuplicate || 'File name already exists');\n return;\n }\n\n // 更新资源名称\n const resourceIndex = resources.findIndex(\n r => r.name === editingFileName.oldName && r.type === editingFileName.type\n );\n\n if (resourceIndex !== -1) {\n const newResources = [...resources];\n newResources[resourceIndex] = {\n ...newResources[resourceIndex],\n name: filteredName,\n };\n this.setState(\n {\n resources: newResources,\n editingFileName: null,\n editingFileNameValue: '',\n },\n () => {\n // 更新文件树和变化状态\n if (this.state.isEdit) {\n // 使用 callback 确保文件树更新完成后再查找新文件\n this.updateEditModeFileTree(fileTree => {\n // 更新选中的文件,使其指向新的文件名\n const { editModeSelectedFile } = this.state;\n if (\n editModeSelectedFile &&\n editModeSelectedFile.fileType === 'resource' &&\n editModeSelectedFile.resourceKey === editingFileName.oldName\n ) {\n if (fileTree && Array.isArray(fileTree)) {\n // 递归查找文件\n const findFileInTree = tree => {\n if (Array.isArray(tree)) {\n for (const node of tree) {\n if (\n node.fileType === 'resource' &&\n node.resourceKey === filteredName &&\n node.resource?.type === editingFileName.type\n ) {\n return node;\n }\n if (node.children) {\n const found = findFileInTree(node.children);\n if (found) return found;\n }\n }\n }\n return null;\n };\n\n const newFile = findFileInTree(fileTree);\n if (newFile) {\n this.setState({ editModeSelectedFile: newFile });\n } else {\n // 如果找不到,清空选中状态\n this.setState({ editModeSelectedFile: null });\n }\n }\n }\n\n const hasChanged = this.hasDataChanged();\n if (hasChanged !== this.state.hasChanges) {\n this.setState({ hasChanges: hasChanged });\n }\n });\n }\n }\n );\n } else {\n this.setState({ editingFileName: null, editingFileNameValue: '' });\n }\n };\n\n // 取消编辑文件名\n handleCancelEditFileName = () => {\n this.setState({ editingFileName: null, editingFileNameValue: '' });\n };\n\n // 在文件树中新增文件(支持指定目标文件夹)\n handleAddFileInTree = folderType => {\n // 确定目标文件夹路径\n let targetFolder = '';\n if (typeof folderType === 'string') {\n // 从文件夹按钮直接传入的路径\n targetFolder = folderType;\n } else if (\n this.state.editModeSelectedFile &&\n this.state.editModeSelectedFile.fileType === 'resource'\n ) {\n // 从当前选中文件推断文件夹\n targetFolder = this.state.editModeSelectedFile.resource?.type || '';\n }\n\n const newResource = {\n name: `new_file_${Date.now()}`,\n type: targetFolder,\n content: '',\n metadata: null,\n };\n\n const newResources = [...this.state.resources, newResource];\n\n // 递归查找文件树中的文件\n const findFileInTree = nodes => {\n if (!nodes || !Array.isArray(nodes)) return null;\n for (const node of nodes) {\n if (node.fileType === 'resource' && node.resourceKey === newResource.name) {\n return node;\n }\n if (node.type === 'folder' && node.children) {\n const found = findFileInTree(node.children);\n if (found) return found;\n }\n }\n return null;\n };\n\n this.setState(\n {\n resources: newResources,\n },\n () => {\n // 更新文件树\n if (this.state.isEdit) {\n this.updateEditModeFileTree(fileTree => {\n // 选中新文件并进入编辑模式(递归查找)\n if (fileTree && Array.isArray(fileTree)) {\n const newFile = findFileInTree(fileTree);\n if (newFile) {\n this.setState({ editModeSelectedFile: newFile });\n // 自动开始编辑文件名\n setTimeout(() => {\n this.setState({\n editingFileName: {\n nodeKey: newResource.name,\n oldName: newResource.name,\n type: targetFolder,\n },\n editingFileNameValue: newResource.name,\n });\n }, 100);\n }\n }\n const hasChanged = this.hasDataChanged();\n if (hasChanged !== this.state.hasChanges) {\n this.setState({ hasChanges: hasChanged });\n }\n });\n }\n }\n );\n };\n\n // 拖拽开始\n handleDragStart = (node, e) => {\n if (node.fileType === 'resource' && node.name !== 'SKILL.md') {\n e.dataTransfer.effectAllowed = 'move';\n e.dataTransfer.setData(\n 'text/plain',\n JSON.stringify({\n resourceKey: node.resourceKey || node.name,\n name: node.name,\n type: node.resource?.type || '',\n })\n );\n this.setState({\n draggingFile: {\n resourceKey: node.resourceKey || node.name,\n name: node.name,\n type: node.resource?.type || '',\n },\n });\n } else {\n e.preventDefault();\n }\n };\n\n // 拖拽结束\n handleDragEnd = () => {\n this.setState({\n draggingFile: null,\n dragOverFolder: null,\n });\n };\n\n // 拖拽悬停在文件夹上\n handleDragOver = (folderName, e) => {\n e.preventDefault();\n e.stopPropagation();\n e.dataTransfer.dropEffect = 'move';\n if (this.state.dragOverFolder !== folderName) {\n this.setState({ dragOverFolder: folderName });\n }\n };\n\n // 拖拽离开文件夹\n handleDragLeave = e => {\n e.preventDefault();\n e.stopPropagation();\n // 只有当真正离开文件夹区域时才清除状态\n const relatedTarget = e.relatedTarget;\n if (!relatedTarget || !e.currentTarget.contains(relatedTarget)) {\n this.setState({ dragOverFolder: null });\n }\n };\n\n // 文件拖放到文件夹\n handleDrop = (folderName, e) => {\n e.preventDefault();\n e.stopPropagation();\n\n const { draggingFile, resources } = this.state;\n if (!draggingFile) {\n this.setState({ dragOverFolder: null });\n return;\n }\n\n // 找到要移动的资源\n const resourceIndex = resources.findIndex(\n r => r.name === draggingFile.name && r.type === draggingFile.type\n );\n\n if (resourceIndex !== -1) {\n const newResources = [...resources];\n // 更新资源的 type 为文件夹名称\n newResources[resourceIndex] = {\n ...newResources[resourceIndex],\n type: folderName,\n };\n\n this.setState(\n {\n resources: newResources,\n draggingFile: null,\n dragOverFolder: null,\n },\n () => {\n // 更新文件树和变化状态\n if (this.state.isEdit) {\n this.updateEditModeFileTree();\n const hasChanged = this.hasDataChanged();\n if (hasChanged !== this.state.hasChanges) {\n this.setState({ hasChanges: hasChanged });\n }\n }\n }\n );\n } else {\n this.setState({ draggingFile: null, dragOverFolder: null });\n }\n };\n\n toggleFolderExpanded = (fileTree, targetKey) => {\n if (!fileTree || !Array.isArray(fileTree)) {\n return fileTree;\n }\n\n const toggleNode = (nodes, currentKey = '') => {\n return nodes.map(node => {\n const nodeKey = node.resourceKey\n ? `${currentKey}/${node.resourceKey}`\n : currentKey\n ? `${currentKey}/${node.name}`\n : node.name;\n\n if (nodeKey === targetKey && node.type === 'folder') {\n return { ...node, expanded: !node.expanded };\n }\n\n if (node.type === 'folder' && node.children) {\n return {\n ...node,\n children: toggleNode(node.children, nodeKey),\n };\n }\n\n return node;\n });\n };\n\n return toggleNode(fileTree);\n };\n\n renderFileTree = (fileList, level = 0, parentKey = '', isEditMode = false) => {\n if (!fileList) {\n return null;\n }\n\n // If fileList is an array, render each item\n if (Array.isArray(fileList)) {\n return fileList.map(node => this.renderFileTree(node, level, parentKey, isEditMode));\n }\n\n // If it's a single node\n const node = fileList;\n const nodeKey = node.resourceKey\n ? `${parentKey}/${node.resourceKey}`\n : parentKey\n ? `${parentKey}/${node.name}`\n : node.name;\n\n const selectedFile = this.state.editModeSelectedFile;\n const isSelected =\n selectedFile &&\n selectedFile.name === node.name &&\n selectedFile.fileType === node.fileType &&\n selectedFile.resourceKey === node.resourceKey;\n\n if (node.type === 'folder') {\n const { dragOverFolder, draggingFile } = this.state;\n const isDragOver = dragOverFolder === node.name && draggingFile;\n const isExpanded = node.expanded !== false; // Default to true if not set\n const folderStyle = {\n paddingLeft: level === 0 ? '8px' : `${level * 20 + 8}px`,\n paddingTop: '8px',\n paddingBottom: '6px',\n paddingRight: '12px',\n marginTop: level === 0 ? '8px' : '12px',\n marginBottom: '4px',\n display: 'flex',\n alignItems: 'center',\n fontWeight: 600,\n color: '#666',\n fontSize: '13px',\n backgroundColor: isDragOver ? '#e6f7ff' : 'transparent',\n borderRadius: 4,\n transition: 'background-color 0.2s',\n cursor: 'pointer',\n };\n\n const toggleExpand = e => {\n e.stopPropagation();\n const updatedTree = this.toggleFolderExpanded(\n isEditMode ? this.state.editModeFileTree : this.state.fileTree,\n nodeKey\n );\n if (isEditMode) {\n this.setState({ editModeFileTree: updatedTree });\n } else {\n this.setState({ fileTree: updatedTree });\n }\n };\n\n return (\n <div key={nodeKey} className=\"file-tree-folder\">\n <div\n className=\"file-tree-item file-tree-folder-item\"\n style={folderStyle}\n onClick={toggleExpand}\n onDragOver={isEditMode ? e => this.handleDragOver(node.name, e) : undefined}\n onDragLeave={isEditMode ? this.handleDragLeave : undefined}\n onDrop={isEditMode ? e => this.handleDrop(node.name, e) : undefined}\n >\n <Icon\n type={isExpanded ? 'arrow-down' : 'arrow-right'}\n style={{ marginRight: 4, fontSize: 12, color: '#999' }}\n />\n <Icon type=\"folder\" style={{ marginRight: 8, color: '#666' }} />\n <span style={{ fontWeight: 600, flex: 1 }}>{node.name}</span>\n {isEditMode && (\n <Icon\n type=\"add\"\n size=\"xs\"\n style={{ color: '#999', cursor: 'pointer', marginLeft: 4, flexShrink: 0 }}\n onClick={e => {\n e.stopPropagation();\n this.handleAddFileInTree(nodeKey);\n }}\n />\n )}\n </div>\n {isExpanded &&\n node.children &&\n node.children.map(child => this.renderFileTree(child, level + 1, nodeKey, isEditMode))}\n </div>\n );\n } else {\n const itemStyle = {\n paddingLeft: level === 0 ? '8px' : `${level * 20 + 8}px`,\n paddingTop: '10px',\n paddingBottom: '10px',\n paddingRight: '12px',\n cursor: 'pointer',\n backgroundColor: isSelected ? '#e6f7ff' : 'transparent',\n color: isSelected ? '#1890ff' : '#333',\n fontWeight: isSelected ? 500 : 'normal',\n borderRadius: 4,\n margin: '2px 4px',\n display: 'flex',\n alignItems: 'center',\n };\n\n const { editingFileName, draggingFile } = this.state;\n const isEditing =\n editingFileName &&\n editingFileName.nodeKey === (node.resourceKey || node.name) &&\n editingFileName.oldName === node.name;\n const canEdit = isEditMode && node.fileType === 'resource' && node.name !== 'SKILL.md';\n const isDragging =\n draggingFile &&\n draggingFile.resourceKey === (node.resourceKey || node.name) &&\n draggingFile.name === node.name;\n\n const row = (\n <div\n key={nodeKey}\n className={`file-tree-item file-tree-file-item ${isSelected ? 'selected' : ''}`}\n style={{\n ...itemStyle,\n opacity: isDragging ? 0.5 : 1,\n cursor: canEdit ? 'move' : 'pointer',\n }}\n draggable={canEdit && !isEditing}\n onDragStart={canEdit && !isEditing ? e => this.handleDragStart(node, e) : undefined}\n onDragEnd={canEdit ? this.handleDragEnd : undefined}\n onClick={e => {\n e.preventDefault();\n e.stopPropagation();\n if (isEditMode && !isEditing) {\n this.handleEditModeFileClick(node, e);\n }\n }}\n onDoubleClick={e => {\n e.preventDefault();\n e.stopPropagation();\n if (canEdit) {\n this.handleStartEditFileName(node, e);\n }\n }}\n >\n <Icon type=\"file\" style={{ marginRight: 8 }} />\n {isEditing ? (\n <Input\n size=\"small\"\n value={this.state.editingFileNameValue}\n autoFocus\n style={{ flex: 1, marginRight: 4 }}\n onChange={value => {\n this.handleEditingFileNameChange(value);\n }}\n onBlur={e => {\n this.handleSaveFileName(e.target.value);\n }}\n onPressEnter={e => {\n this.handleSaveFileName(e.target.value);\n }}\n onKeyDown={e => {\n if (e.key === 'Escape') {\n this.handleCancelEditFileName();\n }\n }}\n onClick={e => {\n e.stopPropagation();\n }}\n />\n ) : (\n <span\n style={{\n pointerEvents: 'none',\n flex: 1,\n minWidth: 0,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n }}\n >\n {node.name}\n </span>\n )}\n </div>\n );\n if (!isEditing) {\n return (\n <Balloon\n trigger={row}\n triggerType=\"hover\"\n delay={100}\n align=\"tl\"\n closable={false}\n popupStyle={{\n padding: '4px 8px',\n fontSize: 12,\n }}\n >\n <span style={{ whiteSpace: 'nowrap' }}>{node.name}</span>\n </Balloon>\n );\n }\n return row;\n }\n };\n\n renderEditModeFileContent = () => {\n const { editModeSelectedFile } = this.state;\n const values = this.field.getValues();\n const { resources } = this.state;\n\n if (!editModeSelectedFile) {\n return (\n <div\n className=\"file-content-empty\"\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n height: '100%',\n color: '#999',\n fontSize: '14px',\n }}\n >\n {this.getLocaleValue('selectFileToEdit', 'Select a file to edit')}\n </div>\n );\n }\n\n if (editModeSelectedFile.fileType === 'skill-md') {\n const markdown = this.buildSkillMarkdown(this.buildPreviewData());\n return (\n <div\n className=\"file-content\"\n style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}\n >\n <div\n className=\"file-content-header\"\n style={{\n padding: '12px 16px',\n borderBottom: '1px solid #e6e6e6',\n display: 'flex',\n alignItems: 'center',\n fontWeight: 500,\n background: '#fafafa',\n }}\n >\n <Icon type=\"file\" style={{ marginRight: 8 }} />\n <span>{editModeSelectedFile.name}</span>\n </div>\n <div style={{ flex: 1, overflow: 'hidden', padding: '16px' }}>\n <div style={{ border: '1px solid #d9d9d9', borderRadius: '4px', height: '100%' }}>\n <MonacoEditor\n language=\"markdown\"\n width=\"100%\"\n height=\"100%\"\n value={values.instruction || ''}\n onChange={value => {\n this.field.setValue('instruction', value);\n // 更新文件树和变化状态\n setTimeout(() => {\n if (this.state.isEdit) {\n this.updateEditModeFileTree();\n // 检查是否有变化\n const hasChanged = this.hasDataChanged();\n if (hasChanged !== this.state.hasChanges) {\n this.setState({ hasChanges: hasChanged });\n }\n }\n }, 0);\n }}\n options={{\n wordWrap: 'on',\n minimap: { enabled: false },\n lineNumbers: 'on',\n scrollBeyondLastLine: false,\n }}\n />\n </div>\n </div>\n </div>\n );\n } else if (editModeSelectedFile.fileType === 'resource') {\n const resource = editModeSelectedFile.resource;\n // 找到对应的资源索引\n const resourceIndex = resources.findIndex(r => {\n if (editModeSelectedFile.resourceKey) {\n return r.name === editModeSelectedFile.resourceKey;\n }\n return r.name === resource.name && r.type === resource.type;\n });\n\n if (resourceIndex === -1) {\n return (\n <div\n className=\"file-content-empty\"\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n height: '100%',\n color: '#999',\n fontSize: '14px',\n }}\n >\n {this.getLocaleValue('resourceNotFound', 'Resource not found')}\n </div>\n );\n }\n\n const currentResource = resources[resourceIndex];\n return (\n <div\n className=\"file-content\"\n style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}\n >\n <div\n className=\"file-content-header\"\n style={{\n padding: '12px 16px',\n borderBottom: '1px solid #e6e6e6',\n display: 'flex',\n alignItems: 'center',\n fontWeight: 500,\n background: '#fafafa',\n }}\n >\n <Icon type=\"file\" style={{ marginRight: 8 }} />\n <span>{editModeSelectedFile.name}</span>\n </div>\n <div style={{ flex: 1, overflow: 'hidden', padding: '16px' }}>\n <div style={{ border: '1px solid #d9d9d9', borderRadius: '4px', height: '100%' }}>\n <MonacoEditor\n key={`${editModeSelectedFile.resourceKey ||\n editModeSelectedFile.name}-${getLanguageFromFileName(\n currentResource.name || ''\n )}`}\n language={getLanguageFromFileName(currentResource.name || '')}\n width=\"100%\"\n height=\"100%\"\n value={currentResource.content || ''}\n onChange={value => {\n this.handleResourceChange(resourceIndex, 'content', value);\n }}\n options={{\n wordWrap: 'on',\n minimap: { enabled: false },\n lineNumbers: 'on',\n scrollBeyondLastLine: false,\n }}\n />\n </div>\n </div>\n </div>\n );\n }\n\n return null;\n };\n\n render() {\n const { locale = {} } = this.props;\n const {\n loading,\n generating,\n isEdit,\n isDraft,\n resources,\n expandedKeys,\n showAiGenerateInput,\n } = this.state;\n\n const formItemLayout = {\n labelCol: { span: 3 },\n wrapperCol: { span: 20 },\n };\n\n // 检查是否有数据变化(使用state中的值,避免每次render都计算)\n const hasChanged = isEdit ? this.state.hasChanges : false;\n\n return (\n <div className=\"new-skill-container\">\n <div\n className=\"page-title\"\n style={{\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n marginBottom: 8,\n marginTop: 8,\n }}\n >\n <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>\n <span style={{ fontSize: 28, height: 40, fontWeight: 500 }}>\n {isEdit\n ? isDraft\n ? this.getLocaleValue('editDraft', 'Edit Draft')\n : this.getLocaleValue('editSkill', 'Edit Skill')\n : this.getLocaleValue('createSkill', 'Create Skill')}\n </span>\n <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>\n {!isEdit && localStorage.getItem(COPILOT_ENABLED) === 'true' && (\n <Button type=\"primary\" onClick={this.handleShowAiGenerate}>\n <MagicWandIcon size={16} style={{ marginRight: 4, verticalAlign: 'middle' }} />{' '}\n {this.getLocaleValue('aiGenerate', 'AI生成')}\n </Button>\n )}\n {isEdit && localStorage.getItem(COPILOT_ENABLED) === 'true' && (\n <Button onClick={this.handleShowOptimizeDialog}>\n <MagicWandIcon size={16} style={{ marginRight: 4, verticalAlign: 'middle' }} />{' '}\n {this.getLocaleValue('aiOptimize', 'AI 优化')}\n </Button>\n )}\n {isEdit && (\n <>\n <Button\n type=\"primary\"\n onClick={this.handleSubmit}\n loading={loading}\n disabled={!hasChanged}\n >\n {this.getLocaleValue('update', 'Update')}\n </Button>\n <Button warning onClick={this.handleGoBack} disabled={!hasChanged}>\n {this.getLocaleValue('cancel', 'Cancel')}\n </Button>\n </>\n )}\n </div>\n </div>\n {!isEdit && (\n <div>\n <Button onClick={this.handleGoBack}>{this.getLocaleValue('cancel', 'Cancel')}</Button>\n </div>\n )}\n </div>\n\n {isEdit ? (\n // 编辑模式:使用文件目录形式展示(与查看详情页面一致)\n <div\n className=\"skill-detail-container\"\n style={{\n background: '#fff',\n borderRadius: '4px',\n border: '1px solid #e6e6e6',\n height: 'calc(100vh - 200px)',\n display: 'flex',\n overflow: 'hidden',\n }}\n >\n <div\n className=\"skill-detail-sidebar\"\n style={{\n width: '300px',\n borderRight: '1px solid #e6e6e6',\n display: 'flex',\n flexDirection: 'column',\n background: '#fafafa',\n }}\n >\n <div\n className=\"skill-detail-sidebar-header\"\n style={{\n padding: '12px 16px',\n borderBottom: '1px solid #e6e6e6',\n fontWeight: 500,\n background: '#fff',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n }}\n >\n <span>\n {this.field.getValue('name') || this.getLocaleValue('projectFiles', '项目文件')}\n </span>\n <Button\n size=\"small\"\n type=\"primary\"\n text\n onClick={this.handleAddFileInTree}\n style={{ padding: '0 4px' }}\n title={this.getLocaleValue('addFile', 'Add File')}\n >\n <Icon type=\"add\" size=\"small\" />\n </Button>\n </div>\n <div\n className=\"skill-detail-file-tree\"\n style={{ flex: 1, overflowY: 'auto', padding: '8px 0' }}\n onDragOver={e => {\n e.preventDefault();\n e.stopPropagation();\n if (this.state.draggingFile) {\n e.dataTransfer.dropEffect = 'move';\n if (this.state.dragOverFolder !== '') {\n this.setState({ dragOverFolder: '' });\n }\n }\n }}\n onDrop={e => {\n e.preventDefault();\n e.stopPropagation();\n const { draggingFile, resources } = this.state;\n if (!draggingFile) return;\n\n const resourceIndex = resources.findIndex(\n r => r.name === draggingFile.name && r.type === draggingFile.type\n );\n\n if (resourceIndex !== -1) {\n const newResources = [...resources];\n newResources[resourceIndex] = {\n ...newResources[resourceIndex],\n type: '', // 拖到根目录,清空 type\n };\n this.setState(\n {\n resources: newResources,\n draggingFile: null,\n dragOverFolder: null,\n },\n () => {\n if (this.state.isEdit) {\n this.updateEditModeFileTree();\n const hasChanged = this.hasDataChanged();\n if (hasChanged !== this.state.hasChanges) {\n this.setState({ hasChanges: hasChanged });\n }\n }\n }\n );\n }\n }}\n >\n {this.state.editModeFileTree ? (\n this.renderFileTree(this.state.editModeFileTree, 0, '', true)\n ) : (\n <div\n className=\"file-tree-empty\"\n style={{\n padding: '40px 20px',\n textAlign: 'center',\n color: '#999',\n fontSize: '13px',\n }}\n >\n {this.getLocaleValue('noPreviewData', 'No preview data available')}\n </div>\n )}\n </div>\n </div>\n <div\n className=\"skill-detail-content-area\"\n style={{\n flex: 1,\n display: 'flex',\n flexDirection: 'column',\n overflow: 'hidden',\n background: '#fff',\n }}\n >\n {this.renderEditModeFileContent()}\n </div>\n </div>\n ) : (\n // 创建模式:使用传统表单形式\n <div\n style={{\n background: '#fff',\n padding: '20px',\n borderRadius: '4px',\n border: '1px solid #e6e6e6',\n }}\n >\n <Form field={this.field} {...formItemLayout} className=\"new-skill-form\">\n <Form.Item\n label={this.getLocaleValue('skillName', 'Skill Name')}\n required\n validator={this.validateSkillName}\n >\n <Input\n name=\"name\"\n placeholder={this.getLocaleValue(\n 'skillNamePlaceholder',\n 'Please enter Skill name (letters, underscore, hyphen only)'\n )}\n disabled={isEdit}\n maxLength={255}\n onChange={this.handleSkillNameChange}\n />\n </Form.Item>\n\n <Form.Item label={this.getLocaleValue('description', 'Description')}>\n <Input.TextArea\n name=\"description\"\n placeholder={this.getLocaleValue(\n 'descriptionPlaceholder',\n 'Please enter Skill description'\n )}\n rows={3}\n maxLength={1000}\n />\n </Form.Item>\n\n <Form.Item\n label={this.getLocaleValue('instruction', 'Instruction')}\n required\n validator={this.validateRequired}\n >\n <div\n style={{ border: '1px solid #d9d9d9', borderRadius: '4px', minHeight: '400px' }}\n >\n <MonacoEditor\n language=\"markdown\"\n width=\"100%\"\n height={400}\n value={this.field.getValue('instruction') || ''}\n onChange={value => {\n this.field.setValue('instruction', value);\n }}\n options={{\n wordWrap: 'on',\n minimap: { enabled: false },\n lineNumbers: 'on',\n }}\n />\n </div>\n </Form.Item>\n\n <Form.Item label={this.getLocaleValue('resources', 'Resources')}>\n <div className=\"resources-section\">\n <Button\n type=\"primary\"\n onClick={this.handleAddResource}\n style={{ marginBottom: 16 }}\n >\n <Icon type=\"add\" /> {this.getLocaleValue('addResource', 'Add Resource')}\n </Button>\n\n {resources.length > 0 && (\n <Collapse expandedKeys={expandedKeys} onExpand={this.handleExpandChange}>\n {resources.map((resource, index) => {\n const isEditing = this.state.editingResourceIndex === index;\n const displayText =\n resource.type && resource.name\n ? `${resource.type}/${resource.name}`\n : `${this.getLocaleValue('resource', 'Resource')} ${index + 1}`;\n\n return (\n <Collapse.Panel\n key={String(index)}\n title={\n <div className=\"resource-panel-header\">\n {isEditing ? (\n <div\n className=\"resource-title-editor\"\n onClick={e => e.stopPropagation()}\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n flex: 1,\n }}\n >\n <Input\n size=\"small\"\n value={resource.type || ''}\n onChange={value => {\n const filteredValue = value.replace(/[^a-zA-Z.-]/g, '');\n const newResources = [...this.state.resources];\n newResources[index] = {\n ...newResources[index],\n type: filteredValue,\n };\n this.setState({ resources: newResources }, () => {\n if (this.state.isEdit) {\n this.updateEditModeFileTree();\n }\n });\n }}\n placeholder={this.getLocaleValue(\n 'resourceTypePlaceholder',\n 'Type'\n )}\n style={{ width: '120px' }}\n onPressEnter={() => {\n this.setState({ editingResourceIndex: null });\n }}\n />\n <span>/</span>\n <Input\n size=\"small\"\n value={resource.name || ''}\n onChange={value => {\n const filteredValue = value.replace(/[^a-zA-Z0-9._-]/g, '');\n const newResources = [...this.state.resources];\n newResources[index] = {\n ...newResources[index],\n name: filteredValue,\n };\n this.setState({ resources: newResources }, () => {\n if (this.state.isEdit) {\n this.updateEditModeFileTree();\n }\n });\n }}\n placeholder={this.getLocaleValue(\n 'resourceNamePlaceholder',\n 'Name'\n )}\n style={{ flex: 1 }}\n onPressEnter={() => {\n this.setState({ editingResourceIndex: null });\n }}\n />\n <Button\n text\n size=\"small\"\n onClick={e => {\n e.stopPropagation();\n this.handleResourceTitleCancel();\n }}\n >\n <Icon type=\"close\" />\n </Button>\n </div>\n ) : (\n <>\n <span\n className=\"resource-title-text\"\n onClick={e => this.handleResourceTitleClick(index, e)}\n onMouseEnter={e =>\n this.handleResourceTitleMouseEnter(index, e)\n }\n onMouseMove={this.handleResourceTitleMouseMove}\n onMouseLeave={this.handleResourceTitleMouseLeave}\n style={{ cursor: 'pointer', flex: 1 }}\n >\n {displayText}\n </span>\n <Button\n text\n warning\n onClick={e => {\n e.stopPropagation();\n this.handleRemoveResource(index);\n }}\n >\n <Icon type=\"delete\" />{' '}\n {this.getLocaleValue('delete', 'Delete')}\n </Button>\n </>\n )}\n </div>\n }\n >\n <Form.Item>\n <div style={{ border: '1px solid #e6e6e6', borderRadius: '4px' }}>\n <MonacoEditor\n language={getLanguageFromFileName(resource.name || '')}\n width=\"100%\"\n height={300}\n value={resource.content || ''}\n onChange={value =>\n this.handleResourceChange(index, 'content', value)\n }\n options={{\n readOnly: false,\n wordWrap: 'on',\n minimap: { enabled: false },\n lineNumbers: 'on',\n scrollBeyondLastLine: false,\n theme: 'vs-dark-enhanced',\n }}\n />\n </div>\n </Form.Item>\n </Collapse.Panel>\n );\n })}\n </Collapse>\n )}\n\n {resources.length === 0 && (\n <div className=\"empty-resources\">\n {this.getLocaleValue(\n 'noResources',\n 'No resources added yet. Click \"Add Resource\" to add one.'\n )}\n </div>\n )}\n </div>\n </Form.Item>\n\n <Form.Item wrapperCol={{ offset: 3, span: 20 }}>\n <Button\n type=\"primary\"\n onClick={this.handleSubmit}\n loading={loading}\n style={{ marginRight: 10 }}\n >\n {this.getLocaleValue('create', 'Create')}\n </Button>\n </Form.Item>\n </Form>\n </div>\n )}\n\n {!isEdit && (\n <Dialog\n visible={this.state.showAiGenerateDialog}\n title={this.getLocaleValue('aiGenerate', 'AI生成')}\n onClose={this.handleCloseAiGenerateDialog}\n onCancel={this.handleCloseAiGenerateDialog}\n onOk={\n this.state.generatedSkill ? this.handleApplyGeneratedSkill : this.handleGenerateSkill\n }\n okProps={{\n loading: this.state.generating,\n children: this.state.generatedSkill\n ? this.getLocaleValue('apply', 'Apply')\n : this.getLocaleValue('generateSkill', 'Generate Skill'),\n }}\n cancelProps={{\n children: this.getLocaleValue('cancel', 'Cancel'),\n }}\n style={{ width: 1000 }}\n >\n {!this.state.generatedSkill ? (\n <div>\n {/* 用户输入区域 - 生成前显示完整,生成后折叠 */}\n {!this.state.showInputCollapsed ? (\n <div>\n <Form.Item\n label={this.getLocaleValue('backgroundInfo', 'Background Information')}\n >\n <Input.TextArea\n value={this.state.backgroundInfo}\n onChange={this.handleBackgroundInfoChange}\n placeholder={this.getLocaleValue(\n 'backgroundInfoPlaceholder',\n 'Please describe what you want the Skill to do, e.g., \"I need a Skill to check Nacos configuration status and provide solutions when issues are found\"'\n )}\n rows={6}\n maxLength={2000}\n />\n </Form.Item>\n\n <Form.Item\n label={this.getLocaleValue('selectMcpTools', 'Select MCP Tools (Optional)')}\n >\n <Loading visible={this.state.loadingMcpServers} style={{ width: '100%' }}>\n <Select\n placeholder={this.getLocaleValue('selectMcpServer', 'Select MCP Server')}\n value={this.state.selectedMcpServer}\n onChange={this.handleMcpServerChange}\n style={{ width: '100%', marginBottom: 12 }}\n dataSource={this.state.mcpServers.map(server => ({\n label: server.name,\n value: server.id || server.name,\n }))}\n />\n </Loading>\n\n {this.state.selectedMcpServer && (\n <Loading visible={this.state.loadingMcpTools} style={{ width: '100%' }}>\n <Input\n placeholder={this.getLocaleValue(\n 'searchTools',\n 'Search tools by name...'\n )}\n value={this.state.mcpToolSearchKeyword}\n onChange={this.handleMcpToolSearchChange}\n style={{ width: '100%', marginBottom: 12 }}\n hasClear\n />\n <div\n style={{\n border: '1px solid #e6e6e6',\n borderRadius: 4,\n padding: 12,\n maxHeight: 200,\n overflowY: 'auto',\n background: '#fafafa',\n }}\n >\n {this.getFilteredMcpTools().length > 0 ? (\n this.getFilteredMcpTools().map((tool, index) => (\n <Checkbox\n key={index}\n checked={this.state.selectedMcpTools.some(\n t => t.name === tool.name\n )}\n onChange={checked => this.handleMcpToolChange(checked, tool)}\n style={{ display: 'block', marginBottom: 8 }}\n >\n <div>\n <strong>{tool.name}</strong>\n {tool.description && (\n <div\n style={{ fontSize: '12px', color: '#666', marginTop: 4 }}\n >\n {tool.description}\n </div>\n )}\n </div>\n </Checkbox>\n ))\n ) : (\n <div style={{ color: '#999', fontSize: '12px' }}>\n {this.state.mcpToolSearchKeyword.trim()\n ? this.getLocaleValue(\n 'noToolsFound',\n 'No tools found matching your search'\n )\n : this.getLocaleValue(\n 'noToolsAvailable',\n 'No tools available in this MCP server'\n )}\n </div>\n )}\n </div>\n </Loading>\n )}\n </Form.Item>\n\n <Form.Item\n label={this.getLocaleValue(\n 'conversationHistory',\n 'Conversation History (Optional)'\n )}\n >\n <div style={{ marginBottom: 8 }}>\n <Button\n text\n size=\"small\"\n onClick={() =>\n this.setState({\n showConversationHistory: !this.state.showConversationHistory,\n })\n }\n >\n {this.state.showConversationHistory\n ? this.getLocaleValue(\n 'hideConversationHistory',\n 'Hide Conversation History'\n )\n : this.getLocaleValue(\n 'showConversationHistory',\n 'Show Conversation History'\n )}\n <Icon\n type={this.state.showConversationHistory ? 'arrow-up' : 'arrow-down'}\n style={{ marginLeft: 4 }}\n />\n </Button>\n </div>\n {this.state.showConversationHistory && (\n <div>\n <Input.TextArea\n value={this.state.conversationHistoryJson}\n onChange={value => this.setState({ conversationHistoryJson: value })}\n placeholder={this.getLocaleValue(\n 'conversationHistoryPlaceholder',\n 'Enter conversation history in JSON format:\\n{\\n \"title\": \"Conversation Title\",\\n \"context\": \"Context description\",\\n \"messages\": [\\n {\\n \"type\": \"user\",\\n \"content\": \"User input\"\\n },\\n {\\n \"type\": \"tool_call\",\\n \"toolName\": \"tool_name\",\\n \"toolInput\": {\"param\": \"value\"},\\n \"toolOutput\": \"result\"\\n },\\n {\\n \"type\": \"model\",\\n \"content\": \"Model response\"\\n }\\n ]\\n}'\n )}\n rows={12}\n style={{ fontFamily: 'monospace', fontSize: '12px' }}\n />\n <div style={{ marginTop: 8, color: '#999', fontSize: '12px' }}>\n {this.getLocaleValue(\n 'conversationHistoryHint',\n 'The system will analyze the conversation history to determine if it is suitable for skill generation. Include user inputs, tool calls, and model responses.'\n )}\n </div>\n </div>\n )}\n </Form.Item>\n\n <div style={{ marginTop: 8, color: '#999', fontSize: '12px' }}>\n {this.getLocaleValue(\n 'generateHint',\n 'Enter background information and click to generate a Skill based on best practices'\n )}\n </div>\n </div>\n ) : (\n this.renderUserInput()\n )}\n\n {/* 结果解析中提示 */}\n {this.state.parsingResult && (\n <div style={{ marginTop: 16, textAlign: 'center', padding: '20px' }}>\n <Loading\n visible={true}\n tip={this.getLocaleValue('parsingResult', 'Parsing result...')}\n />\n </div>\n )}\n\n {/* 流式内容 - 思考内容和结果内容 */}\n {(this.state.streaming || this.state.thinkingContent || this.state.streamContent) &&\n this.renderStreamContent()}\n </div>\n ) : (\n this.renderGeneratedSkill()\n )}\n\n {/* Tooltip for user input */}\n {this.state.inputTooltipVisible && (\n <div\n style={{\n position: 'fixed',\n left: `${this.state.inputTooltipPosition.x}px`,\n top: `${this.state.inputTooltipPosition.y - 10}px`,\n transform: 'translate(-50%, -100%)',\n background: '#333',\n color: '#fff',\n padding: '12px 16px',\n borderRadius: '4px',\n fontSize: '12px',\n maxWidth: '500px',\n maxHeight: '400px',\n overflowY: 'auto',\n boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',\n zIndex: 9999,\n pointerEvents: 'none',\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n }}\n >\n {(() => {\n const { backgroundInfo, selectedMcpTools } = this.state;\n const selectedToolsText =\n selectedMcpTools.length > 0\n ? selectedMcpTools.map(t => t.name).join(', ')\n : this.getLocaleValue('noToolsSelected', 'No tools selected');\n return `${this.getLocaleValue(\n 'backgroundInfo',\n 'Background Information'\n )}: ${backgroundInfo}\\n\\n${this.getLocaleValue(\n 'selectedTools',\n 'Selected Tools'\n )}: ${selectedToolsText}`;\n })()}\n </div>\n )}\n\n {/* Tooltip for thinking content */}\n {this.state.thinkingTooltipVisible && this.state.thinkingContent && (\n <div\n style={{\n position: 'fixed',\n left: `${this.state.thinkingTooltipPosition.x}px`,\n top: `${this.state.thinkingTooltipPosition.y - 10}px`,\n transform: 'translate(-50%, -100%)',\n background: '#333',\n color: '#fff',\n padding: '12px 16px',\n borderRadius: '4px',\n fontSize: '12px',\n maxWidth: '500px',\n maxHeight: '400px',\n overflowY: 'auto',\n boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',\n zIndex: 9999,\n pointerEvents: 'none',\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n }}\n >\n {this.state.thinkingContent}\n </div>\n )}\n </Dialog>\n )}\n\n {isEdit && (\n <SkillOptimizeDialog\n visible={this.state.optimizeDialogVisible}\n skill={this.state.currentSkillData}\n fileTree={this.state.editModeFileTree}\n selectedFile={this.state.editModeSelectedFile}\n onClose={this.handleOptimizeDialogClose}\n onSuccess={this.handleOptimizeSuccess}\n locale={this.props.locale}\n history={this.props.history}\n />\n )}\n\n {/* Custom Tooltip for resource title - follows mouse position */}\n {this.state.tooltipVisible && this.state.tooltipResourceIndex !== null && (\n <div\n style={{\n position: 'fixed',\n left: `${this.state.tooltipPosition.x}px`,\n top: `${this.state.tooltipPosition.y - 10}px`,\n transform: 'translate(-50%, -100%)',\n background: '#333',\n color: '#fff',\n padding: '8px 12px',\n borderRadius: '4px',\n fontSize: '12px',\n boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',\n zIndex: 9999,\n pointerEvents: 'none',\n whiteSpace: 'nowrap',\n }}\n >\n {this.getLocaleValue('clickToEdit', '点击进行编辑')}\n </div>\n )}\n </div>\n );\n }\n}\n\nexport default NewSkill;\n"}}}}