feat: implement local image compression using sharp in Evernote import script
This commit is contained in:
1 parent
81261f2e0e
commit
d11171cfc6
6 files changed
+548
-151
No files matched your search
+18
-16
@@ -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) => {
|
||||
|
||||
@@ -111,7 +111,7 @@ const getStepIcon = (index: string): ReactNode => {
|
||||
return <Download className="h-4 w-4" />;
|
||||
}
|
||||
|
||||
if (index === "2") {
|
||||
if (index === "2" || index === "3" || index === "4") {
|
||||
return <UploadCloud className="h-4 w-4" />;
|
||||
}
|
||||
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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=="],
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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 <path> ENEX file or a directory containing one ENEX file per notebook.
|
||||
--profile <name> 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 <path> ENEX file or a directory containing one ENEX file per notebook.
|
||||
--profile <name> 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 <names> Comma-separated notebook/stack names to import (only these).
|
||||
--exclude <names> Comma-separated notebook/stack names to exclude.
|
||||
--include-tag <tag> Only import notes containing this tag.
|
||||
--exclude-tag <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(/<note[\s>]/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);
|
||||
}
|
||||
Reference in new issue
Block a user