node-operations.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. /**
  2. * 节点操作(复制、删除、创建、粘贴)
  3. */
  4. import { createEchoInputPorts } from '../node-renderer/echo/echo-node.js';
  5. import { createAppendInputPorts } from '../node-renderer/append/append-node.js';
  6. /**
  7. * 创建新节点
  8. * @param {string} nodeType - 节点类型
  9. * @param {number} x - X 坐标
  10. * @param {number} y - Y 坐标
  11. * @returns {Object} 新节点对象
  12. */
  13. export function createNode(nodeType, x, y, nodeData = {}) {
  14. const nodeId = `node_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  15. // 根据节点类型创建默认数据
  16. const defaultData = { ...getDefaultNodeData(nodeType), ...nodeData };
  17. const { inputs, outputs } = getNodePortsFromType(nodeType, defaultData);
  18. return {
  19. id: nodeId,
  20. type: nodeType,
  21. label: getNodeLabelFromType(nodeType, defaultData),
  22. x: Math.round(x),
  23. y: Math.round(y),
  24. inputs,
  25. outputs,
  26. data: defaultData
  27. };
  28. }
  29. /**
  30. * 复制节点
  31. * @param {Object} node - 节点对象
  32. * @param {number} offsetX - X 偏移(默认 50)
  33. * @param {number} offsetY - Y 偏移(默认 50)
  34. * @returns {Object} 复制的节点
  35. */
  36. export function copyNode(node, offsetX = 50, offsetY = 50) {
  37. const newNodeId = `node_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  38. return {
  39. ...node,
  40. id: newNodeId,
  41. x: node.x + offsetX,
  42. y: node.y + offsetY
  43. };
  44. }
  45. /**
  46. * 复制多个节点
  47. * @param {Array} nodes - 节点数组
  48. * @param {number} offsetX - X 偏移
  49. * @param {number} offsetY - Y 偏移
  50. * @returns {Array} 复制的节点数组
  51. */
  52. export function copyNodes(nodes, offsetX = 50, offsetY = 50) {
  53. // 计算节点组的边界
  54. const bounds = getNodesBounds(nodes);
  55. return nodes.map(node => {
  56. const newNodeId = `node_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  57. return {
  58. ...node,
  59. id: newNodeId,
  60. x: node.x - bounds.minX + offsetX,
  61. y: node.y - bounds.minY + offsetY
  62. };
  63. });
  64. }
  65. /**
  66. * 获取节点组的边界
  67. * @param {Array} nodes - 节点数组
  68. * @returns {Object} 边界 {minX, minY, maxX, maxY}
  69. */
  70. export function getNodesBounds(nodes) {
  71. if (!nodes || nodes.length === 0) {
  72. return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
  73. }
  74. let minX = Infinity;
  75. let minY = Infinity;
  76. let maxX = -Infinity;
  77. let maxY = -Infinity;
  78. nodes.forEach(node => {
  79. minX = Math.min(minX, node.x);
  80. minY = Math.min(minY, node.y);
  81. maxX = Math.max(maxX, node.x + 200); // 假设节点宽度 200
  82. maxY = Math.max(maxY, node.y + 100); // 假设节点高度 100
  83. });
  84. return { minX, minY, maxX, maxY };
  85. }
  86. /**
  87. * 根据节点类型获取默认数据
  88. * @param {string} nodeType - 节点类型
  89. * @returns {Object} 默认数据对象
  90. */
  91. function getDefaultNodeData(nodeType) {
  92. const defaults = {
  93. 'begin': { type: 'begin' },
  94. 'adb': { type: 'adb', method: 'click', inVars: [], outVars: [] },
  95. 'if': { type: 'if', condition: '1 == 1', ture: [], false: [] },
  96. 'while': { type: 'while', condition: '1 == 1', ture: [] },
  97. 'delay': { type: 'delay', value: '1s' },
  98. 'set': { type: 'set', variable: '{var}', value: '' },
  99. 'echo': { type: 'echo', value: '' },
  100. 'random': { type: 'random', inVars: ['0', '100'], outVars: ['{num}'] }
  101. };
  102. return defaults[nodeType] || { type: nodeType };
  103. }
  104. /**
  105. * 获取节点参数定义(入参配置)
  106. * @param {string} nodeType - 节点类型
  107. * @param {Object} data - 节点数据(包含 method 等信息)
  108. * @returns {Array} 参数定义数组 [{name, label, type, defaultValue}]
  109. */
  110. export function getNodeParameterDefinitions(nodeType, data = {}) {
  111. const paramDefs = [];
  112. if (nodeType === 'adb') {
  113. const method = data.method || 'click';
  114. switch (method) {
  115. case 'click':
  116. paramDefs.push({ name: 'position', label: '位置坐标', type: 'string', defaultValue: '0,0', description: '格式: "x,y" 或 JSON: "{\"x\":123,\"y\":456}"' });
  117. break;
  118. case 'input':
  119. paramDefs.push({ name: 'text', label: '文本内容', type: 'string', defaultValue: '' });
  120. break;
  121. case 'locate':
  122. paramDefs.push({ name: 'image', label: '图片/文字', type: 'string', defaultValue: '' });
  123. break;
  124. case 'swipe':
  125. paramDefs.push({ name: 'direction', label: '方向', type: 'string', defaultValue: 'up-down', description: 'up-down/down-up/left-right/right-left' });
  126. break;
  127. case 'scroll':
  128. paramDefs.push({ name: 'direction', label: '方向', type: 'string', defaultValue: 'up', description: 'up/down' });
  129. break;
  130. case 'press':
  131. paramDefs.push({ name: 'image', label: '图片路径', type: 'string', defaultValue: '' });
  132. break;
  133. case 'string-press':
  134. paramDefs.push({ name: 'text', label: '文字内容', type: 'string', defaultValue: '' });
  135. break;
  136. case 'keyevent':
  137. paramDefs.push({ name: 'key', label: '按键', type: 'string', defaultValue: '' });
  138. break;
  139. }
  140. } else if (nodeType === 'delay') {
  141. paramDefs.push({ name: 'value', label: '延迟时间', type: 'string', defaultValue: '1s', description: '格式: 1s, 2m, 3h' });
  142. } else if (nodeType === 'set') {
  143. paramDefs.push({ name: 'variable', label: '变量名', type: 'string', defaultValue: '{var}' });
  144. paramDefs.push({ name: 'value', label: '变量值', type: 'string', defaultValue: '' });
  145. } else if (nodeType === 'if') {
  146. paramDefs.push({ name: 'condition', label: '条件表达式', type: 'string', defaultValue: '1 == 1' });
  147. } else if (nodeType === 'while') {
  148. paramDefs.push({ name: 'condition', label: '循环条件', type: 'string', defaultValue: '1 == 1' });
  149. } else if (nodeType === 'echo') {
  150. paramDefs.push({ name: 'value', label: '输出内容', type: 'string', defaultValue: '' });
  151. } else if (nodeType === 'random') {
  152. // random 节点使用 inVars[min, max] 和 outVars[{variable}]
  153. // 从 data 中读取默认值(如果存在)
  154. const minDefault = (data.inVars && Array.isArray(data.inVars) && data.inVars.length > 0)
  155. ? parseInt(data.inVars[0]) || 0
  156. : 0;
  157. const maxDefault = (data.inVars && Array.isArray(data.inVars) && data.inVars.length > 1)
  158. ? parseInt(data.inVars[1]) || 100
  159. : 100;
  160. paramDefs.push({ name: 'min', label: '最小值', type: 'int', defaultValue: minDefault });
  161. paramDefs.push({ name: 'max', label: '最大值', type: 'int', defaultValue: maxDefault });
  162. } else if (nodeType === 'schedule') {
  163. paramDefs.push({ name: 'interval', label: '执行间隔', type: 'string', defaultValue: '1s', description: '格式: 1s, 2m, 3h' });
  164. paramDefs.push({ name: 'repeat', label: '重复次数', type: 'int', defaultValue: -1, description: '-1 表示无限循环' });
  165. }
  166. // 如果节点有自定义参数(通过 params 数组定义)
  167. if (data.params && Array.isArray(data.params)) {
  168. data.params.forEach((param, index) => {
  169. paramDefs.push({
  170. name: param.name || `param_${index}`,
  171. label: param.label || `参数${index + 1}`,
  172. type: param.type || 'string',
  173. defaultValue: param.defaultValue || ''
  174. });
  175. });
  176. }
  177. return paramDefs;
  178. }
  179. /**
  180. * 根据节点类型获取端口定义
  181. * @param {string} nodeType - 节点类型
  182. * @param {Object} data - 节点数据
  183. * @returns {Object} {inputs: [], outputs: []}
  184. */
  185. export function getNodePortsFromType(nodeType, data) {
  186. console.log(`🔌 [getNodePortsFromType] nodeType: ${nodeType}, data:`, data);
  187. const inputs = [];
  188. const outputs = [];
  189. // Begin 节点只有输出端口,没有输入端口
  190. if (nodeType === 'begin') {
  191. outputs.push({ id: 'output_0', label: '', type: 'execution' });
  192. console.log(` ✅ Begin 节点端口生成完成`);
  193. return { inputs, outputs };
  194. }
  195. // Echo 节点特殊处理:根据 value 字段动态创建端口
  196. if (nodeType === 'echo') {
  197. const value = data.value || '';
  198. console.log(` 🎤 Echo 节点 value: "${value}"`);
  199. const echoInputs = createEchoInputPorts(value);
  200. console.log(` ✅ Echo 输入端口数量: ${echoInputs.length}`, echoInputs);
  201. inputs.push(...echoInputs);
  202. outputs.push({ id: 'output_0', label: '', type: 'execution' });
  203. console.log(` ✅ Echo 节点端口生成完成 - inputs: ${inputs.length}, outputs: ${outputs.length}`);
  204. return { inputs, outputs };
  205. }
  206. // Append 节点特殊处理:根据 value 字段动态创建端口(纯数据节点)
  207. if (nodeType === 'append') {
  208. const value = data.value || '';
  209. console.log(` 📎 Append 节点 value: "${value}"`);
  210. const appendInputs = createAppendInputPorts(value);
  211. console.log(` ✅ Append 输入端口数量: ${appendInputs.length}`, appendInputs);
  212. inputs.push(...appendInputs);
  213. outputs.push({
  214. id: 'output_result',
  215. label: 'Result',
  216. type: 'data',
  217. paramType: 'string'
  218. });
  219. console.log(` ✅ Append 节点端口生成完成 - inputs: ${inputs.length}, outputs: ${outputs.length}`);
  220. return { inputs, outputs };
  221. }
  222. // If 节点特殊处理:简单的 Condition 数据端口
  223. if (nodeType === 'if') {
  224. console.log(` 🔀 If 节点`);
  225. // 执行输入
  226. inputs.push({ id: 'input_0', label: '', type: 'execution' });
  227. // 条件输入端口(简单的布尔输入,颜色应该是红色)
  228. inputs.push({
  229. id: 'input_condition',
  230. label: 'Condition',
  231. type: 'data',
  232. paramType: 'bool'
  233. });
  234. // True 和 False 输出(带方括号)
  235. outputs.push({ id: 'output_true', label: '[True]', type: 'execution' });
  236. outputs.push({ id: 'output_false', label: '[False]', type: 'execution' });
  237. console.log(` ✅ If 节点端口生成完成 - inputs: ${inputs.length}, outputs: ${outputs.length}`);
  238. return { inputs, outputs };
  239. }
  240. // 添加执行流程端口
  241. inputs.push({ id: 'input_0', label: '', type: 'execution' });
  242. outputs.push({ id: 'output_0', label: '', type: 'execution' });
  243. // 根据参数定义添加数据输入端口
  244. const paramDefs = getNodeParameterDefinitions(nodeType, data);
  245. paramDefs.forEach((param, index) => {
  246. inputs.push({
  247. id: `input_param_${param.name}`,
  248. label: param.label,
  249. type: 'data',
  250. paramName: param.name,
  251. paramType: param.type
  252. });
  253. });
  254. // 根据类型添加数据输出端口
  255. if (nodeType === 'random') {
  256. // random 节点输出 number 类型
  257. // 支持新格式:outVars[{variable}]
  258. if (data.outVars && Array.isArray(data.outVars) && data.outVars.length > 0) {
  259. const varName = data.outVars[0];
  260. outputs.push({ id: 'output_1', label: varName, type: 'data', paramType: 'number' });
  261. } else if (data.variable) {
  262. // 向后兼容旧格式
  263. outputs.push({ id: 'output_1', label: data.variable, type: 'data', paramType: 'number' });
  264. }
  265. } else if (nodeType === 'set' && data.variable) {
  266. // set 节点输出类型需要从变量定义中获取,默认 string
  267. outputs.push({ id: 'output_1', label: data.variable, type: 'data', paramType: 'string' });
  268. } else if (data.outVars && Array.isArray(data.outVars)) {
  269. // 其他节点的 outVars 输出
  270. data.outVars.forEach((varName, index) => {
  271. outputs.push({ id: `output_${index + 1}`, label: `out[${index}]`, type: 'data' });
  272. });
  273. }
  274. // schedule 和 while 需要循环输入端口(用于连回)
  275. if (nodeType === 'schedule' || nodeType === 'while') {
  276. inputs.push({ id: 'input_1', label: 'loop', type: 'execution' });
  277. }
  278. return { inputs, outputs };
  279. }
  280. /**
  281. * 根据节点类型获取标签
  282. * @param {string} nodeType - 节点类型
  283. * @param {Object} data - 节点数据(可选,用于获取 method 等信息)
  284. * @returns {string} 节点标签
  285. */
  286. function getNodeLabelFromType(nodeType, data = {}) {
  287. // 直接使用原始类型名称,与 processing.json 的 type 字段一致
  288. if (nodeType === 'adb' && data.method) {
  289. // adb 节点显示 method
  290. return `${nodeType}.${data.method}`;
  291. }
  292. return nodeType;
  293. }
  294. /**
  295. * 创建变量节点
  296. * @param {string} varName - 变量名称
  297. * @param {*} varValue - 变量值
  298. * @param {number} x - X 坐标
  299. * @param {number} y - Y 坐标
  300. * @param {string} mode - 模式:'get'(只有输出端口)或 'set'(有执行端口和数据输入端口)
  301. * @param {string} customId - 自定义节点 ID(可选,用于确保 ID 一致性)
  302. * @returns {Object} 变量节点对象
  303. */
  304. export function createVariableNode(varName, varValue, x, y, mode = 'get', customId = null) {
  305. // 如果没有提供自定义 ID,则生成一个(用于用户手动创建的节点)
  306. const nodeId = customId || `var_${mode}_${varName}_${Date.now()}`;
  307. // 根据变量值自动判断类型:bool > number > string
  308. let varType = 'string';
  309. if (typeof varValue === 'boolean') {
  310. varType = 'bool';
  311. } else if (typeof varValue === 'number') {
  312. varType = 'number';
  313. }
  314. let inputs = [];
  315. let outputs = [];
  316. if (mode === 'get') {
  317. // Get 节点:只有右侧输出数据端口
  318. outputs = [{
  319. id: 'output_0',
  320. label: varName,
  321. type: 'data',
  322. paramType: varType
  323. }];
  324. } else if (mode === 'set') {
  325. // Set 节点:有执行输入/输出端口 + 左侧数据输入端口
  326. inputs = [
  327. {
  328. id: 'input_exec',
  329. label: '',
  330. type: 'execution'
  331. },
  332. {
  333. id: 'input_value',
  334. label: varName,
  335. type: 'data',
  336. paramType: varType
  337. }
  338. ];
  339. outputs = [
  340. {
  341. id: 'output_exec',
  342. label: '',
  343. type: 'execution'
  344. }
  345. ];
  346. }
  347. return {
  348. id: nodeId,
  349. type: 'variable',
  350. varMode: mode, // 'get' 或 'set'
  351. label: varName,
  352. varName: varName,
  353. varType: varType,
  354. varValue: varValue,
  355. x: Math.round(x),
  356. y: Math.round(y),
  357. inputs,
  358. outputs,
  359. data: {
  360. varName,
  361. varValue,
  362. varType,
  363. varMode: mode
  364. }
  365. };
  366. }
  367. /**
  368. * 获取所有可用的节点类型列表
  369. * @returns {Array} 节点类型数组,按分类组织
  370. */
  371. export function getAvailableNodeTypes() {
  372. return [
  373. {
  374. category: '流程控制',
  375. types: [
  376. { type: 'begin', label: 'Begin' }
  377. ]
  378. },
  379. {
  380. category: '基础语法',
  381. types: [
  382. { type: 'schedule', label: '定时执行' },
  383. { type: 'if', label: '条件判断' },
  384. { type: 'while', label: '循环' }
  385. ]
  386. },
  387. {
  388. category: '内置操作',
  389. types: [
  390. { type: 'delay', label: '延迟' },
  391. { type: 'set', label: '设置变量' },
  392. { type: 'echo', label: '打印信息' },
  393. { type: 'random', label: '生成随机数' }
  394. ]
  395. },
  396. {
  397. category: 'ADB操作',
  398. types: [
  399. { type: 'adb', label: 'ADB操作', method: 'click' },
  400. { type: 'adb', label: 'ADB输入', method: 'input' },
  401. { type: 'adb', label: 'ADB滑动', method: 'swipe' },
  402. { type: 'adb', label: 'ADB滚动', method: 'scroll' },
  403. { type: 'adb', label: 'ADB定位', method: 'locate' },
  404. { type: 'adb', label: 'ADB按键', method: 'keyevent' }
  405. ]
  406. }
  407. ];
  408. }