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 _mysqlLabor = ["172.16.12.5", "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); }); }); }; let 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现在,请按照讲解顺序,开始为学生进行讲解。` ; const answer = await callAgentChat(agent2a, displayContent, userId, "详细讲解", spaceId); await insertChat(answer, spaceId, userId); let json = { quizSummary: quizSummary, errorAnalysis: errorAnalysis, lectureOutline: lectureOutline, detailedExplanation: oneCExplanation, } 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;