| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567 |
- const express = require("express");
- const axios = require("axios");
- const schedule = require("node-schedule");
- const mysql = require("./mysql");
- const router = express.Router();
- const crypto = require("crypto");
- // 本地
- // 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);
- }
- }
- }
- saveToAiBatchCorrection(aiBatchCorrection, workId, questionId, 0, "批改失败");
- 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,
- });
- const spaceId = spaceRes[0][0].id;
- console.log("addcocostudySpacetea res", spaceRes, "spaceId", spaceId);
- if (spaceId) {
- return getRidData(e.userId, e.testId, spaceId);
- }
- } catch (err) {
- console.log("addcocostudySpacetea err", err.message || err);
- }
- }
- return null;
- };
- 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 {
- const analysisTask = await saveStudent(e);
- console.log(`保存成功 workId=${e.id}`);
- if (analysisTask) {
- try {
- await analysisTask;
- console.log(`错题分析完成 workId=${e.id}`);
- } catch (err) {
- console.error(`错题分析失败 workId=${e.id}`, err.message || err);
- }
- }
- } 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;
- }
- };
- let agent1a = null;
- let agent2a = null;
- let agent_data2 = null;
- const AGENT_IDS = {
- detailed: "fe58652f-d32e-4aec-ba8e-fc5a3398ac96",
- errorAnalysis: "f100cb78-8053-4f5b-9589-cb3a74d1b4da",
- lectureOutline: "ccb95f55-bf4e-4132-b342-07dfe5bb759e",
- };
- const parseAgentJsonMessage = (raw) => {
- if (Array.isArray(raw)) return raw;
- if (typeof raw === "string") {
- const text = raw.trim();
- const fenced = text.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
- const jsonText = (fenced && fenced[1] ? fenced[1] : text).trim();
- return JSON.parse(jsonText);
- }
- if (raw && typeof raw === "object") return [raw];
- return [];
- };
- const loadAgents = async () => {
- if (agent1a && agent2a && agent_data2) return;
- const [detailedRes, errorRes, outlineRes] = await Promise.all([
- axios.get(`https://appapi.cocorobo.cn/api/agents/agent/${AGENT_IDS.detailed}`),
- axios.get(`https://appapi.cocorobo.cn/api/agents/agent/${AGENT_IDS.errorAnalysis}`),
- axios.get(`https://appapi.cocorobo.cn/api/agents/agent/${AGENT_IDS.lectureOutline}`),
- ]);
- agent_data2 = detailedRes.data;
- agent1a = errorRes.data;
- agent2a = outlineRes.data;
- };
- const callAgentChat = async (agent, message, userId, stepName = "Agent") => {
- let lastErr;
- for (let attempt = 1; attempt <= GRADE_RETRY_TIMES; attempt++) {
- try {
- const res = await axios.post("https://appapi.cocorobo.cn/api/agentchats/ai_agent_chat", {
- id: agent.id,
- message,
- userId,
- model: agent.modelType,
- file_ids: [],
- sound_url: "",
- temperature: 0.1,
- top_p: 1,
- max_completion_tokens: 4096000,
- stream: false,
- uid: crypto.randomUUID(),
- session_name: crypto.randomUUID(),
- });
- const reply = res.data?.message;
- if (reply == null || reply === "") {
- throw new Error("AI 返回内容为空");
- }
- if (attempt > 1) {
- console.log(`[cocostudy] ${stepName} 重试成功 第${attempt}次`);
- }
- return reply;
- } catch (err) {
- lastErr = err;
- console.warn(
- `[cocostudy] ${stepName} 调用失败 第${attempt}/${GRADE_RETRY_TIMES}次`,
- err.response?.data || err.message
- );
- if (attempt < GRADE_RETRY_TIMES) {
- await sleep(GRADE_RETRY_DELAY);
- }
- }
- }
- throw lastErr;
- };
- const normalizeQuestionData = (questions, workData) => {
- return questions.map((item) => {
- const next = { ...item };
- next.userAnswer = workData.data?.[item.id] ?? "";
- next.userScore = workData.score?.[item.id] ?? 0;
- if (item.tool === "choice") {
- if (Array.isArray(next.userAnswer)) {
- next.userAnswer = next.userAnswer
- .map((idx) => item.options[idx])
- .filter(Boolean)
- .join("、");
- } else {
- next.userAnswer = item.options[next.userAnswer] || "";
- }
- if (Array.isArray(item.answer)) {
- next.answer = item.answer
- .map((idx) => item.options[idx])
- .filter(Boolean)
- .join("、");
- } else {
- next.answer = item.options[item.answer] || "";
- }
- }
- return next;
- });
- };
- const buildQuizSummary = (testRow, workRow, questions) => {
- const totalScore = questions.reduce((pre, cur) => pre + (cur.score || 0), 0);
- const userScore = Object.values(workRow.score || {}).reduce(
- (pre, cur) => pre + Number(cur || 0),
- 0
- );
- const totalQuestions = questions.length || 1;
- const wrongQuestions = questions.filter((item) => item.userScore != item.score).length;
- const correctRate = `${(((totalQuestions - wrongQuestions) / totalQuestions) * 100).toFixed(0)}%`;
- return {
- 试卷名称: testRow.name,
- 学科: testRow.subname,
- 年级: testRow.graname,
- 章节: testRow.chapters,
- 试卷总分: totalScore,
- 试卷得分: userScore,
- 试卷正确率: correctRate,
- 试卷总题目数: totalQuestions,
- 错题数量: wrongQuestions,
- 错题列表: questions,
- };
- };
- // 生成错题分析
- async function getRidData(userId, testId, spaceId) {
- await loadAgents();
- const res = await callMysqlProc("getCocostudyTestData", {
- uid: userId,
- tid: testId,
- });
- const workRow = res?.[0]?.[0];
- const testRow = res?.[1]?.[0];
- if (!workRow?.work || !testRow?.testJson) {
- throw new Error(`getCocostudyTestData 数据不完整 testId=${testId}`);
- }
- const workData = JSON.parse(workRow.work);
- const questions = normalizeQuestionData(JSON.parse(testRow.testJson), workData);
- const quizSummary = buildQuizSummary(testRow, workData, questions);
- console.log("[cocostudy] 开始错题分析", { userId, testId, spaceId });
- await generateErrorAnalysis(quizSummary, userId, spaceId);
- }
- async function generateErrorAnalysis(quizSummary, userId, spaceId) {
- const raw = await callAgentChat(
- agent1a,
- `题目数据:\n${JSON.stringify(quizSummary)}`,
- userId,
- "错因分析"
- );
- const errorAnalysis = parseAgentJsonMessage(raw);
- await generateLectureOutline(quizSummary, errorAnalysis, userId, spaceId);
- }
- async function generateLectureOutline(quizSummary, errorAnalysis, userId, spaceId) {
- const lectureOutline = await callAgentChat(
- agent2a,
- `==\n## 统计性信息如下:\n${JSON.stringify(quizSummary)}\n\n==\n\n## 错题深度分析如下:\n${JSON.stringify(errorAnalysis)}`,
- userId,
- "讲义大纲"
- );
- await generatedetailedExplanation(quizSummary, errorAnalysis, lectureOutline, userId, spaceId);
- }
- async function generatedetailedExplanation(quizSummary, errorAnalysis, lectureOutline, userId, spaceId) {
- const quizSummaryString = JSON.stringify(quizSummary).replace(/\\(?![\\"/])/g, "\\\\");
- const displayContent =
- "统计性信息:" +
- quizSummaryString +
- "\n错题深度分析:" +
- JSON.stringify(errorAnalysis) +
- "\n生成的Agenda顺序:" +
- JSON.stringify(lectureOutline);
- const answer = await callAgentChat(agent_data2, displayContent, userId, "详细讲解");
- await insertChat(answer, spaceId, userId);
- try {
- await callMysqlProc("updatespaceisanalyze", {
- sid: spaceId,
- isl: 1,
- });
- } catch (err) {
- console.error("[cocostudy] updatespaceisanalyze err", err.message || err);
- }
- console.log("[cocostudy] 错题分析完成", { userId, spaceId });
- }
- async function insertChat(answer, spaceId, userId) {
- const params = {
- userId,
- userName: "系统",
- groupId: spaceId,
- answer: encodeURIComponent(answer),
- problem: encodeURIComponent(""),
- file_id: "",
- session_name: spaceId,
- alltext: answer,
- type: "chat",
- reasoning_content: "",
- jsonData: "{}",
- };
- const res = await axios.post("https://gpt4.cocorobo.cn/insert_chat", params);
- console.log("[cocostudy] insert_chat res", res.data);
- return res;
- }
- // 定时任务:每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;
|