/** * 结合 lab-task-description、仓库扫描与「实训步骤 / 评测对齐」表生成学习建议, * 与 evaluation.learning 合并(任务侧证据优先),供「学习建议(结合本实训)」区块使用。 */ function buildLearningSectionHint(report) { const src = report?.source || ''; const base = '以下建议依据 config/lab-task-description.md、远程仓库脚本/数据/图表扫描与「实训步骤 / 评测对齐」表,并与对话日志信号交叉核验后生成。'; if (src === 'heuristic_fast') { return ( base + ' 当前为快速模式(?fast=1):以可机器核验的结论为主;若需更长文深度点评,可去掉 URL 中的 fast 并配置 CURSOR_API_KEY。' ); } if (src === 'heuristic_only') { return base + ' 当前未调用整页大模型;建议配置 CURSOR_API_KEY 以生成更细致的学业与工具使用分析。'; } if (src === 'ai_full') { return base + ' 已与整页大模型给出的建议合并去重;前列条目优先对应本实训交付物与扫描证据。'; } if (src === 'heuristic_fallback') { return base + ' 整页大模型生成失败,以下为启发式与仓库扫描合并结果。'; } return base; } /** * @param {object} ctx loadStudentRepoContext 返回值 * @param {object} report 报告 JSON(需含 summary.rubric_steps、evaluation.meta 等) * @returns {string[]} */ function buildTaskAlignedLearningList(ctx, report) { const scan = ctx?.scan || {}; const flags = scan.flags || {}; const steps = ctx?.rubric_steps || report?.summary?.rubric_steps || []; const sig = report?.evaluation?.meta?.evaluation_signals || {}; const hookN = report?.summary?.hook_event_count ?? ctx?.heuristic?.summary?.hook_event_count ?? 0; const convN = report?.summary?.conversation_count ?? ctx?.heuristic?.summary?.conversation_count ?? 0; const winN = (typeof sig.transcript_window_count === 'number' ? sig.transcript_window_count : null) ?? convN; const labPct = report?.summary?.lab_progress_percent ?? ctx?.labPct ?? 0; const tips = []; const seen = new Set(); const add = (s) => { const t = String(s).trim(); if (!t || seen.has(t)) return; seen.add(t); tips.push(t); }; add( '【实训目标】读取 scores.csv,用 pandas 完成个人/全班统计并在终端输出;用 matplotlib 绘制全班各科平均分柱状图并保存 score_chart.png;运行报错时附上 Traceback 与工作目录再请 AI 协助。' ); for (const s of steps) { if (s.done) continue; const id = s.id; if (id === 'prep') { add( '【数据交付】未稳定检出 scores.csv:请按头歌路径提交成绩表,代码中路径与评测工作区一致(如与任务说明中的 /data/workspace/myshixun/ 对齐)。' ); } else if (id === 's1') { add('【仓库】检出文件很少:请确认已 push 代码与数据,或检查报告 URL 是否指向正确学员仓库。'); } else if (id === 's2') { add( '【统计脚本】未完成或未检出 read_csv/均值:在 score_analysis.py 中使用 pandas.read_csv 与 mean 等统计,运行后核对终端输出是否与预期一致。' ); } else if (id === 's3') { add( '【运行调试】未体现运行—报错—修复闭环:请在终端运行脚本,出错时把完整 Traceback、当前目录与相关文件名写进对话再问 AI。' ); } else if (id === 's4') { add( '【可视化】未检出 score_chart.png 或作图未完成:用 matplotlib 柱状图展示全班各科平均分,plt.savefig("score_chart.png") 后提交图片文件。' ); } else if (id === 's5') { add('【理解代码】请选择核心统计与作图片段请 AI 解释数据流与参数含义,避免只生成代码不复盘。'); } else if (id === 's6') { add('【提交脚本】请保存并提交 score_analysis.py 到远程仓库,确保自动评测能检出主程序。'); } else { const note = (s.eval_note || '').trim(); add(note ? `【${s.label}】${note.slice(0, 200)}${note.length > 200 ? '…' : ''}` : `【${s.label}】对照任务补全该环节。`); } } if (scan.score_analysis_py && !flags.has_read_csv) { add('【代码扫描】脚本片段未出现 read_csv:请显式使用 pandas.read_csv 指向你的 scores.csv 相对路径。'); } if (scan.score_analysis_py && !flags.has_mean) { add('【代码扫描】未识别均值/汇总:请按任务输出各科或全班平均,并用 print 自查结果。'); } if (scan.score_analysis_py && !flags.has_matplotlib) { add('【代码扫描】未识别 matplotlib:请增加柱状图与 savefig,生成评测所需的 score_chart.png。'); } if (!scan.score_chart_png?.exists && flags.has_matplotlib) { add('【产出文件】脚本含作图但未检出 png:请在本地运行脚本生成 score_chart.png 并加入仓库提交。'); } if ( typeof sig.transcript_window_count === 'number' && typeof sig.conversation_bucket_count === 'number' && sig.conversation_bucket_count > sig.transcript_window_count ) { add( '【上下文】多条日志会话已按同一 Cursor transcript_path 合并;学习建议与作业评价以合并后的聊天窗口口径统计交互,与页面「对话与提问」一致。' ); } if (sig.heuristic_untrustworthy) { add( '【对话方式】日志多为长文单次粘贴,不利于体现个人推敲过程;请拆成小步提问,每轮附带路径、期望输出或报错。' ); } else if (winN >= 1 && hookN > 0 && hookN < 4) { add('【对话方式】交互轮次偏少:可分阶段验证「读表 → 统计 → 作图」,每轮让模型看到上一轮代码或输出。'); } else if (winN >= 1 && hookN >= 16) { add('【对话方式】与模型交互较频:注意每轮目标单一、写清约束,避免同轮混杂无关需求导致跑偏。'); } if (labPct >= 85) { add('【巩固】交付物较齐:可为图表加标题与坐标轴标签,并对照头歌评测脚本再做一次本地自检。'); } return tips; } /** * 将任务侧学习建议与已有 evaluation.learning 合并(去重,任务生成的条目前置)。 */ function mergeLearningWithTaskEvidence(ctx, report) { if (!ctx || !report?.evaluation) return; const taskTips = buildTaskAlignedLearningList(ctx, report); const existing = Array.isArray(report.evaluation.learning) ? report.evaluation.learning : []; const seen = new Set(); const out = []; for (const x of [...taskTips, ...existing]) { const t = String(x).trim(); if (!t || seen.has(t)) continue; seen.add(t); out.push(t); } report.evaluation.learning = out.slice(0, 16); report.ui = report.ui || {}; report.ui.learning_section_hint = buildLearningSectionHint(report); } module.exports = { buildTaskAlignedLearningList, mergeLearningWithTaskEvidence, buildLearningSectionHint, };