Initial commit: import Operit protocol plugins from xiaoli-cli
This commit is contained in:
commit
c6600fc47d
52 files changed
+31492
No files matched your search
@@ -0,0 +1,755 @@
|
||||
/* METADATA
|
||||
{
|
||||
"name": "pdf_vision_parser",
|
||||
"display_name": {
|
||||
"zh": "PDF 逐页识图解析",
|
||||
"en": "PDF Vision Parser"
|
||||
},
|
||||
"description": {
|
||||
"zh": "将 PDF 逐页渲染为图片,并调用用户当前配置的识图模型进行解析,最后按页拼接结果。",
|
||||
"en": "Render a PDF page by page into images, send each image to the user's configured vision model, and merge the results."
|
||||
},
|
||||
"enabledByDefault": false,
|
||||
"category": "File",
|
||||
"tools": [
|
||||
{
|
||||
"name": "parse_pdf_with_vision",
|
||||
"description": {
|
||||
"zh": "将 PDF 逐页拆成图片,逐页调用 IMAGE_RECOGNITION 功能模型识图,并返回分页索引与输出文件路径。",
|
||||
"en": "Split a PDF into page images, analyze each page with the IMAGE_RECOGNITION function model, and return per-page indexes plus output file paths."
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "pdf_path",
|
||||
"description": {
|
||||
"zh": "PDF 文件路径。",
|
||||
"en": "Path to the PDF file."
|
||||
},
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "page_prompt",
|
||||
"description": {
|
||||
"zh": "每页识图提示词;不传则使用默认的尽量转文字提示词。",
|
||||
"en": "Vision prompt for each page; defaults to a text-extraction-oriented prompt."
|
||||
},
|
||||
"type": "string",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "start_page",
|
||||
"description": {
|
||||
"zh": "起始页码,1-based,默认 1。",
|
||||
"en": "Start page number, 1-based, default 1."
|
||||
},
|
||||
"type": "number",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "end_page",
|
||||
"description": {
|
||||
"zh": "结束页码,1-based,默认文档最后一页。",
|
||||
"en": "End page number, 1-based, default is the last page."
|
||||
},
|
||||
"type": "number",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}*/
|
||||
|
||||
const PdfVisionParser = (function () {
|
||||
const DEFAULT_PAGE_PROMPT =
|
||||
"尽量完整提取本页可见文字与结构,保留标题、列表、表格顺序,不做总结,不补充页外信息;公式和图表转成可读文本描述";
|
||||
const TOOL_NAME = "parse_pdf_with_vision";
|
||||
const TOOL_FUNCTION_TYPE = "IMAGE_RECOGNITION";
|
||||
const RENDER_SCALE = 2;
|
||||
const PAGE_CONCURRENCY = 4;
|
||||
const IMAGE_FORMAT = "png";
|
||||
const MERGED_OUTPUT_FILE_NAME = "parsed_output.txt";
|
||||
|
||||
const File = Java.type("java.io.File");
|
||||
const FileOutputStream = Java.type("java.io.FileOutputStream");
|
||||
const OutputStreamWriter = Java.type("java.io.OutputStreamWriter");
|
||||
const BufferedWriter = Java.type("java.io.BufferedWriter");
|
||||
const ParcelFileDescriptor = Java.type("android.os.ParcelFileDescriptor");
|
||||
const PdfRenderer = Java.type("android.graphics.pdf.PdfRenderer");
|
||||
const PdfRendererPage = Java.type("android.graphics.pdf.PdfRenderer$Page");
|
||||
const Bitmap = Java.type("android.graphics.Bitmap");
|
||||
const BitmapConfig = Java.type("android.graphics.Bitmap$Config");
|
||||
const BitmapCompressFormat = Java.type("android.graphics.Bitmap$CompressFormat");
|
||||
const Color = Java.type("android.graphics.Color");
|
||||
const StandardCharsets = Java.type("java.nio.charset.StandardCharsets");
|
||||
const EnhancedAIService = Java.type("com.ai.assistance.operit.api.chat.EnhancedAIService");
|
||||
|
||||
interface ParsePdfWithVisionParams {
|
||||
pdf_path: string;
|
||||
page_prompt?: string;
|
||||
start_page?: number;
|
||||
end_page?: number;
|
||||
}
|
||||
|
||||
interface FailedPageInfo {
|
||||
page_number: number;
|
||||
stage: "precheck" | "render" | "vision";
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface PageResult {
|
||||
page_number: number;
|
||||
image_path: string;
|
||||
image_link: string;
|
||||
output_path: string;
|
||||
}
|
||||
|
||||
interface InternalPageResult extends PageResult {
|
||||
analysis: string;
|
||||
}
|
||||
|
||||
interface PageProcessingFailure {
|
||||
page_number: number;
|
||||
stage: FailedPageInfo["stage"];
|
||||
error: string;
|
||||
partial_results: InternalPageResult[];
|
||||
}
|
||||
|
||||
interface VisionModelInfo {
|
||||
function_type: "IMAGE_RECOGNITION";
|
||||
config_id: string;
|
||||
config_name: string;
|
||||
selected_model: string;
|
||||
}
|
||||
|
||||
interface ParsePdfWithVisionData {
|
||||
pdf_path: string;
|
||||
image_dir: string;
|
||||
output_path: string;
|
||||
total_page_count: number;
|
||||
processed_page_count: number;
|
||||
pages: PageResult[];
|
||||
vision_model: VisionModelInfo;
|
||||
failed_page?: FailedPageInfo;
|
||||
}
|
||||
|
||||
interface ToolResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: ParsePdfWithVisionData;
|
||||
}
|
||||
|
||||
function isBlank(value: unknown): boolean {
|
||||
return String(value ?? "").trim().length === 0;
|
||||
}
|
||||
|
||||
function toTrimmedString(value: unknown): string {
|
||||
return String(value ?? "").trim();
|
||||
}
|
||||
|
||||
function parsePositiveInteger(
|
||||
value: number | undefined,
|
||||
fieldName: string,
|
||||
options?: { allowUndefined?: boolean }
|
||||
): number | undefined {
|
||||
if (value === undefined || value === null) {
|
||||
if (options?.allowUndefined) {
|
||||
return undefined;
|
||||
}
|
||||
throw new Error(`${fieldName} 不能为空。`);
|
||||
}
|
||||
|
||||
if (!Number.isInteger(value) || value <= 0) {
|
||||
throw new Error(`${fieldName} 必须是大于 0 的整数。`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function buildMergedText(pages: InternalPageResult[]): string {
|
||||
return pages
|
||||
.map(page => `===== Page ${page.page_number} =====\n${page.analysis}`)
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
function buildVisionModelInfo(binding: FunctionModelConfigResultData): VisionModelInfo {
|
||||
return {
|
||||
function_type: TOOL_FUNCTION_TYPE,
|
||||
config_id: String(binding.configId || ""),
|
||||
config_name: String(binding.configName || ""),
|
||||
selected_model: String(binding.selectedModel || "")
|
||||
};
|
||||
}
|
||||
|
||||
function buildData(
|
||||
pdfPath: string,
|
||||
imageDir: string,
|
||||
outputPath: string,
|
||||
totalPageCount: number,
|
||||
pages: InternalPageResult[],
|
||||
visionModel: VisionModelInfo,
|
||||
failedPage?: FailedPageInfo
|
||||
): ParsePdfWithVisionData {
|
||||
const data: ParsePdfWithVisionData = {
|
||||
pdf_path: pdfPath,
|
||||
image_dir: imageDir,
|
||||
output_path: outputPath,
|
||||
total_page_count: totalPageCount,
|
||||
processed_page_count: pages.length,
|
||||
pages: pages.map(page => ({
|
||||
page_number: page.page_number,
|
||||
image_path: page.image_path,
|
||||
image_link: page.image_link,
|
||||
output_path: page.output_path
|
||||
})),
|
||||
vision_model: visionModel
|
||||
};
|
||||
|
||||
if (failedPage) {
|
||||
data.failed_page = failedPage;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function buildPrecheckFailure(message: string): ToolResponse {
|
||||
return {
|
||||
success: false,
|
||||
message,
|
||||
data: {
|
||||
pdf_path: "",
|
||||
image_dir: "",
|
||||
output_path: "",
|
||||
total_page_count: 0,
|
||||
processed_page_count: 0,
|
||||
pages: [],
|
||||
vision_model: {
|
||||
function_type: TOOL_FUNCTION_TYPE,
|
||||
config_id: "",
|
||||
config_name: "",
|
||||
selected_model: ""
|
||||
},
|
||||
failed_page: {
|
||||
page_number: 0,
|
||||
stage: "precheck",
|
||||
error: message
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function safeClose(resource: any, label: string): void {
|
||||
if (!resource) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resource.close();
|
||||
} catch (error) {
|
||||
console.warn(`[${TOOL_NAME}] 关闭 ${label} 失败: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function safeRecycle(bitmap: any): void {
|
||||
if (!bitmap) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
bitmap.recycle();
|
||||
} catch (error) {
|
||||
console.warn(`[${TOOL_NAME}] 回收 bitmap 失败: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDirectoryExists(dir: any): void {
|
||||
if (dir.exists()) {
|
||||
if (!dir.isDirectory()) {
|
||||
throw new Error(`目标路径不是目录: ${String(dir.getAbsolutePath())}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const created = dir.mkdirs();
|
||||
if (!created) {
|
||||
throw new Error(`无法创建目录: ${String(dir.getAbsolutePath())}`);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAppContext(): JavaBridgeInstance {
|
||||
if (typeof Java.getApplicationContext === "function") {
|
||||
return Java.getApplicationContext();
|
||||
}
|
||||
if (typeof Java.getContext === "function") {
|
||||
return Java.getContext();
|
||||
}
|
||||
throw new Error("无法获取应用 Context。");
|
||||
}
|
||||
|
||||
function createOutputDirectory(): any {
|
||||
const parserRootDir = new File(OPERIT_CLEAN_ON_EXIT_DIR, "pdf_vision_parser");
|
||||
ensureDirectoryExists(parserRootDir);
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const runDir = new File(parserRootDir, timestamp);
|
||||
ensureDirectoryExists(runDir);
|
||||
return runDir;
|
||||
}
|
||||
|
||||
function writeTextFile(targetFile: any, content: string): string {
|
||||
let outputStream: any = null;
|
||||
let writer: any = null;
|
||||
let bufferedWriter: any = null;
|
||||
try {
|
||||
outputStream = new FileOutputStream(targetFile);
|
||||
writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
|
||||
bufferedWriter = new BufferedWriter(writer);
|
||||
bufferedWriter.write(String(content ?? ""));
|
||||
bufferedWriter.flush();
|
||||
return String(targetFile.getAbsolutePath());
|
||||
} finally {
|
||||
safeClose(bufferedWriter, "bufferedWriter");
|
||||
safeClose(writer, "writer");
|
||||
safeClose(outputStream, "outputStream");
|
||||
}
|
||||
}
|
||||
|
||||
function writePageOutputFile(outputDir: any, pageNumber: number, content: string): string {
|
||||
const textFile = new File(outputDir, `page-${padPageNumber(pageNumber)}.txt`);
|
||||
return writeTextFile(textFile, content);
|
||||
}
|
||||
|
||||
function writeMergedOutputFile(outputDir: any, pages: InternalPageResult[]): string {
|
||||
if (!outputDir) {
|
||||
return "";
|
||||
}
|
||||
const mergedFile = new File(outputDir, MERGED_OUTPUT_FILE_NAME);
|
||||
return writeTextFile(mergedFile, buildMergedText(pages));
|
||||
}
|
||||
|
||||
function padPageNumber(pageNumber: number): string {
|
||||
return String(pageNumber).padStart(4, "0");
|
||||
}
|
||||
|
||||
function assertVisionBinding(binding: FunctionModelConfigResultData): void {
|
||||
if (!binding || isBlank(binding.configId)) {
|
||||
throw new Error("IMAGE_RECOGNITION 当前没有可用绑定。");
|
||||
}
|
||||
if (!binding.config || binding.config.enableDirectImageProcessing !== true) {
|
||||
throw new Error("IMAGE_RECOGNITION 当前绑定的模型未启用识图能力。");
|
||||
}
|
||||
}
|
||||
|
||||
function validatePdfFile(pdfPath: string): any {
|
||||
if (isBlank(pdfPath)) {
|
||||
throw new Error("pdf_path 不能为空。");
|
||||
}
|
||||
|
||||
const pdfFile = new File(pdfPath);
|
||||
if (!pdfFile.exists() || !pdfFile.isFile()) {
|
||||
throw new Error(`PDF 文件不存在: ${pdfPath}`);
|
||||
}
|
||||
|
||||
if (!String(pdfFile.getName()).toLowerCase().endsWith(".pdf")) {
|
||||
throw new Error(`文件不是 PDF: ${pdfPath}`);
|
||||
}
|
||||
|
||||
return pdfFile;
|
||||
}
|
||||
|
||||
function stripThinkingContent(content: string): string {
|
||||
return String(content ?? "")
|
||||
.replace(/<think(?:ing)?>[\s\S]*?(<\/think(?:ing)?>|\z)/gi, "")
|
||||
.replace(/<search>[\s\S]*?(<\/search>|\z)/gi, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function sortPageResults(pages: InternalPageResult[]): InternalPageResult[] {
|
||||
return [...pages].sort((left, right) => left.page_number - right.page_number);
|
||||
}
|
||||
|
||||
function normalizeErrorMessage(error: unknown): string {
|
||||
return String(error instanceof Error ? error.message : error);
|
||||
}
|
||||
|
||||
async function analyzePageImage(
|
||||
service: JavaBridgeInstance,
|
||||
imagePath: string,
|
||||
prompt: string
|
||||
): Promise<string> {
|
||||
const rawAnalysis = String(
|
||||
await service.callSuspend("analyzeImageWithIntent", imagePath, prompt)
|
||||
).trim();
|
||||
const analysis = stripThinkingContent(rawAnalysis);
|
||||
|
||||
if (analysis.length === 0) {
|
||||
throw new Error("识图模型返回了空结果。");
|
||||
}
|
||||
if (/^Image recognition failed:/i.test(rawAnalysis) || /^Image recognition failed:/i.test(analysis)) {
|
||||
throw new Error(rawAnalysis || analysis);
|
||||
}
|
||||
|
||||
return analysis;
|
||||
}
|
||||
|
||||
async function processSinglePage(
|
||||
pdfFile: any,
|
||||
outputDir: any,
|
||||
pageNumber: number,
|
||||
prompt: string,
|
||||
service: JavaBridgeInstance
|
||||
): Promise<InternalPageResult> {
|
||||
console.log(`[${TOOL_NAME}] 开始处理第 ${pageNumber} 页`);
|
||||
|
||||
const zeroBasedPageIndex = pageNumber - 1;
|
||||
const imageFile = new File(
|
||||
outputDir,
|
||||
`page-${padPageNumber(pageNumber)}.${IMAGE_FORMAT}`
|
||||
);
|
||||
const imagePath = String(imageFile.getAbsolutePath());
|
||||
|
||||
let fileDescriptor: any = null;
|
||||
let pdfRenderer: any = null;
|
||||
let page: any = null;
|
||||
let bitmap: any = null;
|
||||
let outputStream: any = null;
|
||||
let stage: FailedPageInfo["stage"] = "render";
|
||||
|
||||
try {
|
||||
fileDescriptor = ParcelFileDescriptor.open(
|
||||
pdfFile,
|
||||
ParcelFileDescriptor.MODE_READ_ONLY
|
||||
);
|
||||
pdfRenderer = new PdfRenderer(fileDescriptor);
|
||||
page = pdfRenderer.openPage(zeroBasedPageIndex);
|
||||
|
||||
const width = Number(page.getWidth()) * RENDER_SCALE;
|
||||
const height = Number(page.getHeight()) * RENDER_SCALE;
|
||||
|
||||
bitmap = Bitmap.createBitmap(width, height, BitmapConfig.ARGB_8888);
|
||||
bitmap.eraseColor(Color.WHITE);
|
||||
page.render(
|
||||
bitmap,
|
||||
null,
|
||||
null,
|
||||
PdfRendererPage.RENDER_MODE_FOR_DISPLAY
|
||||
);
|
||||
|
||||
outputStream = new FileOutputStream(imageFile);
|
||||
const compressed = bitmap.compress(
|
||||
BitmapCompressFormat.PNG,
|
||||
100,
|
||||
outputStream
|
||||
);
|
||||
if (!compressed) {
|
||||
throw new Error(`页面 ${pageNumber} 图片写入失败。`);
|
||||
}
|
||||
|
||||
safeClose(outputStream, `page-${pageNumber} output stream`);
|
||||
outputStream = null;
|
||||
|
||||
const imageLink = String(
|
||||
NativeInterface.registerImageFromPath(imagePath) || ""
|
||||
).trim();
|
||||
if (!imageLink) {
|
||||
throw new Error(`页面 ${pageNumber} 图片链接注册失败。`);
|
||||
}
|
||||
|
||||
stage = "vision";
|
||||
const analysis = await analyzePageImage(service, imagePath, prompt);
|
||||
const pageOutputPath = writePageOutputFile(outputDir, pageNumber, analysis);
|
||||
|
||||
console.log(`[${TOOL_NAME}] 第 ${pageNumber} 页处理完成`);
|
||||
return {
|
||||
page_number: pageNumber,
|
||||
image_path: imagePath,
|
||||
image_link: imageLink,
|
||||
output_path: pageOutputPath,
|
||||
analysis
|
||||
};
|
||||
} catch (error) {
|
||||
throw {
|
||||
page_number: pageNumber,
|
||||
stage,
|
||||
error: normalizeErrorMessage(error),
|
||||
partial_results: []
|
||||
} as PageProcessingFailure;
|
||||
} finally {
|
||||
safeClose(outputStream, `page-${pageNumber} output stream`);
|
||||
safeRecycle(bitmap);
|
||||
safeClose(page, `page-${pageNumber}`);
|
||||
safeClose(pdfRenderer, `page-${pageNumber} pdfRenderer`);
|
||||
safeClose(fileDescriptor, `page-${pageNumber} parcelFileDescriptor`);
|
||||
}
|
||||
}
|
||||
|
||||
async function processPagesWithConcurrency(
|
||||
pageNumbers: number[],
|
||||
limit: number,
|
||||
worker: (pageNumber: number) => Promise<InternalPageResult>
|
||||
): Promise<InternalPageResult[]> {
|
||||
const results: Array<InternalPageResult | undefined> = new Array(pageNumbers.length);
|
||||
let nextIndex = 0;
|
||||
let failure: Omit<PageProcessingFailure, "partial_results"> | null = null;
|
||||
|
||||
async function runWorker(): Promise<void> {
|
||||
while (true) {
|
||||
if (failure) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentIndex = nextIndex;
|
||||
nextIndex += 1;
|
||||
|
||||
if (currentIndex >= pageNumbers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pageNumber = pageNumbers[currentIndex];
|
||||
try {
|
||||
results[currentIndex] = await worker(pageNumber);
|
||||
} catch (error) {
|
||||
if (!failure) {
|
||||
const errorData = error as Partial<PageProcessingFailure> | undefined;
|
||||
failure = {
|
||||
page_number: Number(errorData?.page_number ?? pageNumber),
|
||||
stage: (errorData?.stage as FailedPageInfo["stage"] | undefined) ?? "render",
|
||||
error: normalizeErrorMessage(errorData?.error ?? error)
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workerCount = Math.max(1, Math.min(limit, pageNumbers.length));
|
||||
await Promise.all(
|
||||
Array.from({ length: workerCount }, () => runWorker())
|
||||
);
|
||||
|
||||
const partialResults = sortPageResults(
|
||||
results.filter((page): page is InternalPageResult => Boolean(page))
|
||||
);
|
||||
if (failure) {
|
||||
const failureInfo = failure as Omit<PageProcessingFailure, "partial_results">;
|
||||
throw {
|
||||
page_number: failureInfo.page_number,
|
||||
stage: failureInfo.stage,
|
||||
error: failureInfo.error,
|
||||
partial_results: partialResults
|
||||
} as PageProcessingFailure;
|
||||
}
|
||||
|
||||
return partialResults;
|
||||
}
|
||||
|
||||
async function parsePdfWithVisionInternal(
|
||||
params: ParsePdfWithVisionParams
|
||||
): Promise<ToolResponse> {
|
||||
let binding: FunctionModelConfigResultData;
|
||||
try {
|
||||
binding = await Tools.SoftwareSettings.getFunctionModelConfig(TOOL_FUNCTION_TYPE);
|
||||
assertVisionBinding(binding);
|
||||
} catch (error) {
|
||||
const message = String(error instanceof Error ? error.message : error);
|
||||
return buildPrecheckFailure(message);
|
||||
}
|
||||
|
||||
const visionModel = buildVisionModelInfo(binding);
|
||||
const rawPdfPath = toTrimmedString(params?.pdf_path);
|
||||
|
||||
let pdfFile: any;
|
||||
let startPage: number | undefined;
|
||||
let endPageInput: number | undefined;
|
||||
const resolvedPrompt =
|
||||
toTrimmedString(params?.page_prompt) || DEFAULT_PAGE_PROMPT;
|
||||
|
||||
try {
|
||||
pdfFile = validatePdfFile(rawPdfPath);
|
||||
startPage = parsePositiveInteger(params?.start_page, "start_page", {
|
||||
allowUndefined: true
|
||||
}) ?? 1;
|
||||
endPageInput = parsePositiveInteger(params?.end_page, "end_page", {
|
||||
allowUndefined: true
|
||||
});
|
||||
} catch (error) {
|
||||
const message = String(error instanceof Error ? error.message : error);
|
||||
return {
|
||||
success: false,
|
||||
message,
|
||||
data: {
|
||||
pdf_path: rawPdfPath,
|
||||
image_dir: "",
|
||||
output_path: "",
|
||||
total_page_count: 0,
|
||||
processed_page_count: 0,
|
||||
pages: [],
|
||||
vision_model: visionModel,
|
||||
failed_page: {
|
||||
page_number: 0,
|
||||
stage: "precheck",
|
||||
error: message
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const pages: InternalPageResult[] = [];
|
||||
const context = resolveAppContext();
|
||||
const enhancedAiService = EnhancedAIService.getInstance(context) as JavaBridgeInstance;
|
||||
let outputDir: any = null;
|
||||
let imageDir = "";
|
||||
let outputPath = "";
|
||||
|
||||
let fileDescriptor: any = null;
|
||||
let pdfRenderer: any = null;
|
||||
let totalPageCount = 0;
|
||||
|
||||
try {
|
||||
fileDescriptor = ParcelFileDescriptor.open(
|
||||
pdfFile,
|
||||
ParcelFileDescriptor.MODE_READ_ONLY
|
||||
);
|
||||
pdfRenderer = new PdfRenderer(fileDescriptor);
|
||||
totalPageCount = Number(pdfRenderer.getPageCount());
|
||||
|
||||
if (!Number.isInteger(totalPageCount) || totalPageCount <= 0) {
|
||||
const error = "PDF 没有可处理的页面。";
|
||||
return {
|
||||
success: false,
|
||||
message: error,
|
||||
data: buildData(rawPdfPath, imageDir, outputPath, 0, pages, visionModel, {
|
||||
page_number: 0,
|
||||
stage: "render",
|
||||
error
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const resolvedEndPage = Math.min(endPageInput ?? totalPageCount, totalPageCount);
|
||||
if ((startPage ?? 1) > resolvedEndPage) {
|
||||
const error =
|
||||
(startPage ?? 1) > totalPageCount
|
||||
? `start_page 超出了总页数,文档总页数为 ${totalPageCount}。`
|
||||
: "start_page 不能大于 end_page。";
|
||||
return {
|
||||
success: false,
|
||||
message: error,
|
||||
data: buildData(rawPdfPath, imageDir, outputPath, totalPageCount, pages, visionModel, {
|
||||
page_number: 0,
|
||||
stage: "precheck",
|
||||
error
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
outputDir = createOutputDirectory();
|
||||
imageDir = String(outputDir.getAbsolutePath());
|
||||
const pageNumbers: number[] = [];
|
||||
for (let pageNumber = startPage ?? 1; pageNumber <= resolvedEndPage; pageNumber += 1) {
|
||||
pageNumbers.push(pageNumber);
|
||||
}
|
||||
|
||||
try {
|
||||
const processedPages = await processPagesWithConcurrency(
|
||||
pageNumbers,
|
||||
PAGE_CONCURRENCY,
|
||||
currentPageNumber =>
|
||||
processSinglePage(
|
||||
pdfFile,
|
||||
outputDir,
|
||||
currentPageNumber,
|
||||
resolvedPrompt,
|
||||
enhancedAiService
|
||||
)
|
||||
);
|
||||
pages.push(...processedPages);
|
||||
} catch (error) {
|
||||
const failedPage = error as PageProcessingFailure;
|
||||
pages.push(...sortPageResults(failedPage.partial_results ?? []));
|
||||
outputPath = writeMergedOutputFile(outputDir, pages);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: `第 ${failedPage.page_number} 页处理失败: ${failedPage.error}`,
|
||||
data: buildData(rawPdfPath, imageDir, outputPath, totalPageCount, pages, visionModel, {
|
||||
page_number: failedPage.page_number,
|
||||
stage: failedPage.stage,
|
||||
error: failedPage.error
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
outputPath = writeMergedOutputFile(outputDir, pages);
|
||||
return {
|
||||
success: true,
|
||||
message: `成功解析 ${pages.length} 页 PDF 并完成识图,结果已写入 ${outputPath}。`,
|
||||
data: buildData(rawPdfPath, imageDir, outputPath, totalPageCount, pages, visionModel)
|
||||
};
|
||||
} catch (error) {
|
||||
const message = String(error instanceof Error ? error.message : error);
|
||||
outputPath = writeMergedOutputFile(outputDir, pages);
|
||||
return {
|
||||
success: false,
|
||||
message,
|
||||
data: buildData(rawPdfPath, imageDir, outputPath, totalPageCount, pages, visionModel, {
|
||||
page_number: 0,
|
||||
stage: "render",
|
||||
error: message
|
||||
})
|
||||
};
|
||||
} finally {
|
||||
safeClose(pdfRenderer, "pdfRenderer");
|
||||
safeClose(fileDescriptor, "parcelFileDescriptor");
|
||||
}
|
||||
}
|
||||
|
||||
async function wrapToolExecution(
|
||||
func: (params: ParsePdfWithVisionParams) => Promise<ToolResponse>,
|
||||
params: ParsePdfWithVisionParams
|
||||
): Promise<void> {
|
||||
try {
|
||||
const result = await func(params || ({} as ParsePdfWithVisionParams));
|
||||
complete(result);
|
||||
} catch (error) {
|
||||
const message = String(error instanceof Error ? error.message : error);
|
||||
complete({
|
||||
success: false,
|
||||
message,
|
||||
data: {
|
||||
pdf_path: toTrimmedString(params?.pdf_path),
|
||||
image_dir: "",
|
||||
output_path: "",
|
||||
total_page_count: 0,
|
||||
processed_page_count: 0,
|
||||
pages: [],
|
||||
vision_model: {
|
||||
function_type: TOOL_FUNCTION_TYPE,
|
||||
config_id: "",
|
||||
config_name: "",
|
||||
selected_model: ""
|
||||
},
|
||||
failed_page: {
|
||||
page_number: 0,
|
||||
stage: "precheck",
|
||||
error: message
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function parse_pdf_with_vision(
|
||||
params: ParsePdfWithVisionParams
|
||||
): Promise<void> {
|
||||
await wrapToolExecution(parsePdfWithVisionInternal, params);
|
||||
}
|
||||
|
||||
async function main(params: ParsePdfWithVisionParams): Promise<void> {
|
||||
await wrapToolExecution(parsePdfWithVisionInternal, params);
|
||||
}
|
||||
|
||||
return {
|
||||
parse_pdf_with_vision,
|
||||
main
|
||||
};
|
||||
})();
|
||||
|
||||
exports.parse_pdf_with_vision = PdfVisionParser.parse_pdf_with_vision;
|
||||
exports.main = PdfVisionParser.main;
|
||||
Reference in new issue
Block a user