feat: Paperless-Webhook ersetzt Tag-16-Cron-Job durch Echtzeit-Verarbeitung
Build and Push Multi-Platform Images / build-and-push (push) Successful in 42s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 42s
Statt stündlichem Cron ruft Paperless-NGX nun per Webhook das Backend auf,
sobald ein Dokument bearbeitet wurde. Der Endpunkt verarbeitet ein einzelnes
Dokument (per doc_url/document_id), prüft weiterhin den Tag "paperlessmanager"
(ID 16) und ist per API-Key (X-API-Key) abgesichert, sodass nur Paperless ihn
aufrufen kann.
- paperless-processor.service.ts: @Cron entfernt; neue Methode
processDocumentById; Tag-16 als Konstante; gemeinsamer Helper
processAndEvaluate (Batch + Webhook)
- paperless.module.ts: PaperlessProcessorService exportiert
- webhook.controller.ts: Route auf api/webhook (erreichbar via /api-Proxy);
@Public() -> @UseGuards(ApiKeyGuard); ID-Extraktion aus {{doc_url}}
- webhook.module.ts: PaperlessModule + AuthModule importiert
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
4d05a94681
commit
56596d4482
4 files changed
+110
-24
No files matched your search
@@ -5,32 +5,78 @@ import {
|
||||
Logger,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { Public } from '../auth/public.decorator';
|
||||
import { ApiKeyGuard } from '../auth/api-key.guard';
|
||||
import { PaperlessProcessorService } from '../paperless/paperless-processor.service';
|
||||
|
||||
export interface PaperlessWebhookPayload {
|
||||
document_id: number;
|
||||
action: string;
|
||||
doc_url?: string;
|
||||
document_id?: number; // optional, für manuelles Testen
|
||||
action?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@Controller('webhook')
|
||||
@Controller('api/webhook')
|
||||
export class WebhookController {
|
||||
private readonly logger = new Logger(WebhookController.name);
|
||||
|
||||
@Public()
|
||||
constructor(private readonly paperlessProcessor: PaperlessProcessorService) {}
|
||||
|
||||
@UseGuards(ApiKeyGuard)
|
||||
@Post('paperless')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async handlePaperlessWebhook(
|
||||
@Body() payload: PaperlessWebhookPayload,
|
||||
): Promise<{ status: string }> {
|
||||
async handlePaperlessWebhook(@Body() payload: PaperlessWebhookPayload) {
|
||||
const documentId = this.extractDocumentId(payload);
|
||||
if (!documentId) {
|
||||
this.logger.warn(
|
||||
`Webhook ohne ermittelbare Dokument-ID: ${JSON.stringify(payload)}`,
|
||||
);
|
||||
throw new BadRequestException(
|
||||
'Keine Dokument-ID aus doc_url/document_id ermittelbar',
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Webhook empfangen: action=${payload.action}, document=${payload.document_id}`,
|
||||
`Webhook: action=${payload.action}, document=${documentId}`,
|
||||
);
|
||||
|
||||
// TODO: Business-Logik für verschiedene Webhook-Events
|
||||
// - document_updated → Felder prüfen, Postprocessing auslösen
|
||||
// - document_consumed → GoBD-Archivierung prüfen
|
||||
try {
|
||||
const result =
|
||||
await this.paperlessProcessor.processDocumentById(documentId);
|
||||
return {
|
||||
status: result.processed ? 'processed' : 'skipped',
|
||||
reason: result.reason,
|
||||
};
|
||||
} catch (err) {
|
||||
// Fehler tolerieren (wie der bisherige Cron) und 200 zurückgeben,
|
||||
// damit Paperless keine Retry-Schleife startet.
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
this.logger.error(
|
||||
`Fehler bei Webhook-Verarbeitung von Dokument ${documentId}: ${message}`,
|
||||
);
|
||||
return { status: 'error', message };
|
||||
}
|
||||
}
|
||||
|
||||
return { status: 'received' };
|
||||
/**
|
||||
* Ermittelt die Dokument-ID aus dem Webhook-Payload. Bevorzugt wird das
|
||||
* optionale Feld `document_id` (für manuelles Testen), andernfalls wird die
|
||||
* ID aus dem Paperless-Platzhalter `{{doc_url}}` extrahiert
|
||||
* (z.B. ".../documents/123/").
|
||||
*/
|
||||
private extractDocumentId(payload: PaperlessWebhookPayload): number | null {
|
||||
if (
|
||||
payload?.document_id != null &&
|
||||
!Number.isNaN(Number(payload.document_id))
|
||||
) {
|
||||
return Number(payload.document_id);
|
||||
}
|
||||
if (typeof payload?.doc_url === 'string') {
|
||||
const m = payload.doc_url.match(/\/documents\/(\d+)/);
|
||||
if (m) return Number(m[1]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user