Files
aurak/do-assessment.mjs
Developer c57c3028e2 fix: shuffleArray bug + Playwright多轮对话测试 + 初学者考核脚本
- 修复 shuffleArray 返回新数组但调用处用 const 未接收返回值(3处)
- 新增 test-multiround.mjs Playwright 多轮对话测试(简答+追问全流程)
- 新增 do-assessment.mjs / check-result.mjs 考核体验脚本
- CLAUDE.md 增加 AI 工作流指令规则
- package.json 添加 playwright 依赖

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 22:34:04 +08:00

268 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { chromium } from 'playwright';
const BASE = 'http://localhost:13001';
async function waitForSpinner(page) {
await page.waitForFunction(() => !document.querySelector('.animate-spin'), { timeout: 60000 }).catch(() => {});
await new Promise(r => setTimeout(r, 1000));
}
/** Extract the last assistant message (question text) */
async function getLastQuestion(page) {
return await page.evaluate(() => {
// Find all message bubbles: elements with px-5 py-4 classes
const allBubbles = Array.from(document.querySelectorAll('.px-5.py-4'));
// The last one that's from assistant (white bg, not indigo) is the question
for (let i = allBubbles.length - 1; i >= 0; i--) {
const el = allBubbles[i];
const text = el.textContent || '';
const style = el.getAttribute('class') || '';
// Skip user messages (indigo bg) and empty/footer text
if (style.includes('bg-indigo')) continue;
if (text.length < 20) continue;
return text.substring(0, 600);
}
return '';
});
}
async function run() {
console.log('=== 🧑‍🎓 我来做题! ===\n');
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
// 登录
console.log('[1] 登录...');
await page.goto(`${BASE}/login`, { waitUntil: 'networkidle' });
await page.waitForTimeout(1000);
await page.locator('input[type="text"]').first().fill('admin');
await page.locator('input[type="password"]').first().fill('admin123');
await page.locator('button[type="submit"]').click();
await page.waitForURL('**/');
console.log(' ✅ 登录成功');
// 进入考核
await page.goto(`${BASE}/assessment`, { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
await page.locator('button:has-text("AI协作技巧")').first().click();
await page.waitForTimeout(500);
await page.locator('button:has-text("开始专业评估")').first().click();
// 等出题
console.log('\n[2] 等待出题...');
for (let i = 0; i < 120; i++) {
const text = await page.textContent('body').catch(() => '');
if (text.includes('问题 ') || text.includes('Question ')) break;
await new Promise(r => setTimeout(r, 2000));
}
await waitForSpinner(page);
console.log(' ✅ 题目已加载\n');
// 答题
let qIdx = 1;
const totalQs = 4;
while (qIdx <= totalQs) {
// 判断是选择题还是简答题
const state = await page.evaluate(() => {
// 选择题选项按钮:CSS类 w-full text-left px-5 py-4
const optionBtns = Array.from(document.querySelectorAll('button.w-full.text-left.px-5.py-4'))
.filter(b => !b.textContent?.includes('确认答案'));
// textarea 表示简答题
const ta = document.querySelector('textarea');
const busy = document.querySelector('.animate-spin') !== null;
// 确认答案按钮:找 button 文字包含"确认"
const confirmBtns = Array.from(document.querySelectorAll('button'))
.filter(b => (b.textContent || '').includes('确认'));
return {
optionCount: optionBtns.length,
optionTexts: optionBtns.map(b => b.textContent?.trim() || ''),
hasTextarea: ta !== null && ta.offsetParent !== null,
hasConfirmBtn: confirmBtns.length > 0,
busy,
};
});
if (state.busy) {
await new Promise(r => setTimeout(r, 2000));
continue;
}
// 读取题目
const question = await getLastQuestion(page);
console.log(`\n═══ 第 ${qIdx}/${totalQs} 题 ═══`);
console.log(`📖 ${question}\n`);
if (state.optionCount > 0) {
// ── 选择题 ──
console.log('📋 选项:');
state.optionTexts.forEach((t, i) => {
console.log(` ${String.fromCharCode(65 + i)}) ${t}`);
});
console.log('');
// 凭常识选题
const btns = page.locator('button.w-full.text-left.px-5.py-4');
const count = await btns.count();
// 根据题目内容推理
const qLower = question.toLowerCase();
const texts = state.optionTexts.map(t => t.toLowerCase());
let chosen = 1; // default B
if (qLower.includes('提示词') || qLower.includes('prompt') || qLower.includes('prompts')) {
chosen = texts.findIndex(t => t.includes('清晰') || t.includes('具体') || t.includes('举例') || t.includes('角色'));
} else if (qLower.includes('安全') || qLower.includes('敏感') || qLower.includes('泄露')) {
chosen = texts.findIndex(t => t.includes('脱敏') || t.includes('敏感') || t.includes('安全'));
} else if (qLower.includes('测试') || qLower.includes('测试用例')) {
chosen = texts.findIndex(t => t.includes('测试') || t.includes('质量') || t.includes('验证'));
} else if (qLower.includes('选型') || qLower.includes('成本') || qLower.includes('模型')) {
chosen = texts.findIndex(t => t.includes('成本') || t.includes('任务') || t.includes('性价比'));
} else if (qLower.includes('代码审查') || qLower.includes('review')) {
chosen = texts.findIndex(t => t.includes('安全') || t.includes('质量') || t.includes('逻辑'));
} else if (qLower.includes('幻觉') || qLower.includes('hallucination')) {
chosen = texts.findIndex(t => t.includes('事实') || t.includes('验证') || t.includes('核对'));
}
if (chosen < 0) chosen = 1;
await btns.nth(chosen).click();
await new Promise(r => setTimeout(r, 500));
console.log(` 👉 我选 ${String.fromCharCode(65 + chosen)}`);
// 提交
if (state.hasConfirmBtn) {
await page.locator('button:has-text("确认答案")').click();
console.log(' ✅ 提交');
}
// 如果有下一题的过渡
qIdx++;
} else if (state.hasTextarea) {
// ── 简答题 ──
console.log('✏️ 简答题,我来回答:\n');
// 根据题目内容生成回答
const qLower = question.toLowerCase();
let answer = '';
if (qLower.includes('测试') || qLower.includes('测试用例') || (qLower.includes('写代码') && qLower.includes('测试'))) {
answer = '我觉得即使有AI写代码,测试还是必须写的。AI写的代码也可能有bug,需要人工验证。测试不只是找bug,还能帮我们理解代码逻辑。而且测试用例本身也是需求的一部分,能告诉我们代码应该怎么用。AI可以帮我们写测试代码,但不能完全代替人去思考测试场景。';
} else if (qLower.includes('提示词') || qLower.includes('prompt')) {
answer = '写提示词要清晰具体,告诉AI它的角色是什么。可以给例子,让AI理解格式要求。复杂任务可以让AI一步一步思考。也要注意测试不同提示词的效果,找到最合适的。';
} else if (qLower.includes('安全') || qLower.includes('敏感') || qLower.includes('泄露')) {
answer = '要注意不要把敏感信息发给AI,比如密码、API密钥、客户数据。如果要用真实数据,要先脱敏处理。也要检查AI生成的代码有没有安全漏洞。';
} else if (qLower.includes('代码审查') || qLower.includes('review') || qLower.includes('代码质量')) {
answer = '代码评审要看代码的功能是否正确,有没有bug。还要看代码风格是否一致,性能好不好,有没有安全问题。AI可以帮我们做一部分检查,但最终还是需要人来做判断。';
} else if (qLower.includes('ai协作') || qLower.includes('ai合作') || qLower.includes('协作技巧')) {
answer = '和AI协作要分工明确,AI做它擅长的(生成、总结、分析),人做判断和决策。要给AI清晰的任务描述,分步骤沟通。AI生成的内容要自己核实,不能完全相信。';
} else {
answer = '我觉得首先要理解问题的本质,然后让AI帮忙分析。AI的输出要结合自己的经验和判断。不能完全依赖AI,要多验证AI给出的结果是否合理。安全意识和质量控制很重要。';
}
// 输入回答
await page.locator('textarea').first().click();
await page.evaluate((text) => {
const ta = document.querySelector('textarea');
if (!ta) return;
const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
setter?.call(ta, text);
ta.dispatchEvent(new Event('input', { bubbles: true }));
}, answer);
await new Promise(r => setTimeout(r, 800));
console.log(` 💬 "${answer.substring(0, 50)}..."`);
// 等 button 可用再点
await page.waitForFunction(() => {
const btn = document.querySelector('button:has(svg.lucide-send)');
return btn && !btn.disabled && btn.offsetParent !== null;
}, { timeout: 15000 }).catch(() => {});
await page.locator('button:has(svg.lucide-send)').last().click({ timeout: 8000 }).catch(() => {
page.locator('button:has(svg.lucide-send)').last().click({ force: true, timeout: 5000 }).catch(() => {});
});
console.log(' ✅ 已提交\n');
// 等批改
await waitForSpinner(page);
// 检查是否有追问
const stillTA = await page.evaluate(() => {
const ta = document.querySelector('textarea');
return ta !== null && ta.offsetParent !== null;
});
if (stillTA) {
console.log(' 🔄 AI追问来了!再回答一轮');
// 读追问的题目
const followQ = await getLastQuestion(page);
console.log(` 📖 追问: "${followQ.substring(0, 100)}..."`);
const followAnswer = '还要看代码的可维护性和可读性,团队协作时需要统一的代码风格。也要考虑性能优化和异常处理。总之AI是工具,人是决策者,不能把责任推给AI。';
await page.locator('textarea').first().click();
await page.evaluate((text) => {
const ta = document.querySelector('textarea');
if (!ta) return;
const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
setter?.call(ta, text);
ta.dispatchEvent(new Event('input', { bubbles: true }));
}, followAnswer);
await new Promise(r => setTimeout(r, 800));
await page.waitForFunction(() => {
const btn = document.querySelector('button:has(svg.lucide-send)');
return btn && !btn.disabled;
}, { timeout: 10000 }).catch(() => {});
await page.locator('button:has(svg.lucide-send)').last().click({ timeout: 5000 }).catch(() => {
page.locator('button:has(svg.lucide-send)').last().click({ force: true, timeout: 3000 }).catch(() => {});
});
console.log(' ✅ 追问已回答');
await waitForSpinner(page);
}
qIdx++;
} else {
console.log(' ⏳ 等待...');
await new Promise(r => setTimeout(r, 3000));
continue;
}
// 等过渡
await waitForSpinner(page);
}
// 结果
console.log('\n═══ 考核结果 ═══');
await new Promise(r => setTimeout(r, 5000));
await page.waitForFunction(() => !document.querySelector('.animate-spin'), { timeout: 60000 }).catch(() => {});
const result = await page.evaluate(() => {
const body = document.body.textContent || '';
const scoreMatch = body.match(/(\d+\.?\d*)\/10/);
const levelMatch = body.match(/等级.*?(\w+)/i) || body.match(/LEVEL:\s*(\w+)/i);
const finalScoreMatch = body.match(/最终得分[:]\s*(\d+\.?\d*)/);
const passMatch = body.includes('合格') || body.includes('VERIFIED');
const failMatch = body.includes('不合格') || body.includes('FAIL');
// 各题得分
const allScores = Array.from(body.matchAll(/(\d+)\/10/g)).map(m => m[1]);
return {
score: scoreMatch?.[1] || finalScoreMatch?.[1] || '?',
level: levelMatch?.[1] || '?',
passed: passMatch,
failed: failMatch,
allScores: allScores.join(', '),
};
});
console.log(` 📊 各题得分: ${result.allScores || '无'}`);
console.log(` 🏆 等级: ${result.level}`);
console.log(` ${result.passed ? '🎉 合格!' : result.failed ? '😅 不合格...' : '...'}`);
await page.screenshot({ path: 'assessment-result-beginner.png', fullPage: true });
console.log(' 📸 截图已保存');
await browser.close();
console.log('\n=== 完成 ===');
}
run().catch(e => { console.error('\n❌', e.message); process.exit(1); });