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) => { // _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 ? _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 { userId: e.userId, testId: e.testId, spaceId, workId: e.id }; // } // } 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)); // // 1. 批改:全部并发 // 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); // } // }); // // 2. 保存:全部并发(与错题分析解耦) // const saveResults = await Promise.allSettled( // aiBatchCorrection.map(async (e) => { // const meta = await saveStudent(e); // console.log(`保存成功 workId=${e.id}`); // return meta; // }) // ); // const analysisJobs = []; // saveResults.forEach((result, index) => { // const item = aiBatchCorrection[index]; // if (result.status === "rejected") { // console.error(`保存失败 workId=${item.id}`, result.reason?.message || result.reason); // return; // } // if (result.value) { // analysisJobs.push(result.value); // } // }); // // 3. 错题分析:全部并发 // if (analysisJobs.length) { // console.log(`[cocostudy] 开始错题分析 ${analysisJobs.length} 条(全部并发)`); // const analysisResults = await Promise.allSettled( // analysisJobs.map(async ({ userId, testId, spaceId, workId }) => { // await getRidData(userId, testId, spaceId); // console.log(`错题分析完成 workId=${workId}`); // return workId; // }) // ); // analysisResults.forEach((result, index) => { // const job = analysisJobs[index]; // if (result.status === "rejected") { // console.error( // `错题分析失败 workId=${job.workId}`, // result.reason?.message || result.reason // ); // } // }); // } // 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 agent1b = null; let agent2a = null; let agent1c = null; const AGENT_IDS = { // 1a errorAnalysis: "f100cb78-8053-4f5b-9589-cb3a74d1b4da", // 1b lectureOutline: "08028b8b-b067-43c3-b8f5-b3a874f0a502", // 1c oneCExplanation: "cc43167c-e767-4f81-8a73-b0295de49472", // 2a detailed: "a000e7e9-d8f0-47d0-98e6-f5b1c6171934", }; const tryParseJson = (text) => { try { return JSON.parse(text); } catch (_) { return undefined; } }; const normalizeParsedAgentMessage = (parsed) => { if (Array.isArray(parsed)) return parsed; if (parsed && typeof parsed === "object") return [parsed]; return parsed; }; const collectJsonCandidates = (text) => { const candidates = []; const seen = new Set(); const push = (value) => { const next = (value || "").trim(); if (!next || seen.has(next)) return; seen.add(next); candidates.push(next); }; const fenceRe = /```(?:json)?\s*([\s\S]*?)\s*```/gi; let match; while ((match = fenceRe.exec(text)) !== null) { push(match[1]); } push(text); for (const startChar of ["{", "["]) { let idx = text.indexOf(startChar); while (idx !== -1) { push(text.slice(idx)); idx = text.indexOf(startChar, idx + 1); if (candidates.length > 30) break; } } return candidates; }; const parseAgentJsonMessage = (raw) => { if (Array.isArray(raw)) return raw; if (raw && typeof raw === "object") return [raw]; if (typeof raw !== "string") return []; const text = raw.trim(); if (!text) return []; for (const candidate of collectJsonCandidates(text)) { const parsed = tryParseJson(candidate); if (parsed !== undefined) { return normalizeParsedAgentMessage(parsed); } } console.warn( "[cocostudy] Agent 返回非 JSON,原样传递下游:", text.slice(0, 120).replace(/\s+/g, " ") ); return text; }; const loadAgents = async () => { if (agent1a && agent1b && agent2a && agent1c) return; const [detailedRes, errorRes, outlineRes, oneCExplanationRes] = 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}`), axios.get(`https://appapi.cocorobo.cn/api/agents/agent/${AGENT_IDS.oneCExplanation}`), ]); agent2a = detailedRes.data; agent1a = errorRes.data; agent1b = outlineRes.data; agent1c = oneCExplanationRes.data; }; const updateSpaceAnalyzeStatus = async (spaceId, isl) => { try { await callMysqlProc("updatespaceisanalyze", { sid: spaceId, isl, }); console.log("[cocostudy] updatespaceisanalyze", { spaceId, isl }); } catch (err) { console.error("[cocostudy] updatespaceisanalyze err", err.message || err); } }; const callAgentChat = async (agent, message, userId, stepName = "Agent", spaceId) => { 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: agent.id === AGENT_IDS.detailed ? spaceId : 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); } } } if (spaceId) { await updateSpaceAnalyzeStatus(spaceId, 2); } 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 generate1a(quizSummary, userId, spaceId); } async function generate1a(quizSummary, userId, spaceId) { const raw = await callAgentChat( agent1a, `题目数据:\n${JSON.stringify(quizSummary)}`, userId, "错因分析", spaceId ); const errorAnalysis = parseAgentJsonMessage(raw); await generate1b(quizSummary, errorAnalysis, userId, spaceId); } async function generate1b(quizSummary, errorAnalysis, userId, spaceId) { const lectureResponse = await callAgentChat( agent1b, `==\n## 统计性信息如下:\n${JSON.stringify(quizSummary)}\n\n==\n\n## 错题深度分析如下:\n${JSON.stringify(errorAnalysis)}`, userId, "讲义大纲", spaceId ); const lectureOutline = parseAgentJsonMessage(lectureResponse); await generate1c(quizSummary, errorAnalysis, lectureOutline, userId, spaceId); } async function generate1c(quizSummary, errorAnalysis, lectureOutline, userId, spaceId) { const oneCExplanation = await callAgentChat( agent1c, `==\n## 统计性信息如下:\n${JSON.stringify(quizSummary)}\n\n==\n\n## 错题深度分析如下:\n${JSON.stringify(errorAnalysis)}\n\n==\n\n## 讲义大纲如下:\n${JSON.stringify(lectureOutline)}`, userId, "讲解生成", spaceId ); await generatedetailedExplanation(quizSummary, errorAnalysis, lectureOutline, oneCExplanation, userId, spaceId); } async function generatedetailedExplanation(quizSummary, errorAnalysis, lectureOutline, oneCExplanation, userId, spaceId) { const quizSummaryString = JSON.stringify(quizSummary).replace(/\\(?![\\"/])/g, "\\\\"); const errorAnalysisString = JSON.stringify(errorAnalysis).replace(/\\(?![\\"/])/g, "\\\\"); const lectureOutlineString = JSON.stringify(lectureOutline).replace(/\\(?![\\"/])/g, "\\\\"); const oneCExplanationString = String(oneCExplanation ?? ""); const displayContent = `==\n## 统计性信息如下:\n${quizSummaryString}\n\n==\n\n## 错题深度分析如下:\n${errorAnalysisString}\n\n==\n\n## 讲解大纲如下:\n${lectureOutlineString}\n\n==\n\n## 详细讲解如下:\n${oneCExplanationString}`; const answer = await callAgentChat(agent2a, displayContent, userId, "详细讲解", spaceId); await insertChat(answer, spaceId, userId); let json = { quizSummary: quizSummaryString, errorAnalysis: errorAnalysisString, lectureOutline: lectureOutlineString, detailedExplanation: oneCExplanationString, } try { await callMysqlProc("updatecocostudySpacejson", { id: spaceId, json: encodeURIComponent(JSON.stringify(json)), }); } catch (err) { console.error("[cocostudy] updatecocostudySpacejson err", err.message || err); } await updateSpaceAnalyzeStatus(spaceId, 1); 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;