package utils import ( "bytes" "encoding/json" "fmt" "regexp" "strings" ) // ExamPromptQuestionType represents the structure of question type configuration passed from frontend type ExamPromptQuestionType struct { Name string `json:"name"` RomanNumeral string `json:"romanNumeral"` QuestionCount int `json:"questionCount"` ScorePerQuestion int `json:"scorePerQuestion"` } // ExamPromptRequest represents the payload required to build an exam prompt type ExamPromptRequest struct { Mode string `json:"mode"` Client string `json:"client"` ProjectType string `json:"projectType"` ExamTitle string `json:"examTitle"` TotalScore int `json:"totalScore"` QuestionTypes []ExamPromptQuestionType `json:"questionTypes"` PPTContent string `json:"pptContent"` } // SingleQuestionPromptRequest represents the payload required to build a single-question prompt type SingleQuestionPromptRequest struct { Client string `json:"client"` SectionType string `json:"sectionType"` QuestionIndex int `json:"questionIndex"` ProjectType string `json:"projectType"` QuestionType string `json:"questionType"` ScorePerQuestion int `json:"scorePerQuestion"` CurrentQuestion json.RawMessage `json:"currentQuestion"` } type questionStats struct { Score int Count int } // BuildExamPrompt generates the exam prompt text based on mode (ai/ppt) and client (pc/mobile) func BuildExamPrompt(req ExamPromptRequest) (string, error) { mode := strings.ToLower(strings.TrimSpace(req.Mode)) if mode == "" { mode = "ai" } client := strings.ToLower(strings.TrimSpace(req.Client)) if client == "" { client = "pc" } switch client { case "mobile": if mode == "ppt" { return buildMobilePPTExamPrompt(req), nil } return buildMobileAIExamPrompt(req), nil default: if mode == "ppt" { return buildPcPPTExamPrompt(req), nil } return buildPcAIExamPrompt(req), nil } } // BuildSingleQuestionPrompt builds prompts for regenerating a single question func BuildSingleQuestionPrompt(req SingleQuestionPromptRequest) (string, error) { client := strings.ToLower(strings.TrimSpace(req.Client)) if client == "" { client = "pc" } sectionType := normalizeSectionType(req.SectionType) projectType := fallbackString(req.ProjectType, "工程") questionType := fallbackString(req.QuestionType, questionTypeFromSection(sectionType)) scorePerQuestion := safePositive(req.ScorePerQuestion, 1) questionIndex := req.QuestionIndex + 1 currentQuestion := formatCurrentQuestionJSON(req.CurrentQuestion) switch client { case "mobile": return buildMobileSingleQuestionPrompt(projectType, questionType, sectionType, questionIndex, scorePerQuestion, currentQuestion), nil default: return buildPcSingleQuestionPrompt(projectType, questionType, sectionType, questionIndex, scorePerQuestion, currentQuestion), nil } } func buildPcAIExamPrompt(req ExamPromptRequest) string { projectType := fallbackString(req.ProjectType, "工程") examTitle := fallbackString(req.ExamTitle, "考试试卷") questionConfig := buildPcQuestionConfig(req.QuestionTypes) jsonTemplate := buildPcJSONTemplate(req, examTitle) return fmt.Sprintf(`请为%[1]s工程生成一份名为"%[2]s"的考试试卷: 试卷总分:%[3]d分 题型配置: %[4]s 请严格按照以下JSON格式返回试卷内容,不要包含任何其他文字: %[5]s 注意: 1. 所有题目内容必须与%[1]s工程相关 2. 题目难度适中,符合考试要求 3. 严格按照JSON格式返回,不要有多余字符 4. 单选题和判断题的选项要合理 5. 多选题至少要有2个正确答案 6. 简答题要提供清晰的答题要点 7. 必须为每道题设置正确答案: - 单选题:selectedAnswer字段填写正确的选项(A/B/C/D) - 判断题:selectedAnswer字段填写"正确"或"错误" - 多选题:selectedAnswers数组包含所有正确答案选项 8. 简答题答案字数不超过500字 `, projectType, examTitle, safeTotalScore(req.TotalScore), questionConfig, jsonTemplate) } func buildPcPPTExamPrompt(req ExamPromptRequest) string { contentText := sanitizePPTContent(req.PPTContent) if contentText == "" { contentText = fallbackString(req.ExamTitle, "PPT内容") } examTitle := fallbackString(req.ExamTitle, "考试试卷") questionConfig := buildPcQuestionConfig(req.QuestionTypes) jsonTemplate := buildPcJSONTemplate(req, examTitle) return fmt.Sprintf(`请根据标题为%[1]s生成一份考试试卷: 试卷总分:%[2]d分 题型配置: %[3]s 请严格按照以下JSON格式返回试卷内容,不要包含任何其他文字: %[4]s 注意: 1. 所有题目内容必须基于提供的内容生成,与%[1]s工程相关 2. 题目难度适中,符合考试要求 3. 严格按照JSON格式返回,不要有多余字符 4. 单选题和判断题的选项要合理 5. 多选题至少要有2个正确答案 6. 简答题要提供清晰的答题要点 7. 必须为每道题设置正确答案: - 单选题:selectedAnswer字段填写正确的选项(A/B/C/D) - 判断题:selectedAnswer字段填写"正确"或"错误" - 多选题:selectedAnswers数组包含所有正确答案选项 8. 简答题答案字数不超过500字 9. 确保题目内容与提供的内容高度相关 `, contentText, safeTotalScore(req.TotalScore), questionConfig, jsonTemplate) } func buildMobileAIExamPrompt(req ExamPromptRequest) string { projectType := fallbackString(req.ProjectType, "工程") examTitle := fallbackString(req.ExamTitle, "考试试卷") questionConfig := buildMobileQuestionConfig(req.QuestionTypes) jsonTemplate := buildMobileJSONTemplate(req, examTitle) return fmt.Sprintf(`请生成一份%[1]s工程施工技术考核试卷: 试卷要求: - 试卷名称:%[2]s - 试卷总分:%[3]d分 - 题型配置: %[4]s 内容要求: 1. %[1]s工程施工技术的核心知识点 2. 施工工艺流程和质量控制要点 3. 安全操作规程和事故预防措施 4. 相关法规标准和验收规范 请严格按照以下JSON格式返回试卷内容,不要包含任何其他文字: 请在questions数组中生成具体、完整的题目内容! - 单选题需要:text题目内容、options选项数组[{"key":"A","text":"选项1"},{"key":"B","text":"选项2"},{"key":"C","text":"选项3"},{"key":"D","text":"选项4"}]、selectedAnswer正确答案 - 判断题需要:text题目内容、selectedAnswer正确答案(正确/错误) - 多选题需要:text题目内容、options选项数组[{"key":"A","text":"选项1"},{"key":"B","text":"选项2"},{"key":"C","text":"选项3"},{"key":"D","text":"选项4"}]、selectedAnswers正确答案数组 - 简答题需要:text题目内容、outline {keyFactors:关键要点, measures:具体措施} %[5]s`, projectType, examTitle, safeTotalScore(req.TotalScore), questionConfig, jsonTemplate) } func buildMobilePPTExamPrompt(req ExamPromptRequest) string { examTitle := fallbackString(req.ExamTitle, "考试试卷") pptContent := fallbackString(strings.TrimSpace(req.PPTContent), "PPT内容") questionConfig := buildMobileQuestionConfig(req.QuestionTypes) jsonTemplate := buildMobileJSONTemplate(req, examTitle+" - PPT培训考试") return fmt.Sprintf(`请基于以下PPT培训内容生成考试试卷: 培训主题:%[1]s PPT内容:%[2]s 请生成一份包含以下题型的考试试卷: %[3]s 试卷总分:%[4]d分 要求: 1. 重点考查PPT中的核心知识点 2. 涵盖主要概念、工艺流程、操作规程等关键内容 3. 题目难度适中,具有实际应用价值 4. 确保答案准确性和专业性 请严格按照以下JSON格式返回试卷内容,不要包含任何其他文字: 请在questions数组中生成具体、完整的题目内容! - 单选题需要:text题目内容、options选项数组[{"key":"A","text":"选项1"},{"key":"B","text":"选项2"},{"key":"C","text":"选项3"},{"key":"D","text":"选项4"}]、selectedAnswer正确答案 - 判断题需要:text题目内容、selectedAnswer正确答案(正确/错误) - 多选题需要:text题目内容、options选项数组[{"key":"A","text":"选项1"},{"key":"B","text":"选项2"},{"key":"C","text":"选项3"},{"key":"D","text":"选项4"}]、selectedAnswers正确答案数组 - 简答题需要:text题目内容、outline {keyFactors:关键要点, measures:具体措施} %[5]s`, examTitle, pptContent, questionConfig, safeTotalScore(req.TotalScore), jsonTemplate) } func buildPcQuestionConfig(questionTypes []ExamPromptQuestionType) string { if len(questionTypes) == 0 { return "暂无题型配置\n" } var builder strings.Builder for _, qt := range questionTypes { roman := strings.TrimSpace(qt.RomanNumeral) if roman == "" { roman = "•" } score := safePositive(qt.ScorePerQuestion, 1) count := safePositive(qt.QuestionCount, 1) builder.WriteString(fmt.Sprintf("%s、%s:%d题,每题%d分,共%d分\n", roman, qt.Name, count, score, count*score)) } return builder.String() } func buildMobileQuestionConfig(questionTypes []ExamPromptQuestionType) string { if len(questionTypes) == 0 { return "暂无题型配置\n" } var builder strings.Builder for _, qt := range questionTypes { score := safePositive(qt.ScorePerQuestion, 1) count := safePositive(qt.QuestionCount, 1) builder.WriteString(fmt.Sprintf("%s:%d题,每题%d分,共%d分\n", qt.Name, count, score, count*score)) } return builder.String() } func buildPcJSONTemplate(req ExamPromptRequest, title string) string { totalQuestions := sumQuestionCount(req.QuestionTypes) single := statsByName(req.QuestionTypes, "单选题", 2, 15) judge := statsByName(req.QuestionTypes, "判断题", 2, 10) multiple := statsByName(req.QuestionTypes, "多选题", 3, 10) short := statsByName(req.QuestionTypes, "简答题", 10, 2) return fmt.Sprintf(`{ "title": "%[1]s", "totalScore": %[2]d, "totalQuestions": %[3]d, "singleChoice": { "scorePerQuestion": %[4]d, "totalScore": %[5]d, "count": %[6]d, "questions": [ { "text": "题目内容", "options": [ {"key": "A", "text": "选项A内容"}, {"key": "B", "text": "选项B内容"}, {"key": "C", "text": "选项C内容"}, {"key": "D", "text": "选项D内容"} ], "selectedAnswer": "正确答案选项(A/B/C/D)" } ] }, "judge": { "scorePerQuestion": %[7]d, "totalScore": %[8]d, "count": %[9]d, "questions": [ { "text": "题目内容", "selectedAnswer": "正确答案(正确/错误)" } ] }, "multiple": { "scorePerQuestion": %[10]d, "totalScore": %[11]d, "count": %[12]d, "questions": [ { "text": "题目内容", "options": [ {"key": "A", "text": "选项A内容"}, {"key": "B", "text": "选项B内容"}, {"key": "C", "text": "选项C内容"}, {"key": "D", "text": "选项D内容"} ], "selectedAnswers": ["正确答案选项1", "正确答案选项2"] } ] }, "short": { "scorePerQuestion": %[13]d, "totalScore": %[14]d, "count": %[15]d, "questions": [ { "text": "题目内容", "outline": { "keyFactors": "答题要点、关键因素、示例答案" } } ] } }`, title, safeTotalScore(req.TotalScore), totalQuestions, single.Score, single.Score*single.Count, single.Count, judge.Score, judge.Score*judge.Count, judge.Count, multiple.Score, multiple.Score*multiple.Count, multiple.Count, short.Score, short.Score*short.Count, short.Count) } func buildMobileJSONTemplate(req ExamPromptRequest, title string) string { totalQuestions := sumQuestionCount(req.QuestionTypes) q0 := statsByIndex(req.QuestionTypes, 0, 2, 8) q1 := statsByIndex(req.QuestionTypes, 1, 2, 5) q2 := statsByIndex(req.QuestionTypes, 2, 3, 5) q3 := statsByIndex(req.QuestionTypes, 3, 10, 2) return fmt.Sprintf(`{ "title": "%[1]s", "totalScore": %[2]d, "totalQuestions": %[3]d, "singleChoice": { "scorePerQuestion": %[4]d, "totalScore": %[5]d, "count": %[6]d, "questions": [] }, "judge": { "scorePerQuestion": %[7]d, "totalScore": %[8]d, "count": %[9]d, "questions": [] }, "multiple": { "scorePerQuestion": %[10]d, "totalScore": %[11]d, "count": %[12]d, "questions": [] }, "short": { "scorePerQuestion": %[13]d, "totalScore": %[14]d, "count": %[15]d, "questions": [] } }`, title, safeTotalScore(req.TotalScore), totalQuestions, q0.Score, q0.Score*q0.Count, q0.Count, q1.Score, q1.Score*q1.Count, q1.Count, q2.Score, q2.Score*q2.Count, q2.Count, q3.Score, q3.Score*q3.Count, q3.Count) } func statsByName(questionTypes []ExamPromptQuestionType, name string, defaultScore, defaultCount int) questionStats { for _, qt := range questionTypes { if strings.TrimSpace(qt.Name) == name { return questionStats{ Score: safePositive(qt.ScorePerQuestion, defaultScore), Count: safePositive(qt.QuestionCount, defaultCount), } } } return questionStats{Score: defaultScore, Count: defaultCount} } func statsByIndex(questionTypes []ExamPromptQuestionType, index int, defaultScore, defaultCount int) questionStats { if index >= 0 && index < len(questionTypes) { qt := questionTypes[index] return questionStats{ Score: safePositive(qt.ScorePerQuestion, defaultScore), Count: safePositive(qt.QuestionCount, defaultCount), } } return questionStats{Score: defaultScore, Count: defaultCount} } func sumQuestionCount(questionTypes []ExamPromptQuestionType) int { total := 0 for _, qt := range questionTypes { total += safePositive(qt.QuestionCount, 0) } return total } func buildPcSingleQuestionPrompt(projectType, questionType, sectionType string, questionIndex, score int, currentQuestion string) string { format := buildPcSingleQuestionFormat(sectionType) return fmt.Sprintf(`请基于以下%[1]s工程的%[2]s题目,重新生成一道相似主题的题目,要求如下: 当前题目参考: %[3]s 题目类型:%[2]s 每题分值:%[4]d分 题目序号:第%[5]d题 请严格按照以下JSON格式返回,不要包含任何其他文字: %[6]s 注意: 1. 新题目必须与当前题目保持相似的主题和难度 2. 题目内容必须与%[1]s工程相关 3. 题目难度适中,符合考试要求 4. 严格按照JSON格式返回,不要有多余字符 5. 单选题和判断题的选项要合理 6. 多选题至少要有2个正确答案 7. 简答题要提供清晰的答题要点 8. 必须为每道题设置正确答案: - 单选题:selectedAnswer字段填写正确的选项(A/B/C/D) - 判断题:selectedAnswer字段填写"正确"或"错误" - 多选题:selectedAnswers数组包含所有正确答案选项 9. 简答题答案字数不超过500字 10. 新题目应该是当前题目的变体,保持主题一致性但内容要有所变化`, projectType, questionType, currentQuestion, score, questionIndex, format) } func buildMobileSingleQuestionPrompt(projectType, questionType, sectionType string, questionIndex, score int, currentQuestion string) string { format := buildMobileSingleQuestionFormat(sectionType) return fmt.Sprintf(`请基于以下%[1]s工程的%[2]s题目,重新生成一道相似主题的题目: 当前题目参考: %[3]s 题目类型:%[2]s 每题分值:%[4]d分 题目序号:第%[5]d题 请严格按照以下JSON格式返回,不要包含任何其他文字: %[6]s`, projectType, questionType, currentQuestion, score, questionIndex, format) } func buildPcSingleQuestionFormat(sectionType string) string { switch sectionType { case "single": return `{ "text": "题目内容", "options": [ {"key": "A", "text": "选项A内容"}, {"key": "B", "text": "选项B内容"}, {"key": "C", "text": "选项C内容"}, {"key": "D", "text": "选项D内容"} ], "selectedAnswer": "正确答案选项(A/B/C/D)" }` case "judge": return `{ "text": "题目内容", "selectedAnswer": "正确答案(正确/错误)" }` case "multiple": return `{ "text": "题目内容", "options": [ {"key": "A", "text": "选项A内容"}, {"key": "B", "text": "选项B内容"}, {"key": "C", "text": "选项C内容"}, {"key": "D", "text": "选项D内容"} ], "selectedAnswers": ["正确答案选项1", "正确答案选项2"] }` case "short": return `{ "text": "题目内容", "outline": { "keyFactors": "答题要点、关键因素、示例答案" } }` default: return `{ "text": "题目内容" }` } } func buildMobileSingleQuestionFormat(sectionType string) string { switch sectionType { case "single": return `{ "text": "题目内容", "options": [ {"key": "A", "text": "选项A"}, {"key": "B", "text": "选项B"}, {"key": "C", "text": "选项C"}, {"key": "D", "text": "选项D"} ], "selectedAnswer": "A" }` case "judge": return `{ "text": "题目内容", "selectedAnswer": "正确" }` case "multiple": return `{ "text": "题目内容", "options": [ {"key": "A", "text": "选项A"}, {"key": "B", "text": "选项B"}, {"key": "C", "text": "选项C"}, {"key": "D", "text": "选项D"} ], "selectedAnswers": ["A", "B"] }` case "short": return `{ "text": "题目内容", "outline": { "keyFactors": "关键要点内容", "measures": "具体措施内容" } }` default: return `{ "text": "题目内容" }` } } func formatCurrentQuestionJSON(raw json.RawMessage) string { if len(raw) == 0 { return "无" } var buf bytes.Buffer if err := json.Indent(&buf, raw, "", " "); err != nil { return string(raw) } return buf.String() } func questionTypeFromSection(sectionType string) string { switch sectionType { case "single": return "单选题" case "judge": return "判断题" case "multiple": return "多选题" case "short": return "简答题" default: return "题目" } } func normalizeSectionType(sectionType string) string { switch strings.ToLower(strings.TrimSpace(sectionType)) { case "single", "judge", "multiple", "short": return strings.ToLower(strings.TrimSpace(sectionType)) default: return "single" } } func sanitizePPTContent(content string) string { text := strings.TrimSpace(content) if text == "" { return "" } if strings.Contains(text, "PPT文件信息") { re := regexp.MustCompile(`(?s)提取的文本内容:\n(.*?)(\n\n|$)`) if matches := re.FindStringSubmatch(text); len(matches) > 1 { text = strings.TrimSpace(matches[1]) } } return text } func fallbackString(value string, fallback string) string { if strings.TrimSpace(value) == "" { return fallback } return value } func safePositive(value int, fallback int) int { if value <= 0 { return fallback } return value } func safeTotalScore(score int) int { if score <= 0 { return 100 } return score }