Input.js 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156
  1. import { useState, useRef } from 'react';
  2. // useInput - 管理输入组件的状态和逻辑
  3. export function useInput(onSendMessage, onLoadingChange) {
  4. const {
  5. onSend,
  6. onUpload,
  7. uploadedImages,
  8. removeImage,
  9. workflowRequirements,
  10. } = InputLogic(onSendMessage, onLoadingChange);
  11. const [message, setMessage] = useState('');
  12. const [isLoading, setIsLoading] = useState(false);
  13. const fileInputRef = useRef(null);
  14. const textInputRef = useRef(null);
  15. const handleSend = async () => {
  16. const trimmed = message.trim();
  17. // 允许在有图片时发送(即使没有文字),或者有文字时发送
  18. if ((!trimmed && uploadedImages.length === 0) || isLoading) return;
  19. // 先清空输入框(在发送前清空,提供更好的用户体验)
  20. setMessage('');
  21. setIsLoading(true);
  22. try {
  23. await onSend?.(trimmed || ''); // 如果没有文字,传递空字符串
  24. } finally {
  25. setIsLoading(false);
  26. }
  27. };
  28. const handleContainerClick = (e) => {
  29. // 如果点击的是容器本身(不是按钮、输入框或其他交互元素),则聚焦输入框
  30. if (e.target === e.currentTarget) {
  31. textInputRef.current?.focus();
  32. }
  33. };
  34. // 有文字或上传了图片就可以发送
  35. const isSendDisabled = (!message.trim() && uploadedImages.length === 0) || isLoading;
  36. const handleUploadClick = () => {
  37. fileInputRef.current?.click();
  38. };
  39. const handleFileChange = async (e) => {
  40. const files = e.target.files;
  41. if (!files || files.length === 0) return;
  42. for (const file of files) {
  43. if (file.type.startsWith('image/')) {
  44. await onUpload?.(file);
  45. }
  46. }
  47. // 清空input,允许重复选择同一文件
  48. e.target.value = '';
  49. };
  50. const handleSendKeyDown = (e) => {
  51. if ((e.key === 'Enter' || e.key === ' ') && !isSendDisabled) {
  52. e.preventDefault();
  53. handleSend();
  54. }
  55. };
  56. const handleTextareaKeyDown = (e) => {
  57. // Enter 发送(不按 Ctrl)
  58. if (e.key === 'Enter' && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
  59. e.preventDefault();
  60. handleSend();
  61. return;
  62. }
  63. // 其他所有按键都允许默认行为(包括Backspace、Delete等)
  64. // 不要阻止任何其他按键的默认行为
  65. };
  66. const handleUploadKeyDown = (e) => {
  67. if (e.key === 'Enter' || e.key === ' ') {
  68. e.preventDefault();
  69. handleUploadClick();
  70. }
  71. };
  72. const getSendButtonClassName = () => {
  73. return `send-btn ${isSendDisabled ? 'disabled' : ''}`;
  74. };
  75. const getSendButtonOnClick = () => {
  76. return !isSendDisabled ? handleSend : undefined;
  77. };
  78. const getSendButtonTabIndex = () => {
  79. return isSendDisabled ? -1 : 0;
  80. };
  81. return {
  82. message,
  83. setMessage,
  84. isLoading,
  85. fileInputRef,
  86. textInputRef,
  87. handleSend,
  88. handleContainerClick,
  89. handleUploadClick,
  90. handleFileChange,
  91. handleSendKeyDown,
  92. handleTextareaKeyDown,
  93. handleUploadKeyDown,
  94. isSendDisabled,
  95. getSendButtonClassName,
  96. getSendButtonOnClick,
  97. getSendButtonTabIndex,
  98. uploadedImages,
  99. removeImage,
  100. };
  101. }
  102. // InputLogic - 处理消息发送到GPT API
  103. // 第一阶段提示词:分析需求,返回需要的信息
  104. const REQUIREMENTS_PROMPT = `你是一个友好的自动化工作流生成助手。你需要像聊天一样与用户对话。
  105. 如果用户想要创建自动化工作流,你需要分析需求并返回需要的信息。请用自然、友好的语言与用户交流,保持对话的连贯性和友好性。
  106. 语法分层(重要):
  107. - 基础语法(控制流/定时):scheduleOnce、schedule、for、while、if
  108. - 基础 action(模拟手机操作):press、swipe、scroll
  109. - 扩展标签(Func):除基础能力外的所有扩展功能,全部由 src/pages/processing/func/ 目录下的脚本文件决定;每个脚本文件名就是一个"标签"。
  110. 在生成工作流时:
  111. - 逻辑结构尽量用 for/while/if + triggers(schedules) 表达
  112. - 具体手机操作尽量落到 press/swipe/scroll
  113. - 当需求超出基础 action(例如:提取会话/消息记录、文字定位等),请使用对应的 Func 标签(例如:extract-chat-history、string-reg-location、save-chat-history)
  114. (实现层说明:项目内部为兼容与落地,仍存在 locate/click/input/ocr/ai-generate/delay/set 等内置能力;但提示用户与生成语法时,请优先按上述分层来组织。)
  115. 9. if - 条件判断
  116. - condition: 条件表达式(如 "{count} > 5")
  117. - then: 条件为真时执行的操作数组
  118. - else: 条件为假时执行的操作数组(可选)
  119. 10. for - 循环
  120. - variable: 循环变量名
  121. - items: 数组(可以是变量 {arrayVariable})
  122. - body: 循环体操作数组
  123. 11. delay - 延迟
  124. - value: 延迟时间(如 "2s", "1m", "30 minutes")
  125. 12. set - 设置变量
  126. - variable: 变量名
  127. - value: 变量值
  128. 13. scroll/swipe - 滚动/滑动
  129. - value: 方向("up-down", "down-up", "left-right", "right-left")
  130. 定时任务配置:
  131. - 单次执行: {"datetime": "2026/1/14 01:21"}
  132. - 每天执行: {"time": "09:00", "repeat": "daily"}
  133. - 每周执行: {"time": "09:00", "weekdays": ["monday", "wednesday", "friday"]}
  134. - 间隔执行: {"interval": "30 minutes"}
  135. 重要规则:
  136. - 变量使用 {variableName} 格式引用
  137. - 根据用户描述合理设置delay延迟时间
  138. - 如果用户提到循环、条件判断,使用for/if操作
  139. - 如果用户提到定时执行,添加triggers配置
  140. - 如果用户提到AI生成内容,使用ai-generate操作
  141. - 如果用户提到消息记录、聊天记录、历史记录、对话记录,使用extract-messages、save-messages、generate-summary操作
  142. - 图片文件名按顺序命名为"1.png"、"2.png"等
  143. - **特别重要**:如果工作流中需要输入文字(使用 input 或 ai-generate 操作),且这些文字需要根据场景背景和用户角色来生成,你必须询问用户以下信息:
  144. - 场景的背景:这个工作流在什么场景下使用?(例如:微信聊天、客服回复、评论互动等)
  145. - 用户的角色:用户在这个场景中扮演什么角色?(例如:刚认识女方的男生、客服人员、产品推广者等)
  146. - 这些信息对于生成合适的文字内容非常重要,请务必在询问需求时主动询问
  147. 请分析用户的需求,根据上述操作类型,判断需要哪些图片和文字参考:
  148. - 图片(needsImages):用于 locate/click 的 image 方法,或 ocr 的 by-avatar 方法
  149. - 文字参考(needsTexts):用于 locate/click 的 text 方法
  150. 如果用户想要创建自动化工作流:
  151. 1. 先友好地确认理解用户的需求
  152. 2. 然后返回JSON格式的需求信息(不包含在自然语言回复中,仅作为JSON返回)
  153. 需求分析的JSON格式:
  154. {
  155. "needsImages": 2, // 需要多少张图片(用于图像匹配定位)
  156. "needsTexts": 1, // 需要多少个文字参考(用于文字识别定位)
  157. "imageDescriptions": ["登录按钮", "用户名输入框"], // 每张图片的描述
  158. "textDescriptions": ["登录按钮文字"] // 每个文字参考的描述
  159. }
  160. 返回格式:先自然语言回复,然后单独一行JSON。
  161. **特别提醒**:如果工作流涉及输入文字(如自动回复、自动评论、自动发送消息等),请务必询问用户场景背景和角色信息。
  162. 例如(不涉及文字输入的场景):
  163. 我理解了,您想创建一个登录流程的自动化工作流。为了生成准确的工作流配置,我需要您提供以下信息:
  164. 📋 需要的信息:
  165. 🖼️ 需要 2 张图片:登录按钮截图、用户名输入框截图
  166. 📝 需要 1 个文字参考:登录按钮上的文字
  167. \`\`\`json
  168. {
  169. "needsImages": 2,
  170. "needsTexts": 1,
  171. "imageDescriptions": ["登录按钮", "用户名输入框"],
  172. "textDescriptions": ["登录按钮文字"]
  173. }
  174. \`\`\`
  175. 例如(涉及文字输入的场景):
  176. 我理解了,您想创建一个自动聊天回复的自动化工作流。为了生成准确且符合场景的文字内容,我需要了解以下信息:
  177. 📋 需要的信息:
  178. 🖼️ 需要 2 张图片:好友头像截图、我的头像截图
  179. 📝 需要 1 个文字参考:输入框位置
  180. 💬 **场景信息**(重要):
  181. - 场景背景:这个聊天是在什么场景下进行的?(例如:微信聊天、QQ群聊、客服对话等)
  182. - 您的角色:您在这个场景中扮演什么角色?(例如:刚认识女方的男生、客服人员、产品推广者等)
  183. \`\`\`json
  184. {
  185. "needsImages": 2,
  186. "needsTexts": 1,
  187. "imageDescriptions": ["好友头像", "我的头像"],
  188. "textDescriptions": ["输入框位置"]
  189. }
  190. \`\`\`
  191. 如果用户的话题与工作流无关,请友好地自然回复,不需要返回JSON。如果用户是在询问如何使用、或者其他一般性问题,请用自然语言友好地回答。`;
  192. // 第二阶段提示词:生成工作流
  193. const SYSTEM_PROMPT = `你是一个友好的自动化工作流生成助手。用户已经提供了所需的图片和文字参考,现在你需要生成JSON格式的工作流配置文件。
  194. 请用自然、友好的语言与用户交流,保持对话的连贯性。在生成工作流时,请先简短地确认一下,然后生成JSON格式的工作流配置。
  195. 工作流JSON格式(新版本):
  196. {
  197. "version": "1.0",
  198. "name": "工作流名称",
  199. "triggers": [
  200. {
  201. "type": "schedule",
  202. "schedule": {
  203. "time": "09:00",
  204. "weekdays": ["monday", "wednesday", "friday"]
  205. }
  206. }
  207. ],
  208. "variables": {
  209. "topic": "美食"
  210. },
  211. "actions": [
  212. {
  213. "type": "locate",
  214. "method": "image",
  215. "target": "button.png",
  216. "variable": "buttonPos",
  217. "delay": "1s"
  218. },
  219. {
  220. "type": "click",
  221. "method": "position",
  222. "target": "{buttonPos}",
  223. "delay": "2s"
  224. },
  225. {
  226. "type": "input",
  227. "target": "输入框",
  228. "value": "文字内容",
  229. "delay": "1s"
  230. },
  231. {
  232. "type": "ai-generate",
  233. "prompt": "生成关于{topic}的文章",
  234. "variable": "article",
  235. "delay": "10s"
  236. },
  237. {
  238. "type": "if",
  239. "condition": "{count} > 5",
  240. "then": [
  241. {
  242. "type": "click",
  243. "method": "text",
  244. "target": "确定"
  245. }
  246. ]
  247. },
  248. {
  249. "type": "for",
  250. "variable": "item",
  251. "items": ["选项1", "选项2"],
  252. "body": [
  253. {
  254. "type": "click",
  255. "method": "text",
  256. "target": "{item}"
  257. }
  258. ]
  259. }
  260. ]
  261. }
  262. 核心操作类型:
  263. 1. locate - 定位元素
  264. - method: "image"(图像匹配)| "text"(文字识别)| "coordinate"(坐标)
  265. - target: 目标内容
  266. - variable: 保存位置到变量
  267. 2. click - 点击
  268. - method: "position" | "image" | "text" | "coordinate"
  269. - target: 目标(可以是变量 {variable})
  270. 3. input - 输入文字
  271. - target: 输入框位置(文字或变量)
  272. - value: 要输入的文本(可以是变量 {variable})
  273. 4. ai-generate - AI生成内容
  274. - prompt: 提示词(支持变量 {variable})
  275. - variable: 保存结果到变量
  276. 5. ocr - 文字识别
  277. - method: "by-avatar" | "by-position" | "full-screen"
  278. - avatar: 头像图片(by-avatar时)
  279. - variable: 保存识别结果
  280. 6. extract-messages - 提取消息记录
  281. - avatar1: 第一个参与者的头像/标识图片路径
  282. - avatar2: 第二个参与者的头像/标识图片路径
  283. - variable: 保存消息记录到变量(格式:对方: 消息\n我: 消息)
  284. 7. save-messages - 保存消息记录
  285. - variable: 包含消息记录的变量名(保存到 history 文件夹)
  286. 8. generate-summary - 生成消息总结
  287. - variable: 包含消息记录的变量名
  288. - summaryVariable: 保存总结结果的变量名
  289. - model: AI模型名称(可选)
  290. 9. if - 条件判断
  291. - condition: 条件表达式(如 "{count} > 5")
  292. - then: 条件为真时执行的操作数组
  293. - else: 条件为假时执行的操作数组(可选)
  294. 10. for - 循环
  295. - variable: 循环变量名
  296. - items: 数组(可以是变量 {arrayVariable})
  297. - body: 循环体操作数组
  298. 11. delay - 延迟
  299. - value: 延迟时间(如 "2s", "1m", "30 minutes")
  300. 12. set - 设置变量
  301. - variable: 变量名
  302. - value: 变量值
  303. 13. scroll/swipe - 滚动/滑动
  304. - value: 方向("up-down", "down-up", "left-right", "right-left")
  305. 定时任务配置:
  306. - 单次执行: {"datetime": "2026/1/14 01:21"}
  307. - 每天执行: {"time": "09:00", "repeat": "daily"}
  308. - 每周执行: {"time": "09:00", "weekdays": ["monday", "wednesday", "friday"]}
  309. - 间隔执行: {"interval": "30 minutes"}
  310. 重要规则:
  311. - 使用新格式(type字段),但保持向后兼容旧格式
  312. - 变量使用 {variableName} 格式引用
  313. - 根据用户描述合理设置delay延迟时间
  314. - 如果用户提到循环、条件判断,使用for/if操作
  315. - 如果用户提到定时执行,添加triggers配置
  316. - 如果用户提到AI生成内容,使用ai-generate操作
  317. - 如果用户提到消息记录、聊天记录、历史记录、对话记录,使用extract-messages、save-messages、generate-summary操作
  318. - 在ai-generate的prompt中可以使用总结变量引用历史消息记录的总结
  319. - 图片文件名按顺序命名为"1.png"、"2.png"等
  320. 返回格式:
  321. 1. 如果用户已经提供了所有需要的图片和文字参考,且明确要求生成工作流:
  322. - 先用简短的自然语言确认(如"好的,我现在为您生成工作流配置...")
  323. - 然后单独一行返回JSON格式的工作流配置(可以用代码块包裹)
  324. 2. 如果用户只是在询问问题或者还没准备好生成工作流:
  325. - 用自然语言友好地回答用户的问题
  326. - 不需要返回JSON
  327. 请保持对话的自然和友好,不要生硬地只返回JSON。`;
  328. export function InputLogic(onSendMessage, onLoadingChange) {
  329. // 存储上传的图片
  330. const [uploadedImages, setUploadedImages] = useState([]);
  331. // img2text API可用性标记(使用useRef避免重复输出警告)
  332. const img2textApiAvailableRef = useRef(true);
  333. // 调用img2text API获取图片描述
  334. const img2text = async (imageBase64, prompt = '请描述这张图片的内容') => {
  335. // 如果已经知道API不可用,直接返回null,不尝试调用
  336. if (!img2textApiAvailableRef.current) {
  337. return null;
  338. }
  339. try {
  340. // 确保base64数据是完整的data URL格式
  341. let imageUrl = imageBase64;
  342. if (!imageBase64.startsWith('data:')) {
  343. // 如果没有前缀,假设是PNG格式的base64数据
  344. imageUrl = `data:image/png;base64,${imageBase64}`;
  345. }
  346. // 使用AbortController设置超时,快速失败
  347. const controller = new AbortController();
  348. const timeoutId = setTimeout(() => controller.abort(), 1000); // 1秒超时
  349. const response = await fetch('https://ai-anim.com/api/img2text', {
  350. method: 'POST',
  351. headers: {
  352. 'Content-Type': 'application/json',
  353. },
  354. body: JSON.stringify({
  355. prompt: prompt,
  356. imageUrl: imageUrl
  357. }),
  358. signal: controller.signal,
  359. });
  360. clearTimeout(timeoutId);
  361. if (!response.ok) {
  362. const errorText = await response.text();
  363. throw new Error(`img2text API error: ${response.status} ${response.statusText} - ${errorText}`);
  364. }
  365. const data = await response.json();
  366. // 支持多种可能的返回格式
  367. return data.text || data.description || data.result || data.output_text || data.content || '';
  368. } catch (error) {
  369. // img2text API是可选的,失败不影响主要功能
  370. // 标记API不可用,之后直接返回null,不再尝试
  371. if (img2textApiAvailableRef.current) {
  372. img2textApiAvailableRef.current = false;
  373. }
  374. // 静默返回null,不输出任何错误信息(因为这是可选功能)
  375. return null;
  376. }
  377. };
  378. // 存储工作流需求信息(需要多少图片和文字)
  379. const [workflowRequirements, setWorkflowRequirements] = useState(null);
  380. // 存储用户输入的文字参考(用于文字识别)
  381. const [referenceTexts, setReferenceTexts] = useState([]);
  382. // 存储AI生成的工作流JSON(不显示在聊天框)
  383. const [workflowJson, setWorkflowJson] = useState(null);
  384. // 删除图片
  385. const removeImage = (index) => {
  386. setUploadedImages(prev => prev.filter((_, i) => i !== index));
  387. };
  388. // 上传图片并命名
  389. const onUpload = async (file) => {
  390. try {
  391. // 读取图片为base64
  392. const reader = new FileReader();
  393. const base64 = await new Promise((resolve, reject) => {
  394. reader.onload = () => {
  395. const result = reader.result;
  396. // 提取base64数据部分(去掉data:image/...;base64,前缀)
  397. const base64Data = result.split(',')[1];
  398. resolve(base64Data);
  399. };
  400. reader.onerror = reject;
  401. reader.readAsDataURL(file);
  402. });
  403. // 生成临时文件名(使用时间戳)
  404. const timestamp = Date.now();
  405. const tempFileName = `temp_${timestamp}.${file.name.split('.').pop()}`;
  406. // 存储图片信息
  407. const imageInfo = {
  408. file: file,
  409. base64: base64,
  410. tempName: tempFileName,
  411. originalName: file.name,
  412. timestamp: timestamp,
  413. };
  414. setUploadedImages(prev => [...prev, imageInfo]);
  415. console.log('图片已上传:', imageInfo.originalName);
  416. } catch (error) {
  417. console.error('上传图片失败:', error);
  418. }
  419. };
  420. // 根据用户描述给图片命名(使用AI)
  421. const nameImagesWithAI = async (userPrompt, images) => {
  422. if (!images || images.length === 0) return images;
  423. try {
  424. // 构建提示词,让AI根据用户描述给图片命名
  425. const imagePrompt = `用户上传了 ${images.length} 张图片,用于工作流自动化。请根据用户的描述,为每张图片生成一个简洁的描述性文件名(只返回JSON数组,不要其他文字)。
  426. 用户描述:${userPrompt}
  427. 图片信息:
  428. ${images.map((img, index) => `图片${index + 1}: 原始文件名 ${img.originalName}`).join('\n')}
  429. 请返回JSON数组,格式如下:
  430. ["登录按钮.png", "用户名输入框.png", "密码输入框.png"]
  431. 要求:
  432. 1. 文件名要简洁,能清楚描述图片内容
  433. 2. 使用中文或英文都可以
  434. 3. 必须包含文件扩展名(.png)
  435. 4. 文件名要符合工作流中使用的命名规范`;
  436. const response = await fetch('https://ai-anim.com/api/text2textByModel', {
  437. method: 'POST',
  438. headers: { 'Content-Type': 'application/json' },
  439. body: JSON.stringify({
  440. prompt: imagePrompt,
  441. modelName: 'gpt-5-nano-ca'
  442. })
  443. });
  444. if (!response.ok) {
  445. throw new Error('AI命名失败');
  446. }
  447. const data = await response.json();
  448. const responseText = data.data?.output_text || data.text || data.content || '';
  449. // 解析AI返回的文件名数组
  450. let imageNames = [];
  451. try {
  452. // 尝试直接解析JSON
  453. imageNames = JSON.parse(responseText.trim());
  454. } catch (e) {
  455. // 尝试从代码块中提取
  456. const match = responseText.match(/\[[\s\S]*?\]/);
  457. if (match) {
  458. imageNames = JSON.parse(match[0]);
  459. } else {
  460. // 如果解析失败,使用默认命名
  461. imageNames = images.map((img, index) => `${index + 1}.png`);
  462. }
  463. }
  464. // 更新图片信息,添加AI生成的文件名
  465. return images.map((img, index) => ({
  466. ...img,
  467. aiName: imageNames[index] || `${index + 1}.png`
  468. }));
  469. } catch (error) {
  470. console.error('AI命名图片失败:', error);
  471. // 如果AI命名失败,使用默认命名
  472. return images.map((img, index) => ({
  473. ...img,
  474. aiName: `${index + 1}.png`
  475. }));
  476. }
  477. };
  478. const sendToGPT = async (userPrompt, images = []) => {
  479. // 构建图片信息提示(如果图片已经命名)
  480. let imageInfoPrompt = '';
  481. if (images.length > 0) {
  482. imageInfoPrompt = `\n\n用户上传了以下图片,请在生成工作流时使用这些图片文件名:\n${images.map((img, index) => `${index + 1}. ${img.aiName || img.tempName} - ${img.originalName}`).join('\n')}\n\n在生成工作流时,如果操作需要点击图片,请使用上述图片文件名(如 "登录按钮.png")。`;
  483. }
  484. // 构建完整的提示词:系统提示词 + 图片信息 + 用户输入
  485. const fullPrompt = `${SYSTEM_PROMPT}${imageInfoPrompt}\n\n用户需求:${userPrompt}`;
  486. const requestBody = {
  487. input: fullPrompt
  488. };
  489. console.log('Sending request to Doubao API:', requestBody);
  490. const response = await fetch('https://ai-anim.com/api/doubaoText2text', {
  491. method: 'POST',
  492. headers: {
  493. 'Content-Type': 'application/json',
  494. },
  495. body: JSON.stringify(requestBody),
  496. });
  497. console.log('Response status:', response.status, response.statusText);
  498. if (!response.ok) {
  499. let errorText;
  500. try {
  501. errorText = await response.text();
  502. // 尝试解析为 JSON
  503. try {
  504. const errorJson = JSON.parse(errorText);
  505. console.error('API Error Response (JSON):', errorJson);
  506. // 如果 JSON 中有 error 字段,使用它
  507. if (errorJson.error) {
  508. throw new Error(`服务器错误: ${errorJson.error}`);
  509. }
  510. if (errorJson.message) {
  511. throw new Error(`服务器错误: ${errorJson.message}`);
  512. }
  513. } catch (parseError) {
  514. // 不是 JSON,使用原始文本
  515. console.error('API Error Response (Text):', errorText);
  516. }
  517. } catch (readError) {
  518. errorText = `无法读取错误响应: ${readError.message}`;
  519. }
  520. throw new Error(`HTTP ${response.status}: ${errorText}`);
  521. }
  522. const data = await response.json();
  523. console.log('API Response:', data);
  524. return data;
  525. };
  526. // 分析需求,返回需要的信息
  527. const analyzeRequirements = async (userPrompt) => {
  528. const response = await fetch('https://ai-anim.com/api/text2textByModel', {
  529. method: 'POST',
  530. headers: { 'Content-Type': 'application/json' },
  531. body: JSON.stringify({
  532. prompt: `${REQUIREMENTS_PROMPT}\n\n用户需求:${userPrompt}`,
  533. modelName: 'gpt-5-nano-ca'
  534. })
  535. });
  536. if (!response.ok) {
  537. throw new Error('分析需求失败');
  538. }
  539. const data = await response.json();
  540. const responseText = data.data?.output_text || data.text || data.content || '';
  541. // 提取自然语言回复(去除JSON部分)
  542. let naturalLanguageReply = responseText.trim();
  543. // 尝试从回复中提取JSON(可能在代码块中,也可能单独一行)
  544. let requirements = null;
  545. // 方法1: 尝试从代码块中提取JSON
  546. const codeBlockMatch = responseText.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
  547. if (codeBlockMatch) {
  548. try {
  549. requirements = JSON.parse(codeBlockMatch[1]);
  550. // 移除代码块部分,保留自然语言
  551. naturalLanguageReply = responseText.replace(/```(?:json)?\s*\{[\s\S]*?\}\s*```/g, '').trim();
  552. } catch (e) {
  553. // 解析失败,继续尝试其他方法
  554. }
  555. }
  556. // 方法2: 尝试匹配JSON对象
  557. if (!requirements) {
  558. const jsonMatch = responseText.match(/\{[\s\S]*"needsImages"[\s\S]*\}/);
  559. if (jsonMatch) {
  560. try {
  561. requirements = JSON.parse(jsonMatch[0]);
  562. // 移除JSON部分,保留自然语言
  563. naturalLanguageReply = responseText.replace(/\{[\s\S]*"needsImages"[\s\S]*\}/, '').trim();
  564. } catch (e) {
  565. // 解析失败
  566. }
  567. }
  568. }
  569. // 方法3: 尝试直接解析整个文本
  570. if (!requirements) {
  571. try {
  572. requirements = JSON.parse(responseText.trim());
  573. } catch (e) {
  574. // 不是纯JSON
  575. }
  576. }
  577. // 如果找到JSON,返回JSON和自然语言回复
  578. if (requirements && typeof requirements === 'object') {
  579. // 将自然语言回复保存到requirements中,以便后续显示
  580. if (naturalLanguageReply && naturalLanguageReply !== responseText.trim()) {
  581. requirements.naturalLanguageReply = naturalLanguageReply;
  582. }
  583. return requirements;
  584. }
  585. // 如果没有找到JSON,可能是用户的问题与工作流无关,返回错误信息
  586. return {
  587. error: naturalLanguageReply || '请描述你的工作流需求,例如:生成一个登录流程,先点击登录按钮,然后输入用户名和密码'
  588. };
  589. };
  590. // 解析用户输入的文字参考(按行分割)
  591. const parseReferenceTexts = (text) => {
  592. // 如果已经有工作流需求,提取文字参考
  593. if (workflowRequirements && workflowRequirements.needsTexts > 0) {
  594. // 尝试从文本中提取,可能是用换行、逗号或分号分隔
  595. const lines = text.split(/[\n,,;;]/).map(t => t.trim()).filter(t => t);
  596. return lines.slice(0, workflowRequirements.needsTexts);
  597. }
  598. return [];
  599. };
  600. const onSend = async (text) => {
  601. if (!text.trim() && uploadedImages.length === 0) return;
  602. // 保存上传的图片信息(用于后续使用和显示)
  603. const currentImages = [...uploadedImages];
  604. // 添加用户消息(包含图片信息,包含base64数据以便在聊天界面显示)
  605. const userMessage = {
  606. role: 'user',
  607. content: text,
  608. images: currentImages.map(img => ({
  609. name: img.originalName,
  610. tempName: img.tempName,
  611. base64: img.base64, // 包含base64数据,用于在聊天界面显示
  612. originalName: img.originalName
  613. })),
  614. timestamp: new Date(),
  615. };
  616. onSendMessage?.(userMessage);
  617. // 发送消息后立即清空上传的图片,让输入框的提示方块消失
  618. setUploadedImages([]);
  619. // 开始加载
  620. onLoadingChange?.(true);
  621. try {
  622. // 第一阶段:如果没有需求信息,先分析需求
  623. if (!workflowRequirements) {
  624. // 如果有参考图,先调用img2text获取图片描述
  625. let imageDescriptionsPrompt = '';
  626. if (currentImages.length > 0) {
  627. const imageDescriptions = [];
  628. for (const img of currentImages) {
  629. const description = await img2text(img.base64, '请详细描述这张图片的内容,包括图片中的界面元素、按钮、文字等信息');
  630. if (description) {
  631. imageDescriptions.push(description);
  632. }
  633. }
  634. if (imageDescriptions.length > 0) {
  635. imageDescriptionsPrompt = `\n\n用户提供了以下参考图片及其描述:\n${imageDescriptions.map((desc, index) => `图片${index + 1}:${desc}`).join('\n')}\n\n请根据这些图片描述,分析用户需要哪些图片和文字参考来生成工作流。`;
  636. }
  637. }
  638. const requirements = await analyzeRequirements(text + imageDescriptionsPrompt);
  639. if (requirements.error) {
  640. // 如果返回错误信息,直接显示(可能是自然语言回复)
  641. const errorMessage = {
  642. role: 'assistant',
  643. content: requirements.error,
  644. timestamp: new Date(),
  645. };
  646. onSendMessage?.(errorMessage);
  647. return;
  648. }
  649. // 保存需求信息
  650. setWorkflowRequirements(requirements);
  651. // 显示需求信息给用户(优先显示AI的自然语言回复,然后显示需求列表)
  652. const needsImages = requirements.needsImages || 0;
  653. const needsTexts = requirements.needsTexts || 0;
  654. let requirementMessage = '';
  655. // 如果有自然语言回复,先显示
  656. if (requirements.naturalLanguageReply) {
  657. requirementMessage = requirements.naturalLanguageReply + '\n\n';
  658. }
  659. // 然后显示需求信息
  660. if (needsImages > 0 || needsTexts > 0) {
  661. requirementMessage += '📋 根据您的需求,我需要以下信息:\n\n';
  662. if (needsImages > 0) {
  663. requirementMessage += `🖼️ 需要 ${needsImages} 张图片:\n`;
  664. (requirements.imageDescriptions || []).forEach((desc, index) => {
  665. requirementMessage += `${index + 1}. ${desc}\n`;
  666. });
  667. requirementMessage += '\n';
  668. }
  669. if (needsTexts > 0) {
  670. requirementMessage += `📝 需要 ${needsTexts} 个文字参考:\n`;
  671. (requirements.textDescriptions || []).forEach((desc, index) => {
  672. requirementMessage += `${index + 1}. ${desc}\n`;
  673. });
  674. requirementMessage += '\n';
  675. }
  676. requirementMessage += '请按顺序上传图片和输入文字参考,然后再次发送消息。';
  677. }
  678. const reqMessage = {
  679. role: 'assistant',
  680. content: requirementMessage || '好的,我理解了您的需求。',
  681. timestamp: new Date(),
  682. };
  683. onSendMessage?.(reqMessage);
  684. return;
  685. }
  686. // 第二阶段:验证用户提供的信息是否完整
  687. const needsImages = workflowRequirements.needsImages || 0;
  688. const needsTexts = workflowRequirements.needsTexts || 0;
  689. const providedImages = currentImages.length;
  690. const parsedTexts = parseReferenceTexts(text);
  691. const providedTexts = parsedTexts.length;
  692. if (providedImages < needsImages || providedTexts < needsTexts) {
  693. // 信息不完整,提示用户
  694. let missingMessage = '❌ 信息不完整,请提供:\n\n';
  695. if (providedImages < needsImages) {
  696. missingMessage += `🖼️ 还需要 ${needsImages - providedImages} 张图片\n`;
  697. }
  698. if (providedTexts < needsTexts) {
  699. missingMessage += `📝 还需要 ${needsTexts - providedTexts} 个文字参考\n`;
  700. }
  701. missingMessage += '\n请按顺序上传图片和输入文字参考,然后再次发送消息。';
  702. const missingMsg = {
  703. role: 'assistant',
  704. content: missingMessage,
  705. timestamp: new Date(),
  706. };
  707. onSendMessage?.(missingMsg);
  708. return;
  709. }
  710. // 第三阶段:信息完整,生成工作流
  711. let namedImages = [];
  712. if (currentImages.length > 0) {
  713. try {
  714. // 使用需求描述给图片命名
  715. const imageDescriptions = workflowRequirements.imageDescriptions || [];
  716. namedImages = currentImages.map((img, index) => ({
  717. ...img,
  718. aiName: imageDescriptions[index]
  719. ? `${imageDescriptions[index]}.png`
  720. : `${index + 1}.png`
  721. }));
  722. } catch (nameError) {
  723. console.error('图片命名失败,使用默认命名:', nameError);
  724. namedImages = currentImages.map((img, index) => ({
  725. ...img,
  726. aiName: `${index + 1}.png`
  727. }));
  728. }
  729. }
  730. // 构建包含文字参考的提示词
  731. let textReferencesPrompt = '';
  732. if (parsedTexts.length > 0) {
  733. textReferencesPrompt = `\n\n用户提供的文字参考:\n${parsedTexts.map((t, i) => `${i + 1}. ${t}`).join('\n')}\n\n在生成工作流时,如果操作需要定位文字,请使用上述文字参考。`;
  734. // 保存文字参考到state
  735. setReferenceTexts(parsedTexts);
  736. }
  737. // 如果有参考图,调用img2text获取图片描述并加入到prompt中
  738. let imageDescriptionsPrompt = '';
  739. if (currentImages.length > 0) {
  740. const imageDescriptions = [];
  741. for (const img of currentImages) {
  742. const description = await img2text(img.base64, '请详细描述这张图片的内容,包括图片中的界面元素、按钮、文字、布局等信息,这些信息将用于生成自动化工作流');
  743. if (description) {
  744. imageDescriptions.push(description);
  745. }
  746. }
  747. if (imageDescriptions.length > 0) {
  748. imageDescriptionsPrompt = `\n\n参考图片描述:\n${imageDescriptions.map((desc, index) => {
  749. const imgName = namedImages[index]?.aiName || `${index + 1}.png`;
  750. return `图片 ${imgName}:${desc}`;
  751. }).join('\n')}\n\n请根据这些图片描述,在生成工作流时合理使用对应的图片文件名。`;
  752. }
  753. }
  754. // 发送到GPT并获取回复(传递已命名的图片信息和文字参考)
  755. const response = await sendToGPT(text + textReferencesPrompt + imageDescriptionsPrompt, namedImages);
  756. // 根据实际 API 响应结构解析文本
  757. // API 返回: { success: true, data: { output_text: "..." } }
  758. const responseText = response.data?.output_text ||
  759. response.text ||
  760. response.content ||
  761. response.message ||
  762. response.data?.text ||
  763. response.data?.content ||
  764. response.result ||
  765. (typeof response === 'string' ? response : JSON.stringify(response));
  766. // 检测是否是JSON格式的工作流
  767. const detectedWorkflowJson = extractWorkflowJsonFromText(responseText);
  768. if (detectedWorkflowJson) {
  769. // 如果检测到工作流JSON,保存到内存中,不显示在聊天框
  770. setWorkflowJson(detectedWorkflowJson);
  771. // 准备图片数据(base64和文件名)
  772. const imagesData = namedImages.map(img => ({
  773. name: img.aiName || img.tempName,
  774. base64: img.base64,
  775. originalName: img.originalName
  776. }));
  777. // 建立图片文件名映射:将JSON中可能使用的简单文件名(如 "1.png")映射到实际保存的文件名
  778. // 这样即使AI使用了 "1.png"、"2.png" 等简单名称,也能正确映射到实际的文件名
  779. const imageNameMap = {};
  780. namedImages.forEach((img, index) => {
  781. const actualName = img.aiName || img.tempName;
  782. // 映射 "1.png", "2.png" 等
  783. imageNameMap[`${index + 1}.png`] = actualName;
  784. // 也映射原始名称(如果有)
  785. if (img.originalName) {
  786. imageNameMap[img.originalName] = actualName;
  787. }
  788. });
  789. // 在保存前,更新工作流JSON中的图片文件名
  790. const updatedWorkflowJson = updateImagePathsInWorkflow(detectedWorkflowJson, imageNameMap);
  791. // 直接保存工作流(不显示JSON内容)
  792. try {
  793. if (window.electronAPI && window.electronAPI.saveWorkflow) {
  794. const saveResult = await window.electronAPI.saveWorkflow(updatedWorkflowJson, imagesData);
  795. if (saveResult.success) {
  796. // 保存成功,清空所有状态(图片已经在发送时清空)
  797. setWorkflowRequirements(null);
  798. setReferenceTexts([]);
  799. setWorkflowJson(null);
  800. // 显示成功消息(不包含JSON内容)
  801. const successMessage = {
  802. role: 'assistant',
  803. content: `✅ 工作流已成功生成并保存!\n\n文件夹名称:${saveResult.folderName}`,
  804. timestamp: new Date(),
  805. };
  806. onSendMessage?.(successMessage);
  807. // 触发工作流列表刷新事件
  808. window.dispatchEvent(new CustomEvent('workflow-saved'));
  809. return;
  810. } else {
  811. // 保存失败,显示错误
  812. console.error('保存工作流失败:', saveResult.error);
  813. const errorMessage = {
  814. role: 'assistant',
  815. content: `❌ 保存工作流失败:${saveResult.error}`,
  816. timestamp: new Date(),
  817. error: true,
  818. };
  819. onSendMessage?.(errorMessage);
  820. return;
  821. }
  822. }
  823. } catch (saveError) {
  824. console.error('保存工作流时出错:', saveError);
  825. const errorMessage = {
  826. role: 'assistant',
  827. content: `❌ 保存工作流时出错:${saveError.message}`,
  828. timestamp: new Date(),
  829. error: true,
  830. };
  831. onSendMessage?.(errorMessage);
  832. return;
  833. }
  834. }
  835. // 如果没有检测到工作流JSON,显示原始回复
  836. const gptMessage = {
  837. role: 'assistant',
  838. content: responseText || '抱歉,我无法理解您的请求。',
  839. timestamp: new Date(),
  840. };
  841. onSendMessage?.(gptMessage);
  842. } catch (error) {
  843. // 添加错误消息,显示更详细的错误信息
  844. let errorContent = '抱歉,发送消息时出现错误。';
  845. if (error.message) {
  846. if (error.message.includes('服务器错误')) {
  847. errorContent = error.message;
  848. } else if (error.message.includes('HTTP 500')) {
  849. errorContent = '服务器内部错误,请检查 API 端点是否正确,或联系服务器管理员。';
  850. } else if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
  851. errorContent = '网络连接失败,请检查网络连接或服务器是否可访问。';
  852. } else {
  853. errorContent = `错误:${error.message}`;
  854. }
  855. }
  856. const errorMessage = {
  857. role: 'assistant',
  858. content: errorContent,
  859. timestamp: new Date(),
  860. error: true,
  861. };
  862. onSendMessage?.(errorMessage);
  863. } finally {
  864. // 结束加载
  865. onLoadingChange?.(false);
  866. }
  867. };
  868. return {
  869. onSend,
  870. onUpload,
  871. uploadedImages,
  872. removeImage,
  873. workflowRequirements,
  874. setWorkflowRequirements,
  875. referenceTexts,
  876. setReferenceTexts,
  877. };
  878. }
  879. // 更新工作流JSON中的图片路径
  880. function updateImagePathsInWorkflow(workflowJson, imageNameMap) {
  881. // 深拷贝,避免修改原始对象
  882. const updated = JSON.parse(JSON.stringify(workflowJson));
  883. // 递归更新actions中的图片路径
  884. function updateActions(actions) {
  885. if (!Array.isArray(actions)) return;
  886. for (const action of actions) {
  887. // locate 和 click 操作可能使用图片
  888. if ((action.type === 'locate' || action.type === 'click') && action.method === 'image' && action.target) {
  889. const target = action.target;
  890. // 检查是否是映射表中的文件名
  891. if (imageNameMap[target]) {
  892. action.target = imageNameMap[target];
  893. } else {
  894. // 尝试匹配 "1.png", "2.png" 等格式
  895. const match = target.match(/^(\d+)\.png$/i);
  896. if (match) {
  897. const index = parseInt(match[1]) - 1;
  898. if (imageNameMap[`${index + 1}.png`]) {
  899. action.target = imageNameMap[`${index + 1}.png`];
  900. }
  901. }
  902. }
  903. }
  904. // ocr 操作可能使用头像图片
  905. if (action.type === 'ocr' && action.method === 'by-avatar' && action.avatar) {
  906. const avatar = action.avatar;
  907. if (imageNameMap[avatar]) {
  908. action.avatar = imageNameMap[avatar];
  909. } else {
  910. const match = avatar.match(/^(\d+)\.png$/i);
  911. if (match) {
  912. const index = parseInt(match[1]) - 1;
  913. if (imageNameMap[`${index + 1}.png`]) {
  914. action.avatar = imageNameMap[`${index + 1}.png`];
  915. }
  916. }
  917. }
  918. }
  919. // 递归处理嵌套的 actions(if、for、while)
  920. if (action.then && Array.isArray(action.then)) {
  921. updateActions(action.then);
  922. }
  923. if (action.else && Array.isArray(action.else)) {
  924. updateActions(action.else);
  925. }
  926. if (action.body && Array.isArray(action.body)) {
  927. updateActions(action.body);
  928. }
  929. }
  930. }
  931. if (updated.actions && Array.isArray(updated.actions)) {
  932. updateActions(updated.actions);
  933. }
  934. return updated;
  935. }
  936. // 从文本中提取JSON格式的工作流(支持新旧格式)
  937. function extractWorkflowJsonFromText(text) {
  938. if (!text) return null;
  939. // 尝试直接解析整个文本
  940. try {
  941. const parsed = JSON.parse(text.trim());
  942. // 新格式:包含 actions 字段
  943. if (parsed && typeof parsed === 'object' && Array.isArray(parsed.actions)) {
  944. return parsed;
  945. }
  946. // 旧格式:直接是数组
  947. if (Array.isArray(parsed) && parsed.length > 0) {
  948. return { actions: parsed };
  949. }
  950. } catch (e) {
  951. // 不是纯JSON,继续尝试提取
  952. }
  953. // 尝试从代码块中提取JSON
  954. const codeBlockMatch = text.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
  955. if (codeBlockMatch) {
  956. try {
  957. const parsed = JSON.parse(codeBlockMatch[1]);
  958. if (parsed && typeof parsed === 'object') {
  959. // 新格式:包含 actions
  960. if (Array.isArray(parsed.actions)) {
  961. return parsed;
  962. }
  963. // 旧格式:直接是数组
  964. if (Array.isArray(parsed)) {
  965. return { actions: parsed };
  966. }
  967. }
  968. } catch (e) {
  969. // 解析失败
  970. }
  971. }
  972. // 尝试查找JSON对象(查找包含"actions"的对象,支持多行)
  973. const jsonMatch = text.match(/\{[\s\S]*?"actions"[\s\S]*?\}/);
  974. if (jsonMatch) {
  975. try {
  976. const parsed = JSON.parse(jsonMatch[0]);
  977. if (parsed && typeof parsed === 'object' && Array.isArray(parsed.actions)) {
  978. return parsed;
  979. }
  980. } catch (e) {
  981. // 解析失败,尝试更宽松的匹配
  982. try {
  983. // 尝试匹配整个JSON对象(包括嵌套结构)
  984. const fullJsonMatch = text.match(/\{(?:[^{}]|(?:\{[^{}]*\}))*\}/);
  985. if (fullJsonMatch) {
  986. const parsed = JSON.parse(fullJsonMatch[0]);
  987. if (parsed && typeof parsed === 'object' && Array.isArray(parsed.actions)) {
  988. return parsed;
  989. }
  990. }
  991. } catch (e2) {
  992. // 解析失败
  993. }
  994. }
  995. }
  996. return null;
  997. }