| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- 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;
|