Files
ai-dimension/server/src/scripts/generate-quiz.js
T
txai-dev 2a111d21c0 feat: v1.3.0 差异化重构——星图点亮体系
P0 修复:
- 详情页正文渲染(content 分段),71 条知识不再空白
- 问答页去登录墙:游客每日 3 次(IP 限流),登录后每日 5 次
- 修正问答配额为真正的每日重置语义

点亮闭环:
- KnowledgeLit 模型 + 服务端判题(POST /:id/quiz,答案不下发)
- /api/knowledge/stats 星域统计 + 点亮进度 + streak + litIds
- AI 批量生成 71 条读后检测题(scripts/generate-quiz.js)

设计系统 2.0:
- 70 个统一线性图标替换全部 emoji 图标
- 主题 token mixin 化,修复 H5 暗色级联 bug,MP 跟随系统(darkmode)
- 首页星域导航(进度环)、我的页学习者档案、星域列表页重写
- 硬编码色清理,误植 map 页移除

服务端加固:
- trust proxy 真实 IP、错误信息脱敏、ask 独立限流、无效 ObjectId 404
- seo.js ESM→CJS(修复容器启动)、AI 导师"维度精灵"人格化
2026-09-12 23:39:05 +08:00

154 lines
5.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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)
})