|
|
/**
|
|
|
* 使用 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);
|
|
|
});
|