diff --git a/.env.example b/.env.example index 2b36ecc..c511079 100644 --- a/.env.example +++ b/.env.example @@ -35,3 +35,10 @@ COS_SECRET_ID= COS_SECRET_KEY= COS_BUCKET= COS_REGION=ap-guangzhou + +# ── 词云平台(下单后 WCD 生产任务,R4)──────────────── +# 词云服务(FastAPI)地址;留空视为未配置: +# 下单后的 WCD 派单返回结构化 "not_configured",不做假成功(详见 docs/wordcloud-contract.md) +WORDCLOUD_API_URL= +# 请求词云平台超时(毫秒),可选,默认 30000 +WORDCLOUD_TIMEOUT_MS=30000 diff --git a/docs/api-contract-v1.md b/docs/api-contract-v1.md index 14dd87e..0da33ca 100644 --- a/docs/api-contract-v1.md +++ b/docs/api-contract-v1.md @@ -162,16 +162,23 @@ unitPrice: number count: number designData?: { + version?: 1 + category?: { id: string; mask: object; tone?: number[] } + background?: { src: string; color?: string; pos?: { x: number; y: number; scale: number } } + wordcloud?: { jobId?: string; imageUrl: string; names: string[] } stickers?: unknown[] - category?: unknown - imageSrc?: string - imagePos?: { x: number; y: number; scale: number } + imageSrc?: string // 兼容旧版 + imagePos?: { x: number; y: number; scale: number } // 兼容旧版 } }[] } ``` -`items` 为服务端 JSON,需做结构白名单与大小校验(单条 ≤ 1MB)。 +> **designData 结构遵循 `wechat_wc/docs/design-data-contract-v1.md`(R2/R4 冻结契约)。** +> 该契约保证 R4 下单后能据此构造 `.wcd` 投递到词云平台。要点: +> 贴纸图 `src` 必须为 COS 持久 URL(禁止 `wxfile://`/`tmp`)、保留 `wordcloud` 分组、 +> 保留 `category.mask`;后端该 JSON 白名单须放行 `version/background/wordcloud/rotation/zIndex`。 +>`items` 为服务端 JSON,需做结构白名单与大小校验(单条 ≤ 1MB)。 ### PATCH /api/design-list/:id @@ -298,6 +305,35 @@ multipart 请求:`image`(底图)+ `names`(文本名单)+ `params?`。 multipart:`image`。返回处理后图片 URL;失败时前端降级本地灰度。 +### POST /api/orders/:id/dispatch(R4 新增,下单后 WCD 派单触发) + +幂等触发把订单对应设计的 `.wcd` 投递到词云平台形成生产任务。 + +请求体:`{}`(幂等键 `requestId` 可选)。 + +响应 `data`: + +```ts +{ + orderId: string + status: 'queued' | 'running' | 'success' | 'failed' | 'not_configured' + wordcloudJobId?: string + message?: string +} +``` + +- 同一订单重复调用只投递一次(以 `CustomizationTask.orderId` 唯一或状态约束)。 +- `WORDCLOUD_API_URL` 未配置时返回 `status: 'not_configured'` 与可读 `message`,不做假成功。 +- 常规路径:订单进入 `PROCESSING` 时由后端队列自动触发;本接口作为支付未配置期的联调/运营手段。 +- 词云平台侧契约见 `docs/wordcloud-contract.md`(`POST /api/jobs` 可选 `wcd_file`)。 + +### 环境变量(R4 新增) + +| 变量 | 必填 | 说明 | +|---|---|---| +| `WORDCLOUD_API_URL` | 否(为空视为未配置) | 词云平台(FastAPI)地址;未配置时派单返回 `not_configured` | +| `WORDCLOUD_TIMEOUT_MS` | 否 | 请求 wordcloud 超时(毫秒),默认 30000 | + ## 9. 契约维护规则 - 前端与后端同名分支成对开发:`feat/r1-catalog`、`feat/r2-address-design`、`feat/r3-order-pay`、`feat/r4-upload-wordcloud`。 diff --git a/docs/wordcloud-contract.md b/docs/wordcloud-contract.md index f63f69d..646a8bc 100644 --- a/docs/wordcloud-contract.md +++ b/docs/wordcloud-contract.md @@ -1,13 +1,14 @@ -# wordcloud 外部服务契约 v1 +# wordcloud 外部服务契约 v1.1 本文档冻结 `wxmp_backend` 与 `/Users/broccoli/Project/wordcloud` 之间依赖的最小接口。 wordcloud 项目内部仍在大量变更,但只要不违反本文档,`wxmp_backend` 的适配层不受影响。 ## 1. 版本与状态 -- 契约版本:`v1` -- 状态:*.xlsx 名单模式冻结;`names` 直接文本/JSON 模式为推荐扩展点,尚未冻结。 -- 服务地址:由 `wxmp_backend` 环境变量配置,禁止硬编码到代码或小程序前端。 +- 契约版本:`v1.1`(相对 v1 为**兼容扩展**,仅新增可选字段,未改任何既有字段语义)。 +- 状态:*.xlsx 名单模式冻结;`names` 直接文本/JSON 模式为推荐扩展点,尚未冻结; + **WCD 生产任务输入(`wcd_file`)已冻结(v1.1)**,wordcloud 侧实现可后置。 +- 服务地址:由 `wxmp_backend` 环境变量 `WORDCLOUD_API_URL` 配置,禁止硬编码到代码或小程序前端。 - 小程序前端永远不直接访问 wordcloud,只访问 `wxmp_backend`。 ## 2. 冻结接口 @@ -18,30 +19,44 @@ wordcloud 项目内部仍在大量变更,但只要不违反本文档,`wxmp_b | 字段 | 必填 | 说明 | |---|---|---| -| `name_list` | 是 | `.xlsx` 名单文件,v1 必填 | -| `mask_image` | IMAGE 模式必填 | `.png/.jpg/.jpeg` 掩膜 | +| `name_list` | 与 `wcd_file` 二选一 | `.xlsx` 名单文件,v1 必填 | +| `wcd_file` | 与 `name_list` 二选一 | `.wcd` 画布导入导出包(v1.1 新增)。存在时进入“还原设计 → 生产任务”模式 | +| `mask_image` | IMAGE 模式必填 | `.png/.jpg/.jpeg` 掩膜;`wcd_file` 模式下不要求 | | `font_file` | 否 | `.ttf/.ttc/.otf` 临时字体 | | `font_id` | 否 | 已上传字体 id | | `params` | 否 | JSON 字符串,顶层必须是对象 | -`params` 关键值: +`wcd_file` 模式下 `params` 关键值建议带: ```json { - "MODE": "IMAGE", - "DATA_COL_INDEX": 1, + "MODE": "WCD", "SEED": 42, - "N_REPETITIONS": 20, "ENABLE_STROKE_WEIGHTS": false, "FONT_COLOR": "#000000" } ``` -响应:`{ "job_id": "..." }`。 +响应与既有模式一致:`{ "job_id": "..." }`。 + +**WCD 包结构**(wxmp_backend 构造,校验逻辑对齐 wordcloud 现有 +`POST /api/design-templates/import`): + +``` +{wcd}.wcd +├── manifest.json // { format: "wordcloud-canvas", version: 1, canvas: {width,height,background}, +│ // assets: [{id,name,type,mimeType,sha256,size}], meta?: {...} } +├── document.json // CanvasDocument:{width,height,background,layers,layerFolders,elements} +└── assets/. // 每个 sticker/底图的图片字节 +``` + +- `document.elements[]` 中 `type == "sticker"` 的元素通过 `assetId` 引用包内临时 ID, + 导入后由 wordcloud 统一重映射为真实素材 ID。 +- 名单快照可放 `manifest.meta`,仅记录,不影响还原。 ### GET /api/jobs/{job_id} -返回: +返回(与 v1 一致): ```json { @@ -91,19 +106,29 @@ wordcloud 项目内部仍在大量变更,但只要不违反本文档,`wxmp_b ## 4. 错误语义 - 4xx:参数错误,错误信息在 `detail` 或 `message`。 + - `wcd_file` 非 `.wcd` / 非合法 Zip => 400。 + - `manifest.format != "wordcloud-canvas"` 或 `version != 1` => 400。 + - `name_list` 与 `wcd_file` 都缺失 => 400。 - 5xx:服务/任务异常,wxmp_backend 将任务标记 `failed` 并在超时后清理。 - wxmp_backend 适配层应设置请求超时(例如 30s),任务超时上限(例如 10 分钟)。 ## 5. 稳定性规则 -- 上述 4 个端点的路径、字段名、状态枚举、分页/轮询语义冻结为 v1。 +- 上述端点的路径、字段名、状态枚举、分页/轮询语义冻结。 +- `wcd_file` 为 v1.1 兼容扩展:对既有调用方完全向后兼容(纯新增可选字段)。 - wordcloud 内部重构、新增 canvas/assets/projects/templates 能力不影响本契约。 - 契约变更流程:定义 v2 -> 更新本文档 -> wxmp_backend 在 R4 分支升级适配器 -> 双版本并存一个发布周期。 -- 若 wordcloud 希望新增“直接传名字列表”能力,默认视为 v1 兼容扩展,字段必须是 optional。 ## 6. 集成要点 -- `wxmp_backend` 负责把小程序文本名单转换为 `.xlsx` 再调用 `POST /api/jobs`。 -- `wxmp_backend` 保存 `job_id -> userId` 映射,轮询与结果查询必须校验归属。 +- `wxmp_backend` 负责把小程序文本名单转换为 `.xlsx` 再调用 `POST /api/jobs`(名单模式)。 +- **`wxmp_backend` 负责在下单后把订单对应 `designData` 构造为 `.wcd` 再调用 + `POST /api/jobs`(WCD 模式,`MODE=WCD`)**,见 + `wechat_wc/docs/design-data-contract-v1.md`(数据结构)与 + `wechat_wc/docs/routes/route-r4-upload-wordcloud.md` §3(派单流程)。 +- `wxmp_backend` 保存 `job_id -> orderId/userId`(WCD 模式)或 `job_id -> userId`(名单模式)映射, + 轮询与结果查询必须校验归属。 - 结果图由 wxmp_backend 下载并转存 COS,返回给小程序的是 COS 公网 URL。 -- wordcloud 若对外暴露公网,需要加访问 token 或网络白名单,防止被直接刷任务。 +- `WORDCLOUD_API_URL` 未配置时,下单派单返回结构化“未配置”错误(状态字面量 + `not_configured`,见 `api-contract-v1.md` §8 的 `POST /api/orders/:id/dispatch`),不做假成功。 +- wordcloud 若对外暴露公网,需要加访问 token 或网络白名单,防止被直接刷任务。 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 427cd92..07aba9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,9 +22,12 @@ "bullmq": "^5.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cos-nodejs-sdk-v5": "^3.0.0", "dayjs": "^1.11.0", + "exceljs": "^4.4.0", "ioredis": "^5.4.0", "joi": "^17.13.0", + "jszip": "^3.10.1", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", @@ -34,6 +37,7 @@ "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@types/express": "^5.0.0", + "@types/multer": "^2.2.0", "@types/node": "^22.0.0", "@types/passport-jwt": "^4.0.1", "eslint": "^9.0.0", @@ -417,6 +421,47 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "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==", + "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==", + "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==", + "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==", + "license": "MIT" + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -1630,6 +1675,16 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, + "node_modules/@types/multer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.2.0.tgz", + "integrity": "sha512-3U1troeqGV8Ntp7Q3klwf4zr23VEoqYVocYXaswm9+8z3O9UHDYAqLxjJ/h550iRADTjKdOdhhasXw6gD6kYtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "22.20.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", @@ -2127,6 +2182,96 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "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==", + "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/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", + "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/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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -2147,12 +2292,51 @@ "dev": true, "license": "MIT" }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" + }, "node_modules/axios": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", @@ -2169,14 +2353,12 @@ "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", @@ -2206,6 +2388,37 @@ "node": ">=6.0.0" } }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2223,7 +2436,6 @@ "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", @@ -2231,6 +2443,12 @@ "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==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -2272,7 +2490,6 @@ "version": "1.1.18", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2330,7 +2547,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -2351,6 +2567,15 @@ "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==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -2363,6 +2588,23 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "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==", + "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==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/bullmq": { "version": "5.81.3", "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.81.3.tgz", @@ -2510,6 +2752,24 @@ ], "license": "CC-BY-4.0" }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, + "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==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2714,11 +2974,25 @@ "node": ">= 6" } }, + "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==", + "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/concat-stream": { @@ -2793,6 +3067,12 @@ "node": ">=6.6.0" } }, + "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==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -2810,6 +3090,87 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cos-fast-xml-parser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cos-fast-xml-parser/-/cos-fast-xml-parser-1.0.0.tgz", + "integrity": "sha512-kOPJb1cuj+gc5E8jN5ekZn4rgQHaxYTLcQT+jmbHtlTNcNjv7wnOBEYH2uRA0pG3h8giot4z9h4WFLDuXATZwg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + }, + "engines": { + "node": ">= 9" + } + }, + "node_modules/cos-nodejs-sdk-v5": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cos-nodejs-sdk-v5/-/cos-nodejs-sdk-v5-3.0.0.tgz", + "integrity": "sha512-xUqiDdUxfEjfaWoyBA3qsSnlQVHPz13DRFErL+NliadBVUeeZhAPHGVa2inGpUvPTOs52ALIKkMbzL+inSqvXg==", + "license": "ISC", + "dependencies": { + "cos-fast-xml-parser": "^1.0.0", + "cos-request": "^1.3.0", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 9" + } + }, + "node_modules/cos-request": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/cos-request/-/cos-request-1.3.3.tgz", + "integrity": "sha512-zD7fKMAIMfJNHssx8VZE5mUQ67hs3QDi7S2BX7JuD8MzT1XYqEOAN42PJ0BtRhgutnn+8HiUF+Fup+j5zORasQ==", + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.5.6", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "^6.15.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~4.1.4", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cos-request/node_modules/form-data": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz", + "integrity": "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -2860,6 +3221,31 @@ "js-yaml": "bin/js-yaml.js" } }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -2895,6 +3281,18 @@ "node": ">= 8" } }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/dayjs": { "version": "1.11.21", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", @@ -3072,6 +3470,45 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/dynamic-dedupe": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", @@ -3082,6 +3519,16 @@ "xtend": "^4.0.0" } }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3141,6 +3588,15 @@ "node": ">= 0.8" } }, + "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==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.24.5", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", @@ -3457,6 +3913,26 @@ "node": ">=0.8.x" } }, + "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==", + "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/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -3532,6 +4008,21 @@ "devOptional": true, "license": "MIT" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", @@ -3555,6 +4046,19 @@ "node": ">=8.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==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3722,6 +4226,15 @@ } } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", @@ -3784,6 +4297,12 @@ "node": ">= 0.8" } }, + "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==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -3810,7 +4329,6 @@ "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": { @@ -3828,6 +4346,34 @@ "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.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/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==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3874,6 +4420,15 @@ "node": ">= 0.4" } }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/giget": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", @@ -3998,7 +4553,6 @@ "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/has-flag": { @@ -4070,6 +4624,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -4129,6 +4698,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4161,7 +4736,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", @@ -4300,6 +4874,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -4313,6 +4893,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4320,6 +4906,12 @@ "dev": true, "license": "ISC" }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, "node_modules/iterare": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", @@ -4412,6 +5004,12 @@ "js-yaml": "bin/js-yaml.mjs" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4426,6 +5024,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -4440,6 +5044,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4495,6 +5105,63 @@ "npm": ">=6" } }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -4526,6 +5193,48 @@ "json-buffer": "3.0.1" } }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4546,6 +5255,15 @@ "integrity": "sha512-xJxrdqvbl2rtn2MaUJrUejz8J7/uZNC0V77oks2LxYrO/+ZtVpRmz+fEQMuu6VusnEB1fmpByiLS1WXecOnAnw==", "license": "MIT" }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -4553,6 +5271,12 @@ "dev": true, "license": "MIT" }, + "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==", + "license": "ISC" + }, "node_modules/load-esm": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz", @@ -4608,6 +5332,36 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "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==", + "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==", + "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==", + "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==", + "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==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -4620,12 +5374,31 @@ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "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.", + "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==", + "license": "MIT" + }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", "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==", + "license": "MIT" + }, "node_modules/lodash.isnumber": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", @@ -4644,6 +5417,12 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "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==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4657,6 +5436,18 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "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==", + "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==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -4789,7 +5580,6 @@ "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" @@ -4802,7 +5592,6 @@ "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" @@ -4994,7 +5783,6 @@ "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" @@ -5025,6 +5813,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5174,6 +5971,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5265,7 +6068,6 @@ "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" @@ -5344,6 +6146,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5438,6 +6246,12 @@ } } }, + "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==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5460,11 +6274,22 @@ "node": ">=10" } }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5503,6 +6328,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", @@ -5556,6 +6387,36 @@ "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==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "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==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -5607,6 +6468,12 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -5665,7 +6532,6 @@ "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" @@ -5679,7 +6545,6 @@ "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", @@ -5747,6 +6612,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -5882,6 +6759,12 @@ "url": "https://opencollective.com/express" } }, + "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==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -6027,6 +6910,31 @@ "node": ">=0.10.0" } }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -6110,6 +7018,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/strtok3": { "version": "10.3.5", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", @@ -6185,6 +7105,22 @@ "url": "https://opencollective.com/webpack" } }, + "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==", + "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/terser": { "version": "5.49.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.1.tgz", @@ -6331,6 +7267,15 @@ "node": ">=18" } }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6371,6 +7316,39 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -6584,6 +7562,24 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6722,6 +7718,54 @@ "node": ">= 0.8" } }, + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -6763,6 +7807,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6778,6 +7832,16 @@ "node": ">= 0.4.0" } }, + "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).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -6803,6 +7867,26 @@ "node": ">= 0.8" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" + }, "node_modules/watchpack": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", @@ -7013,6 +8097,12 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -7068,6 +8158,62 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "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==", + "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==", + "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" + } + }, + "node_modules/zip-stream/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", + "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" + } } } } diff --git a/package.json b/package.json index 9830f15..82ec5d3 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,12 @@ "bullmq": "^5.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cos-nodejs-sdk-v5": "^3.0.0", "dayjs": "^1.11.0", + "exceljs": "^4.4.0", "ioredis": "^5.4.0", "joi": "^17.13.0", + "jszip": "^3.10.1", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", @@ -46,6 +49,7 @@ "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@types/express": "^5.0.0", + "@types/multer": "^2.2.0", "@types/node": "^22.0.0", "@types/passport-jwt": "^4.0.1", "eslint": "^9.0.0", diff --git a/prisma/migrations/20260812100838_add_word_cloud_job/migration.sql b/prisma/migrations/20260812100838_add_word_cloud_job/migration.sql new file mode 100644 index 0000000..7a5ba4d --- /dev/null +++ b/prisma/migrations/20260812100838_add_word_cloud_job/migration.sql @@ -0,0 +1,22 @@ +-- CreateEnum +CREATE TYPE "WordCloudJobStatus" AS ENUM ('QUEUED', 'RUNNING', 'SUCCESS', 'FAILED'); + +-- CreateTable +CREATE TABLE "WordCloudJob" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "status" "WordCloudJobStatus" NOT NULL DEFAULT 'QUEUED', + "progress" INTEGER NOT NULL DEFAULT 0, + "imageUrl" TEXT, + "error" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "WordCloudJob_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "WordCloudJob_userId_idx" ON "WordCloudJob"("userId"); + +-- AddForeignKey +ALTER TABLE "WordCloudJob" ADD CONSTRAINT "WordCloudJob_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20260812101101_add_remote_job_id/migration.sql b/prisma/migrations/20260812101101_add_remote_job_id/migration.sql new file mode 100644 index 0000000..fe94a8b --- /dev/null +++ b/prisma/migrations/20260812101101_add_remote_job_id/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "WordCloudJob" ADD COLUMN "remoteJobId" TEXT; diff --git a/prisma/migrations/20260812124858_r4_order_design_and_dispatch/migration.sql b/prisma/migrations/20260812124858_r4_order_design_and_dispatch/migration.sql new file mode 100644 index 0000000..1244626 --- /dev/null +++ b/prisma/migrations/20260812124858_r4_order_design_and_dispatch/migration.sql @@ -0,0 +1,15 @@ +-- AlterTable +ALTER TABLE "CustomizationTask" ADD COLUMN "orderId" TEXT, +ADD COLUMN "wordcloudJobId" TEXT; + +-- AlterTable +ALTER TABLE "Order" ADD COLUMN "designListId" TEXT; + +-- CreateIndex +CREATE INDEX "CustomizationTask_orderId_idx" ON "CustomizationTask"("orderId"); + +-- CreateIndex +CREATE INDEX "Order_designListId_idx" ON "Order"("designListId"); + +-- AddForeignKey +ALTER TABLE "Order" ADD CONSTRAINT "Order_designListId_fkey" FOREIGN KEY ("designListId") REFERENCES "DesignList"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2e73458..94ce1a4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,6 +29,7 @@ model User { designLists DesignList[] uploads Upload[] customizations CustomizationTask[] + wordCloudJobs WordCloudJob[] @@index([phone]) } @@ -84,6 +85,7 @@ model DesignList { user User @relation(fields: [userId], references: [id]) customizations CustomizationTask[] + orders Order[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -125,7 +127,10 @@ model Order { status OrderStatus @default(PENDING) totalAmount Decimal @db.Decimal(10, 2) addressSnapshot Json + // 关联的设计清单(R4 下单后 WCD 派单据此读取 designData) + designListId String? user User @relation(fields: [userId], references: [id]) + designList DesignList? @relation(fields: [designListId], references: [id]) items OrderItem[] payment Payment? @@ -134,6 +139,7 @@ model Order { @@index([userId]) @@index([status]) + @@index([designListId]) } enum OrderStatus { @@ -202,6 +208,9 @@ model CustomizationTask { id String @id @default(cuid()) designListId String? userId String? + // R4 下单后 WCD 派单:关联的订单与词云任务 + orderId String? + wordcloudJobId String? status CustomizationTaskStatus @default(PENDING) resultUrl String? queueJobId String? @@ -212,6 +221,7 @@ model CustomizationTask { updatedAt DateTime @updatedAt @@index([designListId]) + @@index([orderId]) @@index([status]) } @@ -221,3 +231,28 @@ enum CustomizationTaskStatus { SUCCESS FAILED } + +// ── 词云任务(R4:小程序交互生成 + 下单 WCD 派单共用)──────────────── +model WordCloudJob { + id String @id @default(cuid()) + userId String + // wordcloud 侧的外部 job_id(POST /api/jobs 返回);空表示尚未在外部创建 + remoteJobId String? + status WordCloudJobStatus @default(QUEUED) + progress Int @default(0) + imageUrl String? + error String? + user User @relation(fields: [userId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([userId]) +} + +enum WordCloudJobStatus { + QUEUED + RUNNING + SUCCESS + FAILED +} diff --git a/src/app.module.ts b/src/app.module.ts index f44e492..7b2cc82 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -16,6 +16,8 @@ import { AddressesModule } from './addresses/addresses.module'; import { OrdersModule } from './orders/orders.module'; import { PaymentsModule } from './payments/payments.module'; import { UploadModule } from './upload/upload.module'; +import { WordCloudModule } from './wordcloud/wordcloud.module'; +import { SketchModule } from './sketch/sketch.module'; import { QueueModule } from './queue/queue.module'; import { HealthModule } from './health/health.module'; @@ -40,6 +42,8 @@ import { HealthModule } from './health/health.module'; OrdersModule, PaymentsModule, UploadModule, + WordCloudModule, + SketchModule, QueueModule, HealthModule, ], diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 90cb26c..fd6010b 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -35,4 +35,10 @@ export default () => ({ bucket: process.env.COS_BUCKET ?? '', region: process.env.COS_REGION ?? 'ap-guangzhou', }, + wordcloud: { + // 词云平台(FastAPI)服务地址;为空视为未配置,下单后的 WCD 派单返回 not_configured + apiUrl: process.env.WORDCLOUD_API_URL ?? '', + // 请求 wordcloud 超时(毫秒) + timeoutMs: parseInt(process.env.WORDCLOUD_TIMEOUT_MS ?? '30000', 10), + }, }); diff --git a/src/config/validation.schema.ts b/src/config/validation.schema.ts index 31344bb..88fe297 100644 --- a/src/config/validation.schema.ts +++ b/src/config/validation.schema.ts @@ -28,4 +28,8 @@ export const validationSchema = Joi.object({ COS_SECRET_KEY: Joi.string().allow('').default(''), COS_BUCKET: Joi.string().allow('').default(''), COS_REGION: Joi.string().default('ap-guangzhou'), + + // 词云平台:允许为空(未配置时下单派单返回 not_configured,见 wordcloud-contract.md) + WORDCLOUD_API_URL: Joi.string().allow('').default(''), + WORDCLOUD_TIMEOUT_MS: Joi.number().default(30000), }); diff --git a/src/cos/cos.module.ts b/src/cos/cos.module.ts new file mode 100644 index 0000000..ebfa006 --- /dev/null +++ b/src/cos/cos.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { CosService } from './cos.service'; + +@Module({ + providers: [CosService], + exports: [CosService], +}) +export class CosModule {} diff --git a/src/cos/cos.service.ts b/src/cos/cos.service.ts new file mode 100644 index 0000000..a274d57 --- /dev/null +++ b/src/cos/cos.service.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import COS from 'cos-nodejs-sdk-v5'; + +/** + * 腾讯云 COS 封装。 + * 用途: + * - 给小程序签发预签名 PUT URL 直传(小程序不持有永久密钥); + * - 后端服务端写入(把 wordcloud 结果图转存到 COS)。 + * 未配置(COS_SECRET_ID/KEY/BUCKET 为空)时 configured=false,调用方返回结构化“未配置”。 + */ +@Injectable() +export class CosService { + private readonly client: COS | null; + + constructor(private readonly config: ConfigService) { + const secretId = this.config.get('cos.secretId') ?? ''; + const secretKey = this.config.get('cos.secretKey') ?? ''; + this.client = secretId && secretKey ? new COS({ SecretId: secretId, SecretKey: secretKey }) : null; + } + + private get bucketName(): string { + return this.config.get('cos.bucket') ?? ''; + } + + private get regionName(): string { + return this.config.get('cos.region') ?? 'ap-guangzhou'; + } + + get configured(): boolean { + return !!this.client && !!this.bucketName; + } + + private bucket(): string { + if (!this.bucketName) throw new Error('COS 未配置 bucket'); + return this.bucketName; + } + + /** 签发小程序直传预签名 PUT URL */ + getPresignedPutUrl(key: string, expires = 600): Promise { + return new Promise((resolve, reject) => { + if (!this.client) return reject(new Error('COS 未配置')); + this.client.getObjectUrl( + { Bucket: this.bucket(), Region: this.regionName, Key: key, Sign: true, Method: 'put', Expires: expires }, + (err, data) => (err ? reject(err) : resolve(data.Url)), + ); + }); + } + + /** 服务端写入对象(转存 wordcloud 结果等) */ + putObject(key: string, body: Buffer, contentType: string): Promise { + return new Promise((resolve, reject) => { + if (!this.client) return reject(new Error('COS 未配置')); + this.client.putObject( + { Bucket: this.bucket(), Region: this.regionName, Key: key, Body: body, ContentType: contentType }, + (err) => (err ? reject(err) : resolve()), + ); + }); + } + + /** 公网访问地址(桶公共读时可直接访问) */ + publicUrl(key: string): string { + return `https://${this.bucketName || this.bucket()}.cos.${this.regionName}.myqcloud.com/${key}`; + } +} diff --git a/src/orders/orders.service.ts b/src/orders/orders.service.ts index da43251..cd256a5 100644 --- a/src/orders/orders.service.ts +++ b/src/orders/orders.service.ts @@ -43,6 +43,8 @@ export class OrdersService { user: { connect: { id: userId } }, totalAmount, addressSnapshot: dto.addressSnapshot as Prisma.InputJsonValue, + // 关联设计清单(R4 下单后 WCD 派单读取 designData;R3 契约 CreateOrderDto 已含该字段) + designList: dto.designListId ? { connect: { id: dto.designListId } } : undefined, items: { create: dto.items.map((it) => ({ productId: it.productId, diff --git a/src/sketch/sketch.controller.ts b/src/sketch/sketch.controller.ts new file mode 100644 index 0000000..993cd65 --- /dev/null +++ b/src/sketch/sketch.controller.ts @@ -0,0 +1,28 @@ +import { + Controller, + Post, + UploadedFile, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiBearerAuth, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { CurrentUser, JwtPayload } from '../common/decorators/current-user.decorator'; +import { SketchService } from './sketch.service'; + +// 单张贴纸/底图 ≤ 10MB +const MAX_IMAGE_BYTES = 10 * 1024 * 1024; + +@ApiTags('线稿') +@ApiBearerAuth() +@Controller('sketch') +export class SketchController { + constructor(private readonly sketchService: SketchService) {} + + @Post() + @ApiOperation({ summary: '线稿处理(贴纸图 → 处理后 URL)' }) + @ApiConsumes('multipart/form-data') + @UseInterceptors(FileInterceptor('image', { limits: { fileSize: MAX_IMAGE_BYTES } })) + sketch(@CurrentUser() user: JwtPayload, @UploadedFile() file: Express.Multer.File) { + return this.sketchService.process(user.sub, file); + } +} diff --git a/src/sketch/sketch.module.ts b/src/sketch/sketch.module.ts new file mode 100644 index 0000000..68f6c9b --- /dev/null +++ b/src/sketch/sketch.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { SketchController } from './sketch.controller'; +import { SketchService } from './sketch.service'; +import { CosModule } from '../cos/cos.module'; + +@Module({ + imports: [CosModule], + controllers: [SketchController], + providers: [SketchService], +}) +export class SketchModule {} diff --git a/src/sketch/sketch.service.ts b/src/sketch/sketch.service.ts new file mode 100644 index 0000000..bb126d7 --- /dev/null +++ b/src/sketch/sketch.service.ts @@ -0,0 +1,26 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { randomUUID } from 'crypto'; +import { CosService } from '../cos/cos.service'; + +// 线稿处理:接收贴纸图,转存 COS 返回可访问 URL(真实 AI 线稿外部服务接入后可替换内部实现) +@Injectable() +export class SketchService { + constructor(private readonly cos: CosService) {} + + async process(userId: string, file: Express.Multer.File) { + if (!file?.buffer) { + throw new HttpException('缺少图片', HttpStatus.BAD_REQUEST); + } + const mime = file.mimetype ?? 'image/png'; + if (!/^image\/(png|jpe?g|webp)$/i.test(mime)) { + throw new HttpException('仅支持 png/jpg/jpeg/webp 图片', HttpStatus.BAD_REQUEST); + } + if (!this.cos.configured) { + throw new HttpException('图床未配置', HttpStatus.SERVICE_UNAVAILABLE); + } + + const key = `uploads/sketch/${userId}/${randomUUID()}.png`; + await this.cos.putObject(key, file.buffer, mime); + return { imageUrl: this.cos.publicUrl(key) }; + } +} \ No newline at end of file diff --git a/src/upload/upload.controller.ts b/src/upload/upload.controller.ts index f2babed..4cca469 100644 --- a/src/upload/upload.controller.ts +++ b/src/upload/upload.controller.ts @@ -10,9 +10,11 @@ export class UploadController { constructor(private readonly uploadService: UploadService) {} @Get('credentials') - @ApiOperation({ summary: '获取 COS 直传临时凭证(占位)' }) + @ApiOperation({ summary: '获取 COS 直传临时凭证(预签名 PUT URL)' }) credentials(@CurrentUser() user: JwtPayload, @Query('key') key: string) { - void user; - return this.uploadService.getUploadCredentials(key ?? `uploads/${Date.now()}`); + return this.uploadService.getUploadCredentials( + user.sub, + key ?? `uploads/${user.sub}/${Date.now()}`, + ); } } diff --git a/src/upload/upload.module.ts b/src/upload/upload.module.ts index 8966ac2..a124647 100644 --- a/src/upload/upload.module.ts +++ b/src/upload/upload.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; import { UploadController } from './upload.controller'; import { UploadService } from './upload.service'; +import { CosModule } from '../cos/cos.module'; @Module({ + imports: [CosModule], controllers: [UploadController], providers: [UploadService], }) diff --git a/src/upload/upload.service.ts b/src/upload/upload.service.ts index 2401ba3..111b003 100644 --- a/src/upload/upload.service.ts +++ b/src/upload/upload.service.ts @@ -1,21 +1,32 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { CosService } from '../cos/cos.service'; +import { PrismaService } from '../prisma/prisma.service'; -// 腾讯云 COS 上传服务(本轮仅提供签名/直传凭证占位,完整实现留后续) +// 上传凭证服务:给小程序签发 COS 直传预签名 URL;未配置时返回结构化“未配置” @Injectable() export class UploadService { - constructor(private readonly config: ConfigService) {} + constructor( + private readonly config: ConfigService, + private readonly cos: CosService, + private readonly prisma: PrismaService, + ) {} - /** 生成小程序直传所需的临时密钥 / 预签名 URL(占位) */ - async getUploadCredentials(key: string) { - // TODO: 通过 COS STS 或预签名 URL 生成临时凭证 - //(SDK 已移除,接入时见 docs/cos-sdk-removal.md) - return { - key, - bucket: this.config.get('cos.bucket'), - region: this.config.get('cos.region'), - // 占位:实际应返回临时 SecretId/SecretKey/Token 或 presigned URL - credentials: null, - }; + /** 小程序直传:签发 COS 预签名 PUT URL 并记录 Upload 归属(满足“Upload 表有记录且归属当前用户”) */ + async getUploadCredentials(userId: string, key: string) { + const bucket = this.config.get('cos.bucket') ?? ''; + const region = this.config.get('cos.region') ?? 'ap-guangzhou'; + + if (!this.cos.configured) { + return { key, bucket, region, presignedUrl: null, configured: false }; + } + + const presignedUrl = await this.cos.getPresignedPutUrl(key); + // 记录上传归属(对象落地后 key/url 生效;失败不阻断凭证下发) + await this.prisma.upload + .create({ data: { userId, cosKey: key, url: this.cos.publicUrl(key) } }) + .catch(() => undefined); + + return { key, bucket, region, presignedUrl, configured: true }; } } diff --git a/src/wordcloud/order-dispatch.controller.ts b/src/wordcloud/order-dispatch.controller.ts new file mode 100644 index 0000000..5435edb --- /dev/null +++ b/src/wordcloud/order-dispatch.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Param, Post } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { CurrentUser, JwtPayload } from '../common/decorators/current-user.decorator'; +import { WordCloudService } from './wordcloud.service'; + +/** + * 下单后 WCD 派单路由。 + * 挂在 orders 基路径下(R3 的 OrdersController 不负责本端点),由 R4 词云服务实现。 + * 幂等语义:同一订单重复投递由上层/CustomizationTask.orderId 唯一约束,本端点只做创建。 + */ +@ApiTags('词云下单派单') +@ApiBearerAuth() +@Controller('orders') +export class OrderDispatchController { + constructor(private readonly wordCloudService: WordCloudService) {} + + @Post(':id/dispatch') + @ApiOperation({ summary: '下单后 WCD 派单(幂等触发)' }) + dispatch(@CurrentUser() user: JwtPayload, @Param('id') id: string) { + return this.wordCloudService.dispatchToWordcloud(id, user.sub); + } +} diff --git a/src/wordcloud/wordcloud.controller.ts b/src/wordcloud/wordcloud.controller.ts new file mode 100644 index 0000000..102409c --- /dev/null +++ b/src/wordcloud/wordcloud.controller.ts @@ -0,0 +1,59 @@ +import { + Body, + Controller, + Get, + HttpException, + HttpStatus, + Param, + Post, + UploadedFile, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiBearerAuth, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { CurrentUser, JwtPayload } from '../common/decorators/current-user.decorator'; +import { WordCloudService } from './wordcloud.service'; + +@ApiTags('词云') +@ApiBearerAuth() +@Controller('wordcloud') +export class WordCloudController { + constructor(private readonly wordCloudService: WordCloudService) {} + + /** 创建词云任务:multipart(image) + form 字段 names/params */ + @Post('generate') + @ApiOperation({ summary: '创建词云任务(底图 + 名单文本)' }) + @ApiConsumes('multipart/form-data') + @UseInterceptors(FileInterceptor('image', { limits: { fileSize: 10 * 1024 * 1024 } })) + async generate( + @CurrentUser() user: JwtPayload, + @UploadedFile() image: Express.Multer.File | undefined, + @Body('names') names: string, + @Body('params') params?: string, + ) { + if (!image) { + throw new HttpException('缺少底图', HttpStatus.BAD_REQUEST); + } + const jobId = await this.wordCloudService.createGenerateJob( + user.sub, + { buffer: image.buffer, originalname: image.originalname }, + names ?? '', + params, + ); + return { jobId }; + } + + /** 轮询词云任务(仅本人) */ + @Get('jobs/:id') + @ApiOperation({ summary: '轮询词云任务状态(仅本人)' }) + async job(@CurrentUser() user: JwtPayload, @Param('id') id: string) { + return this.wordCloudService.getUserJob(user.sub, id); + } + + /** 取词云任务结果(仅本人;产物已转存 COS) */ + @Get('jobs/:id/result') + @ApiOperation({ summary: '词云任务结果(仅本人)' }) + async result(@CurrentUser() user: JwtPayload, @Param('id') id: string) { + return this.wordCloudService.getUserJobResult(user.sub, id); + } +} diff --git a/src/wordcloud/wordcloud.module.ts b/src/wordcloud/wordcloud.module.ts new file mode 100644 index 0000000..a2a0ece --- /dev/null +++ b/src/wordcloud/wordcloud.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { WordCloudController } from './wordcloud.controller'; +import { OrderDispatchController } from './order-dispatch.controller'; +import { WordCloudService } from './wordcloud.service'; +import { CosModule } from '../cos/cos.module'; + +@Module({ + imports: [CosModule], + controllers: [WordCloudController, OrderDispatchController], + providers: [WordCloudService], +}) +export class WordCloudModule {} diff --git a/src/wordcloud/wordcloud.service.ts b/src/wordcloud/wordcloud.service.ts new file mode 100644 index 0000000..6600382 --- /dev/null +++ b/src/wordcloud/wordcloud.service.ts @@ -0,0 +1,483 @@ +import { HttpException, HttpStatus, Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import axios from 'axios'; +import ExcelJS from 'exceljs'; +import JSZip from 'jszip'; +import * as crypto from 'crypto'; +import { PrismaService } from '../prisma/prisma.service'; +import { CosService } from '../cos/cos.service'; + +type RemoteStatus = 'queued' | 'running' | 'success' | 'failed'; + +/** Buffer → 独立的 ArrayBuffer(用于 Blob 构造,规避 Buffer 类型不兼容) */ +function toArrayBuffer(buf: Buffer): ArrayBuffer { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer; +} + +/** + * wordcloud 平台适配器。 + * 契约:与 wordcloud 之间的接口见 docs/wordcloud-contract.md;对小程序暴露的接口见 + * docs/api-contract-v1.md §8。 + * 职责:把小程序的名字文本转成 .xlsx → 代理调 wordcloud POST /api/jobs → 落 WordCloudJob → + * 轮询 wordcloud GET /api/jobs/{id} → 回写状态/产物。 + */ +@Injectable() +export class WordCloudService implements OnModuleInit { + private readonly logger = new Logger(WordCloudService.name); + + constructor( + private readonly config: ConfigService, + private readonly prisma: PrismaService, + private readonly cos: CosService, + ) {} + + /** 词云平台是否已配置(WORDCLOUD_API_URL 非空) */ + get configured(): boolean { + return !!this.config.get('wordcloud.apiUrl'); + } + + private get apiUrl(): string { + return (this.config.get('wordcloud.apiUrl') ?? '').replace(/\/+$/, ''); + } + + private get timeoutMs(): number { + return this.config.get('wordcloud.timeoutMs') ?? 30000; + } + + /** 名单文本 → 单列 .xlsx(wordcloud 按 DATA_COL_INDEX 读名字列,A 列 index=0) */ + private async buildNamesXlsx(names: string[]): Promise { + const workbook = new ExcelJS.Workbook(); + const sheet = workbook.addWorksheet('names'); + names.forEach((n) => sheet.addRow([{ text: n }])); + return Buffer.from(await workbook.xlsx.writeBuffer()); + } + + /** 代理创建 wordcloud 任务,返回外部 job_id */ + private async createRemoteJob( + xlsx: Buffer, + image: { buffer: Buffer; originalname: string }, + params: Record, + ): Promise { + const form = new FormData(); + form.append('name_list', new Blob([toArrayBuffer(xlsx)]), 'names.xlsx'); + form.append('mask_image', new Blob([toArrayBuffer(image.buffer)]), image.originalname || 'mask.png'); + form.append('params', JSON.stringify(params)); + + let res; + try { + res = await axios.post(`${this.apiUrl}/api/jobs`, form, { + timeout: this.timeoutMs, + maxBodyLength: Infinity, + maxContentLength: Infinity, + }); + } catch (e) { + this.logger.error(`创建词云任务失败: ${(e as Error).message}`); + throw new HttpException('词云平台暂不可用', HttpStatus.SERVICE_UNAVAILABLE); + } + const jobId = res.data?.job_id; + if (!jobId) { + throw new HttpException('词云平台未返回任务ID', HttpStatus.BAD_GATEWAY); + } + return jobId; + } + + /** + * 交互式生成:小程序上传底图 + 名单文本 → 在 wordcloud 创建任务并落库,返回内部 jobId。 + */ + async createGenerateJob( + userId: string, + image: { buffer: Buffer; originalname: string }, + namesText: string, + paramsJson = '{}', + ): Promise { + if (!this.configured) { + throw new HttpException('词云平台未配置', HttpStatus.SERVICE_UNAVAILABLE); + } + if (!image?.buffer) { + throw new HttpException('缺少底图', HttpStatus.BAD_REQUEST); + } + const names = namesText + .split(/[\n,]|,/) + .map((s) => s.trim()) + .filter(Boolean); + if (!names.length) { + throw new HttpException('名单不能为空', HttpStatus.BAD_REQUEST); + } + + let userParams: Record = {}; + if (paramsJson) { + try { + userParams = JSON.parse(paramsJson); + } catch { + throw new HttpException('params 必须是合法 JSON', HttpStatus.BAD_REQUEST); + } + } + + // 名单写在 xlsx A 列,DATA_COL_INDEX 强制对齐到 0 + const params: Record = { + MODE: 'IMAGE', + DATA_COL_INDEX: 0, + SEED: 42, + N_REPETITIONS: 20, + ENABLE_STROKE_WEIGHTS: false, + FONT_COLOR: '#000000', + ...userParams, + }; + params.DATA_COL_INDEX = 0; + + const xlsx = await this.buildNamesXlsx(names); + const remoteJobId = await this.createRemoteJob(xlsx, image, params); + + const job = await this.prisma.wordCloudJob.create({ + data: { userId, remoteJobId, status: 'QUEUED' }, + }); + return job.id; + } + + private mapStatus(s?: string): RemoteStatus { + if (s === 'running') return 'running'; + if (s === 'success') return 'success'; + if (s === 'failed') return 'failed'; + return 'queued'; + } + + private mapTaskStatus(s: string): 'queued' | 'running' | 'success' | 'failed' { + if (s === 'RUNNING') return 'running'; + if (s === 'SUCCESS') return 'success'; + if (s === 'FAILED') return 'failed'; + return 'queued'; + } + + /** 查询本人词云任务:归属校验 → 代理轮询 wordcloud → 回写 DB → 返回小程序侧模型 */ + /** 模块启动后开启后台同步:派单/生成的任务自动从 wordcloud 拉状态推进到 success */ + async onModuleInit() { + const SYNC_MS = 30_000; + const tick = async () => { + try { + if (this.configured) { + const active = await this.prisma.wordCloudJob.findMany({ + where: { status: { in: ['QUEUED', 'RUNNING'] } }, + take: 50, + }); + for (const job of active) { + await this.refreshRemoteJob(job).catch(() => undefined); + } + } + } catch { + /* 后台同步失败不阻断 */ + } + setTimeout(tick, SYNC_MS); + }; + setTimeout(tick, SYNC_MS); + } + + /** 从 wordcloud 拉取状态并回写 DB;success 时转存结果图并同步 CustomizationTask */ + private async refreshRemoteJob(job: { + id: string; + remoteJobId: string | null; + progress: number; + imageUrl: string | null; + error: string | null; + status: string; + }) { + if (!job.remoteJobId) return job; + + let remote: Record; + try { + const res = await axios.get(`${this.apiUrl}/api/jobs/${job.remoteJobId}`, { + timeout: this.timeoutMs, + }); + remote = res.data; + } catch { + return job; // 平台暂不可用:保持现状,下一轮再试 + } + + const status = this.mapStatus(remote.status as string | undefined); + const progress = + typeof remote.progress_percent === 'number' ? remote.progress_percent : job.progress; + const imageUrl = status === 'success' ? await this.resolveResultImage(job.remoteJobId) : job.imageUrl; + + const updated = await this.prisma.wordCloudJob.update({ + where: { id: job.id }, + data: { + status: status.toUpperCase() as 'QUEUED' | 'RUNNING' | 'SUCCESS' | 'FAILED', + progress, + imageUrl: imageUrl ?? null, + error: (remote.error as string | null) ?? job.error ?? null, + }, + }); + + // 成功时把关联的定制任务一并推进到 SUCCESS 并写结果 URL + if (status === 'success' && imageUrl) { + await this.prisma.customizationTask + .updateMany({ + where: { wordcloudJobId: job.id, status: { in: ['PENDING', 'RUNNING'] } }, + data: { status: 'SUCCESS', resultUrl: imageUrl }, + }) + .catch(() => undefined); + } + + return updated; + } + + /** 取词云任务结果(产物已由后台同步/轮询转存 COS) */ + async getUserJobResult(userId: string, id: string) { + const job = await this.prisma.wordCloudJob.findFirst({ where: { id, userId } }); + if (!job) { + throw new HttpException('任务不存在', HttpStatus.NOT_FOUND); + } + if (!job.imageUrl) { + throw new HttpException('结果未就绪', HttpStatus.NOT_FOUND); + } + return { imageUrl: job.imageUrl }; + } + + /** 查询本人词云任务:归属校验 → 实时代理轮询 wordcloud → 返回小程序侧模型 */ + async getUserJob(userId: string, id: string) { + const job = await this.prisma.wordCloudJob.findFirst({ where: { id, userId } }); + if (!job) { + throw new HttpException('任务不存在', HttpStatus.NOT_FOUND); + } + if (!job.remoteJobId) { + throw new HttpException('任务尚未创建', HttpStatus.CONFLICT); + } + + let updated: { + id: string; + status: string; + progress: number; + imageUrl: string | null; + error: string | null; + }; + try { + updated = await this.refreshRemoteJob(job); + } catch (e) { + this.logger.warn(`轮询词云任务失败: ${(e as Error).message}`); + throw new HttpException('词云平台暂不可用', HttpStatus.SERVICE_UNAVAILABLE); + } + + const status = this.mapStatus(updated.status.toLowerCase()) as 'queued' | 'running' | 'success' | 'failed'; + return { + id: updated.id, + status, + progress: updated.progress, + imageUrl: updated.imageUrl ?? undefined, + error: updated.error ?? undefined, + }; + } + + /** + * 下单后 WCD 派单(幂等触发层面由 controller/业务方约束)。 + * 读取订单关联设计清单的 designData → 构造 .wcd → POST /api/jobs(wcd_file)。 + * WORDCLOUD_API_URL 未配置时返回结构化 not_configured,不做假成功。 + */ + async dispatchToWordcloud(orderId: string, userId: string) { + if (!this.configured) { + return { orderId, status: 'not_configured' as const, message: '词云平台未配置' }; + } + + const order = await this.prisma.order.findFirst({ where: { id: orderId, userId } }); + if (!order) { + throw new HttpException('订单不存在', HttpStatus.NOT_FOUND); + } + + // 幂等:同一订单已投递过则直接返回既有任务,不重复创建 + const existing = await this.prisma.customizationTask.findFirst({ + where: { orderId: order.id }, + }); + if (existing) { + return { + orderId: order.id, + status: this.mapTaskStatus(existing.status), + wordcloudJobId: existing.wordcloudJobId ?? undefined, + message: '该订单已投递过', + }; + } + + if (!order.designListId) { + throw new HttpException('订单未关联设计清单', HttpStatus.BAD_REQUEST); + } + const dl = await this.prisma.designList.findFirst({ + where: { id: order.designListId, userId }, + }); + if (!dl) { + throw new HttpException('设计清单不存在', HttpStatus.NOT_FOUND); + } + const items = Array.isArray(dl.items) ? (dl.items as Record[]) : []; + const designItem = items.find((it) => it?.designData) ?? items[0]; + if (!designItem?.designData) { + throw new HttpException('订单缺少设计数据', HttpStatus.BAD_REQUEST); + } + + const wcd = await this.buildWcdPackage( + order.orderNo, + designItem.designData as Record, + ); + const remoteJobId = await this.createRemoteWcdJob(wcd, order.orderNo); + + const job = await this.prisma.wordCloudJob.create({ + data: { userId, remoteJobId, status: 'RUNNING' }, + }); + const task = await this.prisma.customizationTask.create({ + data: { + userId, + orderId: order.id, + designListId: dl.id, + wordcloudJobId: job.id, + status: 'RUNNING', + }, + }); + + this.logger.log(`订单 ${order.id} 已投递词云任务 ${remoteJobId} (task=${task.id})`); + return { orderId: order.id, status: 'queued' as const, wordcloudJobId: job.id, message: '已投递词云平台' }; + } + + /** 由 designData 构造 .wcd 包(对齐 wordcloud /api/design-templates/import 的导入契约) */ + private async buildWcdPackage( + orderNo: string, + designData: Record, + ): Promise { + const category = designData.category as { mask?: { width?: number; height?: number } } | undefined; + const mask = category?.mask; + const width = mask?.width ?? 1200; + const height = mask?.height ?? 1200; + const background = (designData.background as { color?: string })?.color ?? '#ffffff'; + + const elements: Record[] = []; + const assets: { id: string; url: string }[] = []; + + const pushImage = ( + url: unknown, + name: string, + opts: { x?: number; y?: number; width?: number; height?: number; rotation?: number } = {}, + ) => { + if (typeof url !== 'string' || !/^https?:/.test(url)) { + return; // 本地/临时路径或缺失:跳过(需先完成贴纸持久化) + } + const id = `asset_${assets.length + 1}`; + assets.push({ id, url }); + elements.push({ + id: `${name}_${assets.length}`, + type: 'sticker', + name, + assetId: id, + x: opts.x ?? 0, + y: opts.y ?? 0, + width: opts.width ?? width, + height: opts.height ?? height, + rotation: opts.rotation ?? 0, + opacity: 1, + }); + }; + + // 底图 + 词云结果图 + 贴纸 + pushImage((designData.background as { src?: string })?.src, 'background'); + pushImage((designData.wordcloud as { imageUrl?: string })?.imageUrl, 'wordcloud'); + for (const s of (designData.stickers as Record[] | undefined) ?? []) { + pushImage(s.src, String(s.id ?? 'sticker'), { + x: s.x as number, + y: s.y as number, + width: s.width as number, + height: s.height as number, + rotation: s.rotation as number, + }); + } + + const document = { + width, + height, + background, + layers: [{ id: 'layer-1', name: '设计', visible: true, locked: false }], + layerFolders: [], + elements, + }; + + const zip = new JSZip(); + const manifestAssets: Record[] = []; + for (const asset of assets) { + const { buffer, mime } = await this.downloadAsset(asset.url); + const ext = mime === 'image/svg+xml' ? 'svg' : mime === 'image/jpeg' ? 'jpg' : 'png'; + if (!buffer.length) continue; + manifestAssets.push({ + id: asset.id, + name: asset.id, + type: ext, + mimeType: mime, + sha256: crypto.createHash('sha256').update(buffer).digest('hex'), + size: buffer.length, + }); + zip.file(`assets/${asset.id}.${ext}`, buffer); + } + + const manifest = { + format: 'wordcloud-canvas', + version: 1, + name: `order-${orderNo}`, + canvas: { width, height, background }, + assets: manifestAssets, + meta: { orderNo, wordcloud: designData.wordcloud ?? null }, + }; + + zip.file('manifest.json', JSON.stringify(manifest)); + zip.file('document.json', JSON.stringify(document)); + return Buffer.from(await zip.generateAsync({ type: 'nodebuffer' })); + } + + /** 从公开 URL 下载素材字节并推断 MIME */ + private async downloadAsset(url: string): Promise<{ buffer: Buffer; mime: string }> { + const lower = url.toLowerCase(); + const mime = lower.endsWith('.svg') + ? 'image/svg+xml' + : lower.match(/\.jpe?g($|\?)/) + ? 'image/jpeg' + : 'image/png'; + const res = await axios.get(url, { responseType: 'arraybuffer', timeout: this.timeoutMs }); + return { buffer: Buffer.from(res.data), mime }; + } + + /** 代理创建 WCD 生产任务(POST /api/jobs,wcd_file 模式;wordcloud 侧实现后置) */ + private async createRemoteWcdJob(wcd: Buffer, orderNo: string): Promise { + if (!this.configured) { + throw new HttpException('词云平台未配置', HttpStatus.SERVICE_UNAVAILABLE); + } + const form = new FormData(); + form.append('wcd_file', new Blob([new Uint8Array(wcd)]), `${orderNo}.wcd`); + form.append('params', JSON.stringify({ MODE: 'WCD' })); + + let res; + try { + res = await axios.post(`${this.apiUrl}/api/jobs`, form, { + timeout: this.timeoutMs, + maxBodyLength: Infinity, + maxContentLength: Infinity, + }); + } catch (e) { + this.logger.error(`WCD 派单失败: ${(e as Error).message}`); + throw new HttpException('词云平台暂不可用', HttpStatus.SERVICE_UNAVAILABLE); + } + const jobId = res.data?.job_id; + if (!jobId) { + throw new HttpException('词云平台未返回任务ID', HttpStatus.BAD_GATEWAY); + } + return jobId; + } + + /** 下载 wordcloud 结果 PNG 转存 COS,返回小程序可访问的 COS 公网 URL;COS 未配置时返回空 */ + private async resolveResultImage(remoteJobId: string): Promise { + if (!this.cos.configured) { + return null; + } + try { + const res = await axios.get(`${this.apiUrl}/api/jobs/${remoteJobId}/files/png`, { + responseType: 'arraybuffer', + timeout: this.timeoutMs, + }); + const key = `uploads/wordcloud/${remoteJobId}.png`; + await this.cos.putObject(key, Buffer.from(res.data), 'image/png'); + return this.cos.publicUrl(key); + } catch (e) { + this.logger.warn(`转存词云结果失败: ${(e as Error).message}`); + return null; + } + } +}