const express = require("express"); const axios = require("axios"); const schedule = require("node-schedule"); const mysql = require("./mysql"); const router = express.Router(); // 本地 // const _mysqlLabor = ["183.36.26.8", "pbl"]; // const _mysqluser = ["183.36.26.8", "cocorobouser"]; // const _getmysqlLabor2 = ["183.36.26.8", "pbl"]; // const _getmysqlLabor = ["183.36.26.8", "pbl"]; // 线上 const _mysqlLabor = ["172.16.12.5", "pbl"]; // const _mysqluser = ["172.16.12.5", "cocorobouser"]; // const _getmysqlLabor2 = ["172.16.12.7", "pbl"]; // const _getmysqlLabor = ["172.16.12.7", "pbl"]; const testUrl = "https://test-paper-analyzer.cocorobo.cn"; const GRADE_RETRY_TIMES = 3; const GRADE_RETRY_DELAY = 2000; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const saveToAiBatchCorrection = (aiBatchCorrection, workId, questionId, score, feedback) => { const target = aiBatchCorrection.find((item) => item.id === workId); if (!target) { console.warn(`未找到 aiBatchCorrection 记录 workId=${workId}`); return; } target.work.score[questionId] = score; target.work.analysis[questionId] = feedback; }; const callMysqlProc = (procedureName, data) => { return new Promise((resolve, reject) => { const p = [_mysqlLabor[0], _mysqlLabor[1], procedureName, ...Object.values(data)]; mysql.usselect(p, (ret) => { if (ret instanceof Error || (ret && ret.code)) { reject(ret); return; } resolve(ret); }); }); }; const queryAutoScoData = () => { return new Promise((resolve, reject) => { const p = []; p.unshift(_mysqlLabor[0], _mysqlLabor[1], "getcocostudyautosco"); mysql.usselect(p, (ret) => { if (ret instanceof Error || (ret && ret.code)) { reject(ret); return; } resolve(ret[0] || []); }); }); }; const aiBatchCorrectionBtn = (item, aiBatchCorrection) => { if (!item?.work || !item?.testJson) { console.warn("缺少 work 或 testJson,跳过", item?.id); return; } let workParsed; let testjson; try { workParsed = JSON.parse(item.work); testjson = JSON.parse(item.testJson); } catch (err) { console.error("JSON 解析失败", item.id, err.message); return; } const _work = workParsed.data || {}; const work = { score: {}, analysis: {}, data: _work, time: workParsed.time, }; const _test = []; testjson.forEach((w) => { if (w.tool === "choice") { work.score[w.id] = 0; if (w.answer && _work[w.id]) { const correctAnswer = JSON.stringify([...w.answer].sort()); const userAnswer = JSON.stringify([...(_work[w.id] || [])].sort()); work.score[w.id] = correctAnswer === userAnswer ? w.score : 0; } } else { _test.push(w); work.score[w.id] = 0; } }); aiBatchCorrection.push({ id: item.id, name: item.name, subject: item.subject, testId: item.testId, userId: item.userId, work, test: _test, }); }; const gradeQuestionWithRetry = async (aiBatchCorrection, params, workId, questionId) => { let lastErr; for (let attempt = 1; attempt <= GRADE_RETRY_TIMES; attempt++) { try { const res = await axios.post(testUrl + "/llm-extract/analyze/grade-question", params, { headers: { "Content-Type": "application/json" }, }); const _result = res.data?.result || res.data; const _score = _result?.score_awarded ?? 0; const _feedback = _result?.overall_feedback ?? ""; saveToAiBatchCorrection(aiBatchCorrection, workId, questionId, _score, _feedback); if (attempt > 1) { console.log(`批改重试成功 workId=${workId} questionId=${questionId} 第${attempt}次`); } return _result; } catch (err) { lastErr = err; console.warn( `批改失败 workId=${workId} questionId=${questionId} 第${attempt}/${GRADE_RETRY_TIMES}次`, err.response?.data || err.message ); if (attempt < GRADE_RETRY_TIMES) { await sleep(GRADE_RETRY_DELAY); } } } throw lastErr; }; const aiGradingBatchCorrection = async (aiBatchCorrection, val) => { const promises = []; for (let i = 0; i < val.test.length; i++) { const _testIndex = i; const _userAnswer = val.work.data[val.test[i].id]; const _sub_questions = []; const _imgSrc = []; if (val.test[i].subQuestions) { val.test[i].subQuestions.forEach((sub, index) => { _sub_questions.push({ number: `${index + 1}`, stem: sub.title, knowledge_points: sub.knowledgePoint, standard_answer: sub.answer, scoring_criteria: sub.scoringCriteria, }); }); } const params = { question: { number: `${_testIndex}`, type: val.test[i].tool, is_subjective: true, score: val.test[i].score, stem: val.test[i].title, options: {}, sub_questions: _sub_questions, knowledge_points: val.test[i].knowledgePoint ? val.test[i].knowledgePoint : [], standard_answer: val.test[i].answer, scoring_criteria: val.test[i].answer, figure_description: "", figure_bbox: { page: 1, x_min: 0, y_min: 0, x_max: 0, y_max: 0, }, figure_image_urls: _imgSrc, }, student_file_urls: Array.isArray(_userAnswer) ? _userAnswer.map((u) => u.url) : [], student_answer_text: ["fill", "qa"].includes(val.test[i].tool) ? _userAnswer : "", model: "gpt-5.4", }; const questionId = val.test[i].id; promises.push(gradeQuestionWithRetry(aiBatchCorrection, params, val.id, questionId)); } const results = await Promise.allSettled(promises); const failed = results.filter((r) => r.status === "rejected").length; if (failed > 0) { console.warn(`workId=${val.id} 共 ${failed}/${results.length} 道题批改失败,继续保存已有分数`); } return results; }; const saveStudent = async (e) => { const _json = { fullScore: 0, errorId: [], correct: [], total: e.test.length, }; e.test.forEach((i) => { if (e.work.score[i.id] == i.score) { _json.fullScore += 1; _json.correct.push(i.id); } else { _json.errorId.push(i.id); } }); for (const i of _json.errorId) { let test = e.test.find((t) => t.id == i); if (test) { test = JSON.parse(JSON.stringify(test)); test.userAnswer = e.work.data[i]; const p = { uid: e.userId, sid: e.subject, tit: test.title, kno: test.knowledge || "", tid: i, json: JSON.stringify(test), clist: _json.correct.join(","), }; try { const res = await callMysqlProc("addcocostudyminbook", p); console.log("addcocostudyminbook res", res); } catch (err) { console.log("addcocostudyminbook err", err.message || err); } } } const params = { id: e.id, status: 2, test: JSON.stringify(e.work), json: JSON.stringify(_json), }; const res = await callMysqlProc("updateCocostudysco", params); console.log("updateCocostudysco res", res); if (res === 1) { try { const spaceRes = await callMysqlProc("addcocostudySpacetea", { uid: e.userId, rid: e.testId, tit: e.name, }); console.log("addcocostudySpacetea res", spaceRes); } catch (err) { console.log("addcocostudySpacetea err", err.message || err); } } }; const runAutoSco = async () => { console.log("[cocostudy] 自动批改开始", new Date().toISOString()); const aiBatchCorrection = []; const data = await queryAutoScoData(); if (!data.length) { console.log("[cocostudy] 没有待批改数据"); return { count: 0 }; } data.forEach((item) => aiBatchCorrectionBtn(item, aiBatchCorrection)); const gradeResults = await Promise.allSettled( aiBatchCorrection.map((e) => aiGradingBatchCorrection(aiBatchCorrection, e)) ); gradeResults.forEach((result, index) => { const item = aiBatchCorrection[index]; if (result.status === "rejected") { console.error(`学生批改异常 workId=${item.id}`, result.reason?.message || result.reason); } }); for (const e of aiBatchCorrection) { try { await saveStudent(e); console.log(`保存成功 workId=${e.id}`); } catch (err) { console.error(`保存失败 workId=${e.id}`, err.message || err); } } console.log("[cocostudy] 全部处理完成", new Date().toISOString()); return { count: aiBatchCorrection.length }; }; let isRunning = false; const runAutoScoSafe = async () => { if (isRunning) { console.warn("[cocostudy] 上一次自动批改仍在执行,跳过本次"); return; } isRunning = true; try { return await runAutoSco(); } catch (err) { console.error("[cocostudy] 自动批改异常", err.message || err); throw err; } finally { isRunning = false; } }; // 定时任务:每10分钟触发一次 schedule.scheduleJob("*/10 * * * *", async () => { try { await runAutoScoSafe(); } catch (error) { console.error("[cocostudy] 定时任务异常", error.message || error); } }); // 手动触发:GET/POST /api/cocostudy/autosco router.all("/autosco", async (req, res) => { try { const result = await runAutoScoSafe(); res.json({ code: 200, msg: "全部处理完成", data: result }); } catch (err) { res.status(500).json({ code: 500, msg: err.message || "自动批改失败" }); } }); module.exports = router;