错题处理失败时直接显示具体错误信息。
This commit is contained in:
@@ -133,6 +133,7 @@ class WrongQuestion(Base):
|
||||
solution_text: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
mark_regions_json: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
annotated_image_path: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
status: Mapped[WrongQuestionStatus] = mapped_column(
|
||||
Enum(WrongQuestionStatus), default=WrongQuestionStatus.pending
|
||||
)
|
||||
|
||||
@@ -19,6 +19,13 @@ from app.services.student_access import get_student_for_user
|
||||
router = APIRouter(tags=["wrong_questions"])
|
||||
|
||||
|
||||
def _short_error(exc: BaseException, prefix: str = "") -> str:
|
||||
msg = str(exc).strip() or type(exc).__name__
|
||||
if len(msg) > 500:
|
||||
msg = msg[:500] + "…"
|
||||
return f"{prefix}{msg}" if prefix else msg
|
||||
|
||||
|
||||
def _parse_mark_regions(raw: str | None) -> list[dict] | None:
|
||||
if not raw:
|
||||
return None
|
||||
@@ -43,6 +50,7 @@ def _wq_to_out(wq: WrongQuestion) -> WrongQuestionOut:
|
||||
solution_text=wq.solution_text,
|
||||
mark_regions=_parse_mark_regions(wq.mark_regions_json),
|
||||
has_annotated_image=bool(wq.annotated_image_path),
|
||||
error_message=wq.error_message,
|
||||
status=wq.status,
|
||||
created_at=wq.created_at,
|
||||
)
|
||||
@@ -74,6 +82,7 @@ async def _run_ai_pipeline(wq: WrongQuestion, db: Session, ocr_lines: list[dict]
|
||||
wq.solution_approach = approach
|
||||
wq.solution_text = solution_body if approach else solution_full
|
||||
wq.status = WrongQuestionStatus.solved
|
||||
wq.error_message = None
|
||||
|
||||
|
||||
def _process_wrong_question(question_id: uuid.UUID):
|
||||
@@ -88,20 +97,24 @@ def _process_wrong_question(question_id: uuid.UUID):
|
||||
if wq is None:
|
||||
return
|
||||
|
||||
wq.error_message = None
|
||||
image_full = Path(settings.UPLOAD_DIR) / wq.image_path
|
||||
try:
|
||||
ocr_result = ocr_service.run_ocr_with_regions(str(image_full))
|
||||
ocr_text = ocr_result["text"]
|
||||
ocr_lines = ocr_result["lines"]
|
||||
wq.ocr_raw_text = ocr_text or None
|
||||
wq.status = WrongQuestionStatus.ocr_done if ocr_text else WrongQuestionStatus.failed
|
||||
db.commit()
|
||||
except Exception:
|
||||
if not ocr_text:
|
||||
wq.status = WrongQuestionStatus.failed
|
||||
wq.error_message = "OCR 未识别到文字,请拍摄更清晰、光线充足的题目照片"
|
||||
db.commit()
|
||||
return
|
||||
|
||||
if not ocr_text:
|
||||
wq.status = WrongQuestionStatus.ocr_done
|
||||
db.commit()
|
||||
except Exception as exc:
|
||||
wq.status = WrongQuestionStatus.failed
|
||||
wq.error_message = _short_error(exc, "OCR 识别失败:")
|
||||
db.commit()
|
||||
return
|
||||
|
||||
import asyncio
|
||||
@@ -111,8 +124,9 @@ def _process_wrong_question(question_id: uuid.UUID):
|
||||
try:
|
||||
loop.run_until_complete(_run_ai_pipeline(wq, db, ocr_lines, ocr_text))
|
||||
db.commit()
|
||||
except Exception:
|
||||
wq.status = WrongQuestionStatus.ocr_done
|
||||
except Exception as exc:
|
||||
wq.status = WrongQuestionStatus.failed
|
||||
wq.error_message = _short_error(exc, "AI 处理失败:")
|
||||
db.commit()
|
||||
finally:
|
||||
loop.close()
|
||||
@@ -293,6 +307,7 @@ def retry_ocr(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="错题不存在")
|
||||
|
||||
wq.status = WrongQuestionStatus.pending
|
||||
wq.error_message = None
|
||||
db.commit()
|
||||
background_tasks.add_task(_process_wrong_question, wq.id)
|
||||
return _wq_to_out(wq)
|
||||
|
||||
@@ -237,6 +237,7 @@ class WrongQuestionOut(BaseModel):
|
||||
solution_text: str | None
|
||||
mark_regions: list[dict] | None = None
|
||||
has_annotated_image: bool = False
|
||||
error_message: str | None = None
|
||||
status: WrongQuestionStatusEnum
|
||||
created_at: datetime
|
||||
|
||||
|
||||
@@ -75,6 +75,8 @@ def run_migrations() -> None:
|
||||
wq_alters.append("ADD COLUMN mark_regions_json TEXT")
|
||||
if "annotated_image_path" not in wq_columns:
|
||||
wq_alters.append("ADD COLUMN annotated_image_path VARCHAR(512)")
|
||||
if "error_message" not in wq_columns:
|
||||
wq_alters.append("ADD COLUMN error_message TEXT")
|
||||
if wq_alters:
|
||||
with engine.begin() as conn:
|
||||
for clause in wq_alters:
|
||||
|
||||
+64
-64
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -9,7 +9,7 @@
|
||||
<meta name="author" content="马建军" />
|
||||
<meta name="copyright" content="Copyright (c) 马建军. All rights reserved." />
|
||||
<title>中学成绩档案</title>
|
||||
<script type="module" crossorigin src="/assets/index-CKqFHGFD.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-C3DkjkK0.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-GY2etMYN.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons'
|
||||
import { Button, Popconfirm, Tag, Typography, message } from 'antd'
|
||||
import { Button, Popconfirm, Space, Tag, Typography, message } from 'antd'
|
||||
import { wrongQuestionApi } from '../api/client'
|
||||
import type { WrongQuestion } from '../types'
|
||||
import { STATUS_LABELS } from '../types'
|
||||
@@ -14,6 +14,19 @@ interface Props {
|
||||
emptyText?: string
|
||||
}
|
||||
|
||||
function cardSummary(wq: WrongQuestion): { tone: 'error' | 'pending' | 'normal'; text: string } {
|
||||
if (wq.error_message) {
|
||||
return { tone: 'error', text: wq.error_message }
|
||||
}
|
||||
if (wq.status === 'pending') {
|
||||
return { tone: 'pending', text: '正在识别、标注并生成解题思路…' }
|
||||
}
|
||||
return {
|
||||
tone: 'normal',
|
||||
text: wq.question_text || wq.ocr_raw_text || STATUS_LABELS[wq.status],
|
||||
}
|
||||
}
|
||||
|
||||
export default function WrongQuestionList({
|
||||
items,
|
||||
selectedId,
|
||||
@@ -35,22 +48,29 @@ export default function WrongQuestionList({
|
||||
return (
|
||||
<>
|
||||
<div className="wq-grid">
|
||||
{items.map((wq) => (
|
||||
{items.map((wq) => {
|
||||
const summary = cardSummary(wq)
|
||||
return (
|
||||
<div key={wq.id} className="wq-card">
|
||||
<div className="wq-card-click" onClick={() => onSelect(wq.id)}>
|
||||
<AuthenticatedImage questionId={wq.id} variant="annotated" alt="题目" className="wq-card-img" />
|
||||
<div className="wq-card-body">
|
||||
<Space size={4} wrap>
|
||||
<Typography.Text strong>{wq.subject_name}</Typography.Text>
|
||||
{wq.category === 'olympiad' && (
|
||||
<Tag color="gold" style={{ marginLeft: 4 }}>
|
||||
奥数
|
||||
{wq.category === 'olympiad' && <Tag color="gold">奥数</Tag>}
|
||||
<Tag color={wq.error_message || wq.status === 'failed' ? 'error' : wq.status === 'pending' ? 'processing' : 'default'}>
|
||||
{wq.error_message ? '失败' : STATUS_LABELS[wq.status]}
|
||||
</Tag>
|
||||
)}
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12, marginLeft: 8 }}>
|
||||
{STATUS_LABELS[wq.status]}
|
||||
</Typography.Text>
|
||||
<Typography.Paragraph ellipsis={{ rows: 2 }} style={{ margin: '8px 0 0', fontSize: 13 }}>
|
||||
{wq.question_text || wq.ocr_raw_text || '处理中…'}
|
||||
</Space>
|
||||
<Typography.Paragraph
|
||||
ellipsis={{ rows: 3 }}
|
||||
style={{
|
||||
margin: '8px 0 0',
|
||||
fontSize: 13,
|
||||
color: summary.tone === 'error' ? '#ff4d4f' : summary.tone === 'pending' ? '#1677ff' : undefined,
|
||||
}}
|
||||
>
|
||||
{summary.text}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,7 +88,8 @@ export default function WrongQuestionList({
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{items.length === 0 && <Typography.Text type="secondary">{emptyText}</Typography.Text>}
|
||||
{selectedId && (
|
||||
|
||||
@@ -135,10 +135,22 @@ export default function WrongQuestionDetail({
|
||||
<>
|
||||
<Space wrap style={{ marginBottom: 8 }}>
|
||||
<Typography.Text type="secondary">状态:{STATUS_LABELS[wq.status]}</Typography.Text>
|
||||
{wq.has_annotated_image && (
|
||||
{wq.has_annotated_image && !wq.error_message && (
|
||||
<Typography.Text type="danger">红色框为自动标注的错误位置</Typography.Text>
|
||||
)}
|
||||
</Space>
|
||||
{wq.error_message && (
|
||||
<Alert
|
||||
message="处理失败"
|
||||
description={wq.error_message}
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
)}
|
||||
{wq.status === 'pending' && !wq.error_message && (
|
||||
<Alert message="正在识别、标注并生成解题思路,请稍候…" type="info" showIcon style={{ marginBottom: 12 }} />
|
||||
)}
|
||||
{(wq.solution_approach || wq.solution_text) && (
|
||||
<Alert
|
||||
message="AI 识别与标注,请核对后再使用"
|
||||
|
||||
@@ -114,6 +114,7 @@ export interface WrongQuestion {
|
||||
solution_text: string | null
|
||||
mark_regions: MarkRegion[] | null
|
||||
has_annotated_image: boolean
|
||||
error_message: string | null
|
||||
status: WrongQuestionStatus
|
||||
created_at: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user