/** * 使用 Cursor TypeScript SDK 一键调用大模型,按 Skill 约定生成 lab-eval-ai.json * 依赖:CURSOR_API_KEY;可选 LAB_EVAL_MODEL(默认 composer-2) */ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; import { Agent, CursorAgentError } from '@cursor/sdk'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.join(__dirname, '..'); const require = createRequire(import.meta.url); const { analyzeChatLogsFile } = require(path.join(ROOT, 'lib/analyzeChatLogs.js')); const { resolveChatLogPath } = require(path.join(ROOT, 'lib/resolveChatLogPath.cjs')); function readText(p) { return fs.readFileSync(p, 'utf8'); } function extractJson(text) { if (text == null) throw new Error('empty model output'); const s = typeof text === 'string' ? text : JSON.stringify(text); const fence = s.match(/```(?:json)?\s*([\s\S]*?)```/i); if (fence) return JSON.parse(fence[1].trim()); const start = s.indexOf('{'); const end = s.lastIndexOf('}'); if (start >= 0 && end > start) return JSON.parse(s.slice(start, end + 1)); throw new Error('Could not parse JSON from model output'); } function slimReport(full) { const max = 48000; const str = JSON.stringify(full, null, 2); if (str.length <= max) return str; return `${str.slice(0, max)}\n…(truncated)…\n`; } async function main() { const apiKey = process.env.CURSOR_API_KEY; if (!apiKey) { console.error('缺少环境变量 CURSOR_API_KEY。获取方式:https://cursor.com/dashboard/cloud-agents'); process.exit(1); } const chatPath = resolveChatLogPath(ROOT); if (!fs.existsSync(chatPath)) { console.error(`缺少聊天日志文件: ${chatPath}(可设置环境变量 CHAT_LOG_PATH)`); process.exit(1); } const taskMd = readText(path.join(ROOT, 'config/lab-task-description.md')); const dims = readText(path.join(ROOT, 'config/evaluation-dimensions.yaml')); const skillPath = path.join(ROOT, '.cursor/skills/student-lab-ai-evaluation/SKILL.md'); const skill = fs.existsSync(skillPath) ? readText(skillPath) : ''; let heuristic; try { heuristic = analyzeChatLogsFile(chatPath); } catch (e) { console.error(e); process.exit(1); } if (!heuristic.ok) { console.error(heuristic.error || '解析 chat_logs 失败'); process.exit(1); } const prompt = `你是课程助教。请严格遵守下列 SKILL 中的输出约束(只输出一个 JSON 对象,不要其它文字)。\n\n--- SKILL 开始 ---\n${skill}\n--- SKILL 结束 ---\n\n--- 实训任务描述 ---\n${taskMd}\n\n--- 评价维度配置(YAML)---\n${dims}\n\n--- 启发式解析 JSON(来自 chat_logs,仅供参考)---\n${slimReport( heuristic )}\n\n请根据以上材料生成评价 JSON。abilities 必须与 evaluation-dimensions.yaml 中的维度 id 对齐。`; const modelId = process.env.LAB_EVAL_MODEL || 'composer-2'; let result; try { result = await Agent.prompt(prompt, { apiKey, model: { id: modelId }, local: { cwd: ROOT }, }); } catch (err) { if (err instanceof CursorAgentError) { console.error('Agent 启动失败:', err.message, 'retryable=', err.isRetryable); process.exit(1); } throw err; } if (result.status === 'error') { console.error('Agent 运行结束为 error:', result); process.exit(2); } const raw = result.result; let json; try { json = extractJson(raw); } catch (e) { console.error('解析 JSON 失败:', e.message); console.error('模型原始输出(前 2500 字符):\n', String(raw).slice(0, 2500)); process.exit(3); } const outPath = path.join(ROOT, 'lab-eval-ai.json'); fs.writeFileSync(outPath, JSON.stringify(json, null, 2), 'utf8'); console.log('已写入', outPath); console.log('刷新 http://127.0.0.1:53780/ 的作业评价区即可看到合并后的内容。'); } main().catch((e) => { console.error(e); process.exit(1); });