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);
+}