const fs = require("node:fs"); const path = require("node:path"); const { exec, spawn } = require("node:child_process"); const treeKill = require("tree-kill"); // 辅助函数:递归获取所有文件 const getAllFiles = (dirPath, arrayOfFiles) => { const files = fs.readdirSync(dirPath); arrayOfFiles = arrayOfFiles || []; files.forEach((file) => { if (fs.statSync(dirPath + "/" + file).isDirectory()) { arrayOfFiles = getAllFiles(dirPath + "/" + file, arrayOfFiles); } else { arrayOfFiles.push(path.join(dirPath, "/", file)); } }); return arrayOfFiles; }; window.services = { // --- File Service --- // 选择目录 selectDirectory() { try { const result = window.utools.showOpenDialog({ title: "选择项目目录", properties: ["openDirectory"], }); return result; } catch (error) { console.error("选择目录失败:", error); throw error; } }, // 读取目录结构 (单层,用于懒加载) async readDirectory(dirPath) { try { if (!fs.existsSync(dirPath)) { return []; } const items = await fs.promises.readdir(dirPath, { withFileTypes: true }); return items.map((item) => ({ id: path.join(dirPath, item.name), // 使用路径作为 ID name: item.name, path: path.join(dirPath, item.name), type: item.isDirectory() ? "directory" : "file", // children 不设置或设为 undefined,表示未加载;如果设为 [],组件会认为已加载且为空 children: undefined, })); } catch (error) { console.error("读取目录失败:", error); throw error; } }, // 读取文件内容 async readFile(filePath) { return await fs.promises.readFile(filePath, { encoding: "utf-8" }); }, // 写入文件内容 async writeFile(filePath, content) { await fs.promises.writeFile(filePath, content, { encoding: "utf-8" }); return true; }, // 创建文件夹 async createDirectory(dirPath) { if (!fs.existsSync(dirPath)) { await fs.promises.mkdir(dirPath, { recursive: true }); } return true; }, // 删除文件/文件夹 async deleteItem(itemPath) { await fs.promises.rm(itemPath, { recursive: true, force: true }); return true; }, // 重命名 async renameItem(oldPath, newPath) { await fs.promises.rename(oldPath, newPath); return true; }, // 复制文件/文件夹 async copyItem(sourcePath, destPath) { await fs.promises.cp(sourcePath, destPath, { recursive: true }); return true; }, // 检查路径是否存在 pathExists(itemPath) { return fs.existsSync(itemPath); }, // 打开所在目录 openItemLocation(itemPath) { window.utools.shellShowItemInFolder(itemPath); }, // --- Image Service --- // 保存图片 // imageData: base64 字符串 // mdFilePath: 当前 Markdown 文件的路径 async saveImage(imageData, mdFilePath) { try { const mdDir = path.dirname(mdFilePath); const mdName = path.basename(mdFilePath, path.extname(mdFilePath)); const assetsDir = path.join(mdDir, mdName); // 创建图片目录 if (!fs.existsSync(assetsDir)) { await fs.promises.mkdir(assetsDir, { recursive: true }); } // 生成文件名 (时间戳) const timestamp = Date.now(); // 简单判断图片类型,默认 png let ext = "png"; if (imageData.startsWith("data:image/jpeg")) ext = "jpg"; else if (imageData.startsWith("data:image/gif")) ext = "gif"; else if (imageData.startsWith("data:image/webp")) ext = "webp"; const imageName = `${timestamp}.${ext}`; const imagePath = path.join(assetsDir, imageName); // 写入文件 (去掉 base64 头部) const base64Data = imageData.replace(/^data:image\/\w+;base64,/, ""); await fs.promises.writeFile(imagePath, base64Data, { encoding: "base64" }); // 返回相对路径,用于 Markdown 引用 // 使用 forward slashes for markdown compatibility return `./${mdName}/${imageName}`; } catch (error) { console.error("保存图片失败:", error); throw error; } }, // --- Git Service --- // 执行 Git 命令 async execGit(command, cwd) { return new Promise((resolve, reject) => { exec(`git ${command}`, { cwd }, (error, stdout, stderr) => { if (error) { // git status 即使有变动有时也会返回非0,需要特殊处理,但通常 error 是命令执行失败 // 这里简单返回 resolve({ success: false, error: error.message, stderr }); } else { resolve({ success: true, stdout }); } }); }); }, async isGitRepository(dirPath) { const gitDir = path.join(dirPath, ".git"); return fs.existsSync(gitDir); }, async gitStatus(dirPath) { return this.execGit("status --short", dirPath); }, async gitAdd(dirPath, files = ".") { return this.execGit(`add ${files}`, dirPath); }, async gitCommit(dirPath, message) { // 简单处理 message 中的引号 const safeMessage = message.replace(/"/g, '\\"'); return this.execGit(`commit -m "${safeMessage}"`, dirPath); }, async gitPush(dirPath) { return this.execGit("push", dirPath); }, async gitPull(dirPath) { return this.execGit("pull", dirPath); }, // --- Search Service --- // 搜索文件 (文件名) async searchFileName(rootDir, keyword) { try { const allFiles = []; const traverse = async (currentDir) => { const items = await fs.promises.readdir(currentDir, { withFileTypes: true }); for (const item of items) { const fullPath = path.join(currentDir, item.name); if (item.isDirectory()) { if (item.name === ".git" || item.name === "node_modules") continue; // 忽略特定目录 // 如果文件夹名匹配,也加入结果 if (item.name.toLowerCase().includes(keyword.toLowerCase())) { allFiles.push({ name: item.name, path: fullPath, type: "directory", }); } await traverse(fullPath); } else { if (item.name.toLowerCase().includes(keyword.toLowerCase())) { allFiles.push({ name: item.name, path: fullPath, type: "file", }); } } } }; await traverse(rootDir); return allFiles; } catch (error) { console.error("搜索文件名失败:", error); return []; } }, // 搜索内容 async searchContent(rootDir, keyword, useRegex = false) { try { const results = []; const traverse = async (currentDir) => { const items = await fs.promises.readdir(currentDir, { withFileTypes: true }); for (const item of items) { const fullPath = path.join(currentDir, item.name); if (item.isDirectory()) { if (item.name === ".git" || item.name === "node_modules") continue; await traverse(fullPath); } else if (item.isFile() && item.name.endsWith(".md")) { // 仅搜索 markdown 文件 const content = await fs.promises.readFile(fullPath, "utf-8"); let match = false; let matches = []; if (useRegex) { try { const regex = new RegExp(keyword, "g"); let m; while ((m = regex.exec(content)) !== null) { match = true; matches.push({ index: m.index, match: m[0] }); if (matches.length > 10) break; // 限制每个文件的匹配数 } } catch (e) { // 正则错误,忽略 } } else { if (content.includes(keyword)) { match = true; // 简单定位第一个匹配 const index = content.indexOf(keyword); matches.push({ index, match: keyword }); } } if (match) { results.push({ name: item.name, path: fullPath, matches, preview: content.substring(0, 200) + "..." // 简单预览 }); } } } }; await traverse(rootDir); return results; } catch (error) { console.error("搜索内容失败:", error); return []; } }, // --- Config Service --- // 读取配置文件 (.md-helper.json) async loadConfig(rootDir) { const configPath = path.join(rootDir, ".md-helper.json"); try { if (fs.existsSync(configPath)) { const content = await fs.promises.readFile(configPath, "utf-8"); return JSON.parse(content); } } catch (e) { console.error("读取配置失败", e); } return null; }, // 保存配置文件 async saveConfig(rootDir, config) { const configPath = path.join(rootDir, ".md-helper.json"); try { await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8"); return true; } catch (e) { console.error("保存配置失败", e); return false; } }, // --- Global Data Service --- // 保存全局数据(如上次打开的目录、历史记录、主题等) // 存储在 uTools 插件数据目录下的 global-data.json getGlobalDataPath() { // 优先使用 uTools 提供的路径,或者 fallback 到 userData // 注意:window.utools.getPath('userData') 可能在 preload 中不可直接用,需谨慎 // 但通常 preload 中可以使用 window.utools // 为了稳妥,我们可以尝试使用 utools.dbStorage 如果只是简单 KV,但用户要求 JSON 文件。 // 我们这里使用 electron 的 userData 目录 const userDataPath = window.utools.getPath('userData'); return path.join(userDataPath, 'md-helper-global.json'); }, async loadGlobalData() { try { const dataPath = this.getGlobalDataPath(); if (fs.existsSync(dataPath)) { const content = await fs.promises.readFile(dataPath, 'utf-8'); return JSON.parse(content); } } catch (e) { console.error("读取全局数据失败", e); } return { lastOpenedDir: '', historyDirs: [], // string[] theme: { primaryColor: '#1890ff', isDark: false } }; }, async saveGlobalData(data) { try { const dataPath = this.getGlobalDataPath(); await fs.promises.writeFile(dataPath, JSON.stringify(data, null, 2), 'utf-8'); return true; } catch (e) { console.error("保存全局数据失败", e); return false; } } };