Browse Source

修改考试工坊

FanHong 3 ngày trước cách đây
mục cha
commit
133588b1d2

+ 126 - 27
shudao-chat-py/routers/chat.py

@@ -1,5 +1,6 @@
 from fastapi import APIRouter, Depends, Request
 from fastapi.responses import StreamingResponse, JSONResponse
+from sqlalchemy import or_
 from sqlalchemy.orm import Session
 from pydantic import BaseModel
 from typing import Optional
@@ -36,6 +37,28 @@ def _build_conversation_title(conversation: AIConversation) -> str:
     return _build_conversation_preview(conversation.content or "", limit=30)
 
 
+def _is_exam_workshop_conversation(conversation: Optional[AIConversation]) -> bool:
+    if not conversation:
+        return False
+    return conversation.business_type == 3 or bool((conversation.exam_name or "").strip())
+
+
+def _resolve_conversation_metadata(
+    conversation: Optional[AIConversation],
+    requested_business_type: int,
+    requested_exam_name: str,
+) -> tuple[int, str]:
+    requested_exam_name = (requested_exam_name or "").strip()
+
+    if _is_exam_workshop_conversation(conversation):
+        return 3, requested_exam_name or (conversation.exam_name or "").strip()
+
+    if requested_business_type == 3:
+        return 3, requested_exam_name
+
+    return requested_business_type, ""
+
+
 def _refresh_conversation_snapshot(db: Session, conversation_id: int, user_id: int) -> None:
     latest_message = (
         db.query(AIMessage)
@@ -72,7 +95,8 @@ def _refresh_conversation_snapshot(db: Session, conversation_id: int, user_id: i
         if latest_user_message and latest_user_message.content
         else latest_message.content
     )
-    preview_content = _build_conversation_preview(preview_source or "", limit=100)
+    preview_content = _build_conversation_preview(
+        preview_source or "", limit=100)
 
     db.query(AIConversation).filter(
         AIConversation.id == conversation_id,
@@ -176,13 +200,31 @@ async def send_deepseek_message(
 
         conversation_id = data.conversation_id or data.ai_conversation_id
 
+        existing_conversation = None
+        if conversation_id:
+            existing_conversation = (
+                db.query(AIConversation)
+                .filter(
+                    AIConversation.id == conversation_id,
+                    AIConversation.user_id == user.user_id,
+                    AIConversation.is_deleted == 0,
+                )
+                .first()
+            )
+
+        effective_business_type, effective_exam_name = _resolve_conversation_metadata(
+            existing_conversation,
+            data.business_type,
+            data.exam_name,
+        )
+
         # 创建或获取对话
         if not conversation_id:
             conversation = AIConversation(
                 user_id=user.user_id,
                 content=message[:100],
-                business_type=data.business_type,
-                exam_name=data.exam_name if data.business_type == 3 else "",
+                business_type=effective_business_type,
+                exam_name=effective_exam_name,
                 created_at=int(time.time()),
                 updated_at=int(time.time()),
                 is_deleted=0,
@@ -199,8 +241,8 @@ async def send_deepseek_message(
                 AIConversation.is_deleted == 0,
             ).update({
                 "content": message[:100],
-                "business_type": data.business_type,
-                "exam_name": data.exam_name if data.business_type == 3 else "",
+                "business_type": effective_business_type,
+                "exam_name": effective_exam_name,
                 "updated_at": int(time.time()),
             })
             db.commit()
@@ -214,7 +256,8 @@ async def send_deepseek_message(
                 intent_type = ""
                 if isinstance(intent_result, dict):
                     intent_type = (
-                        intent_result.get("intent_type") or intent_result.get("intent") or ""
+                        intent_result.get("intent_type") or intent_result.get(
+                            "intent") or ""
                     ).lower()
 
                 rag_context = ""
@@ -237,7 +280,8 @@ async def send_deepseek_message(
                 try:
                     if isinstance(qwen_response, str) and qwen_response.strip().startswith("{"):
                         response_json = json.loads(qwen_response)
-                        response_text = response_json.get("natural_language_answer", qwen_response)
+                        response_text = response_json.get(
+                            "natural_language_answer", qwen_response)
                     else:
                         response_text = qwen_response
                 except Exception:
@@ -294,6 +338,8 @@ async def send_deepseek_message(
                 system_content = (
                     "你是一个专业的考试题目生成助手,专注于路桥隧轨施工安全领域。\n"
                     "请根据用户需求生成专业的考试题目,包括单选题、多选题、判断题、简答题等。\n"
+                    "用户消息中已经包含考试标题、题型要求和出题依据内容,必须以其中的出题依据内容为核心生成题目,不能脱离依据内容自由发挥。\n"
+                    "题干、选项、答案和解析都要与出题依据内容中的知识点、专业术语、操作流程、规范要求或培训主题直接相关。\n"
                     "输出必须是可直接 JSON.parse 的纯 JSON,不要包含 markdown 代码块、解释文字或额外前后缀。\n"
                     "JSON 顶层结构必须包含 singleChoice、judge、multiple、short 四个字段。\n"
                     "singleChoice.questions 和 multiple.questions 中每道题必须包含 text、options、answer、analysis。\n"
@@ -310,9 +356,41 @@ async def send_deepseek_message(
 
                 response_text = await qwen_service.chat(messages)
 
-                if data.exam_name:
+                now_ts = int(time.time())
+                user_message = AIMessage(
+                    ai_conversation_id=conv_id,
+                    user_id=user.user_id,
+                    type="user",
+                    content=message,
+                    created_at=now_ts,
+                    updated_at=now_ts,
+                    is_deleted=0,
+                )
+                db.add(user_message)
+                db.commit()
+                db.refresh(user_message)
+
+                ai_message = AIMessage(
+                    ai_conversation_id=conv_id,
+                    user_id=user.user_id,
+                    type="ai",
+                    content=response_text,
+                    prev_user_id=user_message.id,
+                    created_at=now_ts,
+                    updated_at=now_ts,
+                    is_deleted=0,
+                )
+                db.add(ai_message)
+                db.commit()
+
+                _refresh_conversation_snapshot(db, conv_id, user.user_id)
+                db.commit()
+
+                if effective_exam_name:
                     db.query(AIConversation).filter(AIConversation.id == conv_id).update(
-                        {"exam_name": data.exam_name, "updated_at": int(time.time())}
+                        {"business_type": 3,
+                            "exam_name": effective_exam_name,
+                            "updated_at": int(time.time())}
                     )
                     db.commit()
             except Exception as e:
@@ -333,7 +411,7 @@ async def send_deepseek_message(
                 "content": response_text,
                 "message": response_text,
                 "user_id": user.user_id,
-                "business_type": data.business_type,
+                "business_type": effective_business_type,
             },
         }
     except Exception as e:
@@ -393,9 +471,18 @@ async def get_history_record(
     )
 
     if business_type is not None:
-        conversations_query = conversations_query.filter(
-            AIConversation.business_type == business_type
-        )
+        if business_type == 3:
+            conversations_query = conversations_query.filter(
+                or_(
+                    AIConversation.business_type == 3,
+                    AIConversation.exam_name.isnot(None),
+                    AIConversation.exam_name != "",
+                )
+            )
+        else:
+            conversations_query = conversations_query.filter(
+                AIConversation.business_type == business_type
+            )
 
     total = conversations_query.count()
     conversations = (
@@ -469,7 +556,8 @@ async def delete_conversation(
                 AIMessage.ai_conversation_id == ai_message.ai_conversation_id,
             ).update({"is_deleted": 1, "updated_at": now_ts})
 
-        _refresh_conversation_snapshot(db, ai_message.ai_conversation_id, user.user_id)
+        _refresh_conversation_snapshot(
+            db, ai_message.ai_conversation_id, user.user_id)
         db.commit()
         return {"statusCode": 200, "msg": "删除成功"}
 
@@ -532,7 +620,8 @@ async def stream_chat(request: Request, data: StreamChatRequest):
             intent_result = await qwen_service.intent_recognition(message)
             if isinstance(intent_result, dict):
                 intent_type = (
-                    intent_result.get("intent_type") or intent_result.get("intent") or ""
+                    intent_result.get("intent_type") or intent_result.get(
+                        "intent") or ""
                 ).lower()
         except Exception as ie:
             logger.warning(f"[stream/chat] 意图识别异常: {ie}")
@@ -644,7 +733,8 @@ async def stream_chat_with_db(request: Request, data: StreamChatWithDBRequest):
                 else:
                     conversation = AIConversation(
                         user_id=user.user_id,
-                        content=_build_conversation_preview(message, limit=100),
+                        content=_build_conversation_preview(
+                            message, limit=100),
                         business_type=data.business_type,
                         exam_name=data.exam_name if data.business_type == 3 else "",
                         created_at=int(time.time()),
@@ -717,9 +807,10 @@ async def stream_chat_with_db(request: Request, data: StreamChatWithDBRequest):
                 context_parts.append(f"知识库内容:\n{rag_context}")
             if data.online_search_content:
                 context_parts.append(f"联网搜索结果:\n{data.online_search_content}")
-            
-            context_json = "\n\n".join(context_parts) if context_parts else "暂无相关知识库内容"
-            
+
+            context_json = "\n\n".join(
+                context_parts) if context_parts else "暂无相关知识库内容"
+
             # 使用prompt加载器加载最终回答prompt
             system_content = load_prompt(
                 "final_answer",
@@ -817,7 +908,8 @@ async def guess_you_want(
         try:
             # 尝试从响应中提取 JSON
             import re
-            json_match = re.search(r'\{[^{}]*"questions"[^{}]*\}', response, re.DOTALL)
+            json_match = re.search(
+                r'\{[^{}]*"questions"[^{}]*\}', response, re.DOTALL)
             if json_match:
                 response_json = json.loads(json_match.group())
             else:
@@ -894,10 +986,12 @@ async def online_search(question: str, request: Request, db: Session = Depends(g
         async with httpx.AsyncClient(timeout=30.0) as client:
             resp = await client.post(dify_config.workflow_url, headers=headers, json=payload)
             if resp.status_code != 200:
-                logger.error(f"[online_search] Dify 调用失败: {resp.status_code}, 响应: {resp.text}")
+                logger.error(
+                    f"[online_search] Dify 调用失败: {resp.status_code}, 响应: {resp.text}")
                 return {"statusCode": 500, "msg": f"搜索服务异常: {resp.status_code}"}
             result = resp.json()
-            search_text = result.get("data", {}).get("outputs", {}).get("text", "")
+            search_text = result.get("data", {}).get(
+                "outputs", {}).get("text", "")
 
         return {
             "statusCode": 200,
@@ -928,7 +1022,8 @@ async def save_online_search_result(
 
     try:
         db.query(AIMessage).filter(AIMessage.id == data.ai_message_id).update(
-            {"search_source": data.search_result, "updated_at": int(time.time())}
+            {"search_source": data.search_result,
+                "updated_at": int(time.time())}
         )
         db.commit()
         return {"statusCode": 200, "msg": "保存成功"}
@@ -964,7 +1059,8 @@ async def intent_recognition(
         response_text = ""
         if isinstance(intent_result, dict):
             intent_type = (
-                intent_result.get("intent_type") or intent_result.get("intent") or ""
+                intent_result.get("intent_type") or intent_result.get(
+                    "intent") or ""
             ).lower()
             response_text = intent_result.get("response", "")
 
@@ -1050,10 +1146,13 @@ async def get_user_recommend_question(
 ):
     """获取推荐问题(支持模糊查询)"""
     try:
-        query = db.query(RecommendQuestion).filter(RecommendQuestion.is_deleted == 0)
+        query = db.query(RecommendQuestion).filter(
+            RecommendQuestion.is_deleted == 0)
         if keyword:
-            query = query.filter(RecommendQuestion.question.like(f"%{keyword}%"))
-        questions = query.order_by(RecommendQuestion.id.desc()).limit(limit).all()
+            query = query.filter(
+                RecommendQuestion.question.like(f"%{keyword}%"))
+        questions = query.order_by(
+            RecommendQuestion.id.desc()).limit(limit).all()
 
         return {
             "statusCode": 200,

+ 7 - 3
shudao-chat-py/routers/exam.py

@@ -58,7 +58,8 @@ async def build_exam_prompt(
             continue
         question_schema_lines.append(f"- {qtype}: {count}道,每道{score}分")
 
-    question_schema = "\n".join(question_schema_lines) if question_schema_lines else "- 未提供有效题型"
+    question_schema = "\n".join(
+        question_schema_lines) if question_schema_lines else "- 未提供有效题型"
 
     prompt = (
         "请根据以下要求直接生成一份完整试卷,并严格返回纯 JSON,不要输出 markdown 代码块、解释说明或额外文字。\n"
@@ -69,8 +70,11 @@ async def build_exam_prompt(
         f"总分:{data.totalScore or 0}\n"
         f"总题量:{total_count}\n"
         f"题型要求:{question_text}\n"
-        f"课件内容:{data.pptContent or '无'}\n"
-        "请结合课件内容、工程类型和题型要求,生成有具体内容、具体选项、具体答案、具体解析的试卷。\n"
+        f"出题依据内容:{data.pptContent or '无'}\n"
+        "出题依据内容是本次试卷的核心来源,所有题目必须围绕该内容中的知识点、术语、流程、规范要求和场景展开。\n"
+        "如果出题依据内容中出现了章节、条款、培训主题或专业术语,题目必须优先考查这些内容,不能偏离到无关知识。\n"
+        "单选题、多选题、判断题和简答题的题干、选项、答案解析都要与出题依据内容直接相关,不能泛泛而谈。\n"
+        "请结合出题依据内容、工程类型和题型要求,生成有具体内容、具体选项、具体答案、具体解析的试卷。\n"
         "禁止输出“选项A”“题目1”这类占位内容,所有题目必须是可直接展示和作答的真实内容。\n"
         "JSON 输出结构必须符合以下格式:\n"
         "{\n"

+ 137 - 19
shudao-vue-frontend/src/views/ExamWorkshop.vue

@@ -123,7 +123,25 @@
                             <div class="slider-container">
                                 <span class="slider-label">数量</span>
                                 <input type="range" class="question-slider" v-model.number="type.questionCount" min="0" :max="type.max || 50" :disabled="isGenerating">
-                                <span class="question-count" style="text-align: right; min-width: 40px;">{{ type.questionCount }} 题</span>
+                                <div class="question-count-stepper">
+                                    <span class="question-count">{{ type.questionCount }} 题</span>
+                                    <div class="stepper-buttons">
+                                        <button
+                                            class="stepper-btn stepper-btn-up"
+                                            type="button"
+                                            @click="adjustQuestionCount(type, 1)"
+                                            :disabled="isGenerating || type.questionCount >= 99"
+                                            aria-label="增加题目数量"
+                                        ></button>
+                                        <button
+                                            class="stepper-btn stepper-btn-down"
+                                            type="button"
+                                            @click="adjustQuestionCount(type, -1)"
+                                            :disabled="isGenerating || type.questionCount <= 0"
+                                            aria-label="减少题目数量"
+                                        ></button>
+                                    </div>
+                                </div>
                             </div>
                         </div>
                     </div>
@@ -641,6 +659,12 @@ const selectedFile = ref(null);
 const isUploadingFile = ref(false);
 const fileContent = ref(''); // 存储文件内容
 const pptContentDescription = ref(''); // 存储用户输入的PPT内容描述
+const questionBasis = computed({
+  get: () => pptContentDescription.value,
+  set: (value) => {
+    pptContentDescription.value = value;
+  }
+});
 
 // 文件处理配置
 const fileConfig = reactive({
@@ -670,6 +694,21 @@ const historyData = ref([])
 const historyTotal = ref(0) // 历史记录总数
 
 
+const isExamWorkshopConversation = (conversation = {}) => {
+  const content = String(conversation.content || '')
+  const title = String(conversation.title || '')
+  const examName = String(conversation.exam_name || '')
+
+  return (
+    Number(conversation.business_type) === 3 ||
+    !!examName.trim() ||
+    title.includes('技术考核') ||
+    content.includes('请根据以下要求直接生成一份完整试卷') ||
+    content.includes('"singleChoice"') ||
+    content.includes('"totalQuestions"')
+  )
+}
+
 // 获取历史记录列表
 const getHistoryRecordList = async () => {
   try {
@@ -688,13 +727,34 @@ const getHistoryRecordList = async () => {
     console.log('📋 考试工坊历史记录列表响应:', response)
     
     if (response.statusCode === 200) {
+      const directConversations = Array.isArray(response.data) ? response.data : []
+      let conversations = [...directConversations]
+
+      const fallbackResponse = await apis.getHistoryRecord({
+        ai_conversation_id: 0
+      })
+
+      if (fallbackResponse.statusCode === 200 && Array.isArray(fallbackResponse.data)) {
+        const inferredExamConversations = fallbackResponse.data.filter(isExamWorkshopConversation)
+        const conversationMap = new Map()
+
+        directConversations.concat(inferredExamConversations).forEach((conversation) => {
+          if (!conversation?.id) return
+          conversationMap.set(conversation.id, conversation)
+        })
+
+        conversations = Array.from(conversationMap.values()).sort((a, b) => {
+          return Number(b.updated_at || 0) - Number(a.updated_at || 0)
+        })
+      }
+
       // 设置历史记录总数
-      historyTotal.value = response.total || 0
+      historyTotal.value = conversations.length
       
       // 转换后端数据为前端格式
-      historyData.value = response.data.map(conversation => ({
+      historyData.value = conversations.map(conversation => ({
         id: conversation.id,
-        title: generateConversationTitle(conversation.exam_name),
+        title: generateConversationTitle(conversation.exam_name || conversation.title || conversation.content),
         time: formatTime(conversation.updated_at),
         businessType: conversation.business_type,
         isActive: false,
@@ -961,8 +1021,9 @@ const handleHistoryItem = async (historyItem) => {
       // 解析试卷数据并恢复
       if (latestRecord && latestRecord.content) {
         try {
-          const examData = JSON.parse(latestRecord.content);
+          const examData = extractExamDataFromContent(latestRecord.content);
           restoreExamFromHistory(examData);
+          showExamDetail.value = true;
         } catch (error) {
           console.error('解析试卷数据失败:', error);
           // 如果解析失败,显示默认详情页
@@ -1056,11 +1117,16 @@ const validateQuestionCount = (type) => {
     type.questionCount = 99;
     ElMessage.warning(`${type.name}题目数量不能超过99题`);
   }
-  if (type.questionCount < 1) {
-    type.questionCount = 1;
+  if (type.questionCount < 0) {
+    type.questionCount = 0;
   }
 };
 
+const adjustQuestionCount = (type, delta) => {
+  type.questionCount = Number(type.questionCount || 0) + delta;
+  validateQuestionCount(type);
+};
+
 const generateExam = async () => {
   if (!examName.value.trim()) {
     ElMessage.warning("请输入试卷名称");
@@ -1275,7 +1341,7 @@ const fetchExamPrompt = async (mode = 'ai') => {
     examTitle: examName.value,
     totalScore: totalScore.value,
     questionTypes: normalizedQuestionTypes,
-    pptContent: selectedFile.value?.content || ''
+    pptContent: selectedFile.value?.content || questionBasis.value || ''
   };
 
   try {
@@ -1291,21 +1357,28 @@ const fetchExamPrompt = async (mode = 'ai') => {
 };
 
 // 解析AI回复
+const extractExamDataFromContent = (content) => {
+  if (!content || typeof content !== 'string') {
+    throw new Error('试卷内容为空');
+  }
+
+  const jsonMatch = content.match(/\{[\s\S]*\}/);
+  if (!jsonMatch) {
+    throw new Error('未找到有效的JSON数据');
+  }
+
+  return JSON.parse(jsonMatch[0]);
+};
+
 const parseAIExamResponse = (aiReply) => {
   try {
-    // 尝试提取JSON内容
-    const jsonMatch = aiReply.match(/\{[\s\S]*\}/);
-    if (jsonMatch) {
-      const examData = JSON.parse(jsonMatch[0]);
-      const normalizedExam = normalizeGeneratedExam(examData);
+    const examData = extractExamDataFromContent(aiReply);
+    const normalizedExam = normalizeGeneratedExam(examData);
 
-      // 确保所有题目都有正确的初始值
-      ensureQuestionInitialValues(normalizedExam);
+    // 确保所有题目都有正确的初始值
+    ensureQuestionInitialValues(normalizedExam);
 
-      return normalizedExam;
-    } else {
-      throw new Error('未找到有效的JSON数据');
-    }
+    return normalizedExam;
   } catch (error) {
     console.error('解析AI回复失败:', error);
     // 返回默认试卷结构
@@ -4013,6 +4086,51 @@ onUnmounted(() => {
         text-align: right;
     }
 
+    .question-count-stepper {
+        display: flex;
+        align-items: center;
+        justify-content: flex-end;
+        gap: 8px;
+        min-width: 76px;
+    }
+
+    .stepper-buttons {
+        display: flex;
+        flex-direction: column;
+        gap: 3px;
+    }
+
+    .stepper-btn {
+        width: 10px;
+        height: 7px;
+        padding: 0;
+        border: none;
+        outline: none;
+        appearance: none;
+        -webkit-appearance: none;
+        background: #2563eb;
+        display: block;
+        cursor: pointer;
+        transition: opacity 0.2s ease, transform 0.2s ease;
+    }
+
+    .stepper-btn-up {
+        clip-path: polygon(50% 0, 0 100%, 100% 100%);
+    }
+
+    .stepper-btn-down {
+        clip-path: polygon(0 0, 100% 0, 50% 100%);
+    }
+
+    .stepper-btn:hover:not(:disabled) {
+        transform: scale(1.08);
+    }
+
+    .stepper-btn:disabled {
+        opacity: 0.35;
+        cursor: not-allowed;
+    }
+
     .action-buttons {
         display: flex;
         justify-content: space-between;

+ 187 - 46
shudao-vue-frontend/src/views/mobile/m-ExamWorkshop.vue

@@ -135,15 +135,33 @@
                     </div>
                     <div class="config-item">
                       <span>一共</span>
-                      <input
+                      <div class="count-stepper">
+                        <input
                         v-model="type.questionCount"
                         type="number"
                         class="count-input-field"
-                        min="1"
+                        min="0"
                         max="99"
                         @input="validateQuestionCount(type)"
                         :disabled="isGenerating || selectedFile"
-                      />
+                        />
+                        <div class="stepper-buttons">
+                          <button
+                            class="stepper-btn stepper-btn-up"
+                            type="button"
+                            @click="adjustQuestionCount(type, 1)"
+                            :disabled="isGenerating || selectedFile || type.questionCount >= 99"
+                            aria-label="增加题目数量"
+                          ></button>
+                          <button
+                            class="stepper-btn stepper-btn-down"
+                            type="button"
+                            @click="adjustQuestionCount(type, -1)"
+                            :disabled="isGenerating || selectedFile || type.questionCount <= 0"
+                            aria-label="减少题目数量"
+                          ></button>
+                        </div>
+                      </div>
                       <span>题</span>
                     </div>
                   </div>
@@ -710,36 +728,47 @@ const createNewTask = () => {
 
 // 处理历史记录点击
 const handleHistoryItem = async (historyItem) => {
-  if (historyItem.isActive) return
+  if (isGenerating.value || isLoadingHistoryItem.value) return
   
   console.log("点击移动端考试工坊历史记录:", historyItem)
-  
-  // 设置当前点击的历史记录为激活状态
-  historyData.value.forEach((item) => {
-    item.isActive = item.id === historyItem.id
-  })
-  
-  // 关闭历史记录抽屉
-  showHistory.value = false
-  
-  // 解析历史试卷数据
   ai_conversation_id.value = historyItem.id
-  currentTime.value = historyItem.time
-  
-  // 如果有原始数据,尝试解析
-  if (historyItem.rawData && historyItem.rawData.content) {
-    try {
-      const examData = JSON.parse(historyItem.rawData.content)
-      currentExam.value = examData
-      showExamDetail.value = true
-    } catch (error) {
-      console.error('解析历史试卷数据失败:', error)
-      // 如果解析失败,显示默认详情页
-      showExamDetail.value = true
+  isLoadingHistoryItem.value = true
+
+  try {
+    historyData.value.forEach((item) => {
+      item.isActive = item.id === historyItem.id
+    })
+
+    showHistory.value = false
+
+    const response = await apis.getHistoryRecord({
+      ai_conversation_id: historyItem.id,
+      business_type: 3
+    })
+
+    if (response.statusCode === 200 && response.data && response.data.length > 0) {
+      const latestAiRecord = [...response.data].reverse().find(record => record.type === 'ai' && record.content)
+
+      if (latestAiRecord?.content) {
+        try {
+          const examData = extractExamDataFromContent(latestAiRecord.content)
+          restoreExamFromHistory(examData)
+          currentTime.value = historyItem.time
+          showExamDetail.value = true
+          return
+        } catch (error) {
+          console.error('解析移动端历史试卷数据失败:', error)
+        }
+      }
     }
-  } else {
-    // 如果没有内容,显示默认详情页
-    showExamDetail.value = true
+
+    console.error('移动端历史记录缺少可恢复的试卷详情:', response)
+    showToast('该历史记录暂无可恢复的试卷内容')
+  } catch (error) {
+    console.error('获取移动端历史记录详情失败:', error)
+    showToast('获取历史记录详情失败,请重试')
+  } finally {
+    isLoadingHistoryItem.value = false
   }
 }
 
@@ -812,6 +841,21 @@ const formatHistoryTime = (timestamp) => {
 }
 
 // 获取历史记录列表
+const isExamWorkshopConversation = (conversation = {}) => {
+  const content = String(conversation.content || '')
+  const title = String(conversation.title || '')
+  const examName = String(conversation.exam_name || '')
+
+  return (
+    Number(conversation.business_type) === 3 ||
+    !!examName.trim() ||
+    title.includes('技术考核') ||
+    content.includes('请根据以下要求直接生成一份完整试卷') ||
+    content.includes('"singleChoice"') ||
+    content.includes('"totalQuestions"')
+  )
+}
+
 const getHistoryRecordList = async () => {
   try {
     console.log('📋 开始获取移动端考试工坊历史记录列表...')
@@ -829,13 +873,34 @@ const getHistoryRecordList = async () => {
     console.log('📋 移动端历史记录列表响应:', response)
     
     if (response.statusCode === 200) {
+      const directConversations = Array.isArray(response.data) ? response.data : []
+      let conversations = [...directConversations]
+
+      const fallbackResponse = await apis.getHistoryRecord({
+        ai_conversation_id: 0
+      })
+
+      if (fallbackResponse.statusCode === 200 && Array.isArray(fallbackResponse.data)) {
+        const inferredExamConversations = fallbackResponse.data.filter(isExamWorkshopConversation)
+        const conversationMap = new Map()
+
+        directConversations.concat(inferredExamConversations).forEach((conversation) => {
+          if (!conversation?.id) return
+          conversationMap.set(conversation.id, conversation)
+        })
+
+        conversations = Array.from(conversationMap.values()).sort((a, b) => {
+          return Number(b.updated_at || 0) - Number(a.updated_at || 0)
+        })
+      }
+
       // 设置历史记录总数
-      historyTotal.value = response.total || 0
+      historyTotal.value = conversations.length
       
       // 转换后端数据为前端格式
-      historyData.value = response.data.map(conversation => ({
+      historyData.value = conversations.map(conversation => ({
         id: conversation.id,
-        title: generateConversationTitle(conversation.content),
+        title: generateConversationTitle(conversation.exam_name || conversation.title || conversation.content),
         time: formatHistoryTime(conversation.updated_at),
         businessType: conversation.business_type,
         isActive: false,
@@ -915,11 +980,16 @@ const validateQuestionCount = (type) => {
     type.questionCount = 99;
     console.warn(`${type.name}题目数量不能超过99题`);
   }
-  if (type.questionCount < 1) {
-    type.questionCount = 1;
+  if (type.questionCount < 0) {
+    type.questionCount = 0;
   }
 };
 
+const adjustQuestionCount = (type, delta) => {
+  type.questionCount = Number(type.questionCount || 0) + delta;
+  validateQuestionCount(type);
+};
+
 // 清除设置
 const clearSettings = () => {
   // 根据当前选择的工程类型设置试卷名称
@@ -1025,6 +1095,14 @@ const generateExam = async () => {
       // 显示考试详情页
       showExamDetail.value = true;
 
+      // 刷新历史记录,确保新生成的试卷详情可被立即查看
+      await getHistoryRecordList();
+      if (ai_conversation_id.value > 0) {
+        historyData.value.forEach((item) => {
+          item.isActive = item.id === ai_conversation_id.value;
+        });
+      }
+
       console.log('✅ 移动端试卷生成完成!');
 
     } else {
@@ -1070,18 +1148,25 @@ const fetchMobileExamPrompt = async (mode = 'ai') => {
 };
 
 // 解析AI考试回复
+const extractExamDataFromContent = (content) => {
+  if (!content || typeof content !== 'string') {
+    throw new Error('历史内容为空');
+  }
+
+  const directMatch = content.match(/\{[\s\S]*\}/);
+  if (!directMatch) {
+    throw new Error('未找到可解析的试卷JSON');
+  }
+
+  return JSON.parse(directMatch[0]);
+};
+
 const parseAIExamResponse = (aiReply) => {
   try {
-    // 尝试提取JSON内容
-    const jsonMatch = aiReply.match(/\{[\s\S]*\}/);
-    if (jsonMatch) {
-      const examData = JSON.parse(jsonMatch[0]);
-      const normalizedExam = normalizeGeneratedExam(examData);
-      ensureQuestionInitialValues(normalizedExam);
-      return normalizedExam;
-    } else {
-      throw new Error('未找到有效的JSON数据');
-    }
+    const examData = extractExamDataFromContent(aiReply);
+    const normalizedExam = normalizeGeneratedExam(examData);
+    ensureQuestionInitialValues(normalizedExam);
+    return normalizedExam;
   } catch (error) {
     console.error('解析AI回复失败:', error);
     // 返回默认试卷结构
@@ -1270,6 +1355,23 @@ const generateDefaultExam = () => {
   }
 };
 
+const restoreExamFromHistory = (examData) => {
+  const exam = examData?.exam || examData || {}
+  const normalizedExam = normalizeGeneratedExam(exam)
+
+  examName.value = normalizedExam.title || examName.value
+  totalScore.value = normalizedExam.totalScore || totalScore.value
+
+  questionTypes.value = [
+    { name: "单选题", scorePerQuestion: normalizedExam.singleChoice.scorePerQuestion, questionCount: normalizedExam.singleChoice.count, romanNumeral: "一" },
+    { name: "判断题", scorePerQuestion: normalizedExam.judge.scorePerQuestion, questionCount: normalizedExam.judge.count, romanNumeral: "二" },
+    { name: "多选题", scorePerQuestion: normalizedExam.multiple.scorePerQuestion, questionCount: normalizedExam.multiple.count, romanNumeral: "三" },
+    { name: "简答题", scorePerQuestion: normalizedExam.short.scorePerQuestion, questionCount: normalizedExam.short.count, romanNumeral: "四" },
+  ]
+
+  currentExam.value = normalizedExam
+}
+
 // 返回配置页面
 const backToConfig = () => {
   showExamDetail.value = false;
@@ -2175,8 +2277,8 @@ onMounted(async () => {
 
 // 监听历史记录抽屉显示状态,显示时加载数据
 watch(showHistory, async (newVal) => {
-  if (newVal && historyData.value.length === 0) {
-    console.log('📋 历史记录抽屉打开,开始加载数据...')
+  if (newVal) {
+    console.log('📋 历史记录抽屉打开,开始刷新数据...')
     await getHistoryRecordList()
   }
 })
@@ -2561,6 +2663,45 @@ watch(showHistory, async (newVal) => {
                     color: #9ca3af;
                   }
                 }
+
+                .count-stepper {
+                  display: flex;
+                  align-items: center;
+                  gap: 6px;
+                }
+
+                .stepper-buttons {
+                  display: flex;
+                  flex-direction: column;
+                  gap: 3px;
+                }
+
+                .stepper-btn {
+                  width: 10px;
+                  height: 7px;
+                  padding: 0;
+                  border: none;
+                  outline: none;
+                  appearance: none;
+                  -webkit-appearance: none;
+                  background: #3e7bfa;
+                  display: block;
+                  cursor: pointer;
+                  transition: opacity 0.2s ease, transform 0.2s ease;
+
+                  &:disabled {
+                    opacity: 0.35;
+                    cursor: not-allowed;
+                  }
+                }
+
+                .stepper-btn-up {
+                  clip-path: polygon(50% 0, 0 100%, 100% 100%);
+                }
+
+                .stepper-btn-down {
+                  clip-path: polygon(0 0, 100% 0, 50% 100%);
+                }
               }
             }
           }