diff --git a/.gitignore b/.gitignore index dbbffd33ef8eb5bf97f64a3ad3badc1fea688d7e..7fd6d3ef2598ec7c6c84501b59f591c38acb3da5 100644 --- a/.gitignore +++ b/.gitignore @@ -16,11 +16,15 @@ wheels/ output/ data/ dashboard/dist/ -dashboard/node_modules/ docker/vendor/* !docker/vendor/.gitkeep .env +# Node.js and TypeScript generated files +node_modules/ +coverage/ +*.tsbuildinfo + # IDE and tools .claude/ .vscode/ diff --git a/api-tests/README.md b/api-tests/README.md new file mode 100644 index 0000000000000000000000000000000000000000..65c526e3f8b249f5932477d2d86f5e7a97c1fb16 --- /dev/null +++ b/api-tests/README.md @@ -0,0 +1,24 @@ +# TypeScript API 测试 + +测试直接调用已启动的 ikkyu HTTP 服务,不会 mock API。运行前需要服务已经完成本地模型预热,并准备一个 API Token。 + +```bash +cd api-tests +npm ci +IKKYU_API_TOKEN='' npm run test:basic +IKKYU_API_TOKEN='' npm run test:standard +``` + +`standard` 会同时运行 `basic`,并覆盖 PNG、PDF、Word、PowerPoint、Excel、Markdown 六种文件的 OCR 转换,以保证异常测试和文档处理测试建立在健康主链路之上。服务地址默认是 `http://127.0.0.1:8500`,可以通过 `IKKYU_API_BASE_URL` 修改;异步任务默认等待 120 秒,可以通过 `IKKYU_TASK_TIMEOUT_MS` 修改。 + +`external-full` 统一使用硅基流动服务,通过本地 Management API 临时写入 embedding、rerank、LLM 和 VL 配置。它会逐项测试模型连接,以 `t2q` 模式执行 basic 工作流,再完整执行 standard 的边界、异常、Chunk、空文件和六种文档类型用例。其中图片识别改用外部 VL,不使用本地 OCR;外部 t2q 入库最长等待 300 秒。测试结束后恢复原配置。Base URL 和模型标识均由测试固定,调用方只需提供 API Key: + +```bash +IKKYU_API_TOKEN='' \ +IKKYU_ADMIN_USERNAME='admin' \ +IKKYU_ADMIN_PASSWORD='' \ +IKKYU_SILICONFLOW_API_KEY='' \ +npm run test:external-full +``` + +固定 Base URL 为 `https://api.siliconflow.cn/v1`,模型为 `Qwen/Qwen3-Embedding-0.6B`、`Qwen/Qwen3-Reranker-0.6B`、`Qwen/Qwen3-8B` 和 `Qwen/Qwen3-VL-8B-Instruct`。Management API 使用登录后取得的 bearer token 鉴权。测试创建的领域和资源使用唯一名称,并在 suite 结束时清理。 diff --git a/api-tests/package-lock.json b/api-tests/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..23f37fb2b50e797f7c1f2aaea527c3c8fe5e6a62 --- /dev/null +++ b/api-tests/package-lock.json @@ -0,0 +1,3583 @@ +{ + "name": "ikkyu-api-tests", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ikkyu-api-tests", + "devDependencies": { + "@types/node": "^24.0.0", + "docx": "^9.5.1", + "exceljs": "^4.4.0", + "pdf-lib": "^1.17.1", + "pptxgenjs": "^4.0.1", + "sharp": "^0.34.3", + "typescript": "^5.8.3", + "vitest": "^3.2.4" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "dev": true, + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dev": true, + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/docx": { + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/docx/-/docx-9.7.1.tgz", + "integrity": "sha512-ilXFf9Moz47ABjFpDiA5s1w9lpb4EFSp7+5iiJSbfyYDM+bpZdAgLlSr7fW4aXhVe/E+F6QCv0EvRVFEd5CsWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^25.2.3", + "hash.js": "^1.1.7", + "jszip": "^3.10.1", + "nanoid": "^5.1.3", + "xml": "^1.0.1", + "xml-js": "^1.6.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/docx/node_modules/@types/node": { + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/docx/node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/docx/node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", + "dev": true, + "license": "ISC" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pptxgenjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz", + "integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^22.8.1", + "https": "^1.0.0", + "image-size": "^1.2.1", + "jszip": "^3.10.1" + } + }, + "node_modules/pptxgenjs/node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/pptxgenjs/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "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" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "dev": true, + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/api-tests/package.json b/api-tests/package.json new file mode 100644 index 0000000000000000000000000000000000000000..6bb5357610dbfb90160d40cd229f0675cfe60ff3 --- /dev/null +++ b/api-tests/package.json @@ -0,0 +1,21 @@ +{ + "name": "ikkyu-api-tests", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run", + "test:basic": "vitest run suites/basic.test.ts", + "test:standard": "vitest run suites/basic.test.ts suites/standard.test.ts", + "test:external-full": "vitest run suites/external-full.test.ts" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "docx": "^9.5.1", + "exceljs": "^4.4.0", + "pdf-lib": "^1.17.1", + "pptxgenjs": "^4.0.1", + "sharp": "^0.34.3", + "typescript": "^5.8.3", + "vitest": "^3.2.4" + } +} diff --git a/api-tests/reports/standard-2026-07-17.md b/api-tests/reports/standard-2026-07-17.md new file mode 100644 index 0000000000000000000000000000000000000000..0c3b830a9a0779a93daca57f38e0490c323cfa8e --- /dev/null +++ b/api-tests/reports/standard-2026-07-17.md @@ -0,0 +1,65 @@ +# Standard API 测试报告 + +## 执行结论 + +- 结果:通过 +- 测试文件:3/3 通过 +- 测试用例:25/25 通过 +- Vitest 总耗时:3.87 秒 +- 执行时间:2026-07-17 20:33:23(Asia/Shanghai) +- 测试目标:Docker 容器暴露的 `http://127.0.0.1:8500` + +## 镜像与容器 + +- 镜像:`ikkyu-kb-arm64:latest` +- 镜像 ID:`sha256:fd7897d63350590da0fcfd698985461ab7f34132d98e69b3e691d051f6e648cf` +- 镜像大小:1,650,008,946 bytes +- 冷构建耗时:约 256 秒 +- 容器:`ikkyu-standard-test` +- 数据卷:`ikkyu-standard-test-data` +- 端口:`8500/tcp -> 8500` +- 模型预热:7.75 秒 +- 健康检查:`200 {"status":"healthy"}` +- 容器日志:未发现 ERROR、WARNING、failed 或 traceback + +## Suite 结果 + +| Suite | 通过 | 失败 | 耗时 | +| --- | ---: | ---: | ---: | +| basic:领域到资源再到查询 | 5 | 0 | 1.517 秒 | +| standard:边界与异常状态 | 14 | 0 | 0.229 秒 | +| standard:OCR 与文档类型覆盖 | 6 | 0 | 2.009 秒 | + +## Basic 用例 + +| 用例 | 结果 | 耗时 | +| --- | --- | ---: | +| 创建知识领域 | 通过 | 72 ms | +| 查询知识领域列表 | 通过 | 8 ms | +| 添加并等待资源入库 | 通过 | 533 ms | +| 查询资源列表 | 通过 | 4 ms | +| 查询知识并命中资源 | 通过 | 367 ms | + +## 边界与异常用例 + +以下 14 项全部通过:匿名访问、无效 Token、重复领域、非法领域名、分页上界、未知字段、错误 Data URL、不存在领域资源入库、空领域查询、`topK=0`、`topK=1000`、不存在任务、不存在资源删除、不存在领域删除。 + +## OCR 用例 + +| 文件类型 | 验证内容 | 结果 | 耗时 | +| --- | --- | --- | ---: | +| PNG 图片 | 独立图片 OCR 转 Markdown | 通过 | 361 ms | +| PDF | 扫描页图片 OCR | 通过 | 771 ms | +| Word (`.docx`) | 正文保留与内嵌图片 OCR | 通过 | 172 ms | +| PowerPoint (`.pptx`) | 文本框保留与内嵌图片 OCR | 通过 | 147 ms | +| Excel (`.xlsx`) | 单元格保留与工作表图片 OCR | 通过 | 74 ms | +| Markdown (`.md`) | 正文保留与 Data URI 图片 OCR | 通过 | 120 ms | + +## 清理状态 + +- 临时 API Token 已删除。 +- basic 和 standard 创建的测试领域及资源已删除。 +- 容器内 Markdown 测试目录无残留。 +- 管理员用户名和密码均已恢复为 `admin`,并验证 `admin/admin` 登录成功、测试账号登录失败。 +- 密码修改 API 要求至少 8 位,因此清理时使用容器内现有认证模块重置持久化密码,未修改业务源码。 +- 容器和独立测试 volume 保持运行,便于继续 API 验证。 diff --git a/api-tests/src/basicWorkflow.ts b/api-tests/src/basicWorkflow.ts new file mode 100644 index 0000000000000000000000000000000000000000..c98aa6bb971e0a76a971325be885e0a3118aa858 --- /dev/null +++ b/api-tests/src/basicWorkflow.ts @@ -0,0 +1,106 @@ +import { afterAll, beforeAll, describe, expect, test } from "vitest"; + +import { ApiClient, waitForTask } from "./client.js"; +import { TEST_CONFIG, uniqueName } from "./config.js"; + +interface WorkflowOptions { + suiteName: string; + enhancedMode: "none" | "t2q" | "t2qa"; + maxChunkSize?: number; + taskTimeoutMs?: number; + knowledge?: string; +} + +export function defineBasicWorkflow(options: WorkflowOptions): void { + describe.sequential(options.suiteName, () => { + const client = new ApiClient(); + const domainName = uniqueName("api-domain"); + const resourceName = "ikkyu-guide.txt"; + const knowledge = options.knowledge ?? "一休知识库的发布暗号是青竹七号。查询时应返回这段原始证据。"; + let domainCreated = false; + let resourceCreated = false; + + beforeAll(() => { + if (!TEST_CONFIG.apiToken) throw new Error("请先设置 IKKYU_API_TOKEN"); + }); + + afterAll(async () => { + if (resourceCreated) { + const deletion = await client.request<{ taskId: string }>("/api/resource/delete", { + domainName, + name: resourceName, + }); + if (deletion.status < 300) await waitForTask(client, deletion.body.taskId); + } + if (domainCreated) await client.request("/api/domain/delete", { name: domainName }); + }); + + // 验证可以创建隔离的知识领域,并正确保存不依赖外部模型的领域参数。 + test("创建知识领域", async () => { + const result = await client.ok<{ success: boolean; domain: { name: string; enhancedMode: string } }>( + "/api/domain/add", + { + name: domainName, + description: "TypeScript API 集成测试领域", + tags: "api-test,basic", + enhancedMode: options.enhancedMode, + maxChunkSize: options.maxChunkSize ?? 300, + vectorWeight: 0.8, + bm25Weight: 0.2, + }, + ); + domainCreated = true; + expect(result.success).toBe(true); + expect(result.domain).toMatchObject({ name: domainName, enhancedMode: options.enhancedMode }); + }); + + // 验证领域列表能按关键字找到刚创建的领域,覆盖领域读取链路。 + test("查询知识领域列表", async () => { + const result = await client.ok<{ domains: Array<{ name: string }>; total: number }>("/api/domain/", { + keyword: domainName, + limit: 10, + offset: 0, + }); + expect(result.total).toBe(1); + expect(result.domains.map((domain) => domain.name)).toContain(domainName); + }); + + // 验证 Data URL 文本资源可以异步完成转换、切块、向量化和索引入库。 + test("添加并等待资源入库", async () => { + const fileInBase64 = `data:text/plain;base64,${Buffer.from(knowledge, "utf8").toString("base64")}`; + const task = await client.ok<{ taskId: string }>("/api/resource/add", { + name: resourceName, + description: "用于验证完整查询链路", + tags: "api-test,guide", + creator: "vitest", + fileInBase64, + domainName, + }); + await waitForTask(client, task.taskId, options.taskTimeoutMs); + resourceCreated = true; + }, options.taskTimeoutMs); + + // 验证资源管理列表包含已完成入库的资源及非零切片数量。 + test("查询资源列表", async () => { + const result = await client.ok<{ resources: Array<{ name: string; chunkCount: number }>; total: number }>( + "/api/resource/", + { domainName, keyword: resourceName, limit: 10, offset: 0 }, + ); + expect(result.total).toBe(1); + expect(result.resources[0]).toMatchObject({ name: resourceName }); + expect(result.resources[0].chunkCount).toBeGreaterThan(0); + }); + + // 验证领域内混合检索可以召回刚入库资源,并保留可核验的原始文本证据。 + test("查询知识并命中资源", async () => { + const result = await client.ok<{ results: Array<{ resourceName: string; text: string }> }>("/api/query", { + domains: [domainName], + tags: "api-test", + text: "一休知识库的发布暗号是什么?", + topK: 5, + }); + expect(result.results.some((item) => item.resourceName === resourceName)).toBe(true); + expect(result.results.some((item) => item.text.includes("青竹七号"))).toBe(true); + }); + }); +} diff --git a/api-tests/src/client.ts b/api-tests/src/client.ts new file mode 100644 index 0000000000000000000000000000000000000000..e68be1081ce071b097a25587d75aecca63e6be63 --- /dev/null +++ b/api-tests/src/client.ts @@ -0,0 +1,69 @@ +import { expect } from "vitest"; + +import { TEST_CONFIG } from "./config.js"; + +export interface ApiResponse { + status: number; + body: T; + text: string; +} + +export class ApiClient { + public constructor( + private readonly token = TEST_CONFIG.apiToken, + private readonly baseUrl = TEST_CONFIG.baseUrl, + ) {} + + public async request(path: string, body?: unknown, method = "POST"): Promise> { + const headers: Record = {}; + if (this.token) headers.Authorization = `Bearer ${this.token}`; + if (body !== undefined) headers["Content-Type"] = "application/json"; + + const response = await fetch(`${this.baseUrl}${path}`, { + method, + headers, + body: body === undefined ? undefined : JSON.stringify(body), + }); + const text = await response.text(); + let parsed: unknown = text; + if (text) { + try { + parsed = JSON.parse(text); + } catch { + // Markdown 和文件下载接口返回的不是 JSON,保留原始文本供断言使用。 + } + } + return { status: response.status, body: parsed as T, text }; + } + + public async ok(path: string, body?: unknown, method = "POST"): Promise { + const response = await this.request(path, body, method); + expect(response.status, `${method} ${path}: ${response.text}`).toBeGreaterThanOrEqual(200); + expect(response.status, `${method} ${path}: ${response.text}`).toBeLessThan(300); + return response.body; + } +} + +export interface TaskInfo { + taskId: string; + status: string; + success: boolean | null; + errorMessage?: string | null; +} + +export async function waitForTask( + client: ApiClient, + taskId: string, + timeoutMs = TEST_CONFIG.taskTimeoutMs, +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const task = await client.ok("/api/task/", { taskId }); + if (task.success !== null) { + expect(task.success, task.errorMessage ?? `任务 ${taskId} 执行失败`).toBe(true); + return task; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + throw new Error(`等待任务 ${taskId} 超时(${timeoutMs}ms)`); +} diff --git a/api-tests/src/config.ts b/api-tests/src/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..aaf7f4b9e6f9bd656615fc2cde6563c628a8d573 --- /dev/null +++ b/api-tests/src/config.ts @@ -0,0 +1,11 @@ +export const TEST_CONFIG = { + baseUrl: (process.env.IKKYU_API_BASE_URL ?? "http://127.0.0.1:8500").replace(/\/$/, ""), + apiToken: process.env.IKKYU_API_TOKEN ?? "", + adminUsername: process.env.IKKYU_ADMIN_USERNAME ?? "admin", + adminPassword: process.env.IKKYU_ADMIN_PASSWORD ?? "admin", + taskTimeoutMs: Number(process.env.IKKYU_TASK_TIMEOUT_MS ?? 120_000), +}; + +export function uniqueName(prefix: string): string { + return `${prefix}-${Date.now()}-${process.pid}`; +} diff --git a/api-tests/src/documentFixtures.ts b/api-tests/src/documentFixtures.ts new file mode 100644 index 0000000000000000000000000000000000000000..c204fcbdcc2f6996124f4f6f93403afbcdda61d9 --- /dev/null +++ b/api-tests/src/documentFixtures.ts @@ -0,0 +1,71 @@ +import { Document, ImageRun, Packer, Paragraph, TextRun } from "docx"; +import ExcelJS from "exceljs"; +import { PDFDocument } from "pdf-lib"; +import pptxgen from "pptxgenjs"; +import sharp from "sharp"; + +export interface DocumentFixture { + name: string; + mimeType: string; + data: Buffer; + expectedText?: string; +} + +const OCR_TEXT = "OCR TEST 7319"; + +async function createOcrImage(): Promise { + return sharp({ + create: { width: 900, height: 240, channels: 3, background: "white" }, + }) + .composite([{ + input: Buffer.from(`${OCR_TEXT}`), + }]) + .png() + .toBuffer(); +} + +export async function createDocumentFixtures(): Promise { + const image = await createOcrImage(); + + const pdf = await PDFDocument.create(); + const pdfPage = pdf.addPage([900, 300]); + const pdfImage = await pdf.embedPng(image); + pdfPage.drawImage(pdfImage, { x: 0, y: 30, width: 900, height: 240 }); + + const wordText = "WORD BODY 7319"; + const word = new Document({ sections: [{ children: [ + new Paragraph({ children: [new TextRun(wordText)] }), + new Paragraph({ children: [new ImageRun({ data: image, transformation: { width: 600, height: 160 }, type: "png" })] }), + ] }] }); + + const pptText = "PPT BODY 7319"; + // pptxgenjs 4.0.1 的 ESM 运行时可构造,但其 NodeNext 导出类型被解析为模块命名空间。 + // @ts-expect-error 上游类型声明与 ESM exports 不一致。 + const presentation = new pptxgen(); + const slide = presentation.addSlide(); + slide.addText(pptText, { x: 0.5, y: 0.3, w: 4, h: 0.5 }); + slide.addImage({ data: `data:image/png;base64,${image.toString("base64")}`, x: 0.5, y: 1, w: 7.5, h: 2 }); + + const excelText = "EXCEL BODY 7319"; + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet("OCR"); + worksheet.getCell("A1").value = excelText; + const imageId = workbook.addImage({ buffer: image as unknown as ExcelJS.Buffer, extension: "png" }); + worksheet.addImage(imageId, { tl: { col: 0, row: 2 }, ext: { width: 600, height: 160 } }); + + const markdownText = "MARKDOWN BODY 7319"; + const markdown = Buffer.from(`# OCR fixture\n\n${markdownText}\n\n![OCR](data:image/png;base64,${image.toString("base64")})`, "utf8"); + + return [ + { name: "ocr-image.png", mimeType: "image/png", data: image }, + { name: "ocr-document.pdf", mimeType: "application/pdf", data: Buffer.from(await pdf.save()) }, + { name: "ocr-document.docx", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", data: await Packer.toBuffer(word), expectedText: wordText }, + { name: "ocr-document.pptx", mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation", data: Buffer.from(await presentation.write({ outputType: "nodebuffer" }) as Buffer), expectedText: pptText }, + { name: "ocr-document.xlsx", mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", data: Buffer.from(await workbook.xlsx.writeBuffer()), expectedText: excelText }, + { name: "ocr-document.md", mimeType: "text/markdown", data: markdown, expectedText: markdownText }, + ]; +} + +export function toDataUri(fixture: DocumentFixture): string { + return `data:${fixture.mimeType};name=${fixture.name};base64,${fixture.data.toString("base64")}`; +} diff --git a/api-tests/src/standardSuites.ts b/api-tests/src/standardSuites.ts new file mode 100644 index 0000000000000000000000000000000000000000..f4dcdc743447fa16a9226fd09629708505292e75 --- /dev/null +++ b/api-tests/src/standardSuites.ts @@ -0,0 +1,294 @@ +import { afterAll, beforeAll, describe, expect, test } from "vitest"; + +import { ApiClient } from "./client.js"; +import { TEST_CONFIG, uniqueName } from "./config.js"; +import { createDocumentFixtures, DocumentFixture, toDataUri } from "./documentFixtures.js"; + +interface ConvertResult { + success: boolean; + markdown: string; + chunks: string[]; + logs: string; +} + +interface ChunkResult { + success: boolean; + chunks: string[]; + logs: string; +} + +interface StandardSuiteOptions { + suitePrefix: string; + imageProcessModel: "ocr" | "vl"; +} + +export function defineStandardSuites(options: StandardSuiteOptions): void { + describe.sequential(`${options.suitePrefix}:边界与异常状态`, () => { + const client = new ApiClient(); + const domainName = uniqueName("api-standard"); + + beforeAll(async () => { + if (!TEST_CONFIG.apiToken) throw new Error("请先设置 IKKYU_API_TOKEN"); + await client.ok("/api/domain/add", { name: domainName, enhancedMode: "none" }); + }); + + afterAll(async () => { + await client.request("/api/domain/delete", { name: domainName }); + }); + + // 验证受保护 API 在缺少 Bearer Token 时拒绝访问,避免鉴权配置意外失效。 + test("拒绝匿名访问受保护接口", async () => { + const response = await new ApiClient("").request("/api/domain/", {}); + expect(response.status).toBe(401); + }); + + // 验证错误 Bearer Token 与缺少 Token 一样返回未授权,而不是进入业务处理。 + test("拒绝无效 Bearer Token", async () => { + const response = await new ApiClient("invalid-api-token").request("/api/domain/", {}); + expect(response.status).toBe(401); + }); + + // 验证同名领域不能重复创建,确保领域唯一性约束通过 API 暴露为冲突错误。 + test("拒绝创建重复知识领域", async () => { + const response = await client.request("/api/domain/add", { name: domainName, enhancedMode: "none" }); + expect(response.status).toBe(409); + }); + + // 验证领域名禁止空白字符,覆盖名称格式校验的非法边界。 + test("拒绝包含空白的领域名", async () => { + const response = await client.request("/api/domain/add", { name: "invalid domain", enhancedMode: "none" }); + expect(response.status).toBe(400); + }); + + // 验证领域分页 limit 的上界由请求模型拦截,防止无界列表查询。 + test("拒绝超过上限的领域分页参数", async () => { + const response = await client.request("/api/domain/", { limit: 5001, offset: 0 }); + expect(response.status).toBe(422); + }); + + // 验证请求体出现未声明字段时返回校验错误,防止调用方拼错字段却静默成功。 + test("拒绝未知请求字段", async () => { + const response = await client.request("/api/domain/", { offset: 0, unexpected: true }); + expect(response.status).toBe(422); + }); + + // 验证资源 Data URL 必须包含 MIME、base64 标记和有效载荷。 + test("拒绝格式错误的资源 Data URL", async () => { + const response = await client.request("/api/resource/add", { + name: "invalid.txt", + creator: "vitest", + fileInBase64: "plain text", + domainName, + }); + expect(response.status).toBe(422); + }); + + // 验证资源入库接口拒绝零字节文件,避免创建无法检索的空资源和无效后台任务。 + test("拒绝零字节资源文件", async () => { + const response = await client.request("/api/resource/add", { + name: "empty.txt", + creator: "vitest", + fileInBase64: "data:text/plain;base64,", + domainName, + }); + expect(response.status).toBe(422); + }); + + // 验证向不存在领域添加资源会返回未找到,且不会产生后台脏任务。 + test("拒绝向不存在领域添加资源", async () => { + const data = `data:text/plain;base64,${Buffer.from("test").toString("base64")}`; + const response = await client.request("/api/resource/add", { + name: "missing.txt", + creator: "vitest", + fileInBase64: data, + domainName: `${domainName}-missing`, + }); + expect(response.status).toBe(404); + }); + + // 验证查询至少需要一个领域,覆盖空集合这一最小边界。 + test("拒绝空领域集合查询", async () => { + const response = await client.request("/api/query", { domains: [], text: "test", topK: 1 }); + expect(response.status).toBe(422); + }); + + // 验证 topK 不能为零,避免底层检索收到无意义的结果数量。 + test("拒绝 topK 等于零", async () => { + const response = await client.request("/api/query", { domains: [domainName], text: "test", topK: 0 }); + expect(response.status).toBe(422); + }); + + // 验证 topK 最大允许值 1000 可以正常进入业务查询,覆盖合法上边界。 + test("接受 topK 合法上边界", async () => { + const response = await client.request("/api/query", { domains: [domainName], text: "test", topK: 1000 }); + expect(response.status).toBe(200); + }); + + // 验证读取不存在任务时返回未找到,便于调用方区分任务失败和任务 ID 无效。 + test("查询不存在的异步任务", async () => { + const response = await client.request("/api/task/", { taskId: "missing-task-id" }); + expect(response.status).toBe(404); + }); + + // 验证删除不存在资源时立即返回未找到,而不是错误地报告已进入队列。 + test("删除不存在的资源", async () => { + const response = await client.request("/api/resource/delete", { domainName, name: "missing.txt" }); + expect(response.status).toBe(404); + }); + + // 验证删除不存在领域时返回未找到,并保持现有测试领域不受影响。 + test("删除不存在的知识领域", async () => { + const response = await client.request("/api/domain/delete", { name: `${domainName}-missing` }); + expect(response.status).toBe(404); + }); + }); + + describe.sequential(`${options.suitePrefix}:Chunk 边界与空文件`, () => { + const client = new ApiClient(""); + + async function chunk(content: string, maxChunkSize: number): Promise { + return client.ok("/api/util/chunk", { content, maxChunkSize }); + } + + function expectLosslessChunks(result: ChunkResult, content: string, maxChunkSize: number): void { + expect(result.success).toBe(true); + expect(result.chunks.join("")).toBe(content); + expect(result.chunks.every((item) => item.length <= maxChunkSize)).toBe(true); + } + + // 验证关闭切块时完整返回原文,不因标题、换行或标点产生隐式切片。 + test("maxChunkSize 为零时不切块", async () => { + const content = "# 标题\n\n第一段。\n\n第二段。"; + const result = await chunk(content, 0); + expect(result).toMatchObject({ success: true, chunks: [content] }); + }); + + // 验证空字符串执行切块时成功返回空数组,而不是产生一个无内容切片。 + test("空文本不产生 Chunk", async () => { + const result = await chunk("", 64); + expect(result).toMatchObject({ success: true, chunks: [] }); + }); + + // 验证纯空白文本不会产生 Chunk,避免进入后续扩增、向量化和索引管线。 + test("纯空白文本不产生 Chunk", async () => { + const content = " \n\t "; + const result = await chunk(content, 64); + expect(result).toMatchObject({ success: true, chunks: [] }); + }); + + // 验证有效正文两侧的空格、空行和制表符在切块过程中被完整保留。 + test("有效文本中的空白字符保持不变", async () => { + const content = " \n\t正文内容\n\n "; + const result = await chunk(content, 64); + expectLosslessChunks(result, content, 64); + }); + + // 验证文本长度恰好等于限制时,所有结果均不超过边界且原文可无损还原。 + test("文本长度等于 Chunk 上限", async () => { + const content = "边".repeat(64); + const result = await chunk(content, 64); + expectLosslessChunks(result, content, 64); + }); + + // 验证比限制多一个字符时会被安全拆分,并确保边界字符没有遗漏或重复。 + test("文本长度超过 Chunk 上限一个字符", async () => { + const content = "界".repeat(65); + const result = await chunk(content, 64); + expectLosslessChunks(result, content, 64); + expect(result.chunks.length).toBeGreaterThanOrEqual(2); + }); + + // 验证跨越两个完整边界的长文本至少产生三个合法切片,且顺序保持不变。 + test("文本跨越多个 Chunk 边界", async () => { + const content = "段".repeat(129); + const result = await chunk(content, 64); + expectLosslessChunks(result, content, 64); + expect(result.chunks.length).toBeGreaterThanOrEqual(3); + }); + + // 验证中英文、数字和标点混合内容按字符边界处理,不破坏 Unicode 文本。 + test("混合 Unicode 内容切块无损", async () => { + const content = "知识库API测试,Version 2。".repeat(12); + const result = await chunk(content, 48); + expectLosslessChunks(result, content, 48); + }); + + // 验证极小上限仍能终止处理,每个 Chunk 最多包含一个字符且全文无损。 + test("Chunk 上限为一", async () => { + const content = "边界测试"; + const result = await chunk(content, 1); + expectLosslessChunks(result, content, 1); + expect(result.chunks).toHaveLength(content.length); + }); + + // 验证缺少必填 content 字段时由 API 请求模型返回校验错误。 + test("拒绝缺少 content 的切块请求", async () => { + const response = await client.request("/api/util/chunk", { maxChunkSize: 64 }); + expect(response.status).toBe(422); + }); + + // 验证工具转换接口可以显式处理零字节文本文件,并返回可判定的空结果。 + test("转换零字节文本文件", async () => { + const result = await client.ok("/api/util/convert-file", { + data: "data:text/plain;base64,", + imageProcessModel: options.imageProcessModel, + maxChunkSize: 0, + }); + expect(result).toMatchObject({ success: true, markdown: "", chunks: [""] }); + }); + }); + + describe.sequential(`${options.suitePrefix}:图片识别与文档类型覆盖`, () => { + const client = new ApiClient(); + let fixtures: DocumentFixture[] = []; + + beforeAll(async () => { + fixtures = await createDocumentFixtures(); + }); + + async function convert(name: string): Promise { + const fixture = fixtures.find((item) => item.name === name); + if (!fixture) throw new Error(`找不到测试文件:${name}`); + const result = await client.ok("/api/util/convert-file", { + data: toDataUri(fixture), + imageProcessModel: options.imageProcessModel, + maxChunkSize: 0, + }); + expect(result.success).toBe(true); + expect(result.markdown.trim()).not.toBe(""); + if (fixture.expectedText) expect(result.markdown).toContain(fixture.expectedText); + expect(result.logs.toLowerCase()).toContain(options.imageProcessModel); + return result; + } + + // 验证独立 PNG 图片通过所选图片识别管线转换为非空 Markdown 文本。 + test("识别图片", async () => { + await convert("ocr-image.png"); + }); + + // 验证仅含扫描图片的 PDF 会逐页提取图片并识别,而不是返回空文档。 + test("识别 PDF 中的扫描图片", async () => { + await convert("ocr-document.pdf"); + }); + + // 验证 Word 正文与内嵌图片识别结果都会保留在转换后的 Markdown 中。 + test("识别 Word 内嵌图片", async () => { + await convert("ocr-document.docx"); + }); + + // 验证 PowerPoint 文本框与幻灯片内嵌图片可以共同进入转换结果。 + test("识别 PowerPoint 内嵌图片", async () => { + await convert("ocr-document.pptx"); + }); + + // 验证 Excel 单元格内容与工作表图片识别结果可以共同进入转换结果。 + test("识别 Excel 内嵌图片", async () => { + await convert("ocr-document.xlsx"); + }); + + // 验证 Markdown 中的 Data URI 图片会被识别文本替换,同时保留原始正文。 + test("识别 Markdown 内嵌图片", async () => { + await convert("ocr-document.md"); + }); + }); +} diff --git a/api-tests/suites/basic.test.ts b/api-tests/suites/basic.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..60fc9816bd15f8bc5d375fa6776d4ee50ba3c2b2 --- /dev/null +++ b/api-tests/suites/basic.test.ts @@ -0,0 +1,3 @@ +import { defineBasicWorkflow } from "../src/basicWorkflow.js"; + +defineBasicWorkflow({ suiteName: "basic:领域到资源再到查询", enhancedMode: "none" }); diff --git a/api-tests/suites/external-full.test.ts b/api-tests/suites/external-full.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..d2ab5f76080f980c376890b6d01f724c4c9bf07b --- /dev/null +++ b/api-tests/suites/external-full.test.ts @@ -0,0 +1,117 @@ +import { afterAll, beforeAll, describe, expect, test } from "vitest"; + +import { defineBasicWorkflow } from "../src/basicWorkflow.js"; +import { ApiClient } from "../src/client.js"; +import { TEST_CONFIG } from "../src/config.js"; +import { defineStandardSuites } from "../src/standardSuites.js"; + +const MODEL_KEYS = [ + "EMBEDDING_PROVIDER", "EMBEDDING_BASE_URL", "EMBEDDING_API_KEY", "EMBEDDING_MODEL", "EMBEDDING_DIMENSION", + "RERANK_PROVIDER", "RERANK_BASE_URL", "RERANK_API_KEY", "RERANK_MODEL", + "LLM_ENABLED", "LLM_BASE_URL", "LLM_API_KEY", "LLM_MODEL", + "VL_PROVIDER", "VL_BASE_URL", "VL_API_KEY", "VL_MODEL", +] as const; + +function requiredExternalValues(): Record { + const baseUrl = "https://api.siliconflow.cn/v1"; + const apiKey = process.env.IKKYU_SILICONFLOW_API_KEY ?? ""; + if (!apiKey) throw new Error("external-full suite 缺少 IKKYU_SILICONFLOW_API_KEY"); + + return { + EMBEDDING_PROVIDER: "openai", + EMBEDDING_BASE_URL: baseUrl, + EMBEDDING_API_KEY: apiKey, + EMBEDDING_MODEL: "Qwen/Qwen3-Embedding-0.6B", + EMBEDDING_DIMENSION: "512", + RERANK_PROVIDER: "openai", + RERANK_BASE_URL: baseUrl, + RERANK_API_KEY: apiKey, + RERANK_MODEL: "Qwen/Qwen3-Reranker-0.6B", + LLM_ENABLED: "1", + LLM_BASE_URL: baseUrl, + LLM_API_KEY: apiKey, + LLM_MODEL: "Qwen/Qwen3-8B", + VL_PROVIDER: "openai", + VL_BASE_URL: baseUrl, + VL_API_KEY: apiKey, + VL_MODEL: "Qwen/Qwen3-VL-8B-Instruct", + }; +} + +describe.sequential("external-full:全量外部模型集成测试", () => { + let managementClient: ApiClient; + let originalValues: Record = {}; + + beforeAll(async () => { + const anonymous = new ApiClient(""); + const login = await anonymous.ok<{ access_token: string }>("/management-api/auth/login", { + username: TEST_CONFIG.adminUsername, + password: TEST_CONFIG.adminPassword, + }); + managementClient = new ApiClient(login.access_token); + + const current = await managementClient.ok<{ variables: Array<{ key: string; value: string }> }>( + "/management-api/environment-config?reveal_secrets=true", undefined, "GET", + ); + originalValues = Object.fromEntries( + current.variables + .filter((item) => MODEL_KEYS.includes(item.key as typeof MODEL_KEYS[number])) + .map((item) => [item.key, item.value]), + ); + await managementClient.ok("/management-api/environment-config", { + values: requiredExternalValues(), + reveal_secrets: false, + }, "PATCH"); + }); + + afterAll(async () => { + if (managementClient && Object.keys(originalValues).length > 0) { + await managementClient.request("/management-api/environment-config", { values: originalValues }, "PATCH"); + } + }); + + // 验证 external-full 使用的外部 embedding 服务可连接,并返回配置的向量维度。 + test("验证外部 embedding 连接", async () => { + const result = await managementClient.ok<{ success: boolean; dimension: number }>( + "/management-api/model-connection-test", { service: "embedding", values: {} }, + ); + expect(result.success).toBe(true); + expect(result.dimension).toBe(512); + }); + + // 验证 external-full 使用的外部 rerank 服务可连接并返回排序结果。 + test("验证外部 rerank 连接", async () => { + const result = await managementClient.ok<{ success: boolean; result_count: number }>( + "/management-api/model-connection-test", { service: "rerank", values: {} }, + ); + expect(result.success).toBe(true); + expect(result.result_count).toBeGreaterThan(0); + }); + + // 验证 external-full 使用的外部 LLM 可完成真实 Completion 调用。 + test("验证外部 LLM 连接", async () => { + const result = await managementClient.ok<{ success: boolean; content: string }>( + "/management-api/model-connection-test", { service: "llm", values: {} }, + ); + expect(result.success).toBe(true); + expect(result.content.length).toBeGreaterThan(0); + }); + + // 验证 external-full 使用的外部 VL 可完成带图片的真实多模态调用。 + test("验证外部 VL 连接", async () => { + const result = await managementClient.ok<{ success: boolean; content: string }>( + "/management-api/model-connection-test", { service: "vl", values: {} }, + ); + expect(result.success).toBe(true); + expect(result.content.length).toBeGreaterThan(0); + }); + + defineBasicWorkflow({ + suiteName: "external-full:使用外部模型执行 basic 工作流", + enhancedMode: "t2q", + maxChunkSize: 0, + taskTimeoutMs: 300_000, + knowledge: "一休知识库的发布暗号是青竹七号。例行维护时间是每周三凌晨两点。服务负责人是竹林小组。", + }); + defineStandardSuites({ suitePrefix: "external-full", imageProcessModel: "vl" }); +}); diff --git a/api-tests/suites/standard.test.ts b/api-tests/suites/standard.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..c9475eeb44351c11506c38659e5eed0545f22d23 --- /dev/null +++ b/api-tests/suites/standard.test.ts @@ -0,0 +1,3 @@ +import { defineStandardSuites } from "../src/standardSuites.js"; + +defineStandardSuites({ suitePrefix: "standard", imageProcessModel: "ocr" }); diff --git a/api-tests/tsconfig.json b/api-tests/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..265a064726dd429c05d333f7d6f9766ed6b57f1d --- /dev/null +++ b/api-tests/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "strict": true, + "noEmit": true, + "types": ["node", "vitest/globals"] + }, + "include": ["**/*.ts"] +} diff --git a/api-tests/vitest.config.ts b/api-tests/vitest.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..24c2d19fbc49efe7795402499eb5cbdf04da24a4 --- /dev/null +++ b/api-tests/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + testTimeout: 360_000, + hookTimeout: 360_000, + sequence: { concurrent: false }, + }, +}); diff --git a/app/config.py b/app/config.py index 52a127af501ebc6f41d549aee492132e4e6894c3..8aa44b5b64d1071936772a3e02403195372a300b 100644 --- a/app/config.py +++ b/app/config.py @@ -131,6 +131,7 @@ EMBEDDING_PROVIDER = os.getenv("EMBEDDING_PROVIDER", "builtin") EMBEDDING_BASE_URL = os.getenv("EMBEDDING_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1") EMBEDDING_API_KEY = os.getenv("EMBEDDING_API_KEY", "") EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "") +EMBEDDING_DIMENSION = int(os.getenv("EMBEDDING_DIMENSION", "512")) EMBEDDING_API_TIMEOUT = int(os.getenv("EMBEDDING_API_TIMEOUT", "300")) # OCR 检测与识别模型路径(本地目录) @@ -164,7 +165,6 @@ REQUEST_SIZE_LIMIT_IN_MB = int(os.getenv("REQUEST_SIZE_LIMIT_IN_MB", "600")) IKKYU_ADMIN_USERNAME = os.getenv("IKKYU_ADMIN_USERNAME", "admin") IKKYU_ADMIN_PASSWORD = os.getenv("IKKYU_ADMIN_PASSWORD", "admin") IKKYU_MANAGEMENT_SESSION_TTL_SECONDS = int(os.getenv("IKKYU_MANAGEMENT_SESSION_TTL_SECONDS", "86400")) -IKKYU_MANAGEMENT_ALLOWED_HOSTS = os.getenv("IKKYU_MANAGEMENT_ALLOWED_HOSTS", "") # ==================== VL-Page 模式配置 ==================== # VL-Page 模式下 PDF 页面截图的目标宽度(像素) diff --git a/app/logic/chunk.py b/app/logic/chunk.py index fb4b8e28d2aacfb1a351f623dd7731c8cdf380ee..b1939678b437a9dd64b51a0dc4f1d87095c3ded5 100644 --- a/app/logic/chunk.py +++ b/app/logic/chunk.py @@ -336,7 +336,7 @@ def chunk_text_bert(content: str, max_size: int, logs: Optional[List[str]] = Non post_time = time.time() - post_start total_time = time.time() - start_time - result = [chunk.strip() for chunk in final_chunks if chunk.strip()] + result = [chunk for chunk in final_chunks if chunk.strip()] if logs is not None: min_size = max(1, math.ceil(max_size * MIN_CHUNK_SIZE_RATIO)) diff --git a/app/logic/embedding.py b/app/logic/embedding.py index a8fc08343e540ef834f9b2768a59fc25731ca7d8..afcf2d49c444dcd3eec818de6dd4cef97ca763a3 100644 --- a/app/logic/embedding.py +++ b/app/logic/embedding.py @@ -130,9 +130,9 @@ def __prefixed_text(text: str, text_type: str) -> str: return f"{text_type}: {text}" -def __normalize_embedding(vector: list[float]) -> list[float]: - if len(vector) != ORIGINAL_DIMENSION: - raise ValueError(f"Embedding dimension must be {ORIGINAL_DIMENSION}, got {len(vector)}") +def __normalize_embedding(vector: list[float], expected_dimension: int = ORIGINAL_DIMENSION) -> list[float]: + if len(vector) != expected_dimension: + raise ValueError(f"Embedding dimension must be {expected_dimension}, got {len(vector)}") normalized = [float(value) for value in vector] if not all(math.isfinite(value) for value in normalized): @@ -199,10 +199,11 @@ def generate_openai_embedding( if not api_key or not model: raise ValueError("EMBEDDING_API_KEY and EMBEDDING_MODEL must be configured for external embedding") + dimension = int(app_config.EMBEDDING_DIMENSION) payload = { "model": model, "input": __prefixed_text(text, text_type), - "dimensions": ORIGINAL_DIMENSION, + "dimensions": dimension, } headers = { "Authorization": f"Bearer {api_key}", @@ -226,9 +227,15 @@ def generate_openai_embedding( if not isinstance(raw_embedding, list): raise RuntimeError("External embedding response did not include an embedding list") - embedding_vector = __normalize_embedding(raw_embedding) + embedding_vector = __normalize_embedding(raw_embedding, expected_dimension=dimension) duration = time.time() - start_time - return embedding_vector, ORIGINAL_DIMENSION, duration + return embedding_vector, dimension, duration + + +def configured_embedding_dimension() -> int: + if app_config.EMBEDDING_PROVIDER == "openai": + return int(app_config.EMBEDDING_DIMENSION) + return ORIGINAL_DIMENSION def generate_embedding( diff --git a/app/logic/environment_config.py b/app/logic/environment_config.py index 7ed896383773b09af43d4522702e7da51411d9e1..1d3f81b80f5956e95be2355a9160804f90cda586 100644 --- a/app/logic/environment_config.py +++ b/app/logic/environment_config.py @@ -39,6 +39,7 @@ _ENV_ITEMS = [ ("EMBEDDING_BASE_URL", "Embedding OpenAI 兼容地址", "Embedding", False, False), ("EMBEDDING_API_KEY", "Embedding API 密钥", "Embedding", True, False), ("EMBEDDING_MODEL", "Embedding OpenAI 兼容模型", "Embedding", False, False), + ("EMBEDDING_DIMENSION", "Embedding 输出维度", "Embedding", False, False), ("EMBEDDING_API_TIMEOUT", "Embedding 调用超时秒数", "Embedding", False, False), ("RERANK_MODEL_PATH", "Rerank 模型路径", "模型", False, True), ("RERANK_PROVIDER", "Rerank 提供方", "Rerank", False, False), @@ -71,6 +72,7 @@ _INT_KEYS = { "DOWNLOAD_TIMEOUT", "VL_API_TIMEOUT", "EMBEDDING_API_TIMEOUT", + "EMBEDDING_DIMENSION", "RERANK_API_TIMEOUT", "LLM_API_TIMEOUT", "LLM_MAX_CONCURRENCY", @@ -80,6 +82,13 @@ _INT_KEYS = { _FLOAT_KEYS = {"VL_PAGE_MIN_ZOOM", "VL_PAGE_MAX_ZOOM"} _PROVIDER_KEYS = {"EMBEDDING_PROVIDER", "RERANK_PROVIDER", "VL_PROVIDER"} _BOOLEAN_KEYS = {"LLM_ENABLED"} +_EMBEDDING_CONFIG_KEYS = { + "EMBEDDING_PROVIDER", + "EMBEDDING_BASE_URL", + "EMBEDDING_API_KEY", + "EMBEDDING_MODEL", + "EMBEDDING_DIMENSION", +} _ALLOWED_KEYS = {item[0] for item in _ENV_ITEMS} _RESTART_REQUIRED_KEYS = {item[0] for item in _ENV_ITEMS if item[4]} _PENDING_RESTART_VALUES: dict[str, str] = {} @@ -100,6 +109,8 @@ def update_environment_variables(values: dict[str, Any], reveal_secrets: bool = __validate_value(key, value) normalized_values[key] = value + __normalize_embedding_config(normalized_values) + __validate_embedding_config_mutation(normalized_values) __validate_vector_database_config(normalized_values) __write_environment_file(normalized_values) @@ -128,6 +139,8 @@ def __environment_info( if value is None and hasattr(app_config, key): value = str(getattr(app_config, key)) value = value or "" + if key == "EMBEDDING_DIMENSION" and __effective_value("EMBEDDING_PROVIDER", {}) != "openai": + value = "384" should_mask = is_secret or key.endswith(_SECRET_SUFFIXES) masked = should_mask and bool(value) and not reveal_secrets @@ -156,6 +169,8 @@ def __validate_value(key: str, value: str) -> None: raise BadRequestError(f"{key} must be an integer") from exc if key == "LLM_MAX_CONCURRENCY" and integer_value < 1: raise BadRequestError("LLM_MAX_CONCURRENCY must be greater than or equal to 1") + if key == "EMBEDDING_DIMENSION" and integer_value < 1: + raise BadRequestError("EMBEDDING_DIMENSION must be greater than or equal to 1") if key in _FLOAT_KEYS: try: float(value) @@ -175,6 +190,37 @@ def __validate_vector_database_config(values: dict[str, str]) -> None: raise BadRequestError("QDRANT_URL must be configured when VECTOR_DB_PROVIDER is qdrant") +def __normalize_embedding_config(values: dict[str, str]) -> None: + provider = __effective_value("EMBEDDING_PROVIDER", values) or "builtin" + if provider == "builtin": + if "EMBEDDING_DIMENSION" in values and values["EMBEDDING_DIMENSION"] != "384": + raise BadRequestError("Built-in embedding dimension is fixed at 384") + if "EMBEDDING_PROVIDER" in values: + values["EMBEDDING_DIMENSION"] = "384" + return + + current_provider = __effective_value("EMBEDDING_PROVIDER", {}) or "builtin" + if current_provider != "openai" and "EMBEDDING_DIMENSION" not in values: + values["EMBEDDING_DIMENSION"] = "512" + + +def __validate_embedding_config_mutation(values: dict[str, str]) -> None: + changed_keys = { + key + for key in _EMBEDDING_CONFIG_KEYS.intersection(values) + if values[key] != __effective_value(key, {}) + } + if not changed_keys: + return + + from app.logic.knowledge_base import get_knowledge_base_summary + + if get_knowledge_base_summary().resource_file_count > 0: + raise BadRequestError( + "Embedding configuration cannot be changed while resource files exist" + ) + + def __effective_value(key: str, values: dict[str, str]) -> str: if key in values: return values[key] diff --git a/app/logic/knowledge_base.py b/app/logic/knowledge_base.py index a608c6c2fefb88dd5133552b7c9379135e2ef84c..7d381971717b20505be3f02fcb8c127c473fb9c9 100644 --- a/app/logic/knowledge_base.py +++ b/app/logic/knowledge_base.py @@ -23,7 +23,7 @@ from typing import Any import app.config as app_config from app.config import IKKYU_DATA_DIR -from app.logic.embedding import ORIGINAL_DIMENSION, generate_embedding +from app.logic.embedding import ORIGINAL_DIMENSION, configured_embedding_dimension, generate_embedding from app.logic.errors import BadRequestError, LogicError, ProcessingError, TaskAbortedError from app.logic.models import ( KnowledgeAugmentationMode, @@ -928,7 +928,7 @@ def _ensure_default_resource_domain() -> None: create_knowledge_domain( name=_DEFAULT_RESOURCE_DOMAIN, description="Dashboard 使用的默认资源文件库", - dimension=ORIGINAL_DIMENSION, + dimension=configured_embedding_dimension(), metric="cosine", augmentation="none", vector_weight=1.0, diff --git a/app/logic/model_connection.py b/app/logic/model_connection.py index a74cc6d4253561765f79bb19ffb226b7fc3938fb..8df574a3ed6cca5661f69729c2507fa5234b43aa 100644 --- a/app/logic/model_connection.py +++ b/app/logic/model_connection.py @@ -15,7 +15,7 @@ from app.logic.rerank import rerank_openai_documents _TEST_IMAGE_DATA_URI = ( "data:image/png;base64," - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=" + "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAN0lEQVR4nO3RwQ0AMAjDwJT9d05HMB9+vgGCZF7bXJrT9XhgwR8gEyETIRMhEyETIRMhEyEThXzH8QM9OMM6fAAAAABJRU5ErkJggg==" ) diff --git a/app/site/auth.py b/app/site/auth.py index 267d46a32c0c798c3577e7b26eea9c1f72498ab3..54cf873a67b205caf5939aa91cca633ce44d2f77 100644 --- a/app/site/auth.py +++ b/app/site/auth.py @@ -2,14 +2,10 @@ from __future__ import annotations -import os -from ipaddress import ip_address - from fastapi import Request from fastapi.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware -import app.config as app_config from app.logic.auth import validate_api_token, validate_management_session from app.logic.errors import LogicError @@ -23,9 +19,6 @@ class AuthenticationMiddleware(BaseHTTPMiddleware): return error_response if _is_management_path(path): - local_response = _validate_local_management_request(request) - if local_response is not None: - return local_response if not _is_public_management_path(path, request): auth_response = _validate_management_session_request(request) if auth_response is not None: @@ -56,12 +49,6 @@ def _validate_management_session_request(request: Request) -> JSONResponse | Non return None -def _validate_local_management_request(request: Request) -> JSONResponse | None: - if _is_local_request(request): - return None - return JSONResponse(status_code=403, content={"detail": "Management API is only available locally"}) - - def _bearer_token(request: Request) -> str | None: authorization = request.headers.get("authorization", "") scheme, _, token = authorization.partition(" ") @@ -102,43 +89,6 @@ def _is_public_management_path(path: str, request: Request) -> bool: ) -def _is_local_request(request: Request) -> bool: - client_host = request.client.host if request.client else "" - if _is_allowed_local_host(client_host): - return True - - host_header = request.headers.get("host", "") - host = _host_without_port(host_header) - return _is_allowed_local_host(host) - - -def _is_allowed_local_host(host: str) -> bool: - value = (host or "").strip().lower() - if value in {"testclient", "localhost"}: - return True - if value in _configured_management_hosts(): - return True - try: - return ip_address(value).is_loopback - except ValueError: - return False - - -def _configured_management_hosts() -> set[str]: - raw = os.getenv("IKKYU_MANAGEMENT_ALLOWED_HOSTS", app_config.IKKYU_MANAGEMENT_ALLOWED_HOSTS) - return {item.strip().lower() for item in raw.split(",") if item.strip()} - - -def _host_without_port(value: str) -> str: - host = value.strip() - if host.startswith("["): - end = host.find("]") - return host[1:end] if end >= 0 else host.strip("[]") - if host.count(":") == 1: - return host.split(":", 1)[0] - return host - - def _unauthorized(detail: str) -> JSONResponse: return JSONResponse( status_code=401, diff --git a/app/site/routes.py b/app/site/routes.py index d7ee29c08c84fb12e5ec242f523a946be01683f1..ca2154b0169941f474bfd9761d80329887733461 100644 --- a/app/site/routes.py +++ b/app/site/routes.py @@ -17,7 +17,7 @@ from app.logic.auth import ( login_admin, validate_resource_download_ticket, ) -from app.logic.embedding import ORIGINAL_DIMENSION, generate_embedding +from app.logic.embedding import configured_embedding_dimension, generate_embedding from app.logic.environment_config import list_environment_variables, update_environment_variables from app.logic.errors import LogicError from app.logic.file_resource import ( @@ -770,7 +770,7 @@ async def api_domain_add(request: KnowledgeDomainCreateRequest): domain = create_knowledge_domain( name=request.name, description=request.description, - dimension=ORIGINAL_DIMENSION, + dimension=configured_embedding_dimension(), metric="cosine", augmentation=request.enhanced_mode, max_chunk_size=request.max_chunk_size, diff --git a/doc/dev.md b/doc/dev.md index 09427b2d335627a820127d5f88cdd5ca3e131476..7cac7e43f238fda4091bdb3ec8c0dccf0da04c77 100644 --- a/doc/dev.md +++ b/doc/dev.md @@ -41,6 +41,12 @@ uv run python scripts/download_model.py ### 编译镜像 +Linux(宿主机只需安装 Docker Engine,产物固定为 `linux/amd64`): + +```bash +./scripts/compile-linux.sh +``` + Windows: ```bash @@ -56,6 +62,7 @@ Mac: 强制重新下载模型并编译: ```bash +./scripts/compile-linux.sh -f scripts\compile-windows.bat -f ./scripts/compile-mac.sh -f ``` @@ -150,6 +157,7 @@ ikkyu/ │ └── vendor/ # AI 模型目录(离线运行必需) ├── scripts/ │ ├── compile-mac.sh # Mac 编译脚本 +│ ├── compile-linux.sh # Linux Docker-only 一键编译脚本 │ ├── compile-windows.bat # Windows 编译脚本 │ ├── download_model.py # 模型下载脚本 │ ├── run-mac.sh # Mac 本地启动脚本(不依赖 Docker) diff --git a/doc/install.md b/doc/install.md index c38d08d2e8b9fe1fdefb65f40b519e6b89081f52..032cc92a1c18ceac75fd18c2cc857bf020ecec8e 100644 --- a/doc/install.md +++ b/doc/install.md @@ -2,6 +2,26 @@ 本文档介绍部署方式、启动检查、模型预热和环境变量配置。 +## 在 Linux 服务器上一键编译 + +服务器已安装并启动 Docker Engine 后,在项目根目录执行: + +```bash +./scripts/compile-linux.sh +``` + +脚本不要求宿主机安装 Python、uv、Node.js 或 npm。模型下载、Dashboard 构建和镜像构建均固定使用 `linux/amd64` 平台,最终生成经过架构校验的 `ikkyu-kb-x64:latest` 镜像。首次执行需要访问 Docker Hub、Hugging Face(或配置的镜像源)以及 Debian 软件源。 + +自定义镜像名称、标签或模型下载参数: + +```bash +IMAGE_NAME=registry.example.com/team/ikkyu-kb IMAGE_TAG=1.0.0 ./scripts/compile-linux.sh +HF_ENDPOINT=https://hf-mirror.com HF_DOWNLOAD_MAX_WORKERS=1 ./scripts/compile-linux.sh +./scripts/compile-linux.sh --force +``` + +脚本会验证 Docker daemon 是否可用,并复用已经下载的模型、npm 缓存和未变化的镜像层。`--force` 会删除已有模型后重新下载。 + ## 使用预编译镜像部署 ### 拉取镜像 diff --git a/docker/assets/env.example b/docker/assets/env.example index 6b0a5b7ef9474d8c818df9470a271e32b32cfd6c..7a040c2b106accf22929e8c8e63e690a6d3e6e20 100644 --- a/docker/assets/env.example +++ b/docker/assets/env.example @@ -19,6 +19,9 @@ LLM_API_KEY= LLM_MODEL= LLM_MAX_CONCURRENCY=10 +# 外部 Embedding 输出维度,默认 512;内置模型固定为 384 +EMBEDDING_DIMENSION=512 + # 超时配置(秒)- 支持大文档处理 # DOWNLOAD_TIMEOUT=1200 # VL_API_TIMEOUT=300 diff --git a/docker/assets/runtime.env b/docker/assets/runtime.env index 174e914f4349e405d333e74f5201749b88629127..c909e433bfeeea80b2c1e9a9dacaccb4caaad949 100644 --- a/docker/assets/runtime.env +++ b/docker/assets/runtime.env @@ -26,7 +26,6 @@ DOWNLOAD_TIMEOUT=1200 IKKYU_ADMIN_USERNAME=admin IKKYU_ADMIN_PASSWORD=admin IKKYU_MANAGEMENT_SESSION_TTL_SECONDS=86400 -IKKYU_MANAGEMENT_ALLOWED_HOSTS= VL_PROVIDER=builtin VL_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 @@ -41,6 +40,7 @@ EMBEDDING_PROVIDER=builtin EMBEDDING_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 EMBEDDING_API_KEY= EMBEDDING_MODEL= +EMBEDDING_DIMENSION=512 EMBEDDING_API_TIMEOUT=300 RERANK_PROVIDER=builtin diff --git a/scripts/compile-linux.sh b/scripts/compile-linux.sh new file mode 100755 index 0000000000000000000000000000000000000000..9e114d056d963c96f48a785b2408c632c0dfd933 --- /dev/null +++ b/scripts/compile-linux.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +IMAGE_NAME="${IMAGE_NAME:-ikkyu-kb-x64}" +IMAGE_TAG="${IMAGE_TAG:-latest}" +IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" +TARGET_PLATFORM="linux/amd64" +PYTHON_IMAGE="${PYTHON_IMAGE:-python:3.12-slim}" +NODE_IMAGE="${NODE_IMAGE:-node:22-bookworm-slim}" +HF_DOWNLOAD_MAX_WORKERS="${HF_DOWNLOAD_MAX_WORKERS:-2}" +FORCE_DOWNLOAD=0 + +usage() { + cat <<'EOF' +用法: ./scripts/compile-linux.sh [-f|--force] + +仅依赖 Linux、bash 和 Docker Engine,依次下载模型、构建 Dashboard 和 x64 Docker 镜像。 + +选项: + -f, --force 删除已有模型并重新下载 + -h, --help 显示帮助 + +可选环境变量: + IMAGE_NAME 镜像仓库名(默认: ikkyu-kb-x64) + IMAGE_TAG 镜像标签(默认: latest) + HF_DOWNLOAD_MAX_WORKERS 模型下载并发数(默认: 2) + HF_ENDPOINT Hugging Face 镜像地址 + HF_TOKEN Hugging Face 访问令牌 + HTTP_PROXY/HTTPS_PROXY 下载及构建代理 + PYTHON_IMAGE/NODE_IMAGE 临时构建容器镜像 +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -f|--force) + FORCE_DOWNLOAD=1 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "未知参数: $1" >&2 + usage >&2 + exit 2 + ;; + esac + shift +done + +if ! command -v docker >/dev/null 2>&1; then + echo "错误: 未找到 docker 命令,请先安装 Docker Engine。" >&2 + exit 1 +fi +if ! docker info >/dev/null 2>&1; then + echo "错误: 无法连接 Docker daemon,请确认服务已启动且当前用户有访问权限。" >&2 + exit 1 +fi +if [[ ! "$HF_DOWNLOAD_MAX_WORKERS" =~ ^[1-9][0-9]*$ ]]; then + echo "错误: HF_DOWNLOAD_MAX_WORKERS 必须是大于 0 的整数。" >&2 + exit 2 +fi + +mkdir -p docker/vendor dashboard/node_modules .cache/npm + +USER_ID="$(id -u)" +GROUP_ID="$(id -g)" +WORKSPACE_MOUNT="${ROOT_DIR}:/workspace" +COMMON_RUN_ARGS=( + --rm + --platform "$TARGET_PLATFORM" + --user "${USER_ID}:${GROUP_ID}" + --volume "$WORKSPACE_MOUNT" + --workdir /workspace +) +FORWARDED_ENV_VARS=( + HF_ENDPOINT HF_TOKEN HTTP_PROXY HTTPS_PROXY NO_PROXY + http_proxy https_proxy no_proxy +) +for variable_name in "${FORWARDED_ENV_VARS[@]}"; do + if [[ -n "${!variable_name:-}" ]]; then + COMMON_RUN_ARGS+=(--env "$variable_name") + fi +done + +python_in_container() { + docker run "${COMMON_RUN_ARGS[@]}" \ + --env HOME=/tmp \ + "$PYTHON_IMAGE" python "$@" +} + +image_label() { + docker image inspect "$1" --format "{{ index .Config.Labels \"$2\" }}" 2>/dev/null || true +} + +image_architecture() { + docker image inspect "$1" --format '{{.Architecture}}' 2>/dev/null || true +} + +echo "Step 1/4: 下载并校验模型..." +DOWNLOAD_ARGS=(--max-workers "$HF_DOWNLOAD_MAX_WORKERS") +if [[ "$FORCE_DOWNLOAD" == "1" ]]; then + DOWNLOAD_ARGS+=(--force) +fi +docker run "${COMMON_RUN_ARGS[@]}" \ + --env HOME=/tmp \ + "$PYTHON_IMAGE" \ + sh -c 'python -m pip install --quiet --disable-pip-version-check --target /tmp/python-deps "huggingface-hub[hf-xet]==1.23.0" && PYTHONPATH=/tmp/python-deps exec python scripts/download_model.py "$@"' \ + sh "${DOWNLOAD_ARGS[@]}" + +echo +echo "Step 2/4: 构建 Dashboard..." +docker run "${COMMON_RUN_ARGS[@]}" \ + --env HOME=/tmp \ + --env NPM_CONFIG_CACHE=/workspace/.cache/npm \ + --workdir /workspace/dashboard \ + "$NODE_IMAGE" \ + sh -c 'npm ci && npm run build' + +echo +echo "Step 3/4: 构建模型层镜像..." +MODEL_FINGERPRINT="$(python_in_container -c 'from scripts.build_cache import _model_fingerprint; print(_model_fingerprint())')" +MODEL_IMAGE="${IMAGE_NAME}-models:${MODEL_FINGERPRINT:0:16}" +if [[ "$(image_label "$MODEL_IMAGE" ikkyu.models.fingerprint)" == "$MODEL_FINGERPRINT" ]] \ + && [[ "$(image_architecture "$MODEL_IMAGE")" == "amd64" ]]; then + echo "模型层未变化,复用镜像: $MODEL_IMAGE" +else + docker build \ + --platform "$TARGET_PLATFORM" \ + --build-arg "MODEL_FINGERPRINT=$MODEL_FINGERPRINT" \ + -f docker/Dockerfile.models \ + -t "$MODEL_IMAGE" \ + . +fi + +echo +echo "Step 4/4: 构建应用镜像..." +BUILD_FINGERPRINT="$(python_in_container -c 'from scripts.build_cache import _docker_build_fingerprint, _model_fingerprint; print(_docker_build_fingerprint(_model_fingerprint()))')" +if [[ "$(image_label "$IMAGE" ikkyu.build.fingerprint)" == "$BUILD_FINGERPRINT" ]] \ + && [[ "$(image_architecture "$IMAGE")" == "amd64" ]]; then + echo "应用与模型均未变化,复用镜像: $IMAGE" +else + docker build \ + --platform "$TARGET_PLATFORM" \ + --build-arg "BUILD_FINGERPRINT=$BUILD_FINGERPRINT" \ + --build-arg "MODELS_IMAGE=$MODEL_IMAGE" \ + -f docker/Dockerfile \ + -t "$IMAGE" \ + . +fi + +echo +IMAGE_ARCHITECTURE="$(docker image inspect "$IMAGE" --format '{{.Architecture}}')" +if [[ "$IMAGE_ARCHITECTURE" != "amd64" ]]; then + echo "错误: 镜像架构应为 amd64,实际为 $IMAGE_ARCHITECTURE。" >&2 + exit 1 +fi +echo "x64 镜像构建完成: $IMAGE" +docker image inspect "$IMAGE" --format '镜像 ID: {{.Id}} 大小: {{.Size}} bytes' diff --git a/test/auth_test.py b/test/auth_test.py index 9564f856de1135b0f40f13f4db726fdf3b359f46..b0285ae451fe4b27988abe12f04db7aa19e57ad5 100644 --- a/test/auth_test.py +++ b/test/auth_test.py @@ -7,6 +7,9 @@ import unittest from pathlib import Path from unittest.mock import patch +from fastapi import FastAPI +from fastapi.testclient import TestClient + from app.logic.auth import ( create_resource_download_ticket, delete_api_token, @@ -15,11 +18,46 @@ from app.logic.auth import ( validate_resource_download_ticket, ) from app.logic.errors import LogicError +from app.site.auth import AuthenticationMiddleware _BUILTIN_API_TOKEN = "sk-1234567890qwertyuiop" +class AuthenticationMiddlewareTest(unittest.TestCase): + def test_remote_requests_use_token_authentication_without_a_host_restriction(self) -> None: + app = FastAPI() + app.add_middleware(AuthenticationMiddleware) + app.add_api_route("/management-api/auth/login", lambda: {"access_token": "session-token"}, methods=["POST"]) + app.add_api_route("/management-api/version", lambda: {"version": "test"}) + app.add_api_route("/api/domain", lambda: {"name": "test"}) + app.add_api_route("/api/util/chunk", lambda: {"chunks": []}, methods=["POST"]) + client = TestClient(app, client=("203.0.113.10", 50000)) + + self.assertEqual(client.post("/management-api/auth/login").status_code, 200) + self.assertEqual(client.get("/management-api/version").status_code, 401) + self.assertEqual(client.get("/api/domain").status_code, 401) + self.assertEqual(client.post("/api/util/chunk").status_code, 200) + + with ( + patch("app.site.auth.validate_management_session") as validate_management_session, + patch("app.site.auth.validate_api_token") as validate_api_token, + ): + management_response = client.get( + "/management-api/version", + headers={"Authorization": "Bearer session-token"}, + ) + api_response = client.get( + "/api/domain", + headers={"Authorization": "Bearer api-token"}, + ) + + self.assertEqual(management_response.status_code, 200) + self.assertEqual(api_response.status_code, 200) + validate_management_session.assert_called_once_with("session-token") + validate_api_token.assert_called_once_with("api-token") + + class BuiltinApiTokenTest(unittest.TestCase): def test_builtin_api_token_is_listed_and_accepted_without_persisted_auth_state(self) -> None: with tempfile.TemporaryDirectory() as temp_data_dir: diff --git a/test/chunk_test.py b/test/chunk_test.py index 128e75f4352fd78491e3e4ca0f607c0c2138e724..55145f40801eff92ab2987130efe486ffb774877 100644 --- a/test/chunk_test.py +++ b/test/chunk_test.py @@ -9,7 +9,8 @@ from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) -from app.logic import chunk +from app.logic import chunk, knowledge_base +from app.logic.errors import BadRequestError class ChunkTest(unittest.TestCase): @@ -83,6 +84,35 @@ class ChunkTest(unittest.TestCase): self.assertEqual(processed_windows[1][-100:], processed_windows[2][:100]) self.assertEqual("".join(chunks), content) + def test_preserves_whitespace_around_non_empty_chunk_content(self) -> None: + content = " \n\t正文内容\n\n " + + with ( + patch.object(chunk, "__initialize_model"), + patch.object( + chunk, + "__chunk_by_bert", + return_value=([content], [0]), + ), + ): + chunks = chunk.chunk_text_bert(content, max_size=100) + + self.assertEqual(chunks, [content]) + + def test_discards_content_that_contains_only_whitespace(self) -> None: + self.assertEqual(chunk.chunk_text_bert(" \n\t ", max_size=100), []) + + def test_resource_pipeline_filters_whitespace_only_chunks_before_indexing(self) -> None: + chunks = knowledge_base._normalized_document_chunks( + {"chunks": [" \n", "\t", " 正文 ", "\n\n"]} + ) + + self.assertEqual(chunks, [" 正文 "]) + + def test_resource_pipeline_rejects_document_with_only_whitespace_chunks(self) -> None: + with self.assertRaisesRegex(BadRequestError, "did not produce any chunks"): + knowledge_base._normalized_document_chunks({"chunks": [" \n", "\t"]}) + if __name__ == "__main__": unittest.main() diff --git a/test/environment_config_embedding_test.py b/test/environment_config_embedding_test.py new file mode 100644 index 0000000000000000000000000000000000000000..34b88ad119309a67a46f7c8c2d3c353f73652d09 --- /dev/null +++ b/test/environment_config_embedding_test.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import os +import sys +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +import app.config as app_config +from app.logic import environment_config +from app.logic.errors import BadRequestError + + +class EnvironmentConfigEmbeddingTest(unittest.TestCase): + def setUp(self) -> None: + self._original_environment = { + key: os.environ.get(key) + for key in ( + "EMBEDDING_PROVIDER", + "EMBEDDING_BASE_URL", + "EMBEDDING_API_KEY", + "EMBEDDING_MODEL", + "EMBEDDING_DIMENSION", + ) + } + self._original_config = { + key: getattr(app_config, key) + for key in self._original_environment + } + os.environ["EMBEDDING_PROVIDER"] = "builtin" + os.environ["EMBEDDING_DIMENSION"] = "512" + app_config.EMBEDDING_PROVIDER = "builtin" + app_config.EMBEDDING_DIMENSION = 512 + environment_config._PENDING_RESTART_VALUES.clear() + + def tearDown(self) -> None: + for key, value in self._original_environment.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + for key, value in self._original_config.items(): + setattr(app_config, key, value) + + def test_external_embedding_defaults_to_dimension_512(self) -> None: + with self._configuration_context(resource_count=0): + variables = environment_config.update_environment_variables( + {"EMBEDDING_PROVIDER": "openai"} + ) + + values = {item.key: item.value for item in variables} + self.assertEqual(values["EMBEDDING_DIMENSION"], "512") + self.assertEqual(app_config.EMBEDDING_DIMENSION, 512) + + def test_builtin_embedding_forces_dimension_384(self) -> None: + with self._configuration_context(resource_count=0): + variables = environment_config.update_environment_variables( + {"EMBEDDING_PROVIDER": "builtin"} + ) + + values = {item.key: item.value for item in variables} + self.assertEqual(values["EMBEDDING_DIMENSION"], "384") + self.assertEqual(app_config.EMBEDDING_DIMENSION, 384) + + def test_rejects_embedding_changes_when_resource_exists(self) -> None: + with self._configuration_context(resource_count=1): + with self.assertRaisesRegex( + BadRequestError, + "cannot be changed while resource files exist", + ): + environment_config.update_environment_variables( + {"EMBEDDING_MODEL": "another-model"} + ) + + def _configuration_context(self, resource_count: int): + temp_dir = tempfile.TemporaryDirectory() + self.addCleanup(temp_dir.cleanup) + env_file = Path(temp_dir.name) / ".env" + return _EmbeddingConfigContext(env_file, resource_count) + + +class _EmbeddingConfigContext: + def __init__(self, env_file: Path, resource_count: int) -> None: + self._patches = ( + patch.object(app_config, "RUNTIME_ENV_FILE", env_file), + patch( + "app.logic.knowledge_base.get_knowledge_base_summary", + return_value=SimpleNamespace(resource_file_count=resource_count), + ), + ) + + def __enter__(self): + for current_patch in self._patches: + current_patch.start() + return self + + def __exit__(self, exc_type, exc_value, traceback): + for current_patch in reversed(self._patches): + current_patch.stop() + + +if __name__ == "__main__": + unittest.main() diff --git a/test/smoke_test.py b/test/smoke_test.py index b5476002b2d1b3d2462dde8aba10c70a4b1a29d0..eeda71b974e56e92ca3c4ca2a1fce1c88d0d8acf 100644 --- a/test/smoke_test.py +++ b/test/smoke_test.py @@ -593,6 +593,28 @@ def __test_api_smoke() -> None: from app.site.main import app client = TestClient(app) + remote_client = TestClient(app, client=("203.0.113.10", 50000)) + remote_missing_auth_response = remote_client.get("/management-api/version") + if remote_missing_auth_response.status_code != 401: + raise AssertionError("remote management endpoints should require management login") + remote_login_response = remote_client.post( + "/management-api/auth/login", + json={"username": "admin", "password": "admin"}, + ) + if remote_login_response.status_code != 200: + raise AssertionError( + "remote management login should be allowed: " + f"{remote_login_response.status_code} {remote_login_response.text}" + ) + remote_management_response = remote_client.get( + "/management-api/version", + headers={"Authorization": f"Bearer {remote_login_response.json()['access_token']}"}, + ) + if remote_management_response.status_code != 200: + raise AssertionError( + "remote management session should be accepted: " + f"{remote_management_response.status_code} {remote_management_response.text}" + ) missing_api_auth_response = client.post("/api/domain/", json={}) if missing_api_auth_response.status_code != 401: raise AssertionError("/api endpoints should require bearer authentication") @@ -759,6 +781,7 @@ def __test_api_smoke() -> None: raise AssertionError("/management-api/environment-config did not include IKKYU_ORIGINAL_ROOT") for key in ( "EMBEDDING_PROVIDER", + "EMBEDDING_DIMENSION", "RERANK_PROVIDER", "VL_PROVIDER", "LLM_ENABLED", @@ -791,9 +814,6 @@ def __test_api_smoke() -> None: ) if updated_provider["value"] != "qdrant" or not updated_provider["restart_required"]: raise AssertionError("environment config should expose the pending vector provider and restart requirement") - updated_environment_keys = {item["key"] for item in environment_update_response.json()["variables"]} - if "EMBEDDING_DIMENSION" in updated_environment_keys: - raise AssertionError("EMBEDDING_DIMENSION should not be configurable") runtime_env_file = Path(temp_data_dir) / ".env" if not runtime_env_file.exists(): raise AssertionError("environment config update should persist values to runtime .env") @@ -836,6 +856,7 @@ def __test_api_smoke() -> None: original_embedding_api_key = app_config.EMBEDDING_API_KEY original_embedding_model = app_config.EMBEDDING_MODEL original_embedding_base_url = app_config.EMBEDDING_BASE_URL + original_embedding_dimension = app_config.EMBEDDING_DIMENSION original_httpx_post = embedding_module.httpx.post captured_embedding_payload = {} @@ -844,7 +865,7 @@ def __test_api_smoke() -> None: return None def json(self): - return {"data": [{"embedding": [1.0] + [0.0] * 383}]} + return {"data": [{"embedding": [1.0] + [0.0] * 511}]} def fake_embedding_post(url, headers, json, timeout): captured_embedding_payload["url"] = url @@ -857,13 +878,14 @@ def __test_api_smoke() -> None: app_config.EMBEDDING_API_KEY = "smoke-key" app_config.EMBEDDING_MODEL = "smoke-embedding" app_config.EMBEDDING_BASE_URL = "https://embedding.example/v1/" + app_config.EMBEDDING_DIMENSION = 512 embedding_module.httpx.post = fake_embedding_post try: external_vector, external_dimension, _ = embedding_module.generate_embedding("alpha", text_type="query") - if external_dimension != 384 or len(external_vector) != 384: - raise AssertionError("external embedding should always return dimension 384") - if captured_embedding_payload["json"].get("dimensions") != 384: - raise AssertionError("external embedding request should force dimensions=384") + if external_dimension != 512 or len(external_vector) != 512: + raise AssertionError("external embedding should use the configured dimension") + if captured_embedding_payload["json"].get("dimensions") != 512: + raise AssertionError("external embedding request should use the configured dimension") if captured_embedding_payload["json"].get("input") != "query: alpha": raise AssertionError("external embedding request should preserve query prefix") if captured_embedding_payload["url"] != "https://embedding.example/v1/embeddings": @@ -873,6 +895,7 @@ def __test_api_smoke() -> None: app_config.EMBEDDING_API_KEY = original_embedding_api_key app_config.EMBEDDING_MODEL = original_embedding_model app_config.EMBEDDING_BASE_URL = original_embedding_base_url + app_config.EMBEDDING_DIMENSION = original_embedding_dimension embedding_module.httpx.post = original_httpx_post from app.logic import model_connection, rerank as rerank_module, llm as llm_module @@ -3263,6 +3286,29 @@ def __test_windows_batch_scripts() -> None: raise AssertionError("compile-windows.bat must use build_cache.py: " + ", ".join(missing)) +def __test_linux_compile_script() -> None: + compile_script = ROOT / "scripts" / "compile-linux.sh" + text = compile_script.read_text(encoding="utf-8") + required_fragments = [ + "docker info", + 'IMAGE_NAME="${IMAGE_NAME:-ikkyu-kb-x64}"', + 'IMAGE_TAG="${IMAGE_TAG:-latest}"', + 'TARGET_PLATFORM="linux/amd64"', + 'HF_DOWNLOAD_MAX_WORKERS="${HF_DOWNLOAD_MAX_WORKERS:-2}"', + "scripts/download_model.py", + "npm ci", + "npm run build", + "docker/Dockerfile.models", + "docker/Dockerfile", + "Architecture", + "amd64", + "--force", + ] + missing = [fragment for fragment in required_fragments if fragment not in text] + if missing: + raise AssertionError("compile-linux.sh missing required fragments: " + ", ".join(missing)) + + def __test_build_cache_config() -> None: helper = ROOT / "scripts" / "build_cache.py" mac_script = ROOT / "scripts" / "compile-mac.sh" @@ -3398,6 +3444,7 @@ def main() -> int: ("Rerank Transformers 5 compatibility", __test_rerank_transformers_5_compatibility), ("Dockerfile config", __test_dockerfile_config), ("Windows batch scripts", __test_windows_batch_scripts), + ("Linux Docker-only compile script", __test_linux_compile_script), ("build cache config", __test_build_cache_config), ("docker run docs", __test_docker_run_docs), ]