| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659 |
- 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: 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;
|