feat: support image uploads in direct review flow
- accept png/jpg/jpeg in the uploaded-contract review path - add local image OCR extraction and quality gating - keep existing PDF/DOCX/TXT review behavior intact Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,10 @@ try:
|
||||
from pytesseract import Output as TesseractOutput
|
||||
except ImportError: # pragma: no cover - optional local dependency path
|
||||
pytesseract = None
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError: # pragma: no cover - optional local dependency path
|
||||
Image = None
|
||||
from fastapi import HTTPException, status
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
@@ -312,11 +316,16 @@ PDF_SOURCE_KIND_UNKNOWN = "unknown_pdf"
|
||||
SUPPORTED_TEXT_SUFFIXES = (".txt", ".md", ".csv", ".json")
|
||||
DOCX_SUFFIX = ".docx"
|
||||
PDF_SUFFIX = ".pdf"
|
||||
SUPPORTED_REVIEW_UPLOAD_SUFFIXES = (PDF_SUFFIX, DOCX_SUFFIX, ".txt")
|
||||
IMAGE_PNG_SUFFIX = ".png"
|
||||
IMAGE_JPG_SUFFIXES = (".jpg", ".jpeg")
|
||||
SUPPORTED_REVIEW_IMAGE_SUFFIXES = (IMAGE_PNG_SUFFIX, *IMAGE_JPG_SUFFIXES)
|
||||
SUPPORTED_REVIEW_UPLOAD_SUFFIXES = (PDF_SUFFIX, DOCX_SUFFIX, ".txt", *SUPPORTED_REVIEW_IMAGE_SUFFIXES)
|
||||
SUPPORTED_REVIEW_UPLOAD_MIME_TYPES = (
|
||||
"application/pdf",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"text/plain",
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
)
|
||||
PDF_FONT_NAME = "STSong-Light"
|
||||
pdfmetrics.registerFont(UnicodeCIDFont(PDF_FONT_NAME))
|
||||
@@ -584,6 +593,68 @@ class ReviewService:
|
||||
},
|
||||
}
|
||||
|
||||
def _extract_text_from_image_with_ocr(self, file_bytes: bytes) -> Dict[str, Any]:
|
||||
image_source = {
|
||||
"kind": "image_upload",
|
||||
"page_count": 1,
|
||||
"text_pages": 0,
|
||||
"image_pages": 1,
|
||||
}
|
||||
if Image is None or pytesseract is None:
|
||||
return {
|
||||
"text": None,
|
||||
"failure_message": "检测到图片合同,但 OCR 运行时不可用,请改用更清晰的图片、可复制文本 PDF / DOCX / TXT,或直接粘贴合同正文。",
|
||||
"metadata": {
|
||||
"document_kind": image_source["kind"],
|
||||
"extraction_mode": "ocr_runtime_missing",
|
||||
"ocr_gate_status": OCR_GATE_BLOCK,
|
||||
"ocr_quality": {"status": OCR_GATE_BLOCK, "char_count": 0, "failure_message": "ocr runtime missing"},
|
||||
},
|
||||
}
|
||||
try:
|
||||
image = Image.open(io.BytesIO(file_bytes))
|
||||
image = image.convert("RGB")
|
||||
data = pytesseract.image_to_data(image, lang="chi_sim+eng", output_type=TesseractOutput.DICT)
|
||||
words = [str(item).strip() for item in data.get("text", []) if str(item).strip()]
|
||||
confidences: List[float] = []
|
||||
for conf in data.get("conf", []):
|
||||
try:
|
||||
conf_value = float(conf)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if conf_value >= 0:
|
||||
confidences.append(conf_value)
|
||||
text = self._normalize_extracted_text(" ".join(words))
|
||||
quality = self._evaluate_ocr_quality(text, confidences, image_source)
|
||||
if quality["status"] == OCR_GATE_BLOCK:
|
||||
failure_message = "检测到图片合同,但 OCR 识别质量不足,请上传更清晰的图片、可复制文本 PDF / DOCX / TXT,或直接粘贴合同正文。"
|
||||
elif quality["status"] == OCR_GATE_WARN:
|
||||
failure_message = "已通过 OCR 提取图片文本,但识别质量一般,建议人工复核原图后再使用审查结果。"
|
||||
else:
|
||||
failure_message = None
|
||||
quality["failure_message"] = failure_message
|
||||
return {
|
||||
"text": text if quality["status"] != OCR_GATE_BLOCK else None,
|
||||
"failure_message": failure_message,
|
||||
"metadata": {
|
||||
"document_kind": image_source["kind"],
|
||||
"extraction_mode": "ocr",
|
||||
"ocr_gate_status": quality["status"],
|
||||
"ocr_quality": quality,
|
||||
},
|
||||
}
|
||||
except Exception:
|
||||
return {
|
||||
"text": None,
|
||||
"failure_message": "检测到图片合同,但 OCR 解析失败,请改用更清晰的图片、可复制文本 PDF / DOCX / TXT,或直接粘贴合同正文。",
|
||||
"metadata": {
|
||||
"document_kind": image_source["kind"],
|
||||
"extraction_mode": "ocr_parse_failed",
|
||||
"ocr_gate_status": OCR_GATE_BLOCK,
|
||||
"ocr_quality": {"status": OCR_GATE_BLOCK, "char_count": 0, "failure_message": "ocr parse failed"},
|
||||
},
|
||||
}
|
||||
|
||||
def _looks_like_platform_review_report_file_name(self, normalized_name: str) -> bool:
|
||||
return "review-report" in (normalized_name or "") or "审查报告" in (normalized_name or "")
|
||||
|
||||
@@ -795,8 +866,8 @@ class ReviewService:
|
||||
return self._extract_text_from_file(file_bytes, file_name)
|
||||
|
||||
def extract_review_upload_text(self, file_bytes: bytes, file_name: Optional[str]) -> Dict[str, Optional[str]]:
|
||||
text = self.extract_text_from_file(file_bytes=file_bytes, file_name=file_name)
|
||||
normalized_name = (file_name or "").lower()
|
||||
text = None if normalized_name.endswith(SUPPORTED_REVIEW_IMAGE_SUFFIXES) else self.extract_text_from_file(file_bytes=file_bytes, file_name=file_name)
|
||||
extraction_metadata: Dict[str, Any] = {
|
||||
"document_kind": None,
|
||||
"extraction_mode": "text",
|
||||
@@ -837,9 +908,20 @@ class ReviewService:
|
||||
},
|
||||
}
|
||||
|
||||
if normalized_name.endswith(SUPPORTED_REVIEW_IMAGE_SUFFIXES):
|
||||
ocr_result = self._extract_text_from_image_with_ocr(file_bytes)
|
||||
return {
|
||||
"text": ocr_result["text"],
|
||||
"failure_message": ocr_result["failure_message"],
|
||||
"metadata": {
|
||||
**extraction_metadata,
|
||||
**ocr_result["metadata"],
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
"text": text,
|
||||
"failure_message": "无法从上传文件中提取可审查文本,请改为可复制文本 PDF / DOCX / TXT,或直接粘贴合同正文。",
|
||||
"failure_message": "无法从上传文件中提取可审查文本,请改为可复制文本 PDF / DOCX / TXT / PNG / JPG / JPEG,或直接粘贴合同正文。",
|
||||
"metadata": extraction_metadata,
|
||||
}
|
||||
|
||||
@@ -849,20 +931,23 @@ class ReviewService:
|
||||
|
||||
suffix = Path(file_name).suffix.lower()
|
||||
if suffix not in SUPPORTED_REVIEW_UPLOAD_SUFFIXES:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="仅支持 PDF / DOCX / TXT 文件")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="仅支持 PDF / DOCX / TXT / PNG / JPG / JPEG 文件")
|
||||
|
||||
normalized_mime = (mime_type or "").strip().lower()
|
||||
if normalized_mime and not any(normalized_mime.startswith(allowed) for allowed in SUPPORTED_REVIEW_UPLOAD_MIME_TYPES):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="仅支持 PDF / DOCX / TXT 文件")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="仅支持 PDF / DOCX / TXT / PNG / JPG / JPEG 文件")
|
||||
|
||||
detected_type = self._detect_review_upload_type(file_bytes)
|
||||
expected_type = {
|
||||
PDF_SUFFIX: "pdf",
|
||||
DOCX_SUFFIX: "docx",
|
||||
".txt": "txt",
|
||||
IMAGE_PNG_SUFFIX: "png",
|
||||
".jpg": "jpg",
|
||||
".jpeg": "jpg",
|
||||
}[suffix]
|
||||
if detected_type != expected_type:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="仅支持 PDF / DOCX / TXT 文件")
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="仅支持 PDF / DOCX / TXT / PNG / JPG / JPEG 文件")
|
||||
|
||||
def _detect_review_upload_type(self, file_bytes: bytes) -> str:
|
||||
if file_bytes.startswith(b"%PDF"):
|
||||
@@ -877,6 +962,8 @@ class ReviewService:
|
||||
return "unknown"
|
||||
if file_bytes.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||
return "png"
|
||||
if file_bytes.startswith(b"\xff\xd8\xff"):
|
||||
return "jpg"
|
||||
if file_bytes.startswith(b"MZ"):
|
||||
return "exe"
|
||||
if not file_bytes:
|
||||
|
||||
@@ -780,6 +780,43 @@ class ReviewServiceTestCase(unittest.TestCase):
|
||||
self.assertEqual(result["metadata"]["ocr_gate_status"], "BLOCK")
|
||||
self.assertIn("OCR 识别质量不足", result["failure_message"])
|
||||
|
||||
def test_extract_review_upload_text_uses_ocr_for_image_upload(self):
|
||||
self.service._extract_text_from_image_with_ocr = lambda file_bytes: {
|
||||
"text": "图片合同正文\n付款安排",
|
||||
"failure_message": "已通过 OCR 提取图片文本,但识别质量一般,建议人工复核原图后再使用审查结果。",
|
||||
"metadata": {
|
||||
"document_kind": "image_upload",
|
||||
"extraction_mode": "ocr",
|
||||
"ocr_gate_status": "WARN",
|
||||
"ocr_quality": {"status": "WARN", "char_count": 12},
|
||||
},
|
||||
}
|
||||
|
||||
result = self.service.extract_review_upload_text(b"\x89PNG\r\n\x1a\nmock-image", "contract.png")
|
||||
|
||||
self.assertEqual(result["text"], "图片合同正文\n付款安排")
|
||||
self.assertEqual(result["metadata"]["document_kind"], "image_upload")
|
||||
self.assertEqual(result["metadata"]["ocr_gate_status"], "WARN")
|
||||
|
||||
def test_validate_review_upload_accepts_supported_image_suffix(self):
|
||||
self.service.validate_review_upload("contract.png", "image/png", b"\x89PNG\r\n\x1a\nmock-image")
|
||||
self.service.validate_review_upload("contract.jpg", "image/jpeg", b"\xff\xd8\xffmock-jpeg")
|
||||
self.service.validate_review_upload("contract.jpeg", "image/jpeg", b"\xff\xd8\xffmock-jpeg")
|
||||
|
||||
def test_validate_review_upload_rejects_png_renamed_as_txt(self):
|
||||
with self.assertRaises(HTTPException) as context:
|
||||
self.service.validate_review_upload("contract.txt", "text/plain", b"\x89PNG\r\n\x1a\n")
|
||||
|
||||
self.assertEqual(context.exception.status_code, 400)
|
||||
self.assertEqual(context.exception.detail, "仅支持 PDF / DOCX / TXT / PNG / JPG / JPEG 文件")
|
||||
|
||||
def test_validate_review_upload_rejects_jpg_renamed_as_txt(self):
|
||||
with self.assertRaises(HTTPException) as context:
|
||||
self.service.validate_review_upload("contract.txt", "text/plain", b"\xff\xd8\xfffake-jpeg")
|
||||
|
||||
self.assertEqual(context.exception.status_code, 400)
|
||||
self.assertEqual(context.exception.detail, "仅支持 PDF / DOCX / TXT / PNG / JPG / JPEG 文件")
|
||||
|
||||
def test_build_clauses_from_text_groups_multiline_sections_and_skips_metadata(self):
|
||||
clauses = self.service._build_clauses_from_text(
|
||||
"\n".join(
|
||||
@@ -940,17 +977,10 @@ class ReviewServiceTestCase(unittest.TestCase):
|
||||
|
||||
def test_validate_review_upload_rejects_unsupported_suffix(self):
|
||||
with self.assertRaises(HTTPException) as context:
|
||||
self.service.validate_review_upload("contract.png", "image/png", b"\x89PNG\r\n\x1a\n")
|
||||
self.service.validate_review_upload("contract.exe", "application/octet-stream", b"MZ")
|
||||
|
||||
self.assertEqual(context.exception.status_code, 400)
|
||||
self.assertEqual(context.exception.detail, "仅支持 PDF / DOCX / TXT 文件")
|
||||
|
||||
def test_validate_review_upload_rejects_png_renamed_as_txt(self):
|
||||
with self.assertRaises(HTTPException) as context:
|
||||
self.service.validate_review_upload("contract.txt", "text/plain", b"\x89PNG\r\n\x1a\n")
|
||||
|
||||
self.assertEqual(context.exception.status_code, 400)
|
||||
self.assertEqual(context.exception.detail, "仅支持 PDF / DOCX / TXT 文件")
|
||||
self.assertEqual(context.exception.detail, "仅支持 PDF / DOCX / TXT / PNG / JPG / JPEG 文件")
|
||||
|
||||
def test_validate_review_upload_rejects_exe_renamed_as_docx(self):
|
||||
with self.assertRaises(HTTPException) as context:
|
||||
@@ -961,7 +991,7 @@ class ReviewServiceTestCase(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(context.exception.status_code, 400)
|
||||
self.assertEqual(context.exception.detail, "仅支持 PDF / DOCX / TXT 文件")
|
||||
self.assertEqual(context.exception.detail, "仅支持 PDF / DOCX / TXT / PNG / JPG / JPEG 文件")
|
||||
|
||||
def test_evaluate_ocr_quality_returns_pass_for_clean_text(self):
|
||||
quality = self.service._evaluate_ocr_quality(
|
||||
|
||||
@@ -38,6 +38,17 @@ class StubReviewService:
|
||||
"ocr_quality": {"status": "WARN", "char_count": 12},
|
||||
},
|
||||
}
|
||||
if file_name.endswith(".png") or file_name.endswith(".jpg") or file_name.endswith(".jpeg"):
|
||||
return {
|
||||
"text": "图片合同正文\n付款安排",
|
||||
"failure_message": "已通过 OCR 提取图片文本,但识别质量一般,建议人工复核原图后再使用审查结果。",
|
||||
"metadata": {
|
||||
"document_kind": "image_upload",
|
||||
"extraction_mode": "ocr",
|
||||
"ocr_gate_status": "WARN",
|
||||
"ocr_quality": {"status": "WARN", "char_count": 12},
|
||||
},
|
||||
}
|
||||
text = self.extract_text_from_file(file_bytes, file_name)
|
||||
return {
|
||||
"text": text,
|
||||
@@ -50,8 +61,8 @@ class StubReviewService:
|
||||
}
|
||||
|
||||
def validate_review_upload(self, file_name, mime_type, file_bytes):
|
||||
if file_name.endswith(".png") or file_name.endswith(".exe") or file_bytes.startswith(b"\x89PNG") or file_bytes.startswith(b"MZ"):
|
||||
raise HTTPException(status_code=400, detail="仅支持 PDF / DOCX / TXT 文件")
|
||||
if file_name.endswith(".exe") or file_bytes.startswith(b"MZ"):
|
||||
raise HTTPException(status_code=400, detail="仅支持 PDF / DOCX / TXT / PNG / JPG / JPEG 文件")
|
||||
|
||||
def create_review_task(self, tenant_id, user_id, review_as, contract_type, text, file_name, extracted_text=None, extraction_metadata=None):
|
||||
return {
|
||||
@@ -283,15 +294,30 @@ class ReviewsApiTestCase(unittest.TestCase):
|
||||
body = response.json()
|
||||
self.assertEqual(body["code"], 40001)
|
||||
|
||||
def test_create_review_accepts_image_file_for_ocr_review(self):
|
||||
response = self.client.post(
|
||||
"/api/v1/reviews",
|
||||
data={"review_as": "party_a"},
|
||||
files={"file": ("contract.png", b"\x89PNG\r\n\x1a\nmock-image", "image/png")},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = response.json()
|
||||
self.assertEqual(body["code"], 0)
|
||||
self.assertEqual(body["data"]["file_name"], "contract.png")
|
||||
self.assertEqual(body["data"]["merged_text"], "图片合同正文\n付款安排")
|
||||
self.assertEqual(body["data"]["extraction_metadata"]["document_kind"], "image_upload")
|
||||
self.assertEqual(body["data"]["extraction_metadata"]["ocr_gate_status"], "WARN")
|
||||
|
||||
def test_create_review_rejects_unsupported_file_type(self):
|
||||
response = self.client.post(
|
||||
"/api/v1/reviews",
|
||||
data={"review_as": "party_a"},
|
||||
files={"file": ("contract.png", b"png", "image/png")},
|
||||
files={"file": ("contract.exe", b"MZfake-executable", "application/octet-stream")},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.json()["message"], "仅支持 PDF / DOCX / TXT 文件")
|
||||
self.assertEqual(response.json()["message"], "仅支持 PDF / DOCX / TXT / PNG / JPG / JPEG 文件")
|
||||
|
||||
def test_create_review_rejects_unextractable_supported_file_without_text_fallback(self):
|
||||
class UnextractableReviewService(StubReviewService):
|
||||
|
||||
@@ -12,7 +12,7 @@ import { createReview } from "@/services/review";
|
||||
const { Dragger } = Upload;
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const SUPPORTED_REVIEW_FILE_EXTENSIONS = [".pdf", ".docx", ".txt"];
|
||||
const SUPPORTED_REVIEW_FILE_EXTENSIONS = [".pdf", ".docx", ".txt", ".png", ".jpg", ".jpeg"];
|
||||
|
||||
function isSupportedReviewFile(fileName: string) {
|
||||
const normalized = fileName.toLowerCase();
|
||||
@@ -25,7 +25,7 @@ type ReviewFormValues = {
|
||||
text?: string;
|
||||
};
|
||||
|
||||
const reviewHighlights = ["直接审核客户现有合同", "支持 PDF / DOCX / TXT 上传", "扫描 PDF 可先走 OCR", "可叠加补充文本说明", "提交后自动进入结果详情页"];
|
||||
const reviewHighlights = ["直接审核客户现有合同", "支持 PDF / DOCX / TXT / PNG / JPG / JPEG 上传", "扫描 PDF 和图片可先走 OCR", "可叠加补充文本说明", "提交后自动进入结果详情页"];
|
||||
|
||||
type SubmissionGuidance = {
|
||||
type: "info" | "warning" | "error";
|
||||
@@ -57,35 +57,35 @@ function buildSubmissionGuidance(message: string): SubmissionGuidance | null {
|
||||
return {
|
||||
type: "warning",
|
||||
title: "当前上传的是平台审查报告,不是合同原文",
|
||||
description: "请改为上传合同原文 PDF / DOCX / TXT,或直接粘贴合同正文后再审查。",
|
||||
description: "请改为上传合同原文 PDF / DOCX / TXT / PNG / JPG / JPEG,或直接粘贴合同正文后再审查。",
|
||||
};
|
||||
}
|
||||
if (message.includes("OCR 运行时缺失") || message.includes("OCR 未启用") || message.includes("OCR 能力未启用")) {
|
||||
return {
|
||||
type: "warning",
|
||||
title: "当前环境未启用 OCR",
|
||||
description: "扫描件暂时无法直接进入完整审查,请改传可复制文本 PDF / DOCX / TXT,或先补充合同正文文本后再提交。",
|
||||
description: "扫描件或图片暂时无法直接进入完整审查,请改传更清晰的图片、可复制文本 PDF / DOCX / TXT,或先补充合同正文文本后再提交。",
|
||||
};
|
||||
}
|
||||
if (message.includes("OCR 识别质量不足")) {
|
||||
return {
|
||||
type: "warning",
|
||||
title: "扫描件识别质量不足",
|
||||
description: "建议上传更清晰的扫描件、可复制文本 PDF / DOCX / TXT,或直接粘贴合同正文。清晰扫描件已支持 OCR,但低质量扫描件仍会被门禁拦截。",
|
||||
description: "建议上传更清晰的扫描件或图片、可复制文本 PDF / DOCX / TXT,或直接粘贴合同正文。清晰扫描件和图片已支持 OCR,但低质量输入仍会被门禁拦截。",
|
||||
};
|
||||
}
|
||||
if (message.includes("扫描件/图片型 PDF") || message.includes("OCR")) {
|
||||
return {
|
||||
type: "info",
|
||||
title: "检测到扫描件或图片型 PDF",
|
||||
description: "系统会先尝试 OCR;若识别质量不足,会建议你改传更清晰扫描件、可复制文本 PDF / DOCX / TXT,或直接粘贴合同正文。",
|
||||
description: "系统会先尝试 OCR;若识别质量不足,会建议你改传更清晰扫描件或图片、可复制文本 PDF / DOCX / TXT,或直接粘贴合同正文。",
|
||||
};
|
||||
}
|
||||
if (message.includes("无法从上传文件中提取可审查文本")) {
|
||||
return {
|
||||
type: "warning",
|
||||
title: "当前文件未形成可审查正文",
|
||||
description: "请确认文件是可复制文本 PDF / DOCX / TXT;如果是扫描件,请提高扫描清晰度或直接粘贴合同正文。",
|
||||
description: "请确认文件是可复制文本 PDF / DOCX / TXT,或更清晰的图片合同;如果是扫描件或图片,请提高识别清晰度或直接粘贴合同正文。",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -147,7 +147,7 @@ export default function ReviewPage() {
|
||||
setSubmissionGuidance(null);
|
||||
setSubmitting(false);
|
||||
submitLockRef.current = false;
|
||||
messageApi.error("仅支持 PDF / DOCX / TXT 文件");
|
||||
messageApi.error("仅支持 PDF / DOCX / TXT / PNG / JPG / JPEG 文件");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function ReviewPage() {
|
||||
<div className="review-page-hero-copy">
|
||||
<span className="landing-badge">Start Review</span>
|
||||
<Title level={2}>上传客户已有合同,直接进入风险审查。</Title>
|
||||
<Text type="secondary">无论合同是不是系统生成的,都可以直接上传原文 PDF / DOCX / TXT 或粘贴正文,再设置审查视角后进入审查链路。</Text>
|
||||
<Text type="secondary">无论合同是不是系统生成的,都可以直接上传原文 PDF / DOCX / TXT / PNG / JPG / JPEG 或粘贴正文,再设置审查视角后进入审查链路。</Text>
|
||||
<div className="review-page-highlight-list">
|
||||
{reviewHighlights.map((item) => (
|
||||
<span className="review-page-highlight-chip" key={item}>
|
||||
@@ -245,10 +245,10 @@ export default function ReviewPage() {
|
||||
/>
|
||||
) : null}
|
||||
<Dragger
|
||||
accept=".pdf,.docx,.txt"
|
||||
accept=".pdf,.docx,.txt,.png,.jpg,.jpeg"
|
||||
beforeUpload={(file) => {
|
||||
if (!isSupportedReviewFile(file.name)) {
|
||||
messageApi.error("仅支持 PDF / DOCX / TXT 文件");
|
||||
messageApi.error("仅支持 PDF / DOCX / TXT / PNG / JPG / JPEG 文件");
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
return false;
|
||||
@@ -264,8 +264,8 @@ export default function ReviewPage() {
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p>点击或拖拽上传合同原文 PDF / DOCX / TXT</p>
|
||||
<p className="upload-hint">支持客户已有合同原文直接审查;扫描型 PDF 会优先尝试 OCR 抽取后再进入风险识别。</p>
|
||||
<p>点击或拖拽上传合同原文 PDF / DOCX / TXT / PNG / JPG / JPEG</p>
|
||||
<p className="upload-hint">支持客户已有合同原文直接审查;扫描型 PDF 和图片合同会优先尝试 OCR 抽取后再进入风险识别。</p>
|
||||
</Dragger>
|
||||
{selectedFileName ? (
|
||||
<div className="review-selected-file review-selected-file-rich">
|
||||
|
||||
Reference in New Issue
Block a user