Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

277 lines
7.8 KiB
TypeScript

import type { IAgentRuntime } from "@elizaos/core";
import { logger, Service, ServiceType } from "@elizaos/core";
import { getDocumentProxy } from "unpdf";
import type {
PdfConversionResult,
PdfDocumentInfo,
PdfExtractionOptions,
PdfMetadata,
PdfPageInfo,
} from "../types";
type PdfTextItem = { str: string };
export const MAX_PDF_BUFFER_BYTES = 100 * 1024 * 1024;
const PDF_HEADER_BYTES = [0x25, 0x50, 0x44, 0x46, 0x2d] as const;
const PDF_HEADER_SCAN_BYTES = 1024;
function isTextItem(item: unknown): item is PdfTextItem {
return (
typeof item === "object" &&
item !== null &&
"str" in item &&
typeof (item as { str?: unknown }).str === "string"
);
}
function collectTextStrings(items: readonly unknown[]): string[] {
const textItems: string[] = [];
for (const item of items) {
if (isTextItem(item)) {
textItems.push(item.str);
}
}
return textItems;
}
function hasPdfHeader(input: Uint8Array): boolean {
const scanLength = Math.min(input.length, PDF_HEADER_SCAN_BYTES);
for (let offset = 0; offset <= scanLength - PDF_HEADER_BYTES.length; offset++) {
let matches = true;
for (let index = 0; index < PDF_HEADER_BYTES.length; index++) {
if (input[offset + index] !== PDF_HEADER_BYTES[index]) {
matches = false;
break;
}
}
if (matches) {
return true;
}
}
return false;
}
function validatePdfInput(input: unknown): Uint8Array {
if (!(input instanceof Uint8Array)) {
throw new TypeError("PDF input must be a Buffer or Uint8Array");
}
if (input.length === 0) {
throw new RangeError("PDF input is empty");
}
if (input.byteLength > MAX_PDF_BUFFER_BYTES) {
throw new RangeError(`PDF input exceeds maximum size of ${MAX_PDF_BUFFER_BYTES} bytes`);
}
if (!hasPdfHeader(input)) {
throw new TypeError("PDF input is not a supported PDF document");
}
return input;
}
function validatePageOption(value: unknown, name: string): number | undefined {
if (value === undefined) return undefined;
if (
typeof value !== "number" ||
!Number.isFinite(value) ||
!Number.isInteger(value) ||
value < 1
) {
throw new RangeError(`${name} must be a positive finite integer`);
}
return value;
}
function normalizeExtractionOptions(
options: PdfExtractionOptions,
numPages: number
): {
startPage: number;
endPage: number;
} {
const requestedStartPage = validatePageOption(options.startPage, "startPage") ?? 1;
const requestedEndPage = validatePageOption(options.endPage, "endPage") ?? numPages;
if (requestedEndPage < requestedStartPage) {
throw new RangeError("endPage must be greater than or equal to startPage");
}
return {
startPage: Math.min(requestedStartPage, numPages),
endPage: Math.min(requestedEndPage, numPages),
};
}
function parseMetadataDate(value: string | Date | undefined): Date | undefined {
if (value === undefined) return undefined;
const date = value instanceof Date ? value : new Date(value);
return Number.isNaN(date.getTime()) ? undefined : date;
}
export class PdfService extends Service {
static serviceType = ServiceType.PDF;
capabilityDescription = "The agent is able to convert PDF files to text";
static async start(runtime: IAgentRuntime): Promise<PdfService> {
const service = new PdfService(runtime);
return service;
}
static async stop(runtime: IAgentRuntime): Promise<void> {
const service = runtime.getService(ServiceType.PDF);
if (service) {
await service.stop();
}
}
async stop(): Promise<void> {}
async convertPdfToText(pdfBuffer: Buffer | Uint8Array): Promise<string> {
try {
const uint8Array = validatePdfInput(pdfBuffer);
const pdf = await getDocumentProxy(uint8Array);
const numPages = pdf.numPages;
const textPages: string[] = [];
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const textContent = await page.getTextContent();
const pageText = collectTextStrings(textContent.items).join(" ");
textPages.push(pageText);
}
const rawText = textPages.join("\n");
return this.cleanUpContent(rawText);
} catch (error) {
const bufferSize = pdfBuffer instanceof Uint8Array ? pdfBuffer.length : "unknown";
logger.error(
`PdfService: Failed to convert PDF to text - error: ${error}, bufferSize: ${bufferSize}`
);
throw error;
}
}
async convertPdfToTextWithOptions(
pdfBuffer: Buffer | Uint8Array,
options: PdfExtractionOptions = {}
): Promise<PdfConversionResult> {
try {
const uint8Array = validatePdfInput(pdfBuffer);
const pdf = await getDocumentProxy(uint8Array);
const numPages = pdf.numPages;
const { startPage, endPage } = normalizeExtractionOptions(options, numPages);
const textPages: string[] = [];
for (let pageNum = startPage; pageNum <= endPage; pageNum++) {
const page = await pdf.getPage(pageNum);
const textContent = await page.getTextContent();
const pageText = collectTextStrings(textContent.items).join(
options.preserveWhitespace ? "" : " "
);
textPages.push(pageText);
}
let text = textPages.join("\n");
if (options.cleanContent !== false) {
text = this.cleanUpContent(text);
}
return {
success: true,
text,
pageCount: numPages,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
async getDocumentInfo(pdfBuffer: Buffer | Uint8Array): Promise<PdfDocumentInfo> {
const uint8Array = validatePdfInput(pdfBuffer);
const pdf = await getDocumentProxy(uint8Array);
const numPages = pdf.numPages;
const metadataResult = await pdf.getMetadata();
const info = metadataResult.info as Record<string, string | Date | undefined>;
const metadata: PdfMetadata = {
title: info.Title as string | undefined,
author: info.Author as string | undefined,
subject: info.Subject as string | undefined,
keywords: info.Keywords as string | undefined,
creator: info.Creator as string | undefined,
producer: info.Producer as string | undefined,
creationDate: parseMetadataDate(info.CreationDate),
modificationDate: parseMetadataDate(info.ModDate),
};
const pages: PdfPageInfo[] = [];
const allText: string[] = [];
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const viewport = page.getViewport({ scale: 1.0 });
const textContent = await page.getTextContent();
const pageText = collectTextStrings(textContent.items).join(" ");
pages.push({
pageNumber: pageNum,
width: viewport.width,
height: viewport.height,
text: this.cleanUpContent(pageText),
});
allText.push(pageText);
}
return {
pageCount: numPages,
metadata,
text: this.cleanUpContent(allText.join("\n")),
pages,
};
}
cleanUpContent(content: string): string {
try {
const filtered = content
.split("")
.filter((char) => {
const charCode = char.charCodeAt(0);
return !(
charCode === 0 ||
(charCode >= 1 && charCode <= 8) ||
(charCode >= 11 && charCode <= 12) ||
(charCode >= 14 && charCode <= 31) ||
charCode === 127
);
})
.join("");
const cleaned = filtered
.replace(/[^\S\r\n]+/g, " ")
.replace(/[ \t]+(\r?\n)/g, "$1")
.trim();
return cleaned;
} catch (error) {
logger.error(
`PdfService: Failed to clean up content - error: ${error}, contentLength: ${content.length}`
);
return content;
}
}
}
export default PdfService;