feat(delivery): add POST /delivery/parsePDF SSE route
- Accepts multipart/form-data PDF, streams LLM extraction via SSE - Uses libllmorbiter (OpenRouterProvider) with API key from companysetting keys.openrouter - Extracts manifest basedata, notes, and line items from invoices/delivery notes - Events: status, llm_call, tool_call, usage, progress, result, error - Extends generateApiDocs.js: multipart field table + x-sse-events table support - Docs: docs/api/routes/delivery/parsePDF.md (auto-generated)
This commit is contained in:
1066
artikeltabelle.csv
Normal file
1066
artikeltabelle.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -75,6 +75,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
|
||||
| [`routes/customergroup/update.md`](routes/customergroup/update.md) | `PATCH /customergroup/{id}` |
|
||||
| [`routes/delivery/list.md`](routes/delivery/list.md) | `GET /delivery` |
|
||||
| [`routes/delivery/create.md`](routes/delivery/create.md) | `POST /delivery` |
|
||||
| [`routes/delivery/parsePDF.md`](routes/delivery/parsePDF.md) | `POST /delivery/parsePDF` |
|
||||
| [`routes/delivery/delete.md`](routes/delivery/delete.md) | `DELETE /delivery/{id}` |
|
||||
| [`routes/delivery/get.md`](routes/delivery/get.md) | `GET /delivery/{id}` |
|
||||
| [`routes/delivery/update.md`](routes/delivery/update.md) | `PATCH /delivery/{id}` |
|
||||
|
||||
@@ -4040,6 +4040,110 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/delivery/parsePDF": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Delivery"
|
||||
],
|
||||
"summary": "Parse PDF (SSE)",
|
||||
"description": "Accepts a PDF invoice or delivery note (`multipart/form-data`, field `pdf`)\nand streams extraction results as Server-Sent Events until parsing is complete.\n\nThe PDF is read via `pdftotext -layout` and fed page-by-page to an LLM\n(OpenRouter) which fills a structured manifest of header data and line items.\nThe OpenRouter API key is read from the authenticated user's company setting\n`keys.openrouter`; the endpoint returns `400` if the key is not configured.\n\nThe stream ends after the final `result` event (or an `error` event on failure).\n",
|
||||
"security": [
|
||||
{
|
||||
"cookieAuth": []
|
||||
}
|
||||
],
|
||||
"x-markdown": "delivery/parsePDF.md",
|
||||
"x-sse-events": [
|
||||
{
|
||||
"name": "status",
|
||||
"description": "PDF parse confirmation and LLM status messages",
|
||||
"payload": "{ \"message\": \"…\" }"
|
||||
},
|
||||
{
|
||||
"name": "llm_call",
|
||||
"description": "Fired before each LLM request",
|
||||
"payload": "{ \"model\": \"…\", \"promptSize\": 0, \"messages\": 0 }"
|
||||
},
|
||||
{
|
||||
"name": "tool_call",
|
||||
"description": "Fired when the output_manifest tool is invoked",
|
||||
"payload": "{ \"name\": \"output_manifest\" }"
|
||||
},
|
||||
{
|
||||
"name": "usage",
|
||||
"description": "Token usage after each LLM response",
|
||||
"payload": "{ \"input\": 0, \"cached\": 0, \"output\": 0, \"cost\": 0 }"
|
||||
},
|
||||
{
|
||||
"name": "progress",
|
||||
"description": "Intermediate accumulated extraction state (basedata / notes / items)",
|
||||
"payload": "ManifestResult"
|
||||
},
|
||||
{
|
||||
"name": "result",
|
||||
"description": "Final accumulated extraction — emitted just before the stream closes",
|
||||
"payload": "ManifestResult"
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"description": "Emitted on PDF parse or LLM failure; stream closes immediately after",
|
||||
"payload": "{ \"message\": \"…\" }"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pdf"
|
||||
],
|
||||
"properties": {
|
||||
"pdf": {
|
||||
"type": "string",
|
||||
"format": "binary",
|
||||
"description": "PDF file to parse (max 50 MB, application/pdf)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "`text/event-stream` — emits `status`, `llm_call`, `tool_call`, `usage`,\n`progress`, `result` (and `error` on failure). See SSE Events table below.\n\n**ManifestResult shape** (used by `progress` and `result`):\n\n```json\n{\n \"basedata\": {\n \"date\": \"2026-01-15\",\n \"kLieferant\": 42,\n \"lieferantName\": \"Muster GmbH\",\n \"manifestNr\": \"LS-2026-001\",\n \"manifestType\": \"lieferschein\"\n },\n \"notes\": [\"Pos. 3: VPE-Angabe fehlte, Standardwert 1 angenommen.\"],\n \"items\": {\n \"pagesFrom\": 1,\n \"pagesTo\": 2,\n \"items\": [\n {\n \"position\": 1,\n \"count\": 10,\n \"vpe\": 1,\n \"unit\": \"Stk.\",\n \"articleName\": \"Einhängesieb Stahl\",\n \"liefArtNr\": \"ES-100\",\n \"mwst\": \"19\",\n \"eknetto\": 450,\n \"page\": 1\n }\n ]\n }\n}\n```\n",
|
||||
"content": {
|
||||
"text/event-stream": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "No PDF uploaded or OpenRouter key not configured",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/delivery": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
||||
85
docs/api/routes/delivery/parsePDF.md
Normal file
85
docs/api/routes/delivery/parsePDF.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# `POST /delivery/parsePDF`
|
||||
|
||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||
|
||||
Parse PDF (SSE)
|
||||
|
||||
Accepts a PDF invoice or delivery note (`multipart/form-data`, field `pdf`)
|
||||
and streams extraction results as Server-Sent Events until parsing is complete.
|
||||
|
||||
The PDF is read via `pdftotext -layout` and fed page-by-page to an LLM
|
||||
(OpenRouter) which fills a structured manifest of header data and line items.
|
||||
The OpenRouter API key is read from the authenticated user's company setting
|
||||
`keys.openrouter`; the endpoint returns `400` if the key is not configured.
|
||||
|
||||
The stream ends after the final `result` event (or an `error` event on failure).
|
||||
|
||||
|
||||
## Body (`multipart/form-data`)
|
||||
|
||||
| Field | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| `pdf` | yes | file | PDF file to parse (max 50 MB, application/pdf) |
|
||||
|
||||
## SSE Events
|
||||
|
||||
The response is a `text/event-stream`. Events are emitted in order until the stream closes.
|
||||
|
||||
| Event | Description | Payload |
|
||||
|-------|-------------|---------|
|
||||
| `status` | PDF parse confirmation and LLM status messages | `{ "message": "…" }` |
|
||||
| `llm_call` | Fired before each LLM request | `{ "model": "…", "promptSize": 0, "messages": 0 }` |
|
||||
| `tool_call` | Fired when the output_manifest tool is invoked | `{ "name": "output_manifest" }` |
|
||||
| `usage` | Token usage after each LLM response | `{ "input": 0, "cached": 0, "output": 0, "cost": 0 }` |
|
||||
| `progress` | Intermediate accumulated extraction state (basedata / notes / items) | `ManifestResult` |
|
||||
| `result` | Final accumulated extraction — emitted just before the stream closes | `ManifestResult` |
|
||||
| `error` | Emitted on PDF parse or LLM failure; stream closes immediately after | `{ "message": "…" }` |
|
||||
|
||||
## Response `200`
|
||||
|
||||
`text/event-stream` — emits `status`, `llm_call`, `tool_call`, `usage`,
|
||||
`progress`, `result` (and `error` on failure). See SSE Events table below.
|
||||
|
||||
**ManifestResult shape** (used by `progress` and `result`):
|
||||
|
||||
```json
|
||||
{
|
||||
"basedata": {
|
||||
"date": "2026-01-15",
|
||||
"kLieferant": 42,
|
||||
"lieferantName": "Muster GmbH",
|
||||
"manifestNr": "LS-2026-001",
|
||||
"manifestType": "lieferschein"
|
||||
},
|
||||
"notes": ["Pos. 3: VPE-Angabe fehlte, Standardwert 1 angenommen."],
|
||||
"items": {
|
||||
"pagesFrom": 1,
|
||||
"pagesTo": 2,
|
||||
"items": [
|
||||
{
|
||||
"position": 1,
|
||||
"count": 10,
|
||||
"vpe": 1,
|
||||
"unit": "Stk.",
|
||||
"articleName": "Einhängesieb Stahl",
|
||||
"liefArtNr": "ES-100",
|
||||
"mwst": "19",
|
||||
"eknetto": 450,
|
||||
"page": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Errors
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 400 | No PDF uploaded or OpenRouter key not configured — `{"error":"…"}` |
|
||||
| 401 | Unauthorized — `{"error":"…"}` |
|
||||
|
||||
---
|
||||
|
||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||
401
package-lock.json
generated
401
package-lock.json
generated
@@ -15,15 +15,17 @@
|
||||
"crypto": "^1.0.1",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"google-auth-library": "^11.0.0",
|
||||
"google-auth-library": "^11.0.2",
|
||||
"ink": "^7.1.1",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"jszip": "^3.10.1",
|
||||
"jszip": "^3.10.2",
|
||||
"libdashstore": "file:../dashdbschema",
|
||||
"mssql": "^12.7.0",
|
||||
"react": "^19.2.8",
|
||||
"sharp": "^0.35.3"
|
||||
"libllmorbiter": "file:../llmorbiter",
|
||||
"mssql": "^12.7.2",
|
||||
"multer": "^2.4.0",
|
||||
"react": "^19.3.0",
|
||||
"sharp": "^0.35.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"swagger-jsdoc": "^6.3.0"
|
||||
@@ -48,6 +50,13 @@
|
||||
"mssql": "^12.6.0"
|
||||
}
|
||||
},
|
||||
"../llmorbiter": {
|
||||
"version": "0.1.0",
|
||||
"license": "0BSD",
|
||||
"dependencies": {
|
||||
"dotenv": "^17.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@alcalzone/ansi-tokenize": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.0.tgz",
|
||||
@@ -381,9 +390,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz",
|
||||
"integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz",
|
||||
"integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -399,13 +408,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.2"
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz",
|
||||
"integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz",
|
||||
"integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -421,20 +430,20 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.2"
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-freebsd-wasm32": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz",
|
||||
"integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz",
|
||||
"integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"dependencies": {
|
||||
"@img/sharp-wasm32": "0.35.3"
|
||||
"@img/sharp-wasm32": "0.35.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
@@ -444,9 +453,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz",
|
||||
"integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz",
|
||||
"integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -460,9 +469,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz",
|
||||
"integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz",
|
||||
"integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -476,9 +485,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz",
|
||||
"integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz",
|
||||
"integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -495,9 +504,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz",
|
||||
"integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz",
|
||||
"integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -514,9 +523,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz",
|
||||
"integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz",
|
||||
"integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -533,9 +542,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz",
|
||||
"integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz",
|
||||
"integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -552,9 +561,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz",
|
||||
"integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz",
|
||||
"integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -571,9 +580,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz",
|
||||
"integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz",
|
||||
"integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -590,9 +599,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz",
|
||||
"integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz",
|
||||
"integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -609,9 +618,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz",
|
||||
"integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz",
|
||||
"integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -628,9 +637,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz",
|
||||
"integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz",
|
||||
"integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -649,13 +658,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.3.2"
|
||||
"@img/sharp-libvips-linux-arm": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz",
|
||||
"integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz",
|
||||
"integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -674,13 +683,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.2"
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz",
|
||||
"integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz",
|
||||
"integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -699,13 +708,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.2"
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz",
|
||||
"integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz",
|
||||
"integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -724,13 +733,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.2"
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz",
|
||||
"integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz",
|
||||
"integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -749,13 +758,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.2"
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz",
|
||||
"integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz",
|
||||
"integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -774,13 +783,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.3.2"
|
||||
"@img/sharp-libvips-linux-x64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz",
|
||||
"integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz",
|
||||
"integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -799,13 +808,13 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.2"
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz",
|
||||
"integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz",
|
||||
"integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -824,17 +833,17 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.2"
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz",
|
||||
"integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz",
|
||||
"integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==",
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.11.1"
|
||||
"@emnapi/runtime": "^1.11.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
@@ -844,16 +853,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-webcontainers-wasm32": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz",
|
||||
"integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz",
|
||||
"integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@img/sharp-wasm32": "0.35.3"
|
||||
"@img/sharp-wasm32": "0.35.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
@@ -863,9 +872,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz",
|
||||
"integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz",
|
||||
"integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -882,9 +891,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz",
|
||||
"integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz",
|
||||
"integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -901,9 +910,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz",
|
||||
"integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz",
|
||||
"integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1085,6 +1094,12 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/append-field": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -1266,6 +1281,17 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dependencies": {
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -1937,9 +1963,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz",
|
||||
"integrity": "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==",
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz",
|
||||
"integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
@@ -1951,17 +1977,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
|
||||
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-9.0.3.tgz",
|
||||
"integrity": "sha512-2YYnIlHaKBGT2IPg3G2M57hia9Galz15zsEOvw9T3oRf0lSn6KN6VcHQLqby7x8ksYKnjXvp3rp2KJyLCN6zfQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"gaxios": "^7.0.0",
|
||||
"google-logging-utils": "^1.0.0",
|
||||
"gaxios": "^7.1.3",
|
||||
"google-logging-utils": "^2.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/get-east-asian-width": {
|
||||
@@ -2039,16 +2065,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.0.0.tgz",
|
||||
"integrity": "sha512-WC5gkb2pElGhZKgGx9r799trH3ZUx90ecgIOIMmgiiHS/gzO3r4vvFoQWuKI0QmR2xU5uee2M4hDUxRZDh/x7w==",
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.0.2.tgz",
|
||||
"integrity": "sha512-vzpgPutxrghPsnjrjpzLX2bdv8IOL719Rh0oEjGnQu8YCIbnbMuTTQ5zU9LcKvLdOPgCxBwppbvnhgW90Qna5Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"ecdsa-sig-formatter": "^1.0.11",
|
||||
"gaxios": "^7.1.4",
|
||||
"gcp-metadata": "8.1.2",
|
||||
"google-logging-utils": "1.1.3",
|
||||
"gcp-metadata": "^9.0.0",
|
||||
"google-logging-utils": "^2.0.0",
|
||||
"jws": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2056,12 +2082,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/google-logging-utils": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
|
||||
"integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-2.0.1.tgz",
|
||||
"integrity": "sha512-HMhaQghlOTvbcb3c4T5jmmOMtG3JUF1iOQMezaJXL86CDS+Tm2vHd0IeLFRAx3+ewd+bo9E1HFHoy17X5aJa9A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
@@ -2497,9 +2523,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"version": "3.10.2",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz",
|
||||
"integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
@@ -2533,6 +2559,10 @@
|
||||
"resolved": "../dashdbschema",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/libllmorbiter": {
|
||||
"resolved": "../llmorbiter",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
@@ -2698,9 +2728,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mssql": {
|
||||
"version": "12.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mssql/-/mssql-12.7.0.tgz",
|
||||
"integrity": "sha512-J6SJKXi1jYbhHjjooLNtPnX7+s3cq5IJ701Wgy/UW1SXRpgFlJJsYi3IPve9RVgCUkq0Cqv2aaaSJ4IXtIF3mg==",
|
||||
"version": "12.7.2",
|
||||
"resolved": "https://registry.npmjs.org/mssql/-/mssql-12.7.2.tgz",
|
||||
"integrity": "sha512-zhpPw+WXBWLw7d7J6Y0aWgQy0EV8HKDGNSvmkAjP8bSM4H41X0fweAeKWI+bg9Ri+JHCbsf/QScjmACBFcEo8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tediousjs/connection-string": "^1.0.0",
|
||||
@@ -2716,6 +2746,67 @@
|
||||
"node": ">=18.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/multer": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-2.4.0.tgz",
|
||||
"integrity": "sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"append-field": "^1.0.0",
|
||||
"busboy": "^1.6.0",
|
||||
"type-is": "^1.6.18"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/native-duplexpair": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz",
|
||||
@@ -2978,9 +3069,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.8",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
|
||||
"integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -3187,9 +3278,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.35.3",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz",
|
||||
"integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==",
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz",
|
||||
"integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.1.0",
|
||||
@@ -3203,31 +3294,31 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.35.3",
|
||||
"@img/sharp-darwin-x64": "0.35.3",
|
||||
"@img/sharp-freebsd-wasm32": "0.35.3",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.2",
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.2",
|
||||
"@img/sharp-libvips-linux-arm": "1.3.2",
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.2",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.2",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.2",
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.2",
|
||||
"@img/sharp-libvips-linux-x64": "1.3.2",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.2",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.2",
|
||||
"@img/sharp-linux-arm": "0.35.3",
|
||||
"@img/sharp-linux-arm64": "0.35.3",
|
||||
"@img/sharp-linux-ppc64": "0.35.3",
|
||||
"@img/sharp-linux-riscv64": "0.35.3",
|
||||
"@img/sharp-linux-s390x": "0.35.3",
|
||||
"@img/sharp-linux-x64": "0.35.3",
|
||||
"@img/sharp-linuxmusl-arm64": "0.35.3",
|
||||
"@img/sharp-linuxmusl-x64": "0.35.3",
|
||||
"@img/sharp-webcontainers-wasm32": "0.35.3",
|
||||
"@img/sharp-win32-arm64": "0.35.3",
|
||||
"@img/sharp-win32-ia32": "0.35.3",
|
||||
"@img/sharp-win32-x64": "0.35.3"
|
||||
"@img/sharp-darwin-arm64": "0.35.4",
|
||||
"@img/sharp-darwin-x64": "0.35.4",
|
||||
"@img/sharp-freebsd-wasm32": "0.35.4",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.3",
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-arm": "1.3.3",
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.3",
|
||||
"@img/sharp-libvips-linux-x64": "1.3.3",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.3",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.3",
|
||||
"@img/sharp-linux-arm": "0.35.4",
|
||||
"@img/sharp-linux-arm64": "0.35.4",
|
||||
"@img/sharp-linux-ppc64": "0.35.4",
|
||||
"@img/sharp-linux-riscv64": "0.35.4",
|
||||
"@img/sharp-linux-s390x": "0.35.4",
|
||||
"@img/sharp-linux-x64": "0.35.4",
|
||||
"@img/sharp-linuxmusl-arm64": "0.35.4",
|
||||
"@img/sharp-linuxmusl-x64": "0.35.4",
|
||||
"@img/sharp-webcontainers-wasm32": "0.35.4",
|
||||
"@img/sharp-win32-arm64": "0.35.4",
|
||||
"@img/sharp-win32-ia32": "0.35.4",
|
||||
"@img/sharp-win32-x64": "0.35.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
@@ -3377,6 +3468,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
|
||||
12
package.json
12
package.json
@@ -20,15 +20,17 @@
|
||||
"crypto": "^1.0.1",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"google-auth-library": "^11.0.0",
|
||||
"google-auth-library": "^11.0.2",
|
||||
"ink": "^7.1.1",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"jszip": "^3.10.1",
|
||||
"jszip": "^3.10.2",
|
||||
"libdashstore": "file:../dashdbschema",
|
||||
"mssql": "^12.7.0",
|
||||
"react": "^19.2.8",
|
||||
"sharp": "^0.35.3"
|
||||
"libllmorbiter": "file:../llmorbiter",
|
||||
"multer": "^2.4.0",
|
||||
"mssql": "^12.7.2",
|
||||
"react": "^19.3.0",
|
||||
"sharp": "^0.35.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"swagger-jsdoc": "^6.3.0"
|
||||
|
||||
@@ -156,6 +156,23 @@ function renderRequestBody(requestBody) {
|
||||
return lines;
|
||||
}
|
||||
|
||||
// multipart/form-data — render fields as a table when properties are present
|
||||
const multipart = requestBody.content['multipart/form-data'];
|
||||
if (multipart?.schema?.properties) {
|
||||
const schema = multipart.schema;
|
||||
const required = new Set(schema.required || []);
|
||||
lines.push('## Body (`multipart/form-data`)');
|
||||
lines.push('');
|
||||
lines.push('| Field | Required | Type | Description |');
|
||||
lines.push('|-------|----------|------|-------------|');
|
||||
for (const [name, prop] of Object.entries(schema.properties)) {
|
||||
const type = prop.format === 'binary' ? 'file' : (prop.type || '');
|
||||
lines.push(`| \`${name}\` | ${required.has(name) ? 'yes' : 'no'} | ${type} | ${prop.description || ''} |`);
|
||||
}
|
||||
lines.push('');
|
||||
return lines;
|
||||
}
|
||||
|
||||
lines.push('## Body');
|
||||
lines.push('');
|
||||
for (const type of types) {
|
||||
@@ -257,6 +274,20 @@ function renderOperation(apiPath, method, operation) {
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const sseEvents = operation['x-sse-events'];
|
||||
if (Array.isArray(sseEvents) && sseEvents.length) {
|
||||
lines.push('## SSE Events');
|
||||
lines.push('');
|
||||
lines.push('The response is a `text/event-stream`. Events are emitted in order until the stream closes.');
|
||||
lines.push('');
|
||||
lines.push('| Event | Description | Payload |');
|
||||
lines.push('|-------|-------------|---------|');
|
||||
for (const ev of sseEvents) {
|
||||
lines.push(`| \`${ev.name}\` | ${ev.description || ''} | ${ev.payload ? `\`${ev.payload}\`` : ''} |`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const responses = operation.responses || {};
|
||||
const successCodes = Object.keys(responses)
|
||||
.filter((c) => c.startsWith('2'))
|
||||
|
||||
419
src/api/routes/deliveryParsePdf.js
Normal file
419
src/api/routes/deliveryParsePdf.js
Normal file
@@ -0,0 +1,419 @@
|
||||
'use strict';
|
||||
|
||||
import { execFileSync } from 'child_process';
|
||||
import multer from 'multer';
|
||||
import { LLMOrbiter, OpenRouterProvider, defineTool, DirectReturnError } from 'libllmorbiter';
|
||||
import * as dbCompanySetting from '../../db/companysetting.js';
|
||||
|
||||
// ─── Multer — memory storage (no temp files) ────────────────────────────────────
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 50 * 1024 * 1024 }, // 50 MB
|
||||
fileFilter: (_req, file, cb) => {
|
||||
if (file.mimetype === 'application/pdf' || file.originalname?.toLowerCase().endsWith('.pdf')) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only PDF files are accepted'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── SSE helpers ────────────────────────────────────────────────────────────────
|
||||
function sseWrite(res, event, data) {
|
||||
if (res.writableEnded) return;
|
||||
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
||||
}
|
||||
|
||||
// ─── PDF → pages via pdftotext ──────────────────────────────────────────────────
|
||||
function readPdfPages(buffer) {
|
||||
// Write buffer to stdin by passing '-' as output and reading from stdin via
|
||||
// execFileSync input option. pdftotext reads from '-' for stdin.
|
||||
const raw = execFileSync('pdftotext', ['-layout', '-', '-'], {
|
||||
input: buffer,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 50 * 1024 * 1024,
|
||||
});
|
||||
return raw
|
||||
.split('\f')
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p.length > 0);
|
||||
}
|
||||
|
||||
// ─── Extraction prompt ──────────────────────────────────────────────────────────
|
||||
const BASE_PROMPT = `Extract metadata and product line items from the provided invoice or delivery note.
|
||||
Call the output_manifest tool with the extracted data. Do not answer with plain text once extraction is complete.
|
||||
|
||||
Rules:
|
||||
- manifestType: "rechnung" or "lieferschein". For "lieferschein", prices and taxes must be null.
|
||||
- Exclude non-product lines (shipping, handling, etc.).
|
||||
- Line items: Extract count, vpe (default: 1), liefArtNr, mwst (7 or 19), and eknetto per piece(euro cents).
|
||||
- Match supplier against the provided list for kLieferant and lieferantName. For unknown suppliers, extract lieferantName and set kLieferant to null.
|
||||
- Write notes strictly in German.
|
||||
- The output_manifest tool has three strictly optional components: basedata, notes, items. Only include a component when it carries genuinely new data for this batch; omit the rest.
|
||||
- Include basedata ONLY when at least one of its fields is currently null/empty and you can fill it in, or when a value actually changes. If all basedata fields are already set and unchanged, omit basedata.
|
||||
- Include items ONLY when this batch contains new product line items not already present. If there are no new items, omit items.
|
||||
- Include notes ONLY when there is a genuinely new noteworthy detail not already in the notes list. These notes are for a human to double-check the extraction result, so record anything ambiguous, unusual, or worth verifying (e.g. packaging interpretations, excluded lines, unavailable articles).
|
||||
|
||||
VPE (Verpackungseinheit) Logic:
|
||||
VPE represents the number of individually sellable units inside a bulk item. If not explicitly split for individual sale, default to 1.
|
||||
- "20 Boxen á 10 Stück" -> VPE = 20.
|
||||
- "10 Gläser á 100 Filter" -> VPE = 10 (we sell the 10 glasses individually).
|
||||
- "100 Stück" -> VPE = 1 (sold as one bulk pack).
|
||||
- We don't sell the individual filters or Pieces, but typical VPEs like Beutel, Glässer, Boxen, Hefte
|
||||
- That means that in "Y Hefte á X Blatt" the VPE is NOT X but: Y
|
||||
- a so called "Display" also means VPE. So "20er Display" means VPE = 20
|
||||
- eknetto is the price of one full vpe
|
||||
|
||||
Unit (ME) rules:
|
||||
- The unit (ME) must ALWAYS refer to the individual sellable item (e.g. Stk., Beutel, Flasche, Box, Set). It must NEVER be "VE" or "VPE".
|
||||
- A packaging size like "VE100" means the bulk pack contains 100 individual items: set vpe = 100 and unit = the individual item (e.g. Stk.).
|
||||
- Example: "Einhängesiebe Stahl, VE100" -> vpe = 100, unit = "Stk.", count = 1 (one bulk pack of 100 sieves).`;
|
||||
|
||||
// ─── Route ───────────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
* @param {import('express').Application} app
|
||||
* @param {string} [basePath='']
|
||||
*/
|
||||
export function register(app, basePath = '') {
|
||||
/**
|
||||
* @openapi
|
||||
* /delivery/parsePDF:
|
||||
* post:
|
||||
* tags: [Delivery]
|
||||
* summary: Parse PDF (SSE)
|
||||
* description: |
|
||||
* Accepts a PDF invoice or delivery note (`multipart/form-data`, field `pdf`)
|
||||
* and streams extraction results as Server-Sent Events until parsing is complete.
|
||||
*
|
||||
* The PDF is read via `pdftotext -layout` and fed page-by-page to an LLM
|
||||
* (OpenRouter) which fills a structured manifest of header data and line items.
|
||||
* The OpenRouter API key is read from the authenticated user's company setting
|
||||
* `keys.openrouter`; the endpoint returns `400` if the key is not configured.
|
||||
*
|
||||
* The stream ends after the final `result` event (or an `error` event on failure).
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* x-markdown: delivery/parsePDF.md
|
||||
* x-sse-events:
|
||||
* - name: status
|
||||
* description: PDF parse confirmation and LLM status messages
|
||||
* payload: '{ "message": "…" }'
|
||||
* - name: llm_call
|
||||
* description: Fired before each LLM request
|
||||
* payload: '{ "model": "…", "promptSize": 0, "messages": 0 }'
|
||||
* - name: tool_call
|
||||
* description: Fired when the output_manifest tool is invoked
|
||||
* payload: '{ "name": "output_manifest" }'
|
||||
* - name: usage
|
||||
* description: Token usage after each LLM response
|
||||
* payload: '{ "input": 0, "cached": 0, "output": 0, "cost": 0 }'
|
||||
* - name: progress
|
||||
* description: Intermediate accumulated extraction state (basedata / notes / items)
|
||||
* payload: ManifestResult
|
||||
* - name: result
|
||||
* description: Final accumulated extraction — emitted just before the stream closes
|
||||
* payload: ManifestResult
|
||||
* - name: error
|
||||
* description: Emitted on PDF parse or LLM failure; stream closes immediately after
|
||||
* payload: '{ "message": "…" }'
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* multipart/form-data:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [pdf]
|
||||
* properties:
|
||||
* pdf:
|
||||
* type: string
|
||||
* format: binary
|
||||
* description: PDF file to parse (max 50 MB, application/pdf)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: |
|
||||
* `text/event-stream` — emits `status`, `llm_call`, `tool_call`, `usage`,
|
||||
* `progress`, `result` (and `error` on failure). See SSE Events table below.
|
||||
*
|
||||
* **ManifestResult shape** (used by `progress` and `result`):
|
||||
*
|
||||
* ```json
|
||||
* {
|
||||
* "basedata": {
|
||||
* "date": "2026-01-15",
|
||||
* "kLieferant": 42,
|
||||
* "lieferantName": "Muster GmbH",
|
||||
* "manifestNr": "LS-2026-001",
|
||||
* "manifestType": "lieferschein"
|
||||
* },
|
||||
* "notes": ["Pos. 3: VPE-Angabe fehlte, Standardwert 1 angenommen."],
|
||||
* "items": {
|
||||
* "pagesFrom": 1,
|
||||
* "pagesTo": 2,
|
||||
* "items": [
|
||||
* {
|
||||
* "position": 1,
|
||||
* "count": 10,
|
||||
* "vpe": 1,
|
||||
* "unit": "Stk.",
|
||||
* "articleName": "Einhängesieb Stahl",
|
||||
* "liefArtNr": "ES-100",
|
||||
* "mwst": "19",
|
||||
* "eknetto": 450,
|
||||
* "page": 1
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
* content:
|
||||
* text/event-stream:
|
||||
* schema:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: No PDF uploaded or OpenRouter key not configured
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Error'
|
||||
*/
|
||||
app.post(
|
||||
`${basePath}/delivery/parsePDF`,
|
||||
upload.single('pdf'),
|
||||
async (req, res) => {
|
||||
// ── Validate upload ──────────────────────────────────────────────
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'A PDF file is required (field name: pdf)' });
|
||||
}
|
||||
|
||||
// ── Fetch OpenRouter API key from company settings ────────────────
|
||||
let apiKey;
|
||||
try {
|
||||
const setting = await dbCompanySetting.getByKey(req.user.id, 'keys.openrouter');
|
||||
apiKey = setting?.value;
|
||||
} catch (err) {
|
||||
console.error('parsePDF: failed to read OpenRouter key:', err);
|
||||
}
|
||||
if (!apiKey) {
|
||||
return res.status(400).json({ error: 'OpenRouter API key not configured (companysetting: keys.openrouter)' });
|
||||
}
|
||||
|
||||
// ── Open SSE stream ──────────────────────────────────────────────
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.flushHeaders();
|
||||
|
||||
// ── Parse PDF ────────────────────────────────────────────────────
|
||||
let pages;
|
||||
try {
|
||||
pages = readPdfPages(req.file.buffer);
|
||||
} catch (err) {
|
||||
sseWrite(res, 'error', { message: 'Failed to parse PDF: ' + err.message });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
sseWrite(res, 'status', { message: `PDF parsed: ${pages.length} page(s)` });
|
||||
|
||||
// ── Accumulated state ────────────────────────────────────────────
|
||||
const manifestBasedata = {
|
||||
date: null,
|
||||
kLieferant: null,
|
||||
lieferantName: null,
|
||||
manifestNr: null,
|
||||
manifestType: null,
|
||||
};
|
||||
const manifestNotes = [];
|
||||
const manifestItems = { items: [], pagesFrom: null, pagesTo: null };
|
||||
let currentPage = 0;
|
||||
|
||||
// ── Tool definition ──────────────────────────────────────────────
|
||||
const outputManifestTool = defineTool({
|
||||
name: 'output_manifest',
|
||||
description:
|
||||
'Stores extracted invoice/delivery-note data: header metadata (basedata), notes, and product line items. Each component is strictly optional — only include the ones that carry new data.',
|
||||
strict: false,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: [],
|
||||
properties: {
|
||||
basedata: {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
date: { type: ['string', 'null'], description: 'Document date (ISO 8601)' },
|
||||
kLieferant: { type: ['number', 'null'], description: 'Lieferant ID' },
|
||||
lieferantName: { type: 'string', description: 'Lieferant name' },
|
||||
manifestNr: { type: ['string', 'null'], description: 'Lieferscheinnummer or Rechnungsnummer' },
|
||||
manifestType: {
|
||||
anyOf: [
|
||||
{ type: 'string', enum: ['rechnung', 'lieferschein'] },
|
||||
{ type: 'null' },
|
||||
],
|
||||
description: 'Document type',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ type: 'null' },
|
||||
],
|
||||
description: 'Header/metadata. Only include fields that are currently missing or changed.',
|
||||
},
|
||||
notes: {
|
||||
anyOf: [
|
||||
{ type: 'array', items: { type: 'string' } },
|
||||
{ type: 'null' },
|
||||
],
|
||||
description: 'New noteworthy details (in German), for a human to double-check the result.',
|
||||
},
|
||||
items: {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
numberOfItemsToAdd: { type: 'number', description: 'Number of NEW line items being added in this call.' },
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'Invoice/delivery product line items',
|
||||
items: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['position', 'count', 'vpe', 'unit', 'articleName', 'liefArtNr', 'mwst', 'eknetto'],
|
||||
properties: {
|
||||
position: { type: 'number', description: 'Position' },
|
||||
count: { type: 'number', description: 'Quantity (Menge)' },
|
||||
vpe: { type: 'number', description: 'Packaging unit size (VPE). Default: 1.' },
|
||||
unit: { type: 'string', description: 'Unit of the individual sellable item. NEVER "VE" or "VPE".' },
|
||||
articleName: { type: 'string', description: 'Article name' },
|
||||
liefArtNr: { type: 'string', description: 'Supplier article number' },
|
||||
mwst: {
|
||||
anyOf: [{ type: 'string', enum: ['7', '19'] }, { type: 'null' }],
|
||||
description: 'VAT rate; null if missing',
|
||||
},
|
||||
eknetto: { type: ['number', 'null'], description: 'Net price per vpe in euro cents; null if missing' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ type: 'null' },
|
||||
],
|
||||
description: 'New product line items.',
|
||||
},
|
||||
},
|
||||
},
|
||||
invoke: async (args) => {
|
||||
// Merge basedata
|
||||
if (args.basedata && typeof args.basedata === 'object') {
|
||||
for (const key of ['date', 'kLieferant', 'lieferantName', 'manifestNr', 'manifestType']) {
|
||||
if (args.basedata[key] != null) manifestBasedata[key] = args.basedata[key];
|
||||
}
|
||||
}
|
||||
// Merge notes
|
||||
if (Array.isArray(args.notes)) {
|
||||
for (const n of args.notes) {
|
||||
if (n && !manifestNotes.includes(n)) manifestNotes.push(n);
|
||||
}
|
||||
}
|
||||
// Merge items
|
||||
if (args.items && typeof args.items === 'object' && Array.isArray(args.items.items)) {
|
||||
const existing = new Set(manifestItems.items.map((it) => it.position));
|
||||
for (const it of args.items.items) {
|
||||
if (!existing.has(it.position)) {
|
||||
manifestItems.items.push({ ...it, page: currentPage });
|
||||
existing.add(it.position);
|
||||
}
|
||||
}
|
||||
const pages = manifestItems.items.map((it) => it.page).filter((p) => p != null);
|
||||
if (pages.length) {
|
||||
manifestItems.pagesFrom = Math.min(...pages);
|
||||
manifestItems.pagesTo = Math.max(...pages);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit intermediate progress
|
||||
sseWrite(res, 'progress', {
|
||||
basedata: manifestBasedata,
|
||||
notes: manifestNotes,
|
||||
items: manifestItems,
|
||||
});
|
||||
|
||||
throw new DirectReturnError({
|
||||
basedata: manifestBasedata,
|
||||
notes: manifestNotes,
|
||||
items: manifestItems,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// ── LLMOrbiter setup ─────────────────────────────────────────────
|
||||
const model = '~openai/gpt-sol-latest';
|
||||
const provider = new OpenRouterProvider({ apiKey });
|
||||
const orbiter = new LLMOrbiter(provider, {
|
||||
onStatus: (status) => sseWrite(res, 'status', { message: status }),
|
||||
onLLMCall: (call) =>
|
||||
sseWrite(res, 'llm_call', {
|
||||
model: call.model,
|
||||
promptSize: call.promptSize,
|
||||
messages: call.messages,
|
||||
}),
|
||||
onToolCall: (call) => sseWrite(res, 'tool_call', { name: call.name }),
|
||||
onUsage: (usage) =>
|
||||
sseWrite(res, 'usage', {
|
||||
input: usage.input,
|
||||
cached: usage.cached,
|
||||
output: usage.output,
|
||||
cost: usage.cost,
|
||||
}),
|
||||
});
|
||||
|
||||
// ── Process all pages in one batch ───────────────────────────────
|
||||
try {
|
||||
const pageContent = pages
|
||||
.map((p, idx) => `--- Page ${idx + 1} of ${pages.length} ---\n${p}`)
|
||||
.join('\n\n');
|
||||
|
||||
const prompt = `${BASE_PROMPT}
|
||||
|
||||
You are processing all ${pages.length} page(s) of this document.
|
||||
|
||||
Current manifest_basedata (fill in what is still missing):
|
||||
${JSON.stringify(manifestBasedata, null, 2)}
|
||||
|
||||
Current notes:
|
||||
${JSON.stringify(manifestNotes, null, 2)}
|
||||
|
||||
Current manifest_items:
|
||||
${JSON.stringify(manifestItems, null, 2)}
|
||||
|
||||
Page content:
|
||||
${pageContent}`;
|
||||
|
||||
currentPage = 1;
|
||||
await orbiter.run({ prompt, model, tools: [outputManifestTool] });
|
||||
} catch (err) {
|
||||
if (!res.writableEnded) {
|
||||
sseWrite(res, 'error', { message: 'LLM processing failed: ' + err.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Final result ─────────────────────────────────────────────────
|
||||
sseWrite(res, 'result', {
|
||||
basedata: manifestBasedata,
|
||||
notes: manifestNotes,
|
||||
items: manifestItems,
|
||||
});
|
||||
|
||||
res.end();
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user