移动端拍照上传、奥数区、学段约束解题与 AI 模型配置。
- 手机/平板响应式布局,支持拍照与相册上传 - 学生详情新增奥数区,按初/高中学段生成解法并禁止超纲 - 系统设置可配置 Ollama 或 OpenAI 兼容 API - 更新 frontend/dist 与使用说明
This commit is contained in:
@@ -13,6 +13,7 @@ from app.schemas import (
|
||||
AdminUserCreate,
|
||||
AdminUserOut,
|
||||
AdminUserPasswordUpdate,
|
||||
AIProviderEnum,
|
||||
PublicSettingsOut,
|
||||
SystemSettingsOut,
|
||||
SystemSettingsUpdate,
|
||||
@@ -21,6 +22,19 @@ from app.schemas import (
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
|
||||
|
||||
def settings_to_out(row: SystemSettings) -> SystemSettingsOut:
|
||||
return SystemSettingsOut(
|
||||
registration_enabled=row.registration_enabled,
|
||||
ai_provider=AIProviderEnum(row.ai_provider or "ollama"),
|
||||
ollama_base_url=row.ollama_base_url,
|
||||
ollama_model=row.ollama_model,
|
||||
openai_base_url=row.openai_base_url,
|
||||
openai_model=row.openai_model,
|
||||
openai_api_key_set=bool(row.openai_api_key),
|
||||
updated_at=row.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def get_or_create_settings(db: Session) -> SystemSettings:
|
||||
row = db.get(SystemSettings, 1)
|
||||
if row is None:
|
||||
@@ -36,7 +50,7 @@ def get_settings(
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(get_superuser),
|
||||
):
|
||||
return get_or_create_settings(db)
|
||||
return settings_to_out(get_or_create_settings(db))
|
||||
|
||||
|
||||
@router.patch("/settings", response_model=SystemSettingsOut)
|
||||
@@ -48,10 +62,22 @@ def update_settings(
|
||||
row = get_or_create_settings(db)
|
||||
if data.registration_enabled is not None:
|
||||
row.registration_enabled = data.registration_enabled
|
||||
if data.ai_provider is not None:
|
||||
row.ai_provider = data.ai_provider.value
|
||||
if data.ollama_base_url is not None:
|
||||
row.ollama_base_url = data.ollama_base_url or None
|
||||
if data.ollama_model is not None:
|
||||
row.ollama_model = data.ollama_model or None
|
||||
if data.openai_base_url is not None:
|
||||
row.openai_base_url = data.openai_base_url or None
|
||||
if data.openai_model is not None:
|
||||
row.openai_model = data.openai_model or None
|
||||
if data.openai_api_key is not None and data.openai_api_key.strip():
|
||||
row.openai_api_key = data.openai_api_key.strip()
|
||||
row.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return row
|
||||
return settings_to_out(row)
|
||||
|
||||
|
||||
@router.patch("/profile", response_model=AdminUserOut)
|
||||
|
||||
@@ -8,10 +8,10 @@ from sqlalchemy.orm import Session, joinedload
|
||||
from app.core.config import settings
|
||||
from app.core.database import SessionLocal, get_db
|
||||
from app.core.deps import get_current_user
|
||||
from app.models.user import Subject, User, WrongQuestion, WrongQuestionStatus
|
||||
from app.schemas import WrongQuestionOut, WrongQuestionUpdate
|
||||
from app.models.user import Subject, User, WrongQuestion, WrongQuestionCategory, WrongQuestionStatus
|
||||
from app.schemas import WrongQuestionCategoryEnum, WrongQuestionOut, WrongQuestionUpdate
|
||||
from app.services import llm as llm_service
|
||||
from app.services import ocr as ocr_service
|
||||
from app.services import ollama as ollama_service
|
||||
from app.services.student_access import get_student_for_user
|
||||
|
||||
router = APIRouter(tags=["wrong_questions"])
|
||||
@@ -23,6 +23,7 @@ def _wq_to_out(wq: WrongQuestion) -> WrongQuestionOut:
|
||||
student_id=wq.student_id,
|
||||
subject_id=wq.subject_id,
|
||||
subject_name=wq.subject.name if wq.subject else None,
|
||||
category=wq.category,
|
||||
image_path=wq.image_path,
|
||||
ocr_raw_text=wq.ocr_raw_text,
|
||||
question_text=wq.question_text,
|
||||
@@ -60,16 +61,25 @@ def _process_wrong_question(question_id: uuid.UUID):
|
||||
|
||||
subject_name = wq.subject.name if wq.subject else "综合"
|
||||
school_level = wq.student.school_level if wq.student else None
|
||||
olympiad = wq.category == WrongQuestionCategory.olympiad
|
||||
ai_cfg = llm_service.load_ai_config(db)
|
||||
|
||||
import asyncio
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
question_text = loop.run_until_complete(
|
||||
ollama_service.format_question(subject_name, ocr_text, school_level)
|
||||
llm_service.format_question(ai_cfg, subject_name, ocr_text, school_level)
|
||||
)
|
||||
solution_text = loop.run_until_complete(
|
||||
ollama_service.generate_solution(subject_name, question_text, school_level)
|
||||
llm_service.generate_solution(
|
||||
ai_cfg,
|
||||
subject_name,
|
||||
question_text,
|
||||
school_level,
|
||||
olympiad=olympiad,
|
||||
)
|
||||
)
|
||||
wq.question_text = question_text
|
||||
wq.solution_text = solution_text
|
||||
@@ -88,6 +98,7 @@ def _process_wrong_question(question_id: uuid.UUID):
|
||||
def list_wrong_questions(
|
||||
student_id: uuid.UUID,
|
||||
subject_id: int | None = Query(None),
|
||||
category: WrongQuestionCategoryEnum | None = Query(None),
|
||||
q: str | None = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
@@ -100,6 +111,8 @@ def list_wrong_questions(
|
||||
)
|
||||
if subject_id is not None:
|
||||
query = query.filter(WrongQuestion.subject_id == subject_id)
|
||||
if category is not None:
|
||||
query = query.filter(WrongQuestion.category == category.value)
|
||||
if q:
|
||||
pattern = f"%{q}%"
|
||||
query = query.filter(
|
||||
@@ -121,6 +134,7 @@ async def upload_wrong_question(
|
||||
background_tasks: BackgroundTasks,
|
||||
subject_id: int = Form(...),
|
||||
file: UploadFile = File(...),
|
||||
category: WrongQuestionCategoryEnum = Form(WrongQuestionCategoryEnum.regular),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
@@ -137,6 +151,7 @@ async def upload_wrong_question(
|
||||
student_id=student_id,
|
||||
subject_id=subject_id,
|
||||
image_path="",
|
||||
category=WrongQuestionCategory(category.value),
|
||||
status=WrongQuestionStatus.pending,
|
||||
)
|
||||
db.add(wq)
|
||||
@@ -270,21 +285,27 @@ async def regenerate_solution(
|
||||
|
||||
subject_name = wq.subject.name if wq.subject else "综合"
|
||||
school_level = wq.student.school_level if wq.student else None
|
||||
olympiad = wq.category == WrongQuestionCategory.olympiad
|
||||
question_text = wq.question_text or wq.ocr_raw_text or ""
|
||||
ai_cfg = llm_service.load_ai_config(db)
|
||||
|
||||
try:
|
||||
if not wq.question_text and wq.ocr_raw_text:
|
||||
wq.question_text = await ollama_service.format_question(
|
||||
subject_name, wq.ocr_raw_text, school_level
|
||||
wq.question_text = await llm_service.format_question(
|
||||
ai_cfg, subject_name, wq.ocr_raw_text, school_level
|
||||
)
|
||||
question_text = wq.question_text
|
||||
wq.solution_text = await ollama_service.generate_solution(
|
||||
subject_name, question_text, school_level
|
||||
wq.solution_text = await llm_service.generate_solution(
|
||||
ai_cfg,
|
||||
subject_name,
|
||||
question_text,
|
||||
school_level,
|
||||
olympiad=olympiad,
|
||||
)
|
||||
wq.status = WrongQuestionStatus.solved
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY, detail=f"Ollama 调用失败: {exc}"
|
||||
status_code=status.HTTP_502_BAD_GATEWAY, detail=f"AI 调用失败: {exc}"
|
||||
) from exc
|
||||
|
||||
db.commit()
|
||||
|
||||
Reference in New Issue
Block a user