| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156 |
- 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;
- }
|