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; 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; module.exports.getRidData = getRidData;