workflow.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. /**
  2. * 工作流管理模块
  3. * 负责工作流的保存、删除、读取等操作
  4. */
  5. import { ipcMain } from 'electron';
  6. import { readdir, writeFile, readFile, mkdir, rm, stat } from 'fs/promises';
  7. import { join, dirname } from 'path';
  8. import { fileURLToPath } from 'url';
  9. const __filename = fileURLToPath(import.meta.url);
  10. const __dirname = dirname(__filename);
  11. /**
  12. * 获取 static/processing 目录下的所有文件夹
  13. * @returns {Promise<Array<{name: string, createdAt: Date}>>}
  14. */
  15. export async function getStaticFolders() {
  16. try {
  17. const staticPath = join(__dirname, '..', 'static', 'processing');
  18. const entries = await readdir(staticPath, { withFileTypes: true });
  19. const folders = [];
  20. for (const entry of entries) {
  21. if (entry.isDirectory()) {
  22. const folderPath = join(staticPath, entry.name);
  23. const stats = await stat(folderPath);
  24. folders.push({
  25. name: entry.name,
  26. createdAt: stats.birthtime || stats.mtime, // 使用创建时间,如果没有则使用修改时间
  27. });
  28. }
  29. }
  30. // 按创建时间排序,最新的在前
  31. return folders.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
  32. } catch (error) {
  33. return [];
  34. }
  35. }
  36. /**
  37. * 读取 processing.json 文件
  38. * @param {string} folderName - 工作流文件夹名称
  39. * @returns {Promise<Object|null>} 解析后的 JSON 对象,失败返回 null
  40. */
  41. export async function readProcessingJson(folderName) {
  42. try {
  43. const jsonPath = join(__dirname, '..', 'static', 'processing', folderName, 'processing.json');
  44. const jsonContent = await readFile(jsonPath, 'utf-8');
  45. // 解析 JSON(处理可能的格式问题,如注释、尾随逗号等)
  46. // 先尝试直接解析
  47. let parsed;
  48. try {
  49. parsed = JSON.parse(jsonContent);
  50. } catch (parseError) {
  51. // 如果直接解析失败,尝试清理注释和尾随逗号(简单处理)
  52. let cleaned = jsonContent
  53. .replace(/\/\/.*$/gm, '') // 移除单行注释
  54. .replace(/\/\*[\s\S]*?\*\//g, '') // 移除多行注释
  55. .replace(/,(\s*[}\]])/g, '$1'); // 移除尾随逗号(在 ] 或 } 之前的逗号)
  56. try {
  57. parsed = JSON.parse(cleaned);
  58. } catch (retryError) {
  59. throw new Error(`JSON 格式错误: ${parseError.message}`);
  60. }
  61. }
  62. // 处理不同的 JSON 格式
  63. // 如果直接是数组,包装成对象
  64. if (Array.isArray(parsed)) {
  65. return { actions: parsed };
  66. }
  67. // 如果已经是对象,直接返回
  68. if (parsed && typeof parsed === 'object') {
  69. // 如果已经有 actions 字段,直接返回
  70. if (parsed.actions) {
  71. return parsed;
  72. }
  73. // 如果没有 actions 字段,尝试查找数组字段
  74. for (const key in parsed) {
  75. if (Array.isArray(parsed[key])) {
  76. return { actions: parsed[key], ...parsed };
  77. }
  78. }
  79. }
  80. // 如果解析成功但没有 actions 字段,返回错误信息
  81. if (!parsed || (typeof parsed === 'object' && !parsed.actions && !Array.isArray(parsed))) {
  82. return null;
  83. }
  84. return parsed;
  85. } catch (error) {
  86. return null;
  87. }
  88. }
  89. /**
  90. * 保存工作流到 static/processing 目录
  91. * @param {Object} workflowJson - 工作流 JSON 对象
  92. * @param {Array} imagesData - 图片数据数组(包含 base64 和 name)
  93. * @returns {Promise<{success: boolean, error?: string, folderName?: string, path?: string}>}
  94. */
  95. export async function saveWorkflow(workflowJson, imagesData = []) {
  96. try {
  97. // 支持新旧格式
  98. const hasActions = Array.isArray(workflowJson.actions) || Array.isArray(workflowJson);
  99. if (!workflowJson || typeof workflowJson !== 'object' || !hasActions) {
  100. return { success: false, error: '工作流格式错误:缺少 actions 数组' };
  101. }
  102. // 生成文件夹名称(使用时间戳)
  103. const now = new Date();
  104. const timestamp = now.getFullYear() +
  105. String(now.getMonth() + 1).padStart(2, '0') +
  106. String(now.getDate()).padStart(2, '0') + '_' +
  107. String(now.getHours()).padStart(2, '0') +
  108. String(now.getMinutes()).padStart(2, '0') +
  109. String(now.getSeconds()).padStart(2, '0');
  110. const folderName = workflowJson.name || `工作流_${timestamp}`;
  111. // 创建工作流文件夹
  112. const workflowPath = join(__dirname, '..', 'static', 'processing', folderName);
  113. await mkdir(workflowPath, { recursive: true });
  114. // 保存 processing.json
  115. const jsonPath = join(workflowPath, 'processing.json');
  116. const jsonContent = JSON.stringify(workflowJson, null, '\t');
  117. await writeFile(jsonPath, jsonContent, 'utf-8');
  118. // 保存图片
  119. if (imagesData && Array.isArray(imagesData) && imagesData.length > 0) {
  120. for (const imageData of imagesData) {
  121. if (imageData.base64 && imageData.name) {
  122. try {
  123. // 将base64转换为Buffer
  124. const imageBuffer = Buffer.from(imageData.base64, 'base64');
  125. const imagePath = join(workflowPath, imageData.name);
  126. await writeFile(imagePath, imageBuffer);
  127. } catch (imageError) {
  128. // 保存图片失败,忽略错误
  129. }
  130. }
  131. }
  132. }
  133. return { success: true, folderName, path: workflowPath };
  134. } catch (error) {
  135. return { success: false, error: error.message };
  136. }
  137. }
  138. /**
  139. * 删除工作流文件夹
  140. * @param {string} folderName - 工作流文件夹名称
  141. * @returns {Promise<{success: boolean, error?: string, folderName?: string}>}
  142. */
  143. export async function deleteWorkflow(folderName) {
  144. try {
  145. if (!folderName || typeof folderName !== 'string') {
  146. return { success: false, error: '文件夹名称无效' };
  147. }
  148. // 构建文件夹路径
  149. const workflowPath = join(__dirname, '..', 'static', 'processing', folderName);
  150. // 删除整个文件夹(包括所有内容)
  151. await rm(workflowPath, { recursive: true, force: true });
  152. return { success: true, folderName };
  153. } catch (error) {
  154. return { success: false, error: error.message };
  155. }
  156. }
  157. /**
  158. * 读取 bp.json 文件(蓝图节点位置信息)
  159. * @param {string} folderName - 工作流文件夹名称
  160. * @returns {Promise<Object|null>} 解析后的 JSON 对象,失败返回 null
  161. */
  162. export async function readBlueprintJson(folderName) {
  163. try {
  164. const jsonPath = join(__dirname, '..', 'static', 'processing', folderName, 'bp.json');
  165. try {
  166. const jsonContent = await readFile(jsonPath, 'utf-8');
  167. const parsed = JSON.parse(jsonContent);
  168. return parsed;
  169. } catch (readError) {
  170. // 如果文件不存在,返回 null(不是错误)
  171. if (readError.code === 'ENOENT') {
  172. return null;
  173. }
  174. throw readError;
  175. }
  176. } catch (error) {
  177. return null;
  178. }
  179. }
  180. /**
  181. * 保存 bp.json 文件(蓝图节点位置信息)
  182. * @param {string} folderName - 工作流文件夹名称
  183. * @param {Object} blueprintData - 蓝图数据 {nodePositions: {nodeId: {x, y}}}
  184. * @returns {Promise<{success: boolean, error?: string}>}
  185. */
  186. export async function saveBlueprintJson(folderName, blueprintData) {
  187. try {
  188. if (!folderName || typeof folderName !== 'string') {
  189. return { success: false, error: '文件夹名称无效' };
  190. }
  191. const workflowPath = join(__dirname, '..', 'static', 'processing', folderName);
  192. const jsonPath = join(workflowPath, 'bp.json');
  193. const jsonContent = JSON.stringify(blueprintData, null, '\t');
  194. await writeFile(jsonPath, jsonContent, 'utf-8');
  195. return { success: true };
  196. } catch (error) {
  197. return { success: false, error: error.message };
  198. }
  199. }
  200. /**
  201. * 保存 processing.json 文件到现有工作流文件夹
  202. * @param {string} folderName - 工作流文件夹名称
  203. * @param {Object} workflowData - 工作流数据
  204. * @returns {Promise<{success: boolean, error?: string}>}
  205. */
  206. export async function saveProcessingJson(folderName, workflowData) {
  207. try {
  208. if (!folderName || typeof folderName !== 'string') {
  209. return { success: false, error: '文件夹名称无效' };
  210. }
  211. const workflowPath = join(__dirname, '..', 'static', 'processing', folderName);
  212. const jsonPath = join(workflowPath, 'processing.json');
  213. const jsonContent = JSON.stringify(workflowData, null, '\t');
  214. await writeFile(jsonPath, jsonContent, 'utf-8');
  215. return { success: true };
  216. } catch (error) {
  217. return { success: false, error: error.message };
  218. }
  219. }
  220. /**
  221. * 注册工作流管理相关的 IPC handlers
  222. */
  223. export function registerIpcHandlers() {
  224. // 获取工作流文件夹列表
  225. ipcMain.handle('get-static-folders', async () => {
  226. return await getStaticFolders();
  227. });
  228. // 读取 processing.json 文件
  229. ipcMain.handle('read-processing-json', async (event, folderName) => {
  230. return await readProcessingJson(folderName);
  231. });
  232. // 保存 processing.json 文件到现有工作流文件夹
  233. ipcMain.handle('save-processing-json', async (event, folderName, workflowData) => {
  234. return await saveProcessingJson(folderName, workflowData);
  235. });
  236. // 保存工作流(创建新文件夹)
  237. ipcMain.handle('save-workflow', async (event, workflowJson, imagesData) => {
  238. return await saveWorkflow(workflowJson, imagesData);
  239. });
  240. // 删除工作流
  241. ipcMain.handle('delete-workflow', async (event, folderName) => {
  242. return await deleteWorkflow(folderName);
  243. });
  244. // 读取 bp.json 文件
  245. ipcMain.handle('read-blueprint-json', async (event, folderName) => {
  246. return await readBlueprintJson(folderName);
  247. });
  248. // 保存 bp.json 文件
  249. ipcMain.handle('save-blueprint-json', async (event, folderName, blueprintData) => {
  250. return await saveBlueprintJson(folderName, blueprintData);
  251. });
  252. }