/** * AI 批量生成知识检测题 —— 为没有 options 的知识生成 1 道四选一检测题 * 执行: node src/scripts/generate-quiz.js [--dry] [--limit 10] * * 生成后字段: question / options[{text,isCorrect}] / answer(解析) * 幂等:已有 options 的知识自动跳过;单个失败不影响后续 */ const mongoose = require('mongoose') const path = require('path') require('dotenv').config({ path: path.join(__dirname, '..', '..', '.env') }) const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/wdkj' const args = process.argv.slice(2) const DRY = args.includes('--dry') const LIMIT_ARG = args.find(a => a.startsWith('--limit')) const LIMIT = LIMIT_ARG ? parseInt(LIMIT_ARG.split('=')[1] || args[args.indexOf(LIMIT_ARG) + 1]) : 0 async function getModels() { const { AIModel } = require('../models') const active = await AIModel.getActiveModels() if (active && active.length) return active return [{ name: 'env-model', apiUrl: process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions', apiKey: process.env.OPENAI_API_KEY, modelId: process.env.OPENAI_MODEL || 'gpt-3.5-turbo' }] } function extractJson(text) { if (!text) return null let t = text.trim() const fence = t.match(/```(?:json)?\s*([\s\S]*?)```/) if (fence) t = fence[1].trim() const start = t.indexOf('{') const end = t.lastIndexOf('}') if (start === -1 || end === -1) return null try { return JSON.parse(t.slice(start, end + 1)) } catch { return null } } function validate(q) { if (!q || typeof q.question !== 'string' || !q.question.trim()) return null if (!Array.isArray(q.options) || q.options.length < 3 || q.options.length > 5) return null const normalized = q.options.map(o => ({ text: String(o.text || '').trim(), isCorrect: !!o.isCorrect })) if (normalized.some(o => !o.text)) return null const correctCount = normalized.filter(o => o.isCorrect).length if (correctCount !== 1) return null return { question: q.question.trim(), options: normalized, answer: String(q.answer || '').trim() } } async function askAI(models, prompt) { for (const model of models) { try { const headers = { 'Content-Type': 'application/json' } if (model.apiKey && model.apiKey !== 'sk-dummy' && model.apiKey !== '***') { headers['Authorization'] = `Bearer ${model.apiKey}` } const controller = new AbortController() const timer = setTimeout(() => controller.abort(), 60000) let res try { res = await fetch(model.apiUrl, { method: 'POST', headers, body: JSON.stringify({ model: model.modelId, messages: [ { role: 'system', content: '你是知识测验出题官,只输出 JSON,不要输出任何其他文字。' }, { role: 'user', content: prompt } ], temperature: 0.4, max_tokens: 600 }), signal: controller.signal }) } finally { clearTimeout(timer) } if (!res.ok) continue const data = await res.json() const content = data.choices?.[0]?.message?.content || '' return content } catch (e) { console.warn(` 模型 ${model.name} 失败: ${e.message}`) } } return null } async function main() { await mongoose.connect(MONGODB_URI) console.log('✅ 已连接 MongoDB') const { Knowledge } = require('../models') const models = await getModels() console.log(`🤖 出题模型: ${models.map(m => m.name).join(', ')}`) const query = { status: 'approved', $or: [{ options: { $exists: false } }, { options: { $size: 0 } }, { question: { $exists: false } }, { question: null }] } let items = await Knowledge.find(query).sort({ dim: 1, sortOrder: 1 }).lean() if (LIMIT > 0) items = items.slice(0, LIMIT) console.log(`📚 待出题知识: ${items.length} 条${DRY ? '(dry 模式,不写库)' : ''}\n`) let ok = 0 let fail = 0 for (const item of items) { const prompt = `基于以下 AI 知识内容,出一道四选一的中文检测题(考察对核心概念的理解,干扰项要有迷惑性但不歧义)。 标题:${item.title} 内容:${item.content.slice(0, 800)} 严格按此 JSON 格式输出: {"question":"题干","options":[{"text":"选项A","isCorrect":true},{"text":"选项B","isCorrect":false},{"text":"选项C","isCorrect":false},{"text":"选项D","isCorrect":false}],"answer":"一句话解析为什么正确"}` console.log(`→ [dim${item.dim}] ${item.title}`) const raw = await askAI(models, prompt) const quiz = validate(extractJson(raw)) if (!quiz) { console.log(' ✗ 生成或校验失败,跳过') fail++ } else { console.log(` ✓ ${quiz.question} (${quiz.options.length} 选项)`) if (!DRY) { await Knowledge.updateOne( { _id: item._id }, { $set: { question: quiz.question, options: quiz.options, answer: quiz.answer } } ) } ok++ } await new Promise(r => setTimeout(r, 400)) } console.log(`\n完成:成功 ${ok},失败 ${fail},共 ${items.length}`) await mongoose.disconnect() } main().catch(err => { console.error('执行失败:', err) process.exit(1) })