|
|
@@ -3,7 +3,7 @@ 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"];
|
|
|
@@ -255,11 +255,16 @@ const saveStudent = async (e) => {
|
|
|
rid: e.testId,
|
|
|
tit: e.name,
|
|
|
});
|
|
|
- console.log("addcocostudySpacetea res", spaceRes);
|
|
|
+ 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 () => {
|
|
|
@@ -286,8 +291,16 @@ const runAutoSco = async () => {
|
|
|
|
|
|
for (const e of aiBatchCorrection) {
|
|
|
try {
|
|
|
- await saveStudent(e);
|
|
|
+ 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);
|
|
|
}
|
|
|
@@ -301,7 +314,7 @@ let isRunning = false;
|
|
|
|
|
|
const runAutoScoSafe = async () => {
|
|
|
if (isRunning) {
|
|
|
- console.warn("[cocostudy] 上一次自动批改仍在执行,跳过本次");
|
|
|
+ console.warn("[cocostudy] 上一次自动批改/分析仍在执行,跳过本次");
|
|
|
return;
|
|
|
}
|
|
|
isRunning = true;
|
|
|
@@ -314,6 +327,223 @@ const runAutoScoSafe = async () => {
|
|
|
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;
|
|
|
+
|
|
|
+ if (item.tool === "choice") {
|
|
|
+ if (Array.isArray(next.userAnswer)) {
|
|
|
+ next.userAnswer = next.userAnswer
|
|
|
+ .map((idx) => item.options[idx])
|
|
|
+ .filter(Boolean)
|
|
|
+ .join("、");
|
|
|
+ } else {
|
|
|
+ next.userAnswer = item.options[next.userAnswer] || "";
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Array.isArray(item.answer)) {
|
|
|
+ next.answer = item.answer
|
|
|
+ .map((idx) => item.options[idx])
|
|
|
+ .filter(Boolean)
|
|
|
+ .join("、");
|
|
|
+ } else {
|
|
|
+ next.answer = item.options[item.answer] || "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 () => {
|