feat(ocr): 集成 PaddleOCR 服务并优化 OCR 系统

- 新增 PaddleOCR 本地高精度 OCR 服务支持,包括 Dockerfile、API 服务和 provider 实现
- 在 docker-compose 中集成 RapidOCR 和 PaddleOCR 服务,并配置健康检查
- 优化后端 API 路由前缀,移除 `/api` 以简化代理配置
- 更新 Nginx 配置以正确传递请求头和代理 WebSocket 连接
- 在前端设置页面添加 PaddleOCR 和 RapidOCR 的测试与配置选项
- 修复后端 Dockerfile 以支持 Python 原生模块构建
- 更新 OCR 设置指南,反映当前服务状态和部署方式
- 添加上传文件调试日志和权限设置
This commit is contained in:
congsh
2026-02-27 18:43:07 +08:00
parent 764c6a8c0c
commit 9a301cc434
17 changed files with 628 additions and 85 deletions

View File

@@ -16,21 +16,34 @@ server {
add_header X-XSS-Protection "1; mode=block" always;
# API proxy to backend
location /api {
proxy_pass http://backend:13057;
location /api/ {
proxy_pass http://backend:13057/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
# Pass ALL request headers to backend
proxy_pass_request_headers on;
# Set standard proxy headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Explicitly pass authentication headers
proxy_set_header Authorization $http_authorization;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Bypass cache
proxy_cache_bypass $http_upgrade;
proxy_no_cache $http_upgrade;
}
# Upload files proxy to backend
location /uploads {
proxy_pass http://backend:13057/uploads;
location /uploads/ {
proxy_pass http://backend:13057/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;

View File

@@ -22,7 +22,7 @@
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@playwright/test": "^1.58.2",
"@playwright/test": "^1.40.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
@@ -1328,19 +1328,19 @@
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.0.tgz",
"integrity": "sha512-PdW+kn4eV99iP5gxWNSDQCbhMaDVej+RXL5xr6t04nbKLCBwYtA046t7ofoczHOm8u6c+45hpDKQVZqtqwkeQg==",
"deprecated": "Please update to the latest version of Playwright to test up-to-date browsers.",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
"playwright": "1.40.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
"node": ">=16"
}
},
"node_modules/@polka/url": {
@@ -5200,35 +5200,33 @@
}
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.0.tgz",
"integrity": "sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
"playwright-core": "1.40.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.0.tgz",
"integrity": "sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
"node": ">=16"
}
},
"node_modules/playwright/node_modules/fsevents": {
@@ -5237,7 +5235,6 @@
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"

View File

@@ -29,7 +29,7 @@
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@playwright/test": "^1.58.2",
"@playwright/test": "^1.40.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",

View File

@@ -6,7 +6,7 @@ import { Settings, Save, CheckCircle, XCircle, Eye, EyeOff, Server, Globe, Datab
// 从环境变量或 localStorage 获取 API 地址
const getDefaultApiUrl = () => {
return import.meta.env.VITE_API_URL || localStorage.getItem('api_base_url') || '/api';
return import.meta.env.VITE_API_URL || localStorage.getItem('api_base_url') || '/';
};
type ApiConfig = {
@@ -14,13 +14,14 @@ type ApiConfig = {
};
type OCRConfig = {
provider: 'auto' | 'tesseract' | 'baidu' | 'tencent' | 'rapidocr';
provider: 'auto' | 'tesseract' | 'baidu' | 'tencent' | 'rapidocr' | 'paddleocr';
confidenceThreshold: number;
baiduApiKey: string;
baiduSecretKey: string;
tencentSecretId: string;
tencentSecretKey: string;
rapidocrUrl: string;
paddleocrUrl: string;
};
type AIConfig = {
@@ -62,7 +63,8 @@ const defaultOCRConfig: OCRConfig = {
baiduSecretKey: '',
tencentSecretId: '',
tencentSecretKey: '',
rapidocrUrl: 'http://localhost:8080',
rapidocrUrl: 'http://localhost:13058',
paddleocrUrl: 'http://localhost:13059',
};
const defaultAIConfig: AIConfig = {
@@ -504,6 +506,7 @@ export default function SettingsPage() {
<option value="auto"></option>
<option value="tesseract">Tesseract.js ()</option>
<option value="rapidocr">RapidOCR ()</option>
<option value="paddleocr">PaddleOCR ()</option>
<option value="baidu"> OCR ()</option>
<option value="tencent"> OCR ()</option>
</select>
@@ -640,6 +643,45 @@ export default function SettingsPage() {
)}
</div>
</Card>
<Card title="PaddleOCR" variant="bordered">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<Input
value={ocrConfig.paddleocrUrl}
onChange={(e) => updateOcrConfig('paddleocrUrl', e.target.value)}
placeholder="http://localhost:8866"
/>
</div>
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-600">
特点: 免费线 ()
</p>
<p className="text-xs text-gray-500 mt-1">
: <code className="bg-gray-100 px-1 rounded">docker run -p 8866:8866 987846/paddleocr:latest</code>
</p>
</div>
<Button
variant="secondary"
size="sm"
onClick={() => handleTest('paddleocr')}
loading={testing === 'paddleocr'}
>
</Button>
</div>
{testResults.paddleocr && (
<div className={`flex items-center gap-2 text-sm ${testResults.paddleocr.success ? 'text-green-600' : 'text-red-600'}`}>
{testResults.paddleocr.success ? <CheckCircle className="h-4 w-4" /> : <XCircle className="h-4 w-4" />}
{testResults.paddleocr.message}
</div>
)}
</div>
</Card>
</>
)}