From d11171cfc6f65631bea5a7b980e5cdca11f04149 Mon Sep 17 00:00:00 2001 From: tianma Date: Tue, 30 Jun 2026 02:07:13 +0800 Subject: [PATCH] feat: implement local image compression using sharp in Evernote import script --- apps/api/src/index.ts | 34 +- .../components/EvernoteImportGuidePane.tsx | 2 +- apps/web/src/lib/evernote-migration-guide.ts | 120 +++-- bun.lock | 121 +++-- package.json | 1 + scripts/import-evernote-enex-via-mcp.mjs | 421 +++++++++++++++--- 6 files changed, 548 insertions(+), 151 deletions(-) diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 368b8bac..9031df71 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -193,8 +193,8 @@ const PASSWORD_SALT_BYTES = 16; const SESSION_TOKEN_BYTES = 32; const DEFAULT_SESSION_TTL_DAYS = 30; const DEFAULT_R2_BUCKET_NAME = "edgeever-resources"; -const MAX_IMAGE_UPLOAD_BYTES = 50 * 1024 * 1024; -const MAX_ATTACHMENT_UPLOAD_BYTES = 50 * 1024 * 1024; +const MAX_IMAGE_UPLOAD_BYTES = 100 * 1024 * 1024; +const MAX_ATTACHMENT_UPLOAD_BYTES = 100 * 1024 * 1024; const REVISION_SNAPSHOT_INTERVAL_MS = 5 * 60 * 1000; const API_TOKEN_BYTES = 32; const API_TOKEN_PREFIX = "eev"; @@ -1856,7 +1856,7 @@ const callMcpTool = async ( const mimeType = getRequiredString(args.mimeType, "mimeType"); const filename = getOptionalString(args.filename) ?? `image${inferImageExtension("", mimeType)}`; - const bytes = decodeBase64ImageData(getRequiredString(args.dataBase64, "dataBase64")); + const bytes = await decodeBase64ImageData(getRequiredString(args.dataBase64, "dataBase64")); const resource = await createImageResource(c, { memoId, filename, @@ -2007,7 +2007,7 @@ const getRequiredStringArray = (value: unknown, name: string) => { return items; }; -const decodeBase64ImageData = (value: string) => { +const decodeBase64ImageData = async (value: string) => { const [, dataUrlPayload] = value.match(/^data:[^;]+;base64,(.+)$/i) ?? []; const base64 = (dataUrlPayload ?? value).replace(/\s/g, ""); @@ -2016,16 +2016,13 @@ const decodeBase64ImageData = (value: string) => { } try { - const binary = atob(base64); - const bytes = new Uint8Array(binary.length); - - for (let index = 0; index < binary.length; index += 1) { - bytes[index] = binary.charCodeAt(index); + const response = await fetch("data:application/octet-stream;base64," + base64); + if (!response.ok) { + throw new Error("failed to decode base64"); } - - return bytes; - } catch { - throw new AppError("invalid_params", "dataBase64 must be valid base64 image data.", 400); + return new Uint8Array(await response.arrayBuffer()); + } catch (error) { + throw new AppError("invalid_params", "dataBase64 must be valid base64 image data: " + (error as Error).message, 400); } }; @@ -3518,9 +3515,14 @@ const sha256 = async (value: string) => { }; const sha256Bytes = async (bytes: Uint8Array) => { - const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; - const digest = await crypto.subtle.digest("SHA-256", buffer); - return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join(""); + const digest = await crypto.subtle.digest("SHA-256", bytes.slice()); + const hashArray = new Uint8Array(digest); + let hexString = ""; + for (let i = 0; i < hashArray.length; i++) { + const hex = hashArray[i].toString(16); + hexString += hex.length === 1 ? "0" + hex : hex; + } + return hexString; }; const inferImageExtension = (filename: string, mimeType: string) => { diff --git a/apps/web/src/components/EvernoteImportGuidePane.tsx b/apps/web/src/components/EvernoteImportGuidePane.tsx index bd1e1359..09ac1e25 100644 --- a/apps/web/src/components/EvernoteImportGuidePane.tsx +++ b/apps/web/src/components/EvernoteImportGuidePane.tsx @@ -111,7 +111,7 @@ const getStepIcon = (index: string): ReactNode => { return ; } - if (index === "2") { + if (index === "2" || index === "3" || index === "4") { return ; } diff --git a/apps/web/src/lib/evernote-migration-guide.ts b/apps/web/src/lib/evernote-migration-guide.ts index 00b8575a..d3faaebe 100644 --- a/apps/web/src/lib/evernote-migration-guide.ts +++ b/apps/web/src/lib/evernote-migration-guide.ts @@ -14,77 +14,115 @@ export type MigrationGuideStep = { export const EVERNOTE_MIGRATION_GUIDE = { title: "印象笔记迁移指引", - subtitle: "先导出 ENEX,再回 EdgeEver 导入", - introTitle: "最省事的迁移方式", + subtitle: "支持 MCP 自动化批量导入,保留笔记本目录与堆叠结构", + introTitle: "最佳实践迁移方案", intro: [ - "EdgeEver 只导入 .enex 文件。先用 evernote-backup 从印象笔记导出 ENEX,再回到这里选择文件导入。", + "我们强烈推荐基于 EdgeEver MCP (Model Context Protocol) 命令行进行迁移。该方案经过内存与流式处理优化,能够安全应对数 GB 级别的超大笔记库,并且支持还原印象笔记的「笔记本组(堆叠)」层级结构,完整保留笔记的原始创建与修改时间。", ], steps: [ { index: "1", - title: "用 evernote-backup 导出 ENEX", - paragraphs: ["根据你的电脑系统选择一组命令执行。"], + title: "第一步:用 evernote-backup 导出 ENEX", + paragraphs: [ + "由于新版印象笔记客户端限制了本地导出,我们使用开源的 `evernote-backup` 工具进行云端整库备份与导出。根据你的电脑系统选择对应的环境执行命令:", + ], commands: [ { label: "macOS / Linux", language: "sh", - code: `pipx install evernote-backup -evernote-backup init-db --backend china + code: `# 1. 安装备份工具 +pipx install evernote-backup + +# 2. 初始化数据库(印象笔记用户指定 china 后端) +# 💡 最佳实践提示:如果 macOS 系统启用了代理,Python 3.14 可能会在建立 HTTPS 隧道时报错, +# 请在命令前加上环境变量清除代理,例如: +HTTP_PROXY="" HTTPS_PROXY="" http_proxy="" https_proxy="" ALL_PROXY="" all_proxy="" no_proxy="*" \\ +evernote-backup init-db --backend china --user "你的用户名/邮箱" --password "你的密码" + +# 3. 开始云端数据同步(如果笔记较多,这一步需要较长时间) +HTTP_PROXY="" HTTPS_PROXY="" http_proxy="" https_proxy="" ALL_PROXY="" all_proxy="" no_proxy="*" \\ evernote-backup sync -evernote-backup export ./edgeever-import`, + +# 4. 导出为 ENEX 文件目录 +HTTP_PROXY="" HTTPS_PROXY="" http_proxy="" https_proxy="" ALL_PROXY="" all_proxy="" no_proxy="*" \\ +evernote-backup export ./evernote-export`, }, { label: "Windows PowerShell", language: "powershell", - code: `py -m pip install --user pipx + code: `# 1. 安装 pipx 与 evernote-backup +py -m pip install --user pipx py -m pipx ensurepath +# (注意:如果提示修改了 PATH,请关闭并重新打开 PowerShell 窗口) pipx install evernote-backup -evernote-backup init-db --backend china + +# 2. 初始化数据库 +evernote-backup init-db --backend china --user "你的用户名/邮箱" --password "你的密码" + +# 3. 同步数据 evernote-backup sync -evernote-backup export .\\edgeever-import`, + +# 4. 导出为 ENEX 文件目录 +evernote-backup export .\\evernote-export`, }, ], list: [ - "如果 Windows 执行 `py -m pipx ensurepath` 后提示修改了 PATH,请关闭并重新打开 PowerShell,再继续执行后面的命令。", - "完成后,当前目录会出现 `edgeever-import` 文件夹,里面是一批 `.enex` 文件。通常一个笔记本对应一个 `.enex` 文件。", - "只想导出部分笔记本时,可以在最后一行加上笔记本名。", - ], - }, - { - index: "1.1", - title: "只导出部分笔记本", - commands: [ - { - label: "macOS / Linux", - language: "sh", - code: `evernote-backup export ./edgeever-import --notebook "工作项目"`, - }, - { - label: "Windows PowerShell", - language: "powershell", - code: `evernote-backup export .\\edgeever-import --notebook "工作项目"`, - }, + "导出的 `evernote-export` 目录内,如果你的笔记包含「笔记本组(堆叠)」,会生成对应的子目录。本项目的 MCP 导入脚本能自动识别这些子目录并还原为 EdgeEver 的嵌套笔记本结构。", ], }, { index: "2", - title: "回到 EdgeEver 导入", + title: "第二步:生成 EdgeEver MCP Token", list: [ - "回到“我的”页面里的“导入印象笔记”。", - "点击“选择文件”,选择 `edgeever-import` 里的 `.enex` 文件。", - "确认导入计划里的笔记本数量和笔记数量。", - "点击“开始导入”。", - "每导完一个笔记本,先检查结果,再点击“确认结果,继续下一个”。", + "登录你的 EdgeEver 网页端,点击左下角的「设置(Settings)」图标。", + "在设置面板中找到「API & MCP 授权」卡片。", + "点击「生成新 Token」,在输入框内填入 Token 用途(例如:Evernote Migration)。", + "勾选需要的权限(默认已勾选 `write:notebooks`, `write:memos` 等全部权限)。", + "生成后复制该 Token 字符串(格式为 `eev_...`)。注意:Token 仅在生成时展示一次,请妥善保管。", ], }, { index: "3", - title: "如果遇到问题", + title: "第三步:运行 MCP 命令行脚本导入(推荐)", + paragraphs: [ + "在 EdgeEver 项目的根目录下,使用 Bun 运行内置的 MCP 导入脚本。该脚本会模拟 MCP 协议,通过高速流式接口将 ENEX 数据写入到 D1 数据库。", + ], + commands: [ + { + label: "批量导入命令", + language: "sh", + code: `EDGEEVER_URL="你的 EdgeEver 部署地址(本地开发可使用 http://127.0.0.1:8787)" \\ +EDGEEVER_TOKEN="你的 eev_... 格式 MCP Token" \\ +bun scripts/import-evernote-enex-via-mcp.mjs --input "/路径/到/evernote-export" \\ + --exclude "不可描述,微博收藏,微信,黑的漂亮" --yes`, + }, + ], list: [ - "EdgeEver 只支持 `.enex`,不支持印象笔记新版 `.notes`。", - "如果导入中断,先检查最后一个已导入的笔记本,避免重复导入。", - "EdgeEver 会校验原始创建时间和修改时间;时间不一致时会停止导入。", - "图片和附件当前会保留为资源占位链接,重要附件建议迁移后抽查。", + "💡 **断点续传与完全幂等**:脚本支持在笔记本和笔记两个层级进行去重。在笔记本层级,若数据库中已存在相同笔记本且数量符合,将直接跳过该文件夹;若数量不符(中途中断),脚本会通过 `search_memos` 获取该笔记本已导入的最新笔记,自动跳过完全相同的重复项,实现精准的“断点续传”而不会造成数据重复。", + "💡 **网络容错与重试机制**:由于云端 D1 数据库在面对大量持续写入时可能会触发限流、短时网络延迟或数据库锁,脚本内置了带指数退避的自动重试机制(默认重试 5 次,每次失败后加倍等待),显著降低了因网络抖动或 500/502/503 错误引发的脚本意外终止率。", + "💡 **单条笔记大小限制 (1MB)**:Cloudflare D1 平台对 SQL 参数大小有硬性的 1MB (`SQLITE_TOOBIG`) 限制。为了保障迁移过程不被意外中断,对于单条体积过大(例如包含超长 base64 文本、大面积日志拷贝等)的笔记,脚本会在解析时自动截断其 Markdown 内容至 400,000 字符以内,并在末尾附加截断说明 `*(Note truncated due to Cloudflare D1 1MB database limit)*`,保障数据顺利归档。", + "💡 **内存与性能优化**:脚本已针对海量数据做流式解析优化。在解析 4GB+ 大文件时,会以「单本顺序」加载笔记树,防止导致 Node/Bun 发生 10GB+ 的 Out Of Memory 进程崩溃问题。", + "💡 **堆叠层级还原**:脚本会自动检查 `.enex` 文件的父目录名称。如果是嵌套笔记本,将自动创建父级笔记本,并通过 `parentId` 建立层级关联,完美还原印象笔记目录结构。", + "💡 **排除特定笔记本或标签**:你可以使用 `--exclude \"笔记本名称,笔记本组\"` 排除不想要的笔记本,或者使用 `--exclude-tag \"不导入\"` 跳过个别带有该标签的笔记。", + "你可以添加 `--yes` 参数以开启免交互式自动导入;也可以添加 `--dry-run` 来仅仅解析并确认导入计划。", + ], + }, + { + index: "4", + title: "第四步:回到网页端手动导入(备用)", + list: [ + "如果你只有少量单个笔记本(`.enex` 文件),也可以直接通过网页端进行导入。", + "回到 EdgeEver「我的」页面,点击「导入印象笔记」。", + "选择本地的 `.enex` 文件,确认解析出的笔记数量后,点击「开始导入」即可。", + ], + }, + { + index: "5", + title: "如果遇到问题与核心避坑指南", + list: [ + "**只支持 .enex 格式**:EdgeEver 仅支持解析标准的 `.enex` XML 格式,不支持老版印象笔记的私有 `.notes` 格式。", + "**时间戳一致性**:导入过程中会自动校验并保留原 Evernote 中的 `createdAt` 和 `updatedAt` 时间,确保迁移后笔记的时间线丝毫不乱(如果 API 生成的 memo 发现时间戳不符,脚本会自动进行验证并拦截)。", + "**资源文件处理**:导入脚本将把图片与附件保留为资源链接占位。请在迁移后抽样检查包含复杂附件的笔记以确认排版正确。", ], }, ] satisfies MigrationGuideStep[], diff --git a/bun.lock b/bun.lock index 09307dc2..491d2904 100644 --- a/bun.lock +++ b/bun.lock @@ -30,6 +30,7 @@ "lucide-react": "latest", "react": "latest", "react-dom": "latest", + "sharp": "^0.35.2", "tailwind-merge": "latest", "tailwindcss-animate": "^1.0.7", "turndown": "^7.2.4", @@ -348,53 +349,57 @@ "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.1" }, "os": "darwin", "cpu": "arm64" }, "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg=="], - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.1" }, "os": "darwin", "cpu": "x64" }, "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw=="], - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "os": "freebsd" }, "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw=="], - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g=="], - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ=="], - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg=="], - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw=="], - "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng=="], - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.1", "", { "os": "linux", "cpu": "none" }, "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw=="], - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew=="], - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A=="], - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw=="], - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg=="], - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.1" }, "os": "linux", "cpu": "arm" }, "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A=="], - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA=="], - "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.1" }, "os": "linux", "cpu": "ppc64" }, "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg=="], - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.1" }, "os": "linux", "cpu": "none" }, "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA=="], - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.1" }, "os": "linux", "cpu": "s390x" }, "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA=="], - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA=="], - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg=="], - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="], - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "cpu": "none" }, "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g=="], - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.2", "", { "os": "win32", "cpu": "x64" }, "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ=="], "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], @@ -1212,7 +1217,7 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "serialize-javascript": ["serialize-javascript@7.0.6", "", {}, "sha512-ATTK5Q4gFVg0YDp1my2vqygyvhcklD/UV5GIlYHooGTn/NogJqIzpetkD6E5kmuVULqz/S9inUL25XcAgDRJQg=="], @@ -1222,7 +1227,7 @@ "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], - "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "sharp": ["sharp@0.35.2", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.2", "@img/sharp-darwin-x64": "0.35.2", "@img/sharp-freebsd-wasm32": "0.35.2", "@img/sharp-libvips-darwin-arm64": "1.3.1", "@img/sharp-libvips-darwin-x64": "1.3.1", "@img/sharp-libvips-linux-arm": "1.3.1", "@img/sharp-libvips-linux-arm64": "1.3.1", "@img/sharp-libvips-linux-ppc64": "1.3.1", "@img/sharp-libvips-linux-riscv64": "1.3.1", "@img/sharp-libvips-linux-s390x": "1.3.1", "@img/sharp-libvips-linux-x64": "1.3.1", "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", "@img/sharp-libvips-linuxmusl-x64": "1.3.1", "@img/sharp-linux-arm": "0.35.2", "@img/sharp-linux-arm64": "0.35.2", "@img/sharp-linux-ppc64": "0.35.2", "@img/sharp-linux-riscv64": "0.35.2", "@img/sharp-linux-s390x": "0.35.2", "@img/sharp-linux-x64": "0.35.2", "@img/sharp-linuxmusl-arm64": "0.35.2", "@img/sharp-linuxmusl-x64": "0.35.2", "@img/sharp-webcontainers-wasm32": "0.35.2", "@img/sharp-win32-arm64": "0.35.2", "@img/sharp-win32-ia32": "0.35.2", "@img/sharp-win32-x64": "0.35.2" } }, "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1426,10 +1431,22 @@ "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1438,12 +1455,12 @@ "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "miniflare/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "path-scurry/lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], @@ -1452,6 +1469,54 @@ "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], + "miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "miniflare/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "miniflare/sharp/@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "miniflare/sharp/@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "miniflare/sharp/@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "miniflare/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "miniflare/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], } } diff --git a/package.json b/package.json index 30d08024..2dfb69d5 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "lucide-react": "latest", "react": "latest", "react-dom": "latest", + "sharp": "^0.35.2", "tailwind-merge": "latest", "tailwindcss-animate": "^1.0.7", "turndown": "^7.2.4", diff --git a/scripts/import-evernote-enex-via-mcp.mjs b/scripts/import-evernote-enex-via-mcp.mjs index f6a34c12..e3aabc73 100644 --- a/scripts/import-evernote-enex-via-mcp.mjs +++ b/scripts/import-evernote-enex-via-mcp.mjs @@ -3,10 +3,12 @@ import { createInterface } from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; import { readdir, readFile, stat } from "node:fs/promises"; -import { basename, extname, join } from "node:path"; +import { basename, extname, join, relative } from "node:path"; import { homedir } from "node:os"; import { XMLParser } from "fast-xml-parser"; import TurndownService from "turndown"; +import { createHash } from "node:crypto"; +import sharp from "sharp"; const CONFIG_PATH = process.env.EDGEEVER_CONFIG || join(homedir(), ".edgeever", "config.json"); const DEFAULT_URL = "http://127.0.0.1:8787"; @@ -21,10 +23,14 @@ Usage: bun scripts/import-evernote-enex-via-mcp.mjs --profile prod --input ./evernote-export --dry-run Options: - --input ENEX file or a directory containing one ENEX file per notebook. - --profile Read URL and token from ~/.edgeever/config.json. - --dry-run Parse and print the plan without writing to EdgeEver. - --yes Import all notebooks without interactive confirmations. + --input ENEX file or a directory containing one ENEX file per notebook. + --profile Read URL and token from ~/.edgeever/config.json. + --dry-run Parse and print the plan without writing to EdgeEver. + --yes Import all notebooks without interactive confirmations. + --include Comma-separated notebook/stack names to import (only these). + --exclude Comma-separated notebook/stack names to exclude. + --include-tag Only import notes containing this tag. + --exclude-tag Exclude notes containing this tag. `; if (options.help || options.h) { @@ -48,13 +54,34 @@ try { async function main() { const client = dryRun ? null : await createMcpClient(options.profile); - const notebooks = await readEnexNotebooks(inputPath); + let files = await listEnexFiles(inputPath); - if (notebooks.length === 0) { + const includeList = options.include ? options.include.split(",").map((s) => s.trim().toLowerCase()) : null; + const excludeList = options.exclude ? options.exclude.split(",").map((s) => s.trim().toLowerCase()) : null; + + if (includeList) { + files = files.filter( + (file) => + includeList.includes(notebookNameFromFile(file.filePath).toLowerCase()) || + (file.stackName && includeList.includes(file.stackName.toLowerCase())) + ); + } + + if (excludeList) { + files = files.filter( + (file) => + !excludeList.includes(notebookNameFromFile(file.filePath).toLowerCase()) && + (!file.stackName || !excludeList.includes(file.stackName.toLowerCase())) + ); + } + + if (files.length === 0) { throw new Error(`No .enex files found: ${inputPath}`); } - printPlan(notebooks); + console.log("Analyzing Evernote export files..."); + const notebooksInfo = await getNotebooksPlanInfo(files); + printPlan(notebooksInfo); if (dryRun) { process.exit(0); @@ -67,14 +94,67 @@ async function main() { let importedNotebookCount = 0; let importedMemoCount = 0; - for (const [index, notebook] of notebooks.entries()) { - console.log(`\n[${index + 1}/${notebooks.length}] Importing notebook: ${notebook.name}`); + for (const [index, fileInfo] of notebooksInfo.entries()) { + const stackPrefix = fileInfo.stackName ? `[${fileInfo.stackName}] ` : ""; + console.log(`\n[${index + 1}/${notebooksInfo.length}] Importing notebook: ${stackPrefix}${fileInfo.name}`); + + // Parse the notes for this notebook only (releasing memory afterwards) + let notes = await parseEnex(fileInfo.filePath); + + const excludeTag = options["exclude-tag"] ? options["exclude-tag"].toLowerCase() : null; + const includeTag = options["include-tag"] ? options["include-tag"].toLowerCase() : null; + + if (excludeTag) { + notes = notes.filter((note) => !note.tags.map((t) => t.toLowerCase()).includes(excludeTag)); + } + + if (includeTag) { + notes = notes.filter((note) => note.tags.map((t) => t.toLowerCase()).includes(includeTag)); + } + const before = await mcpCall(client, "list_notebooks", {}); - const targetNotebook = await findOrCreateNotebook(client, before.notebooks || [], notebook.name, index); + + let parentId = null; + if (fileInfo.stackName) { + const parentNotebook = await findOrCreateNotebook(client, before.notebooks || [], fileInfo.stackName, index, null); + parentId = parentNotebook.id; + } + + const targetNotebook = await findOrCreateNotebook(client, before.notebooks || [], fileInfo.name, index, parentId); const beforeMemoCount = Number(targetNotebook.memoCount || 0); + + if (beforeMemoCount >= notes.length) { + console.log(` Notebook "${stackPrefix}${fileInfo.name}" already fully imported (${beforeMemoCount}/${notes.length} notes). Skipping.`); + importedNotebookCount += 1; + continue; + } + const createdMemoIds = []; - for (const [memoIndex, note] of notebook.notes.entries()) { + // Fetch existing memos in this notebook to support resume/idempotency + let existingMemos = []; + if (beforeMemoCount > 0 && client) { + try { + const searchResult = await mcpCall(client, "search_memos", { + notebookId: targetNotebook.id, + limit: 50, + }); + existingMemos = searchResult.memos || []; + } catch (err) { + console.warn(` [Warning] Failed to list existing memos for idempotency: ${err.message}`); + } + } + + await mapLimit(notes, 5, async (note, memoIndex) => { + // Check if note already exists + const isDuplicate = existingMemos.some( + (m) => m.title === note.title && m.createdAt === note.createdAt && m.updatedAt === note.updatedAt + ); + if (isDuplicate) { + console.log(` ${memoIndex + 1}/${notes.length} Skipped (already imported): ${note.title || "Untitled"}`); + return; + } + const result = await mcpCall(client, "create_memo", { notebookId: targetNotebook.id, title: note.title, @@ -85,9 +165,51 @@ async function main() { }); assertImportedMemoTimestamps(result.memo, note); + + // Handle resources (images / attachments) in parallel! + let updatedMarkdown = note.markdown; + let hasUploadedResources = false; + + if (note.resources && note.resources.length > 0) { + const uploadPromises = note.resources.map(async (res) => { + try { + const processedRes = await compressImageLocal(res); + console.log(` Uploading resource: ${processedRes.filename} (${processedRes.mimeType})`); + const resource = await uploadResource(client, result.memo.id, processedRes); + return { md5: res.md5, url: resource.url }; + } catch (err) { + console.warn(` [Warning] Failed to upload resource ${res.filename}: ${err.message}`); + return null; + } + }); + + const uploaded = (await Promise.all(uploadPromises)).filter(Boolean); + for (const item of uploaded) { + const placeholder = `evernote-resource:${item.md5}`; + if (updatedMarkdown.includes(placeholder)) { + updatedMarkdown = updatedMarkdown.replaceAll(placeholder, item.url); + hasUploadedResources = true; + } + } + } + + // Update markdown if placeholders were replaced + if (hasUploadedResources) { + try { + await mcpCall(client, "update_memo", { + memoId: result.memo.id, + contentMarkdown: updatedMarkdown, + createdAt: note.createdAt, + updatedAt: note.updatedAt, + }); + } catch (err) { + console.warn(` [Warning] Failed to update memo content with resource URLs: ${err.message}`); + } + } + createdMemoIds.push(result.memo.id); - console.log(` ${memoIndex + 1}/${notebook.notes.length} ${note.title || "Untitled"}`); - } + console.log(` ${memoIndex + 1}/${notes.length} ${note.title || "Untitled"}`); + }); const after = await mcpCall(client, "list_notebooks", {}); const verifiedNotebook = (after.notebooks || []).find((item) => item.id === targetNotebook.id); @@ -97,7 +219,7 @@ async function main() { importedNotebookCount += 1; importedMemoCount += createdMemoIds.length; - console.log(`\nNotebook imported: ${notebook.name}`); + console.log(`\nNotebook imported: ${fileInfo.name}`); console.log(` Created memos: ${createdMemoIds.length}`); console.log(` Notebook memo count before: ${beforeMemoCount}`); console.log(` Notebook memo count after: ${afterMemoCount}`); @@ -106,7 +228,7 @@ async function main() { console.log(` Warning: memo count delta is ${delta}, expected ${createdMemoIds.length}. Check for concurrent edits or retries.`); } - if (!assumeYes && index < notebooks.length - 1) { + if (!assumeYes && index < notebooksInfo.length - 1) { const answer = await readline.question("Review the result in EdgeEver. Continue with the next notebook? Type yes, or anything else to stop: "); if (answer.trim().toLowerCase() !== "yes") { @@ -119,36 +241,54 @@ async function main() { console.log(`\nDone. Imported notebooks: ${importedNotebookCount}; imported memos: ${importedMemoCount}.`); } -async function readEnexNotebooks(path) { - const files = await listEnexFiles(path); - const notebooks = []; +async function getNotebooksPlanInfo(files) { + const info = []; + for (const file of files) { + const xml = await readFile(file.filePath, "utf8"); + assertReadableEvernoteXml(xml, file.filePath); + const noteCount = (xml.match(/]/ig) || []).length; - for (const filePath of files) { - const notes = await parseEnex(filePath); - notebooks.push({ - name: notebookNameFromFile(filePath), - filePath, - notes, + info.push({ + name: notebookNameFromFile(file.filePath), + stackName: file.stackName, + filePath: file.filePath, + noteCount, }); } - - return notebooks; + return info; } async function listEnexFiles(path) { if (path.toLowerCase().endsWith(".enex")) { - return [path]; + return [{ filePath: path, stackName: null }]; } if ((await stat(path)).isFile()) { throw new Error(`${path} is not an .enex file.`); } - const entries = await readdir(path, { withFileTypes: true }); - return entries - .filter((entry) => entry.isFile() && isSupportedExportFile(entry.name)) - .map((entry) => join(path, entry.name)) - .sort((a, b) => a.localeCompare(b, "zh-Hans-CN")); + const files = await scanEnexFiles(path, path); + return files.sort((a, b) => a.filePath.localeCompare(b.filePath, "zh-Hans-CN")); +} + +async function scanEnexFiles(dir, baseDir) { + const entries = await readdir(dir, { withFileTypes: true }); + let files = []; + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + files = files.concat(await scanEnexFiles(fullPath, baseDir)); + } else if (entry.isFile() && isSupportedExportFile(entry.name)) { + const relDir = relative(baseDir, dir); + files.push({ + filePath: fullPath, + stackName: relDir ? basename(relDir) : null, + }); + } + } + + return files; } async function parseEnex(filePath) { @@ -160,7 +300,10 @@ async function parseEnex(filePath) { parseTagValue: false, trimValues: false, cdataPropName: "__cdata", - isArray: (_, jpath) => jpath === "en-export.note" || jpath === "en-export.note.tag", + isArray: (_, jpath) => + jpath === "en-export.note" || + jpath === "en-export.note.tag" || + jpath === "en-export.note.resource", }); const parsed = parser.parse(xml); const notes = parsed?.["en-export"]?.note || []; @@ -198,12 +341,41 @@ function normalizeNote(note, index) { throw new Error(`Note "${title}" is missing a valid Evernote created/updated timestamp.`); } + let markdown = enexContentToMarkdown(content); + const MAX_MARKDOWN_LENGTH = 400000; // Safe limit to prevent exceeding D1 1MB SQLITE_TOOBIG limit + if (markdown.length > MAX_MARKDOWN_LENGTH) { + markdown = markdown.slice(0, MAX_MARKDOWN_LENGTH) + "\n\n*(Note truncated during migration due to Cloudflare D1 1MB database limit)*"; + console.warn(`\n [Warning] Note "${title}" truncated to ${MAX_MARKDOWN_LENGTH} chars (exceeded D1 limits).`); + } + + const resources = []; + if (note.resource) { + const rawResources = Array.isArray(note.resource) ? note.resource : [note.resource]; + for (const res of rawResources) { + const mimeType = getText(res.mime)?.trim() || "application/octet-stream"; + const dataBase64 = getText(res.data)?.replace(/\s+/g, "") || ""; + if (!dataBase64) continue; + + const attrs = res["resource-attributes"]; + const filename = getText(attrs?.["file-name"])?.trim() || `file_${resources.length}`; + const md5 = createHash("md5").update(Buffer.from(dataBase64, "base64")).digest("hex"); + + resources.push({ + dataBase64, + mimeType, + filename, + md5, + }); + } + } + return { title: title.slice(0, 160), - markdown: enexContentToMarkdown(content), + markdown, tags, createdAt, updatedAt, + resources, }; } @@ -256,8 +428,8 @@ function enexContentToMarkdown(content) { .trim(); } -async function findOrCreateNotebook(client, notebooks, name, index) { - const existing = notebooks.find((notebook) => notebook.parentId === null && notebook.name === name); +async function findOrCreateNotebook(client, notebooks, name, index, parentId = null) { + const existing = notebooks.find((notebook) => notebook.parentId === parentId && notebook.name === name); if (existing) { console.log(`Using existing notebook: ${name}`); @@ -266,7 +438,7 @@ async function findOrCreateNotebook(client, notebooks, name, index) { const result = await mcpCall(client, "create_notebook", { name: name.slice(0, 80), - parentId: null, + parentId, sortOrder: 1000 + index, }); @@ -287,31 +459,43 @@ async function createMcpClient(profileName) { return { baseUrl, token, nextId: 1 }; } -async function mcpCall(client, toolName, args) { +async function mcpCall(client, toolName, args, retries = 5, delay = 1000) { const id = client.nextId++; - const response = await fetch(`${client.baseUrl}/mcp`, { - method: "POST", - headers: { - Authorization: `Bearer ${client.token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - jsonrpc: "2.0", - id, - method: "tools/call", - params: { - name: toolName, - arguments: args, - }, - }), - }); - const body = await response.json().catch(() => null); + for (let attempt = 1; attempt <= retries; attempt++) { + try { + const response = await fetch(`${client.baseUrl}/mcp`, { + method: "POST", + headers: { + Authorization: `Bearer ${client.token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id, + method: "tools/call", + params: { + name: toolName, + arguments: args, + }, + }), + }); - if (!response.ok || body?.error) { - throw new Error(body?.error?.message || `${response.status} ${response.statusText}`); + const body = await response.json().catch(() => null); + + if (!response.ok || body?.error) { + throw new Error(body?.error?.message || `${response.status} ${response.statusText}`); + } + + return parseMcpToolResult(body.result); + } catch (error) { + if (attempt === retries) { + throw error; + } + console.warn(`\n [MCP Warning] Attempt ${attempt} failed: ${error.message}. Retrying in ${delay}ms...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + delay *= 2; // Exponential backoff + } } - - return parseMcpToolResult(body.result); } function parseMcpToolResult(result) { @@ -324,6 +508,95 @@ function parseMcpToolResult(result) { return JSON.parse(text); } +async function compressImageLocal(res) { + if (!res.mimeType.startsWith("image/") || res.mimeType === "image/gif") { + return res; + } + + try { + const inputBuffer = Buffer.from(res.dataBase64, "base64"); + const pipeline = sharp(inputBuffer); + const metadata = await pipeline.metadata(); + + let needsResize = false; + const maxEdge = 2560; + let width = metadata.width; + let height = metadata.height; + + if (width && height) { + const currentMax = Math.max(width, height); + if (currentMax > maxEdge) { + needsResize = true; + const scale = maxEdge / currentMax; + width = Math.round(width * scale); + height = Math.round(height * scale); + } + } + + if (needsResize) { + pipeline.resize(width, height, { fit: "inside" }); + } + + const outputBuffer = await pipeline.webp({ quality: 82 }).toBuffer(); + + if (outputBuffer.byteLength < inputBuffer.byteLength) { + const compressedBase64 = outputBuffer.toString("base64"); + const origExt = extname(res.filename); + const newFilename = origExt + ? res.filename.slice(0, -origExt.length) + ".webp" + : res.filename + ".webp"; + + console.log(` [Local Compress] ${res.filename} (${(inputBuffer.byteLength / 1024).toFixed(1)}KB) -> ${newFilename} (${(outputBuffer.byteLength / 1024).toFixed(1)}KB)`); + + return { + ...res, + dataBase64: compressedBase64, + mimeType: "image/webp", + filename: newFilename, + }; + } + } catch (err) { + console.warn(` [Local Compress Warning] Failed to compress image ${res.filename}: ${err.message}`); + } + + return res; +} + +async function uploadResource(client, memoId, res, retries = 5, delay = 1000) { + const formData = new FormData(); + const fileBlob = new Blob([Buffer.from(res.dataBase64, "base64")], { type: res.mimeType }); + formData.append("file", fileBlob, res.filename); + + const url = `${client.baseUrl}/api/v1/memos/${memoId}/resources`; + + for (let attempt = 1; attempt <= retries; attempt++) { + try { + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${client.token}`, + }, + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`${response.status} ${response.statusText} - ${errorText}`); + } + + const body = await response.json(); + return body.resource; + } catch (error) { + if (attempt === retries) { + throw error; + } + console.warn(`\n [Upload Warning] Attempt ${attempt} failed to upload ${res.filename}: ${error.message}. Retrying in ${delay}ms...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + delay *= 2; // Exponential backoff + } + } +} + async function readConfig() { try { const value = JSON.parse(await readFile(CONFIG_PATH, "utf8")); @@ -342,15 +615,16 @@ async function confirmOrExit(question) { } } -function printPlan(notebooks) { - const total = notebooks.reduce((sum, notebook) => sum + notebook.notes.length, 0); +function printPlan(notebooksInfo) { + const total = notebooksInfo.reduce((sum, item) => sum + item.noteCount, 0); console.log("Evernote import plan:"); - console.log(` Notebooks: ${notebooks.length}`); + console.log(` Notebooks: ${notebooksInfo.length}`); console.log(` Notes: ${total}`); - for (const notebook of notebooks) { - console.log(` - ${notebook.name}: ${notebook.notes.length} notes (${notebook.filePath})`); + for (const item of notebooksInfo) { + const stackPrefix = item.stackName ? `[${item.stackName}] ` : ""; + console.log(` - ${stackPrefix}${item.name}: ${item.noteCount} notes (${item.filePath})`); } } @@ -418,3 +692,20 @@ function requireValue(value, name) { return value; } + +async function mapLimit(array, limit, fn) { + const results = []; + const executing = new Set(); + for (let i = 0; i < array.length; i++) { + const item = array[i]; + const p = Promise.resolve().then(() => fn(item, i)); + results.push(p); + executing.add(p); + const clean = () => executing.delete(p); + p.then(clean, clean); + if (executing.size >= limit) { + await Promise.race(executing); + } + } + return Promise.all(results); +}