fix: correct MinerU deployment docs and add PDF provider test button

- Remove bundled MinerU Docker service and Dockerfile; link to official API and self-host docs instead
- Set MinerU requiresApiKey to false (self-hosted doesn't need it)
- Add test connection button to PDF settings panel with backend proxy to avoid CORS
- Add verify-pdf-provider API route for server-side connection testing
- Set Vercel API body size limit to 50MB for document uploads

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yangshen
2026-03-13 19:06:45 +08:00
co-authored by Claude Opus 4.6
parent 71559690e4
commit 697bc547e0
7 changed files with 171 additions and 49 deletions
+2 -9
View File
@@ -122,18 +122,11 @@ cp .env.example .env.local
docker compose up --build
```
如需使用 MinerU 文档解析服务,取消 `docker-compose.yml` 中 `mineru` 服务的注释即可。
### 可选:MinerU(增强文档解析)
如需更好地解析复杂表格和公式:
[MinerU](https://github.com/opendatalab/MinerU) 提供更强的表格、公式和 OCR 解析能力。你可以使用 [MinerU 官方 API](https://mineru.net/) 或[自行部署](https://opendatalab.github.io/MinerU/quick_start/docker_deployment/)。
```bash
docker pull opendatalab/mineru:latest
docker run -d --name mineru -p 8080:8080 opendatalab/mineru:latest
```
在 `.env.local` 中设置 `PDF_MINERU_BASE_URL=http://localhost:8080`。
在 `.env.local` 中设置 `PDF_MINERU_BASE_URL`(如需认证则同时设置 `PDF_MINERU_API_KEY`)。
---
+2 -9
View File
@@ -122,18 +122,11 @@ cp .env.example .env.local
docker compose up --build
```
To use the optional MinerU document parser, uncomment the `mineru` service in `docker-compose.yml`.
### Optional: MinerU (Advanced Document Parsing)
For enhanced parsing of complex tables and formulas:
[MinerU](https://github.com/opendatalab/MinerU) provides enhanced parsing for complex tables, formulas, and OCR. You can use the [MinerU official API](https://mineru.net/) or [self-host your own instance](https://opendatalab.github.io/MinerU/quick_start/docker_deployment/).
```bash
docker pull opendatalab/mineru:latest
docker run -d --name mineru -p 8080:8080 opendatalab/mineru:latest
```
Set `PDF_MINERU_BASE_URL=http://localhost:8080` in `.env.local`.
Set `PDF_MINERU_BASE_URL` (and `PDF_MINERU_API_KEY` if needed) in `.env.local`.
---
+57
View File
@@ -0,0 +1,57 @@
import { NextRequest } from 'next/server';
import { createLogger } from '@/lib/logger';
import { apiError, apiSuccess } from '@/lib/server/api-response';
import { resolvePDFApiKey, resolvePDFBaseUrl } from '@/lib/server/provider-config';
const log = createLogger('Verify PDF Provider');
export async function POST(req: NextRequest) {
try {
const { providerId, apiKey, baseUrl } = await req.json();
if (!providerId) {
return apiError('MISSING_REQUIRED_FIELD', 400, 'Provider ID is required');
}
const resolvedBaseUrl = resolvePDFBaseUrl(providerId, baseUrl);
if (!resolvedBaseUrl) {
return apiError('MISSING_REQUIRED_FIELD', 400, 'Base URL is required');
}
const resolvedApiKey = resolvePDFApiKey(providerId, apiKey);
const headers: Record<string, string> = {};
if (resolvedApiKey) {
headers['Authorization'] = `Bearer ${resolvedApiKey}`;
}
const response = await fetch(resolvedBaseUrl, {
headers,
signal: AbortSignal.timeout(10000),
});
// MinerU's FastAPI root returns 404 (no root route), but the server is reachable.
// Any HTTP response (including 404) means the server is up.
return apiSuccess({
message: 'Connection successful',
status: response.status,
});
} catch (error) {
log.error('PDF provider test error:', error);
let errorMessage = 'Connection failed';
if (error instanceof Error) {
if (error.message.includes('ECONNREFUSED')) {
errorMessage = 'Cannot connect to server, please check the Base URL';
} else if (error.message.includes('ENOTFOUND')) {
errorMessage = 'Server not found, please check the Base URL';
} else if (error.message.includes('timeout') || error.name === 'TimeoutError') {
errorMessage = 'Connection timed out';
} else {
errorMessage = error.message;
}
}
return apiError('INTERNAL_ERROR', 500, errorMessage);
}
}
+105 -20
View File
@@ -1,14 +1,16 @@
"use client";
import { useState } from "react";
import { useState, useCallback } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { useI18n } from "@/lib/hooks/use-i18n";
import { useSettingsStore } from "@/lib/store/settings";
import { PDF_PROVIDERS } from "@/lib/pdf/constants";
import type { PDFProviderId } from "@/lib/pdf/types";
import { CheckCircle2, Eye, EyeOff } from "lucide-react";
import { CheckCircle2, Eye, EyeOff, Loader2, Zap, XCircle } from "lucide-react";
import { cn } from "@/lib/utils";
/**
* Get display label for feature
@@ -32,20 +34,61 @@ interface PDFSettingsProps {
export function PDFSettings({ selectedProviderId }: PDFSettingsProps) {
const { t } = useI18n();
const [showApiKey, setShowApiKey] = useState(false);
const [testStatus, setTestStatus] = useState<"idle" | "testing" | "success" | "error">("idle");
const [testMessage, setTestMessage] = useState("");
const pdfProvidersConfig = useSettingsStore((state) => state.pdfProvidersConfig);
const setPDFProviderConfig = useSettingsStore((state) => state.setPDFProviderConfig);
const pdfProvider = PDF_PROVIDERS[selectedProviderId];
const isServerConfigured = !!pdfProvidersConfig[selectedProviderId]?.isServerConfigured;
const providerConfig = pdfProvidersConfig[selectedProviderId];
const hasBaseUrl = !!(providerConfig?.baseUrl);
const needsRemoteConfig = selectedProviderId === "mineru";
// Reset showApiKey when provider changes (derived state pattern)
// Reset state when provider changes
const [prevSelectedProviderId, setPrevSelectedProviderId] = useState(selectedProviderId);
if (selectedProviderId !== prevSelectedProviderId) {
setPrevSelectedProviderId(selectedProviderId);
setShowApiKey(false);
setTestStatus("idle");
setTestMessage("");
}
const handleTestConnection = useCallback(async () => {
const baseUrl = providerConfig?.baseUrl;
if (!baseUrl) return;
setTestStatus("testing");
setTestMessage("");
try {
const response = await fetch("/api/verify-pdf-provider", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
providerId: selectedProviderId,
apiKey: providerConfig?.apiKey || "",
baseUrl,
}),
});
const data = await response.json();
if (data.success) {
setTestStatus("success");
setTestMessage(t("settings.connectionSuccess"));
} else {
setTestStatus("error");
setTestMessage(`${t("settings.connectionFailed")}: ${data.error}`);
}
} catch (err) {
setTestStatus("error");
const message = err instanceof Error ? err.message : String(err);
setTestMessage(`${t("settings.connectionFailed")}: ${message}`);
}
}, [providerConfig?.baseUrl, providerConfig?.apiKey, selectedProviderId, t]);
return (
<div className="space-y-6 max-w-3xl">
{/* Server-configured notice */}
@@ -55,17 +98,52 @@ export function PDFSettings({ selectedProviderId }: PDFSettingsProps) {
</div>
)}
{/* API Key + Base URL Configuration */}
{(pdfProvider.requiresApiKey || isServerConfigured) && (
{/* Base URL + API Key Configuration (for remote providers like MinerU) */}
{(needsRemoteConfig || isServerConfigured) && (
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-sm">{t("settings.pdfApiKey")}</Label>
<Label className="text-sm">{t("settings.pdfBaseUrl")}</Label>
<div className="flex gap-2">
<Input
placeholder="http://localhost:8080"
value={providerConfig?.baseUrl || ""}
onChange={(e) =>
setPDFProviderConfig(selectedProviderId, { baseUrl: e.target.value })
}
className="text-sm"
/>
<Button
variant="outline"
size="sm"
onClick={handleTestConnection}
disabled={testStatus === "testing" || !hasBaseUrl}
className="gap-1.5 shrink-0"
>
{testStatus === "testing" ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<>
<Zap className="h-3.5 w-3.5" />
{t("settings.testConnection")}
</>
)}
</Button>
</div>
</div>
<div className="space-y-2">
<Label className="text-sm">
{t("settings.pdfApiKey")}
<span className="text-muted-foreground ml-1 font-normal">
({t("settings.optional")})
</span>
</Label>
<div className="relative">
<Input
type={showApiKey ? "text" : "password"}
placeholder={isServerConfigured ? t("settings.optionalOverride") : t("settings.enterApiKey")}
value={pdfProvidersConfig[selectedProviderId]?.apiKey || ""}
value={providerConfig?.apiKey || ""}
onChange={(e) =>
setPDFProviderConfig(selectedProviderId, { apiKey: e.target.value })
}
@@ -84,23 +162,30 @@ export function PDFSettings({ selectedProviderId }: PDFSettingsProps) {
</button>
</div>
</div>
<div className="space-y-2">
<Label className="text-sm">{t("settings.pdfBaseUrl")}</Label>
<Input
placeholder="http://localhost:8080"
value={pdfProvidersConfig[selectedProviderId]?.baseUrl || ""}
onChange={(e) =>
setPDFProviderConfig(selectedProviderId, { baseUrl: e.target.value })
}
className="text-sm"
/>
</div>
</div>
{/* Test result message */}
{testMessage && (
<div
className={cn(
"rounded-lg p-3 text-sm",
testStatus === "success" &&
"bg-green-50 text-green-700 border border-green-200 dark:bg-green-950/30 dark:text-green-300 dark:border-green-800",
testStatus === "error" &&
"bg-red-50 text-red-700 border border-red-200 dark:bg-red-950/30 dark:text-red-300 dark:border-red-800"
)}
>
<div className="flex items-center gap-2">
{testStatus === "success" && <CheckCircle2 className="h-4 w-4 shrink-0" />}
{testStatus === "error" && <XCircle className="h-4 w-4 shrink-0" />}
<span className="break-all">{testMessage}</span>
</div>
</div>
)}
{/* Request URL Preview */}
{(() => {
const effectiveBaseUrl = pdfProvidersConfig[selectedProviderId]?.baseUrl || "";
const effectiveBaseUrl = providerConfig?.baseUrl || "";
if (!effectiveBaseUrl) return null;
const fullUrl = effectiveBaseUrl + "/file_parse";
return (
-9
View File
@@ -11,14 +11,5 @@ services:
- openmaic-data:/app/data
restart: unless-stopped
# Optional: MinerU for advanced document parsing (tables, formulas, OCR)
# Uncomment to enable:
#
# mineru:
# image: opendatalab/mineru:latest
# ports:
# - "8080:8080"
# restart: unless-stopped
volumes:
openmaic-data:
+1 -1
View File
@@ -20,7 +20,7 @@ export const PDF_PROVIDERS: Record<PDFProviderId, PDFProviderConfig> = {
mineru: {
id: "mineru",
name: "MinerU",
requiresApiKey: true,
requiresApiKey: false,
icon: "/logos/mineru.png",
features: ["text", "images", "tables", "formulas", "layout-analysis"],
},
+4 -1
View File
@@ -5,7 +5,10 @@
"buildCommand": "pnpm build",
"functions": {
"app/api/**/*.ts": {
"maxDuration": 300
"maxDuration": 300,
"bodyParser": {
"sizeLimit": "50mb"
}
}
}
}