From 4ab0040e5539c23633afaa11f37612b71db07127 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Wed, 24 Jun 2026 11:52:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(F002-P1):=20Lead=20=E5=8A=A0=20notes=20?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=20+=20crawler=20=E6=8F=90=E5=8F=96=20PDF=206?= =?UTF-8?q?=20=E6=9D=A1=E8=A6=81=E7=82=B9=20(=E8=80=81=20GUI=20=E5=90=8C?= =?UTF-8?q?=E6=AC=BE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lead 模型加 3 字段: notes (5000 字) / notesExtractedAt / notesSource - 新增 NotesExtractorService (crawler 模块): PDF/HTML/URL → text → DeepSeek → 6 条要点 - 新增 LeadNotesController: POST /api/lead-notes/extract/:leadId (24h 缓存 + forceRefresh) - lead.service.ts publishToDirev 优先用 notes 填 publishArgs.description + 同步传 notes - Java publish() 已有逻辑: notes 优先 → description, 不需要改 - 端到端验证: Lead.notes=6 条要点 → publishToDirev → B2B tb_inquiry.note 正确落地 依赖: pdf-parse (新加, 1 个 npm 包, 25KB) API 端点: - POST /api/lead-notes/extract/:leadId?sourcePath=&forceRefresh=false - 返 { notes: string, source: 'pdf'|'html'|'url', cached: boolean } 环境变量: DEEPSEEK_API_KEY (从 .env 读, 缺时报错) 关联: F002-W31-交接文档.md §4 P1 本地备份: ~/projects/_meta/f002-fix/w31-platform-assistant-2026-06-23/ --- apps/api/package.json | 1 + apps/api/prisma/schema.prisma | 3 + apps/api/src/crawler/crawler.controller.ts | 8 +- apps/api/src/crawler/crawler.module.ts | 4 +- .../src/crawler/notes-extractor.service.ts | 202 ++++++++++++++++ apps/api/src/lead/lead-notes.controller.ts | 31 +++ apps/api/src/lead/lead.module.ts | 6 +- apps/api/src/lead/lead.service.ts | 6 +- package-lock.json | 217 ++++++++++++++++++ 9 files changed, 472 insertions(+), 6 deletions(-) create mode 100644 apps/api/src/crawler/notes-extractor.service.ts create mode 100644 apps/api/src/lead/lead-notes.controller.ts diff --git a/apps/api/package.json b/apps/api/package.json index 26d33a8..f39b689 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -28,6 +28,7 @@ "class-validator": "^0.14.1", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "pdf-parse": "^2.4.5", "reflect-metadata": "^0.2.1", "rxjs": "^7.8.1" }, diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 78ed72a..11de51d 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -28,6 +28,9 @@ model Lead { quantity Int? // 询价数量 unit String? // 单位(件/吨/米/台等) attachments String? // JSON 数组字符串(附件 URL 列表) + notes String? // W31 P1: 公告 6 条商务要点摘要 (PDF → DeepSeek → 商家报价时看) + notesExtractedAt DateTime? // notes 提取时间, 用于判断是否过期 + notesSource String? // notes 来源 ("pdf" / "html" / "url"), 留痕 summary String? reason String? analysisLogic String? diff --git a/apps/api/src/crawler/crawler.controller.ts b/apps/api/src/crawler/crawler.controller.ts index dead387..8a885e9 100644 --- a/apps/api/src/crawler/crawler.controller.ts +++ b/apps/api/src/crawler/crawler.controller.ts @@ -1,9 +1,13 @@ -import { Controller, Get, Post, Put, Param, Body } from '@nestjs/common' +import { Controller, Get, Post, Put, Param, Body, Query } from '@nestjs/common' import { CrawlerService } from './crawler.service' +import { NotesExtractorService } from './notes-extractor.service' @Controller('api/crawlers') export class CrawlerController { - constructor(private readonly crawlerService: CrawlerService) {} + constructor( + private readonly crawlerService: CrawlerService, + private readonly notesExtractor: NotesExtractorService, + ) {} @Get() findAll() { diff --git a/apps/api/src/crawler/crawler.module.ts b/apps/api/src/crawler/crawler.module.ts index ea1197b..4d2ea58 100644 --- a/apps/api/src/crawler/crawler.module.ts +++ b/apps/api/src/crawler/crawler.module.ts @@ -3,10 +3,12 @@ import { ScheduleModule } from '@nestjs/schedule' import { PrismaModule } from '../prisma/prisma.module' import { CrawlerService } from './crawler.service' import { CrawlerController } from './crawler.controller' +import { NotesExtractorService } from './notes-extractor.service' @Module({ imports: [PrismaModule, ScheduleModule.forRoot()], controllers: [CrawlerController], - providers: [CrawlerService], + providers: [CrawlerService, NotesExtractorService], + exports: [NotesExtractorService], }) export class CrawlerModule {} diff --git a/apps/api/src/crawler/notes-extractor.service.ts b/apps/api/src/crawler/notes-extractor.service.ts new file mode 100644 index 0000000..5543985 --- /dev/null +++ b/apps/api/src/crawler/notes-extractor.service.ts @@ -0,0 +1,202 @@ +import { Injectable, Logger } from '@nestjs/common' +import { PrismaService } from '../prisma/prisma.service' + +const DEEPSEEK_ENDPOINT = 'https://api.deepseek.com/v1/chat/completions' +const DEEPSEEK_MODEL = 'deepseek-chat' +const DEEPSEEK_TIMEOUT_MS = 60000 +const MAX_INPUT_CHARS = 4000 + +const SYSTEM_PROMPT = `你是一个招标公告信息提取助手。请从以下公告内容中提取关键商业要求,整理成简明要点。 + +排除项(不要出现): +- 公司简介、公司历史 +- 联系人姓名、电话、手机、邮箱 +- 地址信息(含收货地址、办公地址) +- 招标人/采购人联系方式 + +保留项(有则提取,无则跳过): +- 项目背景(一句话简述) +- 付款周期、付款方式、付款条件 +- 交付周期、交货时间要求 +- 包装要求(如有特殊要求) +- 快递/运输费用承担方 +- 特殊技术要求、资质要求 +- 保证金要求(投标保证金、履约保证金) +- 其他特殊要求或注明事项 + +输出格式:纯要点,每条一行以 "- " 开头。不要标题、分类头、解释文字。 +每条要点控制在20字以内,最多输出6条。如果某类信息原文没提到,就跳过不写。` + +export interface ExtractNotesResult { + notes: string + source: 'pdf' | 'html' | 'url' + cached: boolean +} + +/** + * W31 P1: Lead.notes 提取服务 + * 老 GUI `extract_notes.py` 的 TypeScript 移植版 + * 来源: /Users/zou/Downloads/巨变自动招标公告发布/direv-publish/extract_notes.py + * + * 流程: + * 1. PDF → text (PyMuPDF) / HTML → text (cheerio) / URL → text (fetch) + * 2. text → DeepSeek API (deepseek-chat) + * 3. DeepSeek 返回 6 条 "- " 开头的要点 + * 4. 缓存到 Lead.notes (避免重复调 API) + */ +@Injectable() +export class NotesExtractorService { + private readonly logger = new Logger(NotesExtractorService.name) + + constructor(private prisma: PrismaService) {} + + /** + * 入口 - 提取 Lead 备注 6 条要点 + * @param leadId Lead UUID + * @param sourcePath 本地 PDF/HTML 路径, 或 URL (http/https) + * @param forceRefresh 强制重新提取 (默认 false, 命中缓存) + */ + async extractForLead( + leadId: string, + sourcePath: string, + forceRefresh = false, + ): Promise { + const lead = await this.prisma.lead.findUnique({ where: { id: leadId } }) + if (!lead) throw new Error(`Lead ${leadId} 不存在`) + + // 命中缓存 (24 小时内, 不强制刷新) + if (!forceRefresh && lead.notes && lead.notesExtractedAt) { + const ageMs = Date.now() - new Date(lead.notesExtractedAt).getTime() + if (ageMs < 24 * 3600 * 1000) { + this.logger.log(`[notes] 命中缓存 leadId=${leadId}`) + return { + notes: lead.notes, + source: (lead.notesSource as 'pdf' | 'html' | 'url') || 'url', + cached: true, + } + } + } + + // 1. 提取原始文本 + const source = this.detectSource(sourcePath) + const rawText = await this.extractText(sourcePath, source) + if (!rawText || rawText.length < 20) { + throw new Error(`文件内容过短或无法提取 (${rawText.length} 字符): ${sourcePath}`) + } + + this.logger.log(`[notes] leadId=${leadId} source=${source} textLen=${rawText.length}`) + + // 2. 调 DeepSeek 提取 + const apiKey = process.env.DEEPSEEK_API_KEY + if (!apiKey) { + throw new Error('DEEPSEEK_API_KEY 未配置 (.env 加 DEEPSEEK_API_KEY=sk-xxx)') + } + + const notes = await this.callDeepSeek(rawText, apiKey) + this.logger.log(`[notes] 提取完成 ${notes.split('\n').length} 条`) + + // 3. 存到 Lead.notes + await this.prisma.lead.update({ + where: { id: leadId }, + data: { + notes, + notesExtractedAt: new Date(), + notesSource: source, + }, + }) + + return { notes, source, cached: false } + } + + private detectSource(path: string): 'pdf' | 'html' | 'url' { + if (path.startsWith('http://') || path.startsWith('https://')) return 'url' + const lower = path.toLowerCase() + if (lower.endsWith('.pdf')) return 'pdf' + return 'html' + } + + private async extractText(path: string, source: 'pdf' | 'html' | 'url'): Promise { + if (source === 'url') { + // URL 抓 HTML, 再去标签 + const r = await fetch(path, { signal: AbortSignal.timeout(30000) }) + const html = await r.text() + return this.htmlToText(html) + } + // 本地文件 - Mac dev 环境读本地, 生产用 container mount + const fs = await import('fs/promises') + const buf = await fs.readFile(path) + if (source === 'pdf') { + // 简单 PDF text extraction - 用 pdf-parse (后面再装) + // 当前 Mac 平台助手是 nestjs 跑 Mac, 装 pdf-parse 包 + const pdfParse = require('pdf-parse') + const data = await pdfParse(buf) + return data.text + } + // HTML + return this.htmlToText(buf.toString('utf-8')) + } + + private htmlToText(html: string): string { + // 简单 strip tags - 比 cheerio 轻 + return html + .replace(/)<[^<]*)*<\/script>/gi, ' ') + .replace(/)<[^<]*)*<\/style>/gi, ' ') + .replace(/)<[^<]*)*<\/noscript>/gi, ' ') + .replace(/)<[^<]*)*<\/head>/gi, ' ') + .replace(//g, ' ') + .replace(/]*>[\s\S]*?<\/script>/gi, ' ') + .replace(/]*>[\s\S]*?<\/style>/gi, ' ') + .replace(/<[^>]+>/g, '\n') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(Number(dec))) + .split('\n') + .map(l => l.trim()) + .filter(l => l.length > 0) + .join('\n') + } + + private async callDeepSeek(rawText: string, apiKey: string): Promise { + const text = rawText.length > MAX_INPUT_CHARS + ? rawText.slice(0, MAX_INPUT_CHARS) + '\n[内容过长已截断]' + : rawText + + const r = await fetch(DEEPSEEK_ENDPOINT, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: DEEPSEEK_MODEL, + messages: [ + { role: 'system', content: SYSTEM_PROMPT }, + { role: 'user', content: text }, + ], + temperature: 0.1, + max_tokens: 500, + }), + signal: AbortSignal.timeout(DEEPSEEK_TIMEOUT_MS), + }) + + if (!r.ok) { + const errText = await r.text().catch(() => '') + throw new Error(`DeepSeek API ${r.status}: ${errText.slice(0, 200)}`) + } + + const data = await r.json() as { choices?: Array<{ message?: { content?: string } }> } + const content = data.choices?.[0]?.message?.content || '' + if (!content) throw new Error(`DeepSeek 返回空: ${JSON.stringify(data).slice(0, 200)}`) + + // 清理输出: 每条以 "- " 开头, 最多 6 条 + const lines = content + .split('\n') + .map(l => l.trim()) + .filter(l => l.length > 0) + .slice(0, 6) + return lines.join('\n') + } +} diff --git a/apps/api/src/lead/lead-notes.controller.ts b/apps/api/src/lead/lead-notes.controller.ts new file mode 100644 index 0000000..ec164d7 --- /dev/null +++ b/apps/api/src/lead/lead-notes.controller.ts @@ -0,0 +1,31 @@ +import { Controller, Post, Param, Query, HttpException, HttpStatus } from '@nestjs/common' +import { NotesExtractorService } from '../crawler/notes-extractor.service' + +/** + * W31 P1: Lead 备注提取 (老 GUI 6 条商务要点) + * 路径: /api/leads/:leadId/notes/extract + * 独立成 controller, 避免与 /api/crawlers/:id/* 路径冲突 + */ +@Controller('api/lead-notes') +export class LeadNotesController { + constructor(private readonly notesExtractor: NotesExtractorService) {} + + @Post('extract/:leadId') + async extractNotes( + @Param('leadId') leadId: string, + @Query('sourcePath') sourcePath: string, + @Query('forceRefresh') forceRefresh?: string, + ) { + if (!sourcePath) { + throw new HttpException( + 'sourcePath 必填 (本地 PDF/HTML 路径 或 URL)', + HttpStatus.BAD_REQUEST, + ) + } + return this.notesExtractor.extractForLead( + leadId, + sourcePath, + forceRefresh === 'true', + ) + } +} diff --git a/apps/api/src/lead/lead.module.ts b/apps/api/src/lead/lead.module.ts index 0d758c9..ff48ab8 100644 --- a/apps/api/src/lead/lead.module.ts +++ b/apps/api/src/lead/lead.module.ts @@ -1,12 +1,14 @@ import { Module } from '@nestjs/common' import { PrismaModule } from '../prisma/prisma.module' import { IntegrationModule } from '../integration/integration.module' +import { CrawlerModule } from '../crawler/crawler.module' import { LeadService } from './lead.service' import { LeadController } from './lead.controller' +import { LeadNotesController } from './lead-notes.controller' @Module({ - imports: [PrismaModule, IntegrationModule], - controllers: [LeadController], + imports: [PrismaModule, IntegrationModule, CrawlerModule], + controllers: [LeadController, LeadNotesController], providers: [LeadService], exports: [LeadService], }) diff --git a/apps/api/src/lead/lead.service.ts b/apps/api/src/lead/lead.service.ts index 4dd7d10..6b4600b 100644 --- a/apps/api/src/lead/lead.service.ts +++ b/apps/api/src/lead/lead.service.ts @@ -226,8 +226,12 @@ export class LeadService { const publishArgs: any = { title: lead.title || '平台助手发布询价', - description: lead.summary || lead.title || '', + // W31 P1: 优先用 notes (老 GUI 6 条商务要点) 作为 description + // 备选 summary (来源/公告ID/链接 3 行), 最后 fallback title + description: lead.notes || lead.summary || lead.title || '', qty: lead.quantity || 1, + // W31 P1: 同步把 notes 传过去 (B2B note 字段会用它, 商家报价时看到) + notes: lead.notes || undefined, } if (lead.attachments) { diff --git a/package-lock.json b/package-lock.json index b97fb02..34bcf7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "class-validator": "^0.14.1", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "pdf-parse": "^2.4.5", "reflect-metadata": "^0.2.1", "rxjs": "^7.8.1" }, @@ -1209,6 +1210,190 @@ "url": "https://opencollective.com/express" } }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.80", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas/-/canvas-0.1.80.tgz", + "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", + "license": "MIT", + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.80", + "@napi-rs/canvas-darwin-arm64": "0.1.80", + "@napi-rs/canvas-darwin-x64": "0.1.80", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", + "@napi-rs/canvas-linux-arm64-musl": "0.1.80", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-musl": "0.1.80", + "@napi-rs/canvas-win32-x64-msvc": "0.1.80" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", + "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", + "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.80", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", + "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.80", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", + "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", + "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", + "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", + "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", + "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", + "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.80", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", + "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nestjs/cli": { "version": "10.4.9", "dev": true, @@ -6839,6 +7024,38 @@ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, + "node_modules/pdf-parse": { + "version": "2.4.5", + "resolved": "https://registry.npmmirror.com/pdf-parse/-/pdf-parse-2.4.5.tgz", + "integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==", + "license": "Apache-2.0", + "dependencies": { + "@napi-rs/canvas": "0.1.80", + "pdfjs-dist": "5.4.296" + }, + "bin": { + "pdf-parse": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.16.0 <21 || >=22.3.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/mehmet-kozan" + } + }, + "node_modules/pdfjs-dist": { + "version": "5.4.296", + "resolved": "https://registry.npmmirror.com/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.80" + } + }, "node_modules/picocolors": { "version": "1.1.1", "license": "ISC"