import { useState, useRef } from 'react'; // useInput - 管理输入组件的状态和逻辑 export function useInput(onSendMessage, onLoadingChange) { const { onSend, onUpload, uploadedImages, removeImage, workflowRequirements, } = InputLogic(onSendMessage, onLoadingChange); const [message, setMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); const fileInputRef = useRef(null); const textInputRef = useRef(null); const handleSend = async () => { const trimmed = message.trim(); // 允许在有图片时发送(即使没有文字),或者有文字时发送 if ((!trimmed && uploadedImages.length === 0) || isLoading) return; // 先清空输入框(在发送前清空,提供更好的用户体验) setMessage(''); setIsLoading(true); try { await onSend?.(trimmed || ''); // 如果没有文字,传递空字符串 } finally { setIsLoading(false); } }; const handleContainerClick = (e) => { // 如果点击的是容器本身(不是按钮、输入框或其他交互元素),则聚焦输入框 if (e.target === e.currentTarget) { textInputRef.current?.focus(); } }; // 有文字或上传了图片就可以发送 const isSendDisabled = (!message.trim() && uploadedImages.length === 0) || isLoading; const handleUploadClick = () => { fileInputRef.current?.click(); }; const handleFileChange = async (e) => { const files = e.target.files; if (!files || files.length === 0) return; for (const file of files) { if (file.type.startsWith('image/')) { await onUpload?.(file); } } // 清空input,允许重复选择同一文件 e.target.value = ''; }; const handleSendKeyDown = (e) => { if ((e.key === 'Enter' || e.key === ' ') && !isSendDisabled) { e.preventDefault(); handleSend(); } }; const handleTextareaKeyDown = (e) => { // Enter 发送(不按 Ctrl) if (e.key === 'Enter' && !e.ctrlKey && !e.shiftKey && !e.metaKey) { e.preventDefault(); handleSend(); return; } // 其他所有按键都允许默认行为(包括Backspace、Delete等) // 不要阻止任何其他按键的默认行为 }; const handleUploadKeyDown = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleUploadClick(); } }; const getSendButtonClassName = () => { return `send-btn ${isSendDisabled ? 'disabled' : ''}`; }; const getSendButtonOnClick = () => { return !isSendDisabled ? handleSend : undefined; }; const getSendButtonTabIndex = () => { return isSendDisabled ? -1 : 0; }; return { message, setMessage, isLoading, fileInputRef, textInputRef, handleSend, handleContainerClick, handleUploadClick, handleFileChange, handleSendKeyDown, handleTextareaKeyDown, handleUploadKeyDown, isSendDisabled, getSendButtonClassName, getSendButtonOnClick, getSendButtonTabIndex, uploadedImages, removeImage, }; } // InputLogic - 处理消息发送到GPT API // 第一阶段提示词:分析需求,返回需要的信息 const REQUIREMENTS_PROMPT = `你是一个友好的自动化工作流生成助手。你需要像聊天一样与用户对话。 如果用户想要创建自动化工作流,你需要分析需求并返回需要的信息。请用自然、友好的语言与用户交流,保持对话的连贯性和友好性。 语法分层(重要): - 基础语法(控制流/定时):scheduleOnce、schedule、for、while、if - 基础 action(模拟手机操作):press、swipe、scroll - 扩展标签(Func):除基础能力外的所有扩展功能,全部由 src/pages/processing/func/ 目录下的脚本文件决定;每个脚本文件名就是一个"标签"。 在生成工作流时: - 逻辑结构尽量用 for/while/if + triggers(schedules) 表达 - 具体手机操作尽量落到 press/swipe/scroll - 当需求超出基础 action(例如:提取会话/消息记录、文字定位等),请使用对应的 Func 标签(例如:extract-chat-history、string-reg-location、save-chat-history) (实现层说明:项目内部为兼容与落地,仍存在 locate/click/input/ocr/ai-generate/delay/set 等内置能力;但提示用户与生成语法时,请优先按上述分层来组织。) 9. if - 条件判断 - condition: 条件表达式(如 "{count} > 5") - then: 条件为真时执行的操作数组 - else: 条件为假时执行的操作数组(可选) 10. for - 循环 - variable: 循环变量名 - items: 数组(可以是变量 {arrayVariable}) - body: 循环体操作数组 11. delay - 延迟 - value: 延迟时间(如 "2s", "1m", "30 minutes") 12. set - 设置变量 - variable: 变量名 - value: 变量值 13. scroll/swipe - 滚动/滑动 - value: 方向("up-down", "down-up", "left-right", "right-left") 定时任务配置: - 单次执行: {"datetime": "2026/1/14 01:21"} - 每天执行: {"time": "09:00", "repeat": "daily"} - 每周执行: {"time": "09:00", "weekdays": ["monday", "wednesday", "friday"]} - 间隔执行: {"interval": "30 minutes"} 重要规则: - 变量使用 {variableName} 格式引用 - 根据用户描述合理设置delay延迟时间 - 如果用户提到循环、条件判断,使用for/if操作 - 如果用户提到定时执行,添加triggers配置 - 如果用户提到AI生成内容,使用ai-generate操作 - 如果用户提到消息记录、聊天记录、历史记录、对话记录,使用extract-messages、save-messages、generate-summary操作 - 图片文件名按顺序命名为"1.png"、"2.png"等 - **特别重要**:如果工作流中需要输入文字(使用 input 或 ai-generate 操作),且这些文字需要根据场景背景和用户角色来生成,你必须询问用户以下信息: - 场景的背景:这个工作流在什么场景下使用?(例如:微信聊天、客服回复、评论互动等) - 用户的角色:用户在这个场景中扮演什么角色?(例如:刚认识女方的男生、客服人员、产品推广者等) - 这些信息对于生成合适的文字内容非常重要,请务必在询问需求时主动询问 请分析用户的需求,根据上述操作类型,判断需要哪些图片和文字参考: - 图片(needsImages):用于 locate/click 的 image 方法,或 ocr 的 by-avatar 方法 - 文字参考(needsTexts):用于 locate/click 的 text 方法 如果用户想要创建自动化工作流: 1. 先友好地确认理解用户的需求 2. 然后返回JSON格式的需求信息(不包含在自然语言回复中,仅作为JSON返回) 需求分析的JSON格式: { "needsImages": 2, // 需要多少张图片(用于图像匹配定位) "needsTexts": 1, // 需要多少个文字参考(用于文字识别定位) "imageDescriptions": ["登录按钮", "用户名输入框"], // 每张图片的描述 "textDescriptions": ["登录按钮文字"] // 每个文字参考的描述 } 返回格式:先自然语言回复,然后单独一行JSON。 **特别提醒**:如果工作流涉及输入文字(如自动回复、自动评论、自动发送消息等),请务必询问用户场景背景和角色信息。 例如(不涉及文字输入的场景): 我理解了,您想创建一个登录流程的自动化工作流。为了生成准确的工作流配置,我需要您提供以下信息: 📋 需要的信息: 🖼️ 需要 2 张图片:登录按钮截图、用户名输入框截图 📝 需要 1 个文字参考:登录按钮上的文字 \`\`\`json { "needsImages": 2, "needsTexts": 1, "imageDescriptions": ["登录按钮", "用户名输入框"], "textDescriptions": ["登录按钮文字"] } \`\`\` 例如(涉及文字输入的场景): 我理解了,您想创建一个自动聊天回复的自动化工作流。为了生成准确且符合场景的文字内容,我需要了解以下信息: 📋 需要的信息: 🖼️ 需要 2 张图片:好友头像截图、我的头像截图 📝 需要 1 个文字参考:输入框位置 💬 **场景信息**(重要): - 场景背景:这个聊天是在什么场景下进行的?(例如:微信聊天、QQ群聊、客服对话等) - 您的角色:您在这个场景中扮演什么角色?(例如:刚认识女方的男生、客服人员、产品推广者等) \`\`\`json { "needsImages": 2, "needsTexts": 1, "imageDescriptions": ["好友头像", "我的头像"], "textDescriptions": ["输入框位置"] } \`\`\` 如果用户的话题与工作流无关,请友好地自然回复,不需要返回JSON。如果用户是在询问如何使用、或者其他一般性问题,请用自然语言友好地回答。`; // 第二阶段提示词:生成工作流 const SYSTEM_PROMPT = `你是一个友好的自动化工作流生成助手。用户已经提供了所需的图片和文字参考,现在你需要生成JSON格式的工作流配置文件。 请用自然、友好的语言与用户交流,保持对话的连贯性。在生成工作流时,请先简短地确认一下,然后生成JSON格式的工作流配置。 工作流JSON格式(新版本): { "version": "1.0", "name": "工作流名称", "triggers": [ { "type": "schedule", "schedule": { "time": "09:00", "weekdays": ["monday", "wednesday", "friday"] } } ], "variables": { "topic": "美食" }, "actions": [ { "type": "locate", "method": "image", "target": "button.png", "variable": "buttonPos", "delay": "1s" }, { "type": "click", "method": "position", "target": "{buttonPos}", "delay": "2s" }, { "type": "input", "target": "输入框", "value": "文字内容", "delay": "1s" }, { "type": "ai-generate", "prompt": "生成关于{topic}的文章", "variable": "article", "delay": "10s" }, { "type": "if", "condition": "{count} > 5", "then": [ { "type": "click", "method": "text", "target": "确定" } ] }, { "type": "for", "variable": "item", "items": ["选项1", "选项2"], "body": [ { "type": "click", "method": "text", "target": "{item}" } ] } ] } 核心操作类型: 1. locate - 定位元素 - method: "image"(图像匹配)| "text"(文字识别)| "coordinate"(坐标) - target: 目标内容 - variable: 保存位置到变量 2. click - 点击 - method: "position" | "image" | "text" | "coordinate" - target: 目标(可以是变量 {variable}) 3. input - 输入文字 - target: 输入框位置(文字或变量) - value: 要输入的文本(可以是变量 {variable}) 4. ai-generate - AI生成内容 - prompt: 提示词(支持变量 {variable}) - variable: 保存结果到变量 5. ocr - 文字识别 - method: "by-avatar" | "by-position" | "full-screen" - avatar: 头像图片(by-avatar时) - variable: 保存识别结果 6. extract-messages - 提取消息记录 - avatar1: 第一个参与者的头像/标识图片路径 - avatar2: 第二个参与者的头像/标识图片路径 - variable: 保存消息记录到变量(格式:对方: 消息\n我: 消息) 7. save-messages - 保存消息记录 - variable: 包含消息记录的变量名(保存到 history 文件夹) 8. generate-summary - 生成消息总结 - variable: 包含消息记录的变量名 - summaryVariable: 保存总结结果的变量名 - model: AI模型名称(可选) 9. if - 条件判断 - condition: 条件表达式(如 "{count} > 5") - then: 条件为真时执行的操作数组 - else: 条件为假时执行的操作数组(可选) 10. for - 循环 - variable: 循环变量名 - items: 数组(可以是变量 {arrayVariable}) - body: 循环体操作数组 11. delay - 延迟 - value: 延迟时间(如 "2s", "1m", "30 minutes") 12. set - 设置变量 - variable: 变量名 - value: 变量值 13. scroll/swipe - 滚动/滑动 - value: 方向("up-down", "down-up", "left-right", "right-left") 定时任务配置: - 单次执行: {"datetime": "2026/1/14 01:21"} - 每天执行: {"time": "09:00", "repeat": "daily"} - 每周执行: {"time": "09:00", "weekdays": ["monday", "wednesday", "friday"]} - 间隔执行: {"interval": "30 minutes"} 重要规则: - 使用新格式(type字段),但保持向后兼容旧格式 - 变量使用 {variableName} 格式引用 - 根据用户描述合理设置delay延迟时间 - 如果用户提到循环、条件判断,使用for/if操作 - 如果用户提到定时执行,添加triggers配置 - 如果用户提到AI生成内容,使用ai-generate操作 - 如果用户提到消息记录、聊天记录、历史记录、对话记录,使用extract-messages、save-messages、generate-summary操作 - 在ai-generate的prompt中可以使用总结变量引用历史消息记录的总结 - 图片文件名按顺序命名为"1.png"、"2.png"等 返回格式: 1. 如果用户已经提供了所有需要的图片和文字参考,且明确要求生成工作流: - 先用简短的自然语言确认(如"好的,我现在为您生成工作流配置...") - 然后单独一行返回JSON格式的工作流配置(可以用代码块包裹) 2. 如果用户只是在询问问题或者还没准备好生成工作流: - 用自然语言友好地回答用户的问题 - 不需要返回JSON 请保持对话的自然和友好,不要生硬地只返回JSON。`; export function InputLogic(onSendMessage, onLoadingChange) { // 存储上传的图片 const [uploadedImages, setUploadedImages] = useState([]); // img2text API可用性标记(使用useRef避免重复输出警告) const img2textApiAvailableRef = useRef(true); // 调用img2text API获取图片描述 const img2text = async (imageBase64, prompt = '请描述这张图片的内容') => { // 如果已经知道API不可用,直接返回null,不尝试调用 if (!img2textApiAvailableRef.current) { return null; } try { // 确保base64数据是完整的data URL格式 let imageUrl = imageBase64; if (!imageBase64.startsWith('data:')) { // 如果没有前缀,假设是PNG格式的base64数据 imageUrl = `data:image/png;base64,${imageBase64}`; } // 使用AbortController设置超时,快速失败 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 1000); // 1秒超时 const response = await fetch('https://ai-anim.com/api/img2text', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ prompt: prompt, imageUrl: imageUrl }), signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { const errorText = await response.text(); throw new Error(`img2text API error: ${response.status} ${response.statusText} - ${errorText}`); } const data = await response.json(); // 支持多种可能的返回格式 return data.text || data.description || data.result || data.output_text || data.content || ''; } catch (error) { // img2text API是可选的,失败不影响主要功能 // 标记API不可用,之后直接返回null,不再尝试 if (img2textApiAvailableRef.current) { img2textApiAvailableRef.current = false; } // 静默返回null,不输出任何错误信息(因为这是可选功能) return null; } }; // 存储工作流需求信息(需要多少图片和文字) const [workflowRequirements, setWorkflowRequirements] = useState(null); // 存储用户输入的文字参考(用于文字识别) const [referenceTexts, setReferenceTexts] = useState([]); // 存储AI生成的工作流JSON(不显示在聊天框) const [workflowJson, setWorkflowJson] = useState(null); // 删除图片 const removeImage = (index) => { setUploadedImages(prev => prev.filter((_, i) => i !== index)); }; // 上传图片并命名 const onUpload = async (file) => { try { // 读取图片为base64 const reader = new FileReader(); const base64 = await new Promise((resolve, reject) => { reader.onload = () => { const result = reader.result; // 提取base64数据部分(去掉data:image/...;base64,前缀) const base64Data = result.split(',')[1]; resolve(base64Data); }; reader.onerror = reject; reader.readAsDataURL(file); }); // 生成临时文件名(使用时间戳) const timestamp = Date.now(); const tempFileName = `temp_${timestamp}.${file.name.split('.').pop()}`; // 存储图片信息 const imageInfo = { file: file, base64: base64, tempName: tempFileName, originalName: file.name, timestamp: timestamp, }; setUploadedImages(prev => [...prev, imageInfo]); console.log('图片已上传:', imageInfo.originalName); } catch (error) { console.error('上传图片失败:', error); } }; // 根据用户描述给图片命名(使用AI) const nameImagesWithAI = async (userPrompt, images) => { if (!images || images.length === 0) return images; try { // 构建提示词,让AI根据用户描述给图片命名 const imagePrompt = `用户上传了 ${images.length} 张图片,用于工作流自动化。请根据用户的描述,为每张图片生成一个简洁的描述性文件名(只返回JSON数组,不要其他文字)。 用户描述:${userPrompt} 图片信息: ${images.map((img, index) => `图片${index + 1}: 原始文件名 ${img.originalName}`).join('\n')} 请返回JSON数组,格式如下: ["登录按钮.png", "用户名输入框.png", "密码输入框.png"] 要求: 1. 文件名要简洁,能清楚描述图片内容 2. 使用中文或英文都可以 3. 必须包含文件扩展名(.png) 4. 文件名要符合工作流中使用的命名规范`; const response = await fetch('https://ai-anim.com/api/text2textByModel', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: imagePrompt, modelName: 'gpt-5-nano-ca' }) }); if (!response.ok) { throw new Error('AI命名失败'); } const data = await response.json(); const responseText = data.data?.output_text || data.text || data.content || ''; // 解析AI返回的文件名数组 let imageNames = []; try { // 尝试直接解析JSON imageNames = JSON.parse(responseText.trim()); } catch (e) { // 尝试从代码块中提取 const match = responseText.match(/\[[\s\S]*?\]/); if (match) { imageNames = JSON.parse(match[0]); } else { // 如果解析失败,使用默认命名 imageNames = images.map((img, index) => `${index + 1}.png`); } } // 更新图片信息,添加AI生成的文件名 return images.map((img, index) => ({ ...img, aiName: imageNames[index] || `${index + 1}.png` })); } catch (error) { console.error('AI命名图片失败:', error); // 如果AI命名失败,使用默认命名 return images.map((img, index) => ({ ...img, aiName: `${index + 1}.png` })); } }; const sendToGPT = async (userPrompt, images = []) => { // 构建图片信息提示(如果图片已经命名) let imageInfoPrompt = ''; if (images.length > 0) { imageInfoPrompt = `\n\n用户上传了以下图片,请在生成工作流时使用这些图片文件名:\n${images.map((img, index) => `${index + 1}. ${img.aiName || img.tempName} - ${img.originalName}`).join('\n')}\n\n在生成工作流时,如果操作需要点击图片,请使用上述图片文件名(如 "登录按钮.png")。`; } // 构建完整的提示词:系统提示词 + 图片信息 + 用户输入 const fullPrompt = `${SYSTEM_PROMPT}${imageInfoPrompt}\n\n用户需求:${userPrompt}`; const requestBody = { input: fullPrompt }; console.log('Sending request to Doubao API:', requestBody); const response = await fetch('https://ai-anim.com/api/doubaoText2text', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); console.log('Response status:', response.status, response.statusText); if (!response.ok) { let errorText; try { errorText = await response.text(); // 尝试解析为 JSON try { const errorJson = JSON.parse(errorText); console.error('API Error Response (JSON):', errorJson); // 如果 JSON 中有 error 字段,使用它 if (errorJson.error) { throw new Error(`服务器错误: ${errorJson.error}`); } if (errorJson.message) { throw new Error(`服务器错误: ${errorJson.message}`); } } catch (parseError) { // 不是 JSON,使用原始文本 console.error('API Error Response (Text):', errorText); } } catch (readError) { errorText = `无法读取错误响应: ${readError.message}`; } throw new Error(`HTTP ${response.status}: ${errorText}`); } const data = await response.json(); console.log('API Response:', data); return data; }; // 分析需求,返回需要的信息 const analyzeRequirements = async (userPrompt) => { const response = await fetch('https://ai-anim.com/api/text2textByModel', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: `${REQUIREMENTS_PROMPT}\n\n用户需求:${userPrompt}`, modelName: 'gpt-5-nano-ca' }) }); if (!response.ok) { throw new Error('分析需求失败'); } const data = await response.json(); const responseText = data.data?.output_text || data.text || data.content || ''; // 提取自然语言回复(去除JSON部分) let naturalLanguageReply = responseText.trim(); // 尝试从回复中提取JSON(可能在代码块中,也可能单独一行) let requirements = null; // 方法1: 尝试从代码块中提取JSON const codeBlockMatch = responseText.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/); if (codeBlockMatch) { try { requirements = JSON.parse(codeBlockMatch[1]); // 移除代码块部分,保留自然语言 naturalLanguageReply = responseText.replace(/```(?:json)?\s*\{[\s\S]*?\}\s*```/g, '').trim(); } catch (e) { // 解析失败,继续尝试其他方法 } } // 方法2: 尝试匹配JSON对象 if (!requirements) { const jsonMatch = responseText.match(/\{[\s\S]*"needsImages"[\s\S]*\}/); if (jsonMatch) { try { requirements = JSON.parse(jsonMatch[0]); // 移除JSON部分,保留自然语言 naturalLanguageReply = responseText.replace(/\{[\s\S]*"needsImages"[\s\S]*\}/, '').trim(); } catch (e) { // 解析失败 } } } // 方法3: 尝试直接解析整个文本 if (!requirements) { try { requirements = JSON.parse(responseText.trim()); } catch (e) { // 不是纯JSON } } // 如果找到JSON,返回JSON和自然语言回复 if (requirements && typeof requirements === 'object') { // 将自然语言回复保存到requirements中,以便后续显示 if (naturalLanguageReply && naturalLanguageReply !== responseText.trim()) { requirements.naturalLanguageReply = naturalLanguageReply; } return requirements; } // 如果没有找到JSON,可能是用户的问题与工作流无关,返回错误信息 return { error: naturalLanguageReply || '请描述你的工作流需求,例如:生成一个登录流程,先点击登录按钮,然后输入用户名和密码' }; }; // 解析用户输入的文字参考(按行分割) const parseReferenceTexts = (text) => { // 如果已经有工作流需求,提取文字参考 if (workflowRequirements && workflowRequirements.needsTexts > 0) { // 尝试从文本中提取,可能是用换行、逗号或分号分隔 const lines = text.split(/[\n,,;;]/).map(t => t.trim()).filter(t => t); return lines.slice(0, workflowRequirements.needsTexts); } return []; }; const onSend = async (text) => { if (!text.trim() && uploadedImages.length === 0) return; // 保存上传的图片信息(用于后续使用和显示) const currentImages = [...uploadedImages]; // 添加用户消息(包含图片信息,包含base64数据以便在聊天界面显示) const userMessage = { role: 'user', content: text, images: currentImages.map(img => ({ name: img.originalName, tempName: img.tempName, base64: img.base64, // 包含base64数据,用于在聊天界面显示 originalName: img.originalName })), timestamp: new Date(), }; onSendMessage?.(userMessage); // 发送消息后立即清空上传的图片,让输入框的提示方块消失 setUploadedImages([]); // 开始加载 onLoadingChange?.(true); try { // 第一阶段:如果没有需求信息,先分析需求 if (!workflowRequirements) { // 如果有参考图,先调用img2text获取图片描述 let imageDescriptionsPrompt = ''; if (currentImages.length > 0) { const imageDescriptions = []; for (const img of currentImages) { const description = await img2text(img.base64, '请详细描述这张图片的内容,包括图片中的界面元素、按钮、文字等信息'); if (description) { imageDescriptions.push(description); } } if (imageDescriptions.length > 0) { imageDescriptionsPrompt = `\n\n用户提供了以下参考图片及其描述:\n${imageDescriptions.map((desc, index) => `图片${index + 1}:${desc}`).join('\n')}\n\n请根据这些图片描述,分析用户需要哪些图片和文字参考来生成工作流。`; } } const requirements = await analyzeRequirements(text + imageDescriptionsPrompt); if (requirements.error) { // 如果返回错误信息,直接显示(可能是自然语言回复) const errorMessage = { role: 'assistant', content: requirements.error, timestamp: new Date(), }; onSendMessage?.(errorMessage); return; } // 保存需求信息 setWorkflowRequirements(requirements); // 显示需求信息给用户(优先显示AI的自然语言回复,然后显示需求列表) const needsImages = requirements.needsImages || 0; const needsTexts = requirements.needsTexts || 0; let requirementMessage = ''; // 如果有自然语言回复,先显示 if (requirements.naturalLanguageReply) { requirementMessage = requirements.naturalLanguageReply + '\n\n'; } // 然后显示需求信息 if (needsImages > 0 || needsTexts > 0) { requirementMessage += '📋 根据您的需求,我需要以下信息:\n\n'; if (needsImages > 0) { requirementMessage += `🖼️ 需要 ${needsImages} 张图片:\n`; (requirements.imageDescriptions || []).forEach((desc, index) => { requirementMessage += `${index + 1}. ${desc}\n`; }); requirementMessage += '\n'; } if (needsTexts > 0) { requirementMessage += `📝 需要 ${needsTexts} 个文字参考:\n`; (requirements.textDescriptions || []).forEach((desc, index) => { requirementMessage += `${index + 1}. ${desc}\n`; }); requirementMessage += '\n'; } requirementMessage += '请按顺序上传图片和输入文字参考,然后再次发送消息。'; } const reqMessage = { role: 'assistant', content: requirementMessage || '好的,我理解了您的需求。', timestamp: new Date(), }; onSendMessage?.(reqMessage); return; } // 第二阶段:验证用户提供的信息是否完整 const needsImages = workflowRequirements.needsImages || 0; const needsTexts = workflowRequirements.needsTexts || 0; const providedImages = currentImages.length; const parsedTexts = parseReferenceTexts(text); const providedTexts = parsedTexts.length; if (providedImages < needsImages || providedTexts < needsTexts) { // 信息不完整,提示用户 let missingMessage = '❌ 信息不完整,请提供:\n\n'; if (providedImages < needsImages) { missingMessage += `🖼️ 还需要 ${needsImages - providedImages} 张图片\n`; } if (providedTexts < needsTexts) { missingMessage += `📝 还需要 ${needsTexts - providedTexts} 个文字参考\n`; } missingMessage += '\n请按顺序上传图片和输入文字参考,然后再次发送消息。'; const missingMsg = { role: 'assistant', content: missingMessage, timestamp: new Date(), }; onSendMessage?.(missingMsg); return; } // 第三阶段:信息完整,生成工作流 let namedImages = []; if (currentImages.length > 0) { try { // 使用需求描述给图片命名 const imageDescriptions = workflowRequirements.imageDescriptions || []; namedImages = currentImages.map((img, index) => ({ ...img, aiName: imageDescriptions[index] ? `${imageDescriptions[index]}.png` : `${index + 1}.png` })); } catch (nameError) { console.error('图片命名失败,使用默认命名:', nameError); namedImages = currentImages.map((img, index) => ({ ...img, aiName: `${index + 1}.png` })); } } // 构建包含文字参考的提示词 let textReferencesPrompt = ''; if (parsedTexts.length > 0) { textReferencesPrompt = `\n\n用户提供的文字参考:\n${parsedTexts.map((t, i) => `${i + 1}. ${t}`).join('\n')}\n\n在生成工作流时,如果操作需要定位文字,请使用上述文字参考。`; // 保存文字参考到state setReferenceTexts(parsedTexts); } // 如果有参考图,调用img2text获取图片描述并加入到prompt中 let imageDescriptionsPrompt = ''; if (currentImages.length > 0) { const imageDescriptions = []; for (const img of currentImages) { const description = await img2text(img.base64, '请详细描述这张图片的内容,包括图片中的界面元素、按钮、文字、布局等信息,这些信息将用于生成自动化工作流'); if (description) { imageDescriptions.push(description); } } if (imageDescriptions.length > 0) { imageDescriptionsPrompt = `\n\n参考图片描述:\n${imageDescriptions.map((desc, index) => { const imgName = namedImages[index]?.aiName || `${index + 1}.png`; return `图片 ${imgName}:${desc}`; }).join('\n')}\n\n请根据这些图片描述,在生成工作流时合理使用对应的图片文件名。`; } } // 发送到GPT并获取回复(传递已命名的图片信息和文字参考) const response = await sendToGPT(text + textReferencesPrompt + imageDescriptionsPrompt, namedImages); // 根据实际 API 响应结构解析文本 // API 返回: { success: true, data: { output_text: "..." } } const responseText = response.data?.output_text || response.text || response.content || response.message || response.data?.text || response.data?.content || response.result || (typeof response === 'string' ? response : JSON.stringify(response)); // 检测是否是JSON格式的工作流 const detectedWorkflowJson = extractWorkflowJsonFromText(responseText); if (detectedWorkflowJson) { // 如果检测到工作流JSON,保存到内存中,不显示在聊天框 setWorkflowJson(detectedWorkflowJson); // 准备图片数据(base64和文件名) const imagesData = namedImages.map(img => ({ name: img.aiName || img.tempName, base64: img.base64, originalName: img.originalName })); // 建立图片文件名映射:将JSON中可能使用的简单文件名(如 "1.png")映射到实际保存的文件名 // 这样即使AI使用了 "1.png"、"2.png" 等简单名称,也能正确映射到实际的文件名 const imageNameMap = {}; namedImages.forEach((img, index) => { const actualName = img.aiName || img.tempName; // 映射 "1.png", "2.png" 等 imageNameMap[`${index + 1}.png`] = actualName; // 也映射原始名称(如果有) if (img.originalName) { imageNameMap[img.originalName] = actualName; } }); // 在保存前,更新工作流JSON中的图片文件名 const updatedWorkflowJson = updateImagePathsInWorkflow(detectedWorkflowJson, imageNameMap); // 直接保存工作流(不显示JSON内容) try { if (window.electronAPI && window.electronAPI.saveWorkflow) { const saveResult = await window.electronAPI.saveWorkflow(updatedWorkflowJson, imagesData); if (saveResult.success) { // 保存成功,清空所有状态(图片已经在发送时清空) setWorkflowRequirements(null); setReferenceTexts([]); setWorkflowJson(null); // 显示成功消息(不包含JSON内容) const successMessage = { role: 'assistant', content: `✅ 工作流已成功生成并保存!\n\n文件夹名称:${saveResult.folderName}`, timestamp: new Date(), }; onSendMessage?.(successMessage); // 触发工作流列表刷新事件 window.dispatchEvent(new CustomEvent('workflow-saved')); return; } else { // 保存失败,显示错误 console.error('保存工作流失败:', saveResult.error); const errorMessage = { role: 'assistant', content: `❌ 保存工作流失败:${saveResult.error}`, timestamp: new Date(), error: true, }; onSendMessage?.(errorMessage); return; } } } catch (saveError) { console.error('保存工作流时出错:', saveError); const errorMessage = { role: 'assistant', content: `❌ 保存工作流时出错:${saveError.message}`, timestamp: new Date(), error: true, }; onSendMessage?.(errorMessage); return; } } // 如果没有检测到工作流JSON,显示原始回复 const gptMessage = { role: 'assistant', content: responseText || '抱歉,我无法理解您的请求。', timestamp: new Date(), }; onSendMessage?.(gptMessage); } catch (error) { // 添加错误消息,显示更详细的错误信息 let errorContent = '抱歉,发送消息时出现错误。'; if (error.message) { if (error.message.includes('服务器错误')) { errorContent = error.message; } else if (error.message.includes('HTTP 500')) { errorContent = '服务器内部错误,请检查 API 端点是否正确,或联系服务器管理员。'; } else if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) { errorContent = '网络连接失败,请检查网络连接或服务器是否可访问。'; } else { errorContent = `错误:${error.message}`; } } const errorMessage = { role: 'assistant', content: errorContent, timestamp: new Date(), error: true, }; onSendMessage?.(errorMessage); } finally { // 结束加载 onLoadingChange?.(false); } }; return { onSend, onUpload, uploadedImages, removeImage, workflowRequirements, setWorkflowRequirements, referenceTexts, setReferenceTexts, }; } // 更新工作流JSON中的图片路径 function updateImagePathsInWorkflow(workflowJson, imageNameMap) { // 深拷贝,避免修改原始对象 const updated = JSON.parse(JSON.stringify(workflowJson)); // 递归更新actions中的图片路径 function updateActions(actions) { if (!Array.isArray(actions)) return; for (const action of actions) { // locate 和 click 操作可能使用图片 if ((action.type === 'locate' || action.type === 'click') && action.method === 'image' && action.target) { const target = action.target; // 检查是否是映射表中的文件名 if (imageNameMap[target]) { action.target = imageNameMap[target]; } else { // 尝试匹配 "1.png", "2.png" 等格式 const match = target.match(/^(\d+)\.png$/i); if (match) { const index = parseInt(match[1]) - 1; if (imageNameMap[`${index + 1}.png`]) { action.target = imageNameMap[`${index + 1}.png`]; } } } } // ocr 操作可能使用头像图片 if (action.type === 'ocr' && action.method === 'by-avatar' && action.avatar) { const avatar = action.avatar; if (imageNameMap[avatar]) { action.avatar = imageNameMap[avatar]; } else { const match = avatar.match(/^(\d+)\.png$/i); if (match) { const index = parseInt(match[1]) - 1; if (imageNameMap[`${index + 1}.png`]) { action.avatar = imageNameMap[`${index + 1}.png`]; } } } } // 递归处理嵌套的 actions(if、for、while) if (action.then && Array.isArray(action.then)) { updateActions(action.then); } if (action.else && Array.isArray(action.else)) { updateActions(action.else); } if (action.body && Array.isArray(action.body)) { updateActions(action.body); } } } if (updated.actions && Array.isArray(updated.actions)) { updateActions(updated.actions); } return updated; } // 从文本中提取JSON格式的工作流(支持新旧格式) function extractWorkflowJsonFromText(text) { if (!text) return null; // 尝试直接解析整个文本 try { const parsed = JSON.parse(text.trim()); // 新格式:包含 actions 字段 if (parsed && typeof parsed === 'object' && Array.isArray(parsed.actions)) { return parsed; } // 旧格式:直接是数组 if (Array.isArray(parsed) && parsed.length > 0) { return { actions: parsed }; } } catch (e) { // 不是纯JSON,继续尝试提取 } // 尝试从代码块中提取JSON const codeBlockMatch = text.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/); if (codeBlockMatch) { try { const parsed = JSON.parse(codeBlockMatch[1]); if (parsed && typeof parsed === 'object') { // 新格式:包含 actions if (Array.isArray(parsed.actions)) { return parsed; } // 旧格式:直接是数组 if (Array.isArray(parsed)) { return { actions: parsed }; } } } catch (e) { // 解析失败 } } // 尝试查找JSON对象(查找包含"actions"的对象,支持多行) const jsonMatch = text.match(/\{[\s\S]*?"actions"[\s\S]*?\}/); if (jsonMatch) { try { const parsed = JSON.parse(jsonMatch[0]); if (parsed && typeof parsed === 'object' && Array.isArray(parsed.actions)) { return parsed; } } catch (e) { // 解析失败,尝试更宽松的匹配 try { // 尝试匹配整个JSON对象(包括嵌套结构) const fullJsonMatch = text.match(/\{(?:[^{}]|(?:\{[^{}]*\}))*\}/); if (fullJsonMatch) { const parsed = JSON.parse(fullJsonMatch[0]); if (parsed && typeof parsed === 'object' && Array.isArray(parsed.actions)) { return parsed; } } } catch (e2) { // 解析失败 } } } return null; }