添加了可以移动file://格式的外部文件移动到当前文件夹,并修改内部链接格式的选项,适用于QQ信息内的图片移动。
/*
* @Author: 熊猫别熬夜
* @Date: 2024-03-18 02:30:36
* @Last Modified by: 熊猫别熬夜
* @Last Modified time: 2024-12-12 17:55:17
*/
// 导入所需模块
const path = require('path');
const fs = require('fs');
const quickAddApi = app.plugins.plugins.quickadd.api;
const files = app.vault.getFiles();
// 获取当前活动文件和缓存的元数据
const file = app.workspace.getActiveFile();
const cachedMetadata = app.metadataCache.getFileCache(file);
let listPaths = app.vault.getAllFolders().map(f => f.path);
listPaths.unshift("/");
// 导出异步函数
module.exports = {
entry: async (QuickAdd, settings, params) => {
let editor;
let selection;
try {
// 获取选中的文本
editor = app.workspace.activeEditor.editor;
// // 获取选中的文本否则自动获取当前行的文本
// selection = editor.getSelection();
// 选择所在的一行
const line = editor.getLine(editor.getCursor().line);
// 获取选中的文本否则自动获取当前行的文本
selection = editor.getSelection() ? editor.getSelection() : line;
} catch {
}
let selectionEmbed;
if (selection) {
selectionEmbed = matchSelectionEmbed(selection);
}
console.log(selectionEmbed);
let wikiPath = "";
if (selectionEmbed) {
wikiPath = getFilePath(files, selectionEmbed); // 匹配Wiki链接
}
let links = [];
let embeds = [];
// 提取链接和嵌入的文件
if (cachedMetadata.links) {
links = cachedMetadata.links.map(l => l.link);
}
if (cachedMetadata.embeds) {
embeds = cachedMetadata.embeds.map(e => e.link);
}
let allLinks = [...links, ...embeds];
console.log(allLinks);
// 提取外部文件链接 (file://)
let externalFiles = [];
let fileContent = '';
try {
fileContent = await app.vault.read(file);
externalFiles = extractExternalFileLinks(fileContent);
console.log('外部文件:', externalFiles);
} catch (error) {
console.error('读取文件内容失败:', error);
}
// 获取所有文件和链接文件路径
let linkFilePaths = [];
for (let i = 0; i < allLinks.length; i++) {
const link = allLinks[i];
const filePath = getFilePath(files, link);
if (filePath) {
linkFilePaths.push(filePath);
}
};
console.log(linkFilePaths);
linkFilePaths = wikiPath ? [wikiPath] : linkFilePaths;
// 检查链接文件是否在同一文件夹中
const activefile = app.workspace.getActiveFile();
console.log(activefile);
const check = checkLinkNotesInSameFolder(activefile.path, linkFilePaths);
// 筛选可移动的文件类型
const moveFiles = linkFilePaths.filter((filePath, index) => !check[index]);
// 筛选附件和笔记
const attachmentTypes = ['md', 'canvas', 'excalidraw'];
const notes = moveFiles.filter(link => attachmentTypes.some(type => link.endsWith('.' + type)));
const attachments = moveFiles.filter(link => !notes.includes(link));
// 提示用户选择移动类型
const choices = [`📁移动当前文件夹至其他位置`, `📄移动笔记到当前文件夹(${notes.length})`, `📦移动附件到当前文件夹(${attachments.length})`, `🌐移动外部文件到当前文件夹(${externalFiles.length})`, `📥归档当前文件到指定文件夹`];
const choice = await quickAddApi.suggester(choices, choices);
if (!choice) return;
if (choice === choices[0]) {
const choice = await quickAddApi.suggester(listPaths, listPaths);
if (!choice) return;
const newFilePath = path.join(choice, path.basename(path.dirname(activefile.path)));
const oldFolderPath = path.dirname(activefile.path);
await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFolderPath), newFilePath);
new Notice(`📁已移动${oldFolderPath}文件夹至${newFilePath}`, 1000);
return;
} else if (choice === choices[4]) {
// 2024-04-10_15:37:归档文件或者FolderNote
const archiveFolder = settings["归档路径"] + (settings["归档格式"] ? "/" + quickAddApi.date.now(settings["归档格式"]) : "");
if (!(await app.vault.getFolderByPath(archiveFolder))) {
await app.vault.createFolder(archiveFolder);
}
const fileName = path.basename(activefile.path);
const oldFilePath = activefile.path;
const isFolderNote = path.basename(path.dirname(oldFilePath)) === fileName.replace(".md", "").replace(".canvas", "");
if (isFolderNote) {
const newFilePath = path.join(archiveFolder, fileName.replace(".md", "").replace(".canvas", ""));
const oldFolderPath = path.dirname(oldFilePath);
await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFolderPath), newFilePath);
new Notice(`📥已归档📁${oldFolderPath}`, 1000);
} else {
const newFilePath = path.join(archiveFolder, fileName);
await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFilePath), newFilePath);
new Notice(`📥已归档📝${fileName}`, 1000);
}
return;
} else if (choice === choices[3]) {
// 移动外部文件
if (externalFiles.length === 0) {
new Notice('❌未找到外部文件链接', 2000);
return;
}
// 用户选择具体的外部文件
const selectedItems = await quickAddApi.checkboxPrompt(externalFiles, externalFiles);
if (!selectedItems || selectedItems.length === 0) return;
// 复制外部文件到当前文件夹
const activefile = app.workspace.getActiveFile();
const targetDir = path.dirname(activefile.path);
const vaultBasePath = app.vault.adapter.basePath;
// 记录需要替换的链接映射(原始 file:// URL 字符串 -> 新的 Obsidian 链接)
const linkReplacements = new Map();
let updatedContent = fileContent;
for (let i = 0; i < selectedItems.length; i++) {
const externalFilePath = selectedItems[i];
try {
// 检查源文件是否存在
if (!fs.existsSync(externalFilePath)) {
new Notice(`❌文件不存在: ${path.basename(externalFilePath)}`, 2000);
continue;
}
// 目标文件路径(相对于 vault 根目录)
const targetFilePath = path.join(targetDir, path.basename(externalFilePath)).replace(/\\/g, '/');
const vaultTargetPath = path.join(vaultBasePath, targetFilePath);
// 检查目标文件是否已存在
if (fs.existsSync(vaultTargetPath)) {
new Notice(`⚠️文件已存在: ${path.basename(externalFilePath)}`, 2000);
} else {
// 确保目标目录存在
const targetDirPath = path.dirname(vaultTargetPath);
if (!fs.existsSync(targetDirPath)) {
fs.mkdirSync(targetDirPath, { recursive: true });
}
// 复制文件
fs.copyFileSync(externalFilePath, vaultTargetPath);
new Notice(`📋已复制 ${path.basename(externalFilePath)}`, 1000);
}
// 提取文件内容中匹配的原始 file:// URL 字符串
const fileName = path.basename(externalFilePath);
const newLink = `![[${fileName}]]`;
const originalUrl = findFileUrlInContent(fileContent, externalFilePath);
if (originalUrl) {
linkReplacements.set(originalUrl, newLink);
}
} catch (error) {
console.error('复制外部文件失败:', error);
new Notice(`❌复制失败: ${path.basename(externalFilePath)}`, 2000);
}
}
// 更新文件内容中的链接
if (linkReplacements.size > 0) {
try {
// 替换文件内容中的 file:// 链接
for (const [originalUrl, newLink] of linkReplacements.entries()) {
// 匹配  或  格式
// 需要转义原始 URL 中的特殊字符,但保留正则表达式元字符的转义
const escapedUrl = escapeRegExp(originalUrl);
const imgPattern = new RegExp(`!\\[([^\\]]*)\\]\\(${escapedUrl}\\)`, 'g');
updatedContent = updatedContent.replace(imgPattern, (match, altText) => {
return altText ? `![${altText}]${newLink}` : newLink;
});
}
// 保存更新后的内容
if (updatedContent !== fileContent) {
await app.vault.modify(activefile, updatedContent);
new Notice("✅已更新文件中的链接格式", 2000);
}
} catch (error) {
console.error('更新文件链接失败:', error);
new Notice('⚠️文件已复制,但更新链接失败', 2000);
}
}
new Notice("✅已复制外部文件到当前文件夹");
return;
}
// 根据用户选择筛选链接
const filteredLinks = choice === choices[1] ? notes : attachments;
// 用户选择具体文件
const selectedItems = await quickAddApi.checkboxPrompt(filteredLinks, filteredLinks);
if (!selectedItems) return;
// 匹配选择的文件路径
const matchedLinkFilePaths = selectedItems.map((filePath) => {
return linkFilePaths.find((linkFilePath) => linkFilePath.replace(".md", "").endsWith(filePath.replace(".md", "")));
});
if (!matchedLinkFilePaths) return;
console.log(matchedLinkFilePaths);
// 移动文件到当前文件夹
for (let i = 0; i < matchedLinkFilePaths.length; i++) {
const oldFilePath = matchedLinkFilePaths[i];
// 2024-03-22_11:41:如果存在FolderNote,则移动的是整个文件夹
const fileName = path.basename(oldFilePath);
const isFolderNote = path.basename(path.dirname(oldFilePath)) === fileName.replace(".md", "").replace(".canvas", "");
if (isFolderNote) {
const newFilePath = path.join(path.dirname(activefile.path), fileName.replace(".md", "").replace(".canvas", ""));
const oldFolderPath = path.dirname(oldFilePath);
await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFolderPath), newFilePath);
new Notice(`📁已移动${oldFolderPath}文件夹至${newFilePath}`, 1000);
} else {
const newFilePath = path.join(path.dirname(activefile.path), path.basename(oldFilePath));
await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFilePath), newFilePath);
new Notice(`📄已移动${matchedLinkFilePaths[i]}`, 1000);
}
}
new Notice("✅已移动文件到当前文件夹");
},
settings: {
name: "移动子笔记或附件",
author: "熊猫别熬夜",
options: {
"归档路径": {
type: "dropdown",
defaultValue: "800【存档】Archive",
options: listPaths,
},
"归档格式": {
type: "text",
defaultValue: "YYYY",
description: "可以设置以时间格式划分子文件夹,YYYY(年),MM(月),DD(日)",
},
}
}
};
// 获取文件路径函数
// [PKMer\_QuickAdd 脚本 - 移动子笔记或附件到当前文件夹 - #5,来自 Probe - Blog - PKMer](https://forum.pkmer.net/t/topic/1885/5)
function getFilePath(files, baseName) {
// 下一行的原先 baseName => 改成 path.basename(baseName)
let files2 = files.filter(f => path.basename(f.path).replace(".md", "") === path.basename(baseName).replace(".md", ""));
let filePath = files2.map((f) => f.path);
return filePath[0];
}
// 检查链接文件是否在同一文件夹中函数
function checkLinkNotesInSameFolder(activeFilePath, linkFilePaths) {
const activeFileDir = path.dirname(activeFilePath);
let check = [];
for (let i = 0; i < linkFilePaths.length; i++) {
const linkFileDir = path.dirname(linkFilePaths[i]);
if (activeFileDir === linkFileDir) {
check.push(true);
} else {
check.push(false);
}
}
return check;
};
function matchSelectionEmbed(text) {
const regex = /\[\[?([^\]]*?)(\|.*)?\]\]?\(?([^)\n]*)\)?/;
const matches = text.match(regex);
if (!matches) return;
if (matches[3]) return decodeURIComponent(matches[3]);
if (matches[1]) return decodeURIComponent(matches[1]);
}
// 提取外部文件链接 (file://) 函数
function extractExternalFileLinks(content) {
// 匹配 file:// 协议链接,支持 Markdown 图片语法 
const fileProtocolRegex = /file:\/\/[\/]?([^\s\)"\]]+)/g;
const matches = [];
let match;
while ((match = fileProtocolRegex.exec(content)) !== null) {
try {
// 解码 URL 编码的路径
let filePath = decodeURIComponent(match[1]);
// 处理 Windows 路径 (file:///D:/... 或 file:///D:\... 转换为 D:/... 或 D:\...)
if (filePath.startsWith('/')) {
filePath = filePath.substring(1);
}
// 如果路径使用正斜杠,转换为系统路径分隔符
filePath = filePath.replace(/\//g, path.sep);
// 检查文件是否存在
if (fs.existsSync(filePath)) {
matches.push(filePath);
}
} catch (error) {
console.error('处理外部文件路径失败:', match[1], error);
}
}
// 去重
return [...new Set(matches)];
}
// 转义正则表达式特殊字符
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// 在文件内容中查找匹配的 file:// URL 字符串
function findFileUrlInContent(content, filePath) {
// 匹配 file:// 链接格式
const fileUrlPattern = /file:\/\/[\/]?([^\s\)"\]]+)/g;
let match;
while ((match = fileUrlPattern.exec(content)) !== null) {
try {
let decodedPath = decodeURIComponent(match[1]);
if (decodedPath.startsWith('/')) {
decodedPath = decodedPath.substring(1);
}
decodedPath = decodedPath.replace(/\//g, path.sep);
// 规范化路径进行比较
const normalizedFilePath = path.normalize(filePath);
const normalizedDecodedPath = path.normalize(decodedPath);
if (normalizedFilePath === normalizedDecodedPath ||
normalizedFilePath.toLowerCase() === normalizedDecodedPath.toLowerCase()) {
return match[0]; // 返回完整的 file:// URL
}
} catch (error) {
// 继续尝试下一个匹配
}
}
return null;
}