From e223661150fd9d637c387d00470c2d9c754bfd27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=81=E4=BC=9F?= <55093136@qq.com> Date: Fri, 17 Jul 2026 21:05:27 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E5=A2=9E=E5=8A=A0api=E9=9B=86=E6=88=90?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 +- api-tests/README.md | 30 + api-tests/package-lock.json | 3583 ++++++++++++++++++++++ api-tests/package.json | 21 + api-tests/reports/standard-2026-07-17.md | 65 + api-tests/src/basicWorkflow.ts | 103 + api-tests/src/client.ts | 65 + api-tests/src/config.ts | 11 + api-tests/src/documentFixtures.ts | 71 + api-tests/suites/basic.test.ts | 3 + api-tests/suites/extra.test.ts | 92 + api-tests/suites/standard.test.ts | 288 ++ api-tests/tsconfig.json | 12 + api-tests/vitest.config.ts | 9 + app/logic/chunk.py | 2 +- test/chunk_test.py | 32 +- 16 files changed, 4390 insertions(+), 3 deletions(-) create mode 100644 api-tests/README.md create mode 100644 api-tests/package-lock.json create mode 100644 api-tests/package.json create mode 100644 api-tests/reports/standard-2026-07-17.md create mode 100644 api-tests/src/basicWorkflow.ts create mode 100644 api-tests/src/client.ts create mode 100644 api-tests/src/config.ts create mode 100644 api-tests/src/documentFixtures.ts create mode 100644 api-tests/suites/basic.test.ts create mode 100644 api-tests/suites/extra.test.ts create mode 100644 api-tests/suites/standard.test.ts create mode 100644 api-tests/tsconfig.json create mode 100644 api-tests/vitest.config.ts diff --git a/.gitignore b/.gitignore index dbbffd3..7fd6d3e 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 0000000..3ad394e --- /dev/null +++ b/api-tests/README.md @@ -0,0 +1,30 @@ +# 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` 修改。 + +`extra` 会通过本地 Management API 临时统一写入大小模型配置、逐项测试连接,再以 `t2q` 模式复跑 basic 工作流,最后恢复原配置: + +```bash +IKKYU_API_TOKEN='' \ +IKKYU_ADMIN_USERNAME='admin' \ +IKKYU_ADMIN_PASSWORD='' \ +IKKYU_EXTRA_SMALL_BASE_URL='https://small-model.example.com/v1' \ +IKKYU_EXTRA_SMALL_API_KEY='' \ +IKKYU_EXTRA_EMBEDDING_MODEL='' \ +IKKYU_EXTRA_RERANK_MODEL='' \ +IKKYU_EXTRA_LARGE_BASE_URL='https://large-model.example.com/v1' \ +IKKYU_EXTRA_LARGE_API_KEY='' \ +IKKYU_EXTRA_LLM_MODEL='' \ +npm run test:extra +``` + +注意:Management API 只允许从服务所在机器本地访问。测试创建的领域和资源使用唯一名称,并在 suite 结束时清理。 diff --git a/api-tests/package-lock.json b/api-tests/package-lock.json new file mode 100644 index 0000000..23f37fb --- /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 0000000..4a9e31b --- /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:extra": "vitest run suites/extra.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 0000000..0c3b830 --- /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 0000000..eb11609 --- /dev/null +++ b/api-tests/src/basicWorkflow.ts @@ -0,0 +1,103 @@ +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"; +} + +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 = "一休知识库的发布暗号是青竹七号。查询时应返回这段原始证据。"; + 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: 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); + resourceCreated = true; + }); + + // 验证资源管理列表包含已完成入库的资源及非零切片数量。 + 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 0000000..35e564b --- /dev/null +++ b/api-tests/src/client.ts @@ -0,0 +1,65 @@ +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): Promise { + const deadline = Date.now() + TEST_CONFIG.taskTimeoutMs; + 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} 超时(${TEST_CONFIG.taskTimeoutMs}ms)`); +} diff --git a/api-tests/src/config.ts b/api-tests/src/config.ts new file mode 100644 index 0000000..aaf7f4b --- /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 0000000..c204fcb --- /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/suites/basic.test.ts b/api-tests/suites/basic.test.ts new file mode 100644 index 0000000..60fc981 --- /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/extra.test.ts b/api-tests/suites/extra.test.ts new file mode 100644 index 0000000..e99aed5 --- /dev/null +++ b/api-tests/suites/extra.test.ts @@ -0,0 +1,92 @@ +import { afterAll, beforeAll, describe, expect, test } from "vitest"; + +import { ApiClient } from "../src/client.js"; +import { TEST_CONFIG } from "../src/config.js"; +import { defineBasicWorkflow } from "../src/basicWorkflow.js"; + +const MODEL_KEYS = [ + "EMBEDDING_PROVIDER", "EMBEDDING_BASE_URL", "EMBEDDING_API_KEY", "EMBEDDING_MODEL", + "RERANK_PROVIDER", "RERANK_BASE_URL", "RERANK_API_KEY", "RERANK_MODEL", + "LLM_ENABLED", "LLM_BASE_URL", "LLM_API_KEY", "LLM_MODEL", +] as const; + +function requiredExtraValues(): Record { + const values: Record = { + EMBEDDING_PROVIDER: "openai", + EMBEDDING_BASE_URL: process.env.IKKYU_EXTRA_SMALL_BASE_URL ?? "", + EMBEDDING_API_KEY: process.env.IKKYU_EXTRA_SMALL_API_KEY ?? "", + EMBEDDING_MODEL: process.env.IKKYU_EXTRA_EMBEDDING_MODEL ?? "", + RERANK_PROVIDER: "openai", + RERANK_BASE_URL: process.env.IKKYU_EXTRA_SMALL_BASE_URL ?? "", + RERANK_API_KEY: process.env.IKKYU_EXTRA_SMALL_API_KEY ?? "", + RERANK_MODEL: process.env.IKKYU_EXTRA_RERANK_MODEL ?? "", + LLM_ENABLED: "1", + LLM_BASE_URL: process.env.IKKYU_EXTRA_LARGE_BASE_URL ?? "", + LLM_API_KEY: process.env.IKKYU_EXTRA_LARGE_API_KEY ?? "", + LLM_MODEL: process.env.IKKYU_EXTRA_LLM_MODEL ?? "", + }; + for (const [key, value] of Object.entries(values)) { + if (!value) throw new Error(`extra suite 缺少环境变量对应的模型配置:${key}`); + } + return values; +} + +describe.sequential("extra:统一接入大小模型", () => { + 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: requiredExtraValues(), + reveal_secrets: false, + }, "PATCH"); + }); + + afterAll(async () => { + if (managementClient && Object.keys(originalValues).length > 0) { + await managementClient.request("/management-api/environment-config", { values: originalValues }, "PATCH"); + } + }); + + // 验证统一配置后的 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).toBeGreaterThan(0); + }); + + // 验证统一配置后的 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); + }); + + // 验证统一配置后的 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); + }); + + defineBasicWorkflow({ suiteName: "使用大小模型复跑 basic 工作流", enhancedMode: "t2q" }); +}); diff --git a/api-tests/suites/standard.test.ts b/api-tests/suites/standard.test.ts new file mode 100644 index 0000000..12d5527 --- /dev/null +++ b/api-tests/suites/standard.test.ts @@ -0,0 +1,288 @@ +import { afterAll, beforeAll, describe, expect, test } from "vitest"; + +import { ApiClient } from "../src/client.js"; +import { TEST_CONFIG, uniqueName } from "../src/config.js"; +import { createDocumentFixtures, DocumentFixture, toDataUri } from "../src/documentFixtures.js"; + +interface ConvertResult { + success: boolean; + markdown: string; + chunks: string[]; + logs: string; +} + +interface ChunkResult { + success: boolean; + chunks: string[]; + logs: string; +} + +describe.sequential("standard:边界与异常状态", () => { + 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("standard: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: "ocr", + maxChunkSize: 0, + }); + expect(result).toMatchObject({ success: true, markdown: "", chunks: [""] }); + }); +}); + +describe.sequential("standard:OCR 与文档类型覆盖", () => { + 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: "ocr", + maxChunkSize: 0, + }); + expect(result.success).toBe(true); + expect(result.markdown.trim()).not.toBe(""); + if (fixture.expectedText) expect(result.markdown).toContain(fixture.expectedText); + expect(result.markdown.toLowerCase()).toContain("ocr"); + return result; + } + + // 验证独立 PNG 图片通过本地 OCR 管线转换为非空 Markdown 文本。 + test("OCR 识别图片", async () => { + await convert("ocr-image.png"); + }); + + // 验证仅含扫描图片的 PDF 会逐页提取图片并执行 OCR,而不是返回空文档。 + test("OCR 识别 PDF 中的扫描图片", async () => { + await convert("ocr-document.pdf"); + }); + + // 验证 Word 正文与内嵌图片 OCR 结果都会保留在转换后的 Markdown 中。 + test("OCR 识别 Word 内嵌图片", async () => { + await convert("ocr-document.docx"); + }); + + // 验证 PowerPoint 文本框与幻灯片内嵌图片可以共同进入转换结果。 + test("OCR 识别 PowerPoint 内嵌图片", async () => { + await convert("ocr-document.pptx"); + }); + + // 验证 Excel 单元格内容与工作表图片 OCR 结果可以共同进入转换结果。 + test("OCR 识别 Excel 内嵌图片", async () => { + await convert("ocr-document.xlsx"); + }); + + // 验证 Markdown 中的 Data URI 图片会被 OCR 文本替换,同时保留原始正文。 + test("OCR 识别 Markdown 内嵌图片", async () => { + await convert("ocr-document.md"); + }); +}); diff --git a/api-tests/tsconfig.json b/api-tests/tsconfig.json new file mode 100644 index 0000000..265a064 --- /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 0000000..25ce520 --- /dev/null +++ b/api-tests/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + testTimeout: 180_000, + hookTimeout: 180_000, + sequence: { concurrent: false }, + }, +}); diff --git a/app/logic/chunk.py b/app/logic/chunk.py index fb4b8e2..b193967 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/test/chunk_test.py b/test/chunk_test.py index 128e75f..55145f4 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() -- Gitee From 55c48a874bd4a286f2f7bbc5fd45fb25c6f014bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=81=E4=BC=9F?= <55093136@qq.com> Date: Fri, 17 Jul 2026 22:02:11 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api-tests/README.md | 14 +- api-tests/package.json | 2 +- api-tests/src/basicWorkflow.ts | 11 +- api-tests/src/client.ts | 10 +- api-tests/src/standardSuites.ts | 294 ++++++++++++++++++ .../{extra.test.ts => external-full.test.ts} | 81 +++-- api-tests/suites/standard.test.ts | 289 +---------------- api-tests/vitest.config.ts | 4 +- app/config.py | 1 + app/logic/embedding.py | 19 +- app/logic/environment_config.py | 46 +++ app/logic/knowledge_base.py | 4 +- app/logic/model_connection.py | 2 +- app/site/routes.py | 4 +- docker/assets/env.example | 3 + docker/assets/runtime.env | 1 + test/environment_config_embedding_test.py | 109 +++++++ test/smoke_test.py | 17 +- 18 files changed, 557 insertions(+), 354 deletions(-) create mode 100644 api-tests/src/standardSuites.ts rename api-tests/suites/{extra.test.ts => external-full.test.ts} (48%) create mode 100644 test/environment_config_embedding_test.py diff --git a/api-tests/README.md b/api-tests/README.md index 3ad394e..3f18b4c 100644 --- a/api-tests/README.md +++ b/api-tests/README.md @@ -11,20 +11,14 @@ 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` 修改。 -`extra` 会通过本地 Management API 临时统一写入大小模型配置、逐项测试连接,再以 `t2q` 模式复跑 basic 工作流,最后恢复原配置: +`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_EXTRA_SMALL_BASE_URL='https://small-model.example.com/v1' \ -IKKYU_EXTRA_SMALL_API_KEY='' \ -IKKYU_EXTRA_EMBEDDING_MODEL='' \ -IKKYU_EXTRA_RERANK_MODEL='' \ -IKKYU_EXTRA_LARGE_BASE_URL='https://large-model.example.com/v1' \ -IKKYU_EXTRA_LARGE_API_KEY='' \ -IKKYU_EXTRA_LLM_MODEL='' \ -npm run test:extra +IKKYU_SILICONFLOW_API_KEY='' \ +npm run test:external-full ``` -注意:Management API 只允许从服务所在机器本地访问。测试创建的领域和资源使用唯一名称,并在 suite 结束时清理。 +固定 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 只允许从服务所在机器本地访问。测试创建的领域和资源使用唯一名称,并在 suite 结束时清理。 diff --git a/api-tests/package.json b/api-tests/package.json index 4a9e31b..6bb5357 100644 --- a/api-tests/package.json +++ b/api-tests/package.json @@ -6,7 +6,7 @@ "test": "vitest run", "test:basic": "vitest run suites/basic.test.ts", "test:standard": "vitest run suites/basic.test.ts suites/standard.test.ts", - "test:extra": "vitest run suites/extra.test.ts" + "test:external-full": "vitest run suites/external-full.test.ts" }, "devDependencies": { "@types/node": "^24.0.0", diff --git a/api-tests/src/basicWorkflow.ts b/api-tests/src/basicWorkflow.ts index eb11609..c98aa6b 100644 --- a/api-tests/src/basicWorkflow.ts +++ b/api-tests/src/basicWorkflow.ts @@ -6,6 +6,9 @@ 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 { @@ -13,7 +16,7 @@ export function defineBasicWorkflow(options: WorkflowOptions): void { const client = new ApiClient(); const domainName = uniqueName("api-domain"); const resourceName = "ikkyu-guide.txt"; - const knowledge = "一休知识库的发布暗号是青竹七号。查询时应返回这段原始证据。"; + const knowledge = options.knowledge ?? "一休知识库的发布暗号是青竹七号。查询时应返回这段原始证据。"; let domainCreated = false; let resourceCreated = false; @@ -41,7 +44,7 @@ export function defineBasicWorkflow(options: WorkflowOptions): void { description: "TypeScript API 集成测试领域", tags: "api-test,basic", enhancedMode: options.enhancedMode, - maxChunkSize: 300, + maxChunkSize: options.maxChunkSize ?? 300, vectorWeight: 0.8, bm25Weight: 0.2, }, @@ -73,9 +76,9 @@ export function defineBasicWorkflow(options: WorkflowOptions): void { fileInBase64, domainName, }); - await waitForTask(client, task.taskId); + await waitForTask(client, task.taskId, options.taskTimeoutMs); resourceCreated = true; - }); + }, options.taskTimeoutMs); // 验证资源管理列表包含已完成入库的资源及非零切片数量。 test("查询资源列表", async () => { diff --git a/api-tests/src/client.ts b/api-tests/src/client.ts index 35e564b..e68be10 100644 --- a/api-tests/src/client.ts +++ b/api-tests/src/client.ts @@ -51,8 +51,12 @@ export interface TaskInfo { errorMessage?: string | null; } -export async function waitForTask(client: ApiClient, taskId: string): Promise { - const deadline = Date.now() + TEST_CONFIG.taskTimeoutMs; +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) { @@ -61,5 +65,5 @@ export async function waitForTask(client: ApiClient, taskId: string): Promise setTimeout(resolve, 500)); } - throw new Error(`等待任务 ${taskId} 超时(${TEST_CONFIG.taskTimeoutMs}ms)`); + throw new Error(`等待任务 ${taskId} 超时(${timeoutMs}ms)`); } diff --git a/api-tests/src/standardSuites.ts b/api-tests/src/standardSuites.ts new file mode 100644 index 0000000..f4dcdc7 --- /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/extra.test.ts b/api-tests/suites/external-full.test.ts similarity index 48% rename from api-tests/suites/extra.test.ts rename to api-tests/suites/external-full.test.ts index e99aed5..d2ab5f7 100644 --- a/api-tests/suites/extra.test.ts +++ b/api-tests/suites/external-full.test.ts @@ -1,37 +1,44 @@ 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 { defineBasicWorkflow } from "../src/basicWorkflow.js"; +import { defineStandardSuites } from "../src/standardSuites.js"; const MODEL_KEYS = [ - "EMBEDDING_PROVIDER", "EMBEDDING_BASE_URL", "EMBEDDING_API_KEY", "EMBEDDING_MODEL", + "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 requiredExtraValues(): Record { - const values: Record = { +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: process.env.IKKYU_EXTRA_SMALL_BASE_URL ?? "", - EMBEDDING_API_KEY: process.env.IKKYU_EXTRA_SMALL_API_KEY ?? "", - EMBEDDING_MODEL: process.env.IKKYU_EXTRA_EMBEDDING_MODEL ?? "", + EMBEDDING_BASE_URL: baseUrl, + EMBEDDING_API_KEY: apiKey, + EMBEDDING_MODEL: "Qwen/Qwen3-Embedding-0.6B", + EMBEDDING_DIMENSION: "512", RERANK_PROVIDER: "openai", - RERANK_BASE_URL: process.env.IKKYU_EXTRA_SMALL_BASE_URL ?? "", - RERANK_API_KEY: process.env.IKKYU_EXTRA_SMALL_API_KEY ?? "", - RERANK_MODEL: process.env.IKKYU_EXTRA_RERANK_MODEL ?? "", + RERANK_BASE_URL: baseUrl, + RERANK_API_KEY: apiKey, + RERANK_MODEL: "Qwen/Qwen3-Reranker-0.6B", LLM_ENABLED: "1", - LLM_BASE_URL: process.env.IKKYU_EXTRA_LARGE_BASE_URL ?? "", - LLM_API_KEY: process.env.IKKYU_EXTRA_LARGE_API_KEY ?? "", - LLM_MODEL: process.env.IKKYU_EXTRA_LLM_MODEL ?? "", + 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", }; - for (const [key, value] of Object.entries(values)) { - if (!value) throw new Error(`extra suite 缺少环境变量对应的模型配置:${key}`); - } - return values; } -describe.sequential("extra:统一接入大小模型", () => { +describe.sequential("external-full:全量外部模型集成测试", () => { let managementClient: ApiClient; let originalValues: Record = {}; @@ -47,10 +54,12 @@ describe.sequential("extra:统一接入大小模型", () => { "/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]), + 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: requiredExtraValues(), + values: requiredExternalValues(), reveal_secrets: false, }, "PATCH"); }); @@ -61,17 +70,17 @@ describe.sequential("extra:统一接入大小模型", () => { } }); - // 验证统一配置后的 embedding 小模型可以完成真实连接和向量维度探测。 - test("验证 embedding 小模型连接", async () => { + // 验证 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).toBeGreaterThan(0); + expect(result.dimension).toBe(512); }); - // 验证统一配置后的 rerank 小模型可以完成真实连接并返回排序结果。 - test("验证 rerank 小模型连接", async () => { + // 验证 external-full 使用的外部 rerank 服务可连接并返回排序结果。 + test("验证外部 rerank 连接", async () => { const result = await managementClient.ok<{ success: boolean; result_count: number }>( "/management-api/model-connection-test", { service: "rerank", values: {} }, ); @@ -79,8 +88,8 @@ describe.sequential("extra:统一接入大小模型", () => { expect(result.result_count).toBeGreaterThan(0); }); - // 验证统一配置后的 LLM 大模型可以完成真实 Completion 调用。 - test("验证 LLM 大模型连接", async () => { + // 验证 external-full 使用的外部 LLM 可完成真实 Completion 调用。 + test("验证外部 LLM 连接", async () => { const result = await managementClient.ok<{ success: boolean; content: string }>( "/management-api/model-connection-test", { service: "llm", values: {} }, ); @@ -88,5 +97,21 @@ describe.sequential("extra:统一接入大小模型", () => { expect(result.content.length).toBeGreaterThan(0); }); - defineBasicWorkflow({ suiteName: "使用大小模型复跑 basic 工作流", enhancedMode: "t2q" }); + // 验证 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 index 12d5527..c9475ee 100644 --- a/api-tests/suites/standard.test.ts +++ b/api-tests/suites/standard.test.ts @@ -1,288 +1,3 @@ -import { afterAll, beforeAll, describe, expect, test } from "vitest"; +import { defineStandardSuites } from "../src/standardSuites.js"; -import { ApiClient } from "../src/client.js"; -import { TEST_CONFIG, uniqueName } from "../src/config.js"; -import { createDocumentFixtures, DocumentFixture, toDataUri } from "../src/documentFixtures.js"; - -interface ConvertResult { - success: boolean; - markdown: string; - chunks: string[]; - logs: string; -} - -interface ChunkResult { - success: boolean; - chunks: string[]; - logs: string; -} - -describe.sequential("standard:边界与异常状态", () => { - 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("standard: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: "ocr", - maxChunkSize: 0, - }); - expect(result).toMatchObject({ success: true, markdown: "", chunks: [""] }); - }); -}); - -describe.sequential("standard:OCR 与文档类型覆盖", () => { - 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: "ocr", - maxChunkSize: 0, - }); - expect(result.success).toBe(true); - expect(result.markdown.trim()).not.toBe(""); - if (fixture.expectedText) expect(result.markdown).toContain(fixture.expectedText); - expect(result.markdown.toLowerCase()).toContain("ocr"); - return result; - } - - // 验证独立 PNG 图片通过本地 OCR 管线转换为非空 Markdown 文本。 - test("OCR 识别图片", async () => { - await convert("ocr-image.png"); - }); - - // 验证仅含扫描图片的 PDF 会逐页提取图片并执行 OCR,而不是返回空文档。 - test("OCR 识别 PDF 中的扫描图片", async () => { - await convert("ocr-document.pdf"); - }); - - // 验证 Word 正文与内嵌图片 OCR 结果都会保留在转换后的 Markdown 中。 - test("OCR 识别 Word 内嵌图片", async () => { - await convert("ocr-document.docx"); - }); - - // 验证 PowerPoint 文本框与幻灯片内嵌图片可以共同进入转换结果。 - test("OCR 识别 PowerPoint 内嵌图片", async () => { - await convert("ocr-document.pptx"); - }); - - // 验证 Excel 单元格内容与工作表图片 OCR 结果可以共同进入转换结果。 - test("OCR 识别 Excel 内嵌图片", async () => { - await convert("ocr-document.xlsx"); - }); - - // 验证 Markdown 中的 Data URI 图片会被 OCR 文本替换,同时保留原始正文。 - test("OCR 识别 Markdown 内嵌图片", async () => { - await convert("ocr-document.md"); - }); -}); +defineStandardSuites({ suitePrefix: "standard", imageProcessModel: "ocr" }); diff --git a/api-tests/vitest.config.ts b/api-tests/vitest.config.ts index 25ce520..24c2d19 100644 --- a/api-tests/vitest.config.ts +++ b/api-tests/vitest.config.ts @@ -2,8 +2,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - testTimeout: 180_000, - hookTimeout: 180_000, + testTimeout: 360_000, + hookTimeout: 360_000, sequence: { concurrent: false }, }, }); diff --git a/app/config.py b/app/config.py index 52a127a..1bf8820 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 检测与识别模型路径(本地目录) diff --git a/app/logic/embedding.py b/app/logic/embedding.py index a8fc083..afcf2d4 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 7ed8963..1d3f81b 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 a608c6c..7d38197 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 a74cc6d..8df574a 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/routes.py b/app/site/routes.py index d7ee29c..ca2154b 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/docker/assets/env.example b/docker/assets/env.example index 6b0a5b7..7a040c2 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 174e914..9e04a4c 100644 --- a/docker/assets/runtime.env +++ b/docker/assets/runtime.env @@ -41,6 +41,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/test/environment_config_embedding_test.py b/test/environment_config_embedding_test.py new file mode 100644 index 0000000..34b88ad --- /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 b547600..a5298a6 100644 --- a/test/smoke_test.py +++ b/test/smoke_test.py @@ -759,6 +759,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 +792,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 +834,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 +843,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 +856,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 +873,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 -- Gitee From 97c1d56a0f1fd55cf4c438e08572e2ae25a19102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=81=E4=BC=9F?= <55093136@qq.com> Date: Fri, 17 Jul 2026 22:13:25 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/dev.md | 8 ++ doc/install.md | 20 +++++ scripts/compile-linux.sh | 163 +++++++++++++++++++++++++++++++++++++++ test/smoke_test.py | 24 ++++++ 4 files changed, 215 insertions(+) create mode 100755 scripts/compile-linux.sh diff --git a/doc/dev.md b/doc/dev.md index 09427b2..7cac7e4 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 c38d08d..032cc92 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/scripts/compile-linux.sh b/scripts/compile-linux.sh new file mode 100755 index 0000000..9e114d0 --- /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/smoke_test.py b/test/smoke_test.py index a5298a6..5dc7819 100644 --- a/test/smoke_test.py +++ b/test/smoke_test.py @@ -3264,6 +3264,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" @@ -3399,6 +3422,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), ] -- Gitee From 2cac518d8483b4f9fc03e4ae7d988f62a987a9e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=81=E4=BC=9F?= <55093136@qq.com> Date: Sat, 18 Jul 2026 21:39:14 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api-tests/README.md | 2 +- app/config.py | 1 - app/site/auth.py | 50 --------------------------------------- docker/assets/runtime.env | 1 - test/auth_test.py | 38 +++++++++++++++++++++++++++++ test/smoke_test.py | 22 +++++++++++++++++ 6 files changed, 61 insertions(+), 53 deletions(-) diff --git a/api-tests/README.md b/api-tests/README.md index 3f18b4c..65c526e 100644 --- a/api-tests/README.md +++ b/api-tests/README.md @@ -21,4 +21,4 @@ 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 只允许从服务所在机器本地访问。测试创建的领域和资源使用唯一名称,并在 suite 结束时清理。 +固定 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/app/config.py b/app/config.py index 1bf8820..8aa44b5 100644 --- a/app/config.py +++ b/app/config.py @@ -165,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/site/auth.py b/app/site/auth.py index 267d46a..54cf873 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/docker/assets/runtime.env b/docker/assets/runtime.env index 9e04a4c..c909e43 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 diff --git a/test/auth_test.py b/test/auth_test.py index 9564f85..b0285ae 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/smoke_test.py b/test/smoke_test.py index 5dc7819..eeda71b 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") -- Gitee