Explorar el Código

自动批改作业

11wqe1 hace 10 horas
padre
commit
f239d55119
Se han modificado 3 ficheros con 339 adiciones y 1 borrados
  1. 2 0
      app.js
  2. 336 0
      cocostudy.js
  3. 1 1
      mysql.js

+ 2 - 0
app.js

@@ -8,6 +8,8 @@ const mongo = require('./mongo');
 const weixin = require('./weixin');
 const cocoflow = require('./cocoflow');
 const szdjg = require('./szdjg');
+const cocostudy = require('./cocostudy');
+app.use('/api/cocostudy', cocostudy);
 const baoantoken = require('./baoantoken')
 const morgan = require('morgan');
 var path = require("path");

+ 336 - 0
cocostudy.js

@@ -0,0 +1,336 @@
+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;
+  }
+};
+
+// 定时任务:每天16点06分触发
+schedule.scheduleJob("6 16 * * *", 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;

+ 1 - 1
mysql.js

@@ -10,7 +10,7 @@ us.mysqlconnection = function (host, database) {
         let port = host == '172.16.12.7' ? 3306 : 3306;
         if (host == '123.58.32.152') {
             port = 20330;
-        } else if (host == '183.36.25.92') {
+        } else if (host == '183.36.25.92' || host == '183.36.26.8') {
             port = 20007;
         }
         // let port = host == '123.58.32.152' ? 20330 : 20007;