Files
aurak/test-multiround.mjs
T
Developer ba33d517c1 feat: 分层 RBAC 权限管理系统
后端:
- 新增 Role / RolePermission 实体(自动 seed 系统角色)
- PermissionService——通过 isAdmin / TenantMember 链路解析用户权限
- @Permission() 装饰器 + PermissionsGuard 守卫
- /api/permissions 和 /api/roles REST API
- UserController 内联 role 检查迁移到 @Permission()
- PermissionModule 全局注册

前端:
- usePermissions hook——获取当前用户权限集
- PermissionGate 组件级门控
- PermissionSettingsView——角色列表+权限矩阵编辑页面
- SettingsView 新增「权限管理」Tab(仅 admin 可见)
- 权限预览(26 项,7 分类)

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

263 lines
10 KiB
JavaScript
Raw 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';
const SA_REPLIES = [
'需要审查代码质量和安全性', // #1 代码审查
'还要检查逻辑正确性和性能问题', // #2 代码质量
'用清晰的提示词告诉AI具体需求', // #3 prompt技巧
'要注意数据安全和隐私保护', // #4 安全
'AI协作时要明确分工,人做决策', // #5 协作
'检查AI生成的内容是否准确', // #6 验证输出
'要测试边界情况和异常处理', // #7 测试
'把大任务拆成小步骤和AI沟通', // #8 任务拆分
'持续学习和更新对AI工具的认识', // #9 学习成长
'评估成本效益,选最合适的方案', // #10 综合
];
/** Fill a textarea via native setter + input event (reliable for React controlled inputs) */
async function fillTextarea(page, text) {
await page.evaluate((t) => {
const ta = document.querySelector('textarea');
if (!ta) return;
const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
setter?.call(ta, t);
ta.dispatchEvent(new Event('input', { bubbles: true }));
}, text);
await new Promise(r => setTimeout(r, 300));
}
/** Click the send button (wait for it to be enabled, then regular or force click) */
async function clickSendButton(page) {
try {
await page.waitForFunction(() => {
const btn = document.querySelector('button:has(svg.lucide-send)');
return btn && !btn.disabled && btn.offsetParent !== null;
}, { timeout: 15000 });
await page.locator('button:has(svg.lucide-send)').last().click({ timeout: 5000 });
} catch {
await new Promise(r => setTimeout(r, 1000));
await page.locator('button:has(svg.lucide-send)').last().click({ force: true, timeout: 5000 }).catch(() => {});
}
}
/** Wait for spinner to disappear */
async function waitForIdle(page) {
await page.waitForFunction(() => !document.querySelector('.animate-spin'), { timeout: 90000 }).catch(() => {});
await new Promise(r => setTimeout(r, 1500));
}
async function run() {
console.log('=== AuraK 10题考核多轮对话测试 ===\n');
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
// Login
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(' ✅ 登录成功');
// Assessment page
await page.goto(`${BASE}/assessment`, { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
// Select template
await page.locator('button:has-text("AI协作技巧")').first().click();
await page.waitForTimeout(500);
await page.locator('button:has-text("开始专业评估")').first().click();
// Wait for first question
console.log('[2] 等待出题...');
for (let i = 0; i < 90; i++) {
const text = await page.textContent('body').catch(() => '');
if (text.includes('问题 ') || text.includes('Question ')) break;
await new Promise(r => setTimeout(r, 2000));
}
console.log(' ✅ 第 1 题已出现');
await waitForIdle(page);
// Answer questions
let qIdx = 1;
let saCount = 0, followUpCount = 0;
const totalQs = 10;
const startTime = Date.now();
// Per-question timeout: 5 minutes (AI generation can be slow)
const Q_TIMEOUT = 300; // 5 min in seconds
while (qIdx <= totalQs) {
// Detect question type
const state = await page.evaluate(() => {
const allBtns = Array.from(document.querySelectorAll('button'));
const optionBtns = allBtns.filter(b =>
/^[A-D]/.test(b.textContent || '') && (b.textContent || '').length > 5 &&
!(b.textContent || '').includes('AuraK') && !(b.textContent || '').includes('Admin')
);
const confirmBtn = allBtns.find(b => (b.textContent || '').includes('确认答案'));
const ta = document.querySelector('textarea');
return {
choiceCount: optionBtns.length,
hasTextarea: ta !== null && ta.offsetParent !== null,
firstChoice: optionBtns[0]?.textContent?.trim().substring(0, 40) || '',
hasConfirmBtn: confirmBtn !== null,
busy: document.querySelector('.animate-spin') !== null,
hasQuestion: (document.body.textContent || '').includes('问题 ') || (document.body.textContent || '').includes('Question '),
};
});
// If busy (spinner visible), wait and retry
if (state.busy) {
console.log(` ⏳ AI正在处理...`);
await new Promise(r => setTimeout(r, 3000));
continue;
}
// If neither question type detected but question text exists, wait more
if (state.choiceCount === 0 && !state.hasTextarea && state.hasQuestion) {
console.log(` ⏳ 题型未就绪,等待...`);
await new Promise(r => setTimeout(r, 3000));
continue;
}
// Read current question text
const questionText = await page.evaluate(() => {
const bubbles = Array.from(document.querySelectorAll('.px-5.py-4'));
for (let i = bubbles.length - 1; i >= 0; i--) {
const el = bubbles[i];
const text = el.textContent || '';
if (text.length > 25 && !(el.getAttribute('class') || '').includes('bg-indigo')) {
return text.substring(0, 160);
}
}
return '';
});
if (state.choiceCount > 0) {
// ── CHOICE ──
const qShort = questionText.replace(/\s+/g, ' ').substring(0, 100);
console.log(`\n 🟦 第 ${qIdx}/${totalQs} 题 (选择) "${qShort}..."`);
const btns = page.locator('button.w-full.text-left.px-5.py-4');
const count = await btns.count();
if (count > 0) {
await btns.first().click();
await new Promise(r => setTimeout(r, 500));
}
if (state.hasConfirmBtn) {
await page.locator('button:has-text("确认答案")').click();
console.log(` ✅ 已提交`);
}
qIdx++;
// After submitting a choice, wait for transition
await waitForIdle(page);
} else if (state.hasTextarea) {
// ── SHORT ANSWER ──
saCount++;
const replyIdx = Math.min(saCount - 1, SA_REPLIES.length - 1);
console.log(`\n 🟩 第 ${qIdx}/${totalQs} 题 (简答 #${saCount})`);
await page.locator('textarea').first().click();
await fillTextarea(page, SA_REPLIES[replyIdx]);
console.log(` 📝 已输入: "${SA_REPLIES[replyIdx].substring(0, 20)}..."`);
await clickSendButton(page);
console.log(` ✅ 已提交`);
// Wait for grading
await waitForIdle(page);
// Check for follow-up question
const stillTA = await page.evaluate(() => {
const ta = document.querySelector('textarea');
return ta !== null && ta.offsetParent !== null;
});
if (stillTA) {
followUpCount++;
const fReply = SA_REPLIES[Math.min(followUpCount, SA_REPLIES.length - 1)];
console.log(` 🔄 AI 追问 #${followUpCount} 已触发!`);
await page.locator('textarea').first().click();
await fillTextarea(page, fReply);
console.log(` 📝 追问已输入: "${fReply.substring(0, 20)}..."`);
await clickSendButton(page);
console.log(` ✅ 追问已提交`);
await waitForIdle(page);
}
qIdx++;
} else {
// ── WAITING for question to appear ──
// Check for question text in body
const bodyText = await page.textContent('body').catch(() => '');
if (bodyText.includes('问题 ') || bodyText.includes('Question ')) {
// Question is there but types not detected yet - wait for spinner then retry
console.log(` ⏳ 第 ${qIdx} 题文本已见,等待组件渲染...`);
await waitForIdle(page);
continue;
}
// Check if assessment completed
if (bodyText.includes('合格') || bodyText.includes('VERIFIED') || bodyText.includes('LEVEL')) {
console.log(`\n 📋 考核已完成!`);
break;
}
// Check per-question timeout
const elapsed = Math.round((Date.now() - startTime) / 1000);
if (elapsed > Q_TIMEOUT * qIdx + 120) {
console.log(` ⏰ 第 ${qIdx} 题生成超时,跳过`);
qIdx++;
continue;
}
console.log(` ⏳ 等待 AI 生成第 ${qIdx} 题...`);
await waitForIdle(page);
await new Promise(r => setTimeout(r, 5000));
continue;
}
}
// Wait for results page
console.log('\n ⏳ 等待考核结果完成...');
await waitForIdle(page);
await new Promise(r => setTimeout(r, 5000));
const elapsed = Math.round((Date.now() - startTime) / 1000);
const body = await page.textContent('body');
const scores = body.match(/\d+\/10/g);
const level = body.match(/LEVEL:\s*(\w+)/i)?.[1] || body.match(/等级[:]\s*(\w+)/)?.[1] || '?';
const passed = body.includes('合格') || body.includes('VERIFIED');
console.log(`\n 📊 结果 (耗时 ${Math.floor(elapsed/60)}${elapsed%60}秒):`);
console.log(` 总题数: ${totalQs}`);
console.log(` 选择题: ${totalQs - saCount}`);
console.log(` 简答题: ${saCount}`);
console.log(` AI追问: ${followUpCount}`);
console.log(` 分数: ${scores ? scores.join(', ') : '无'}`);
console.log(` 等级: ${level}`);
console.log(` ${passed ? '🎉 合格!' : '😅 未合格'}`);
if (followUpCount > 0) {
console.log(`\n 🎉 多轮对话正常工作!`);
} else if (saCount > 0) {
console.log(`\n ✅ 简答题正常回答,未触发追问(回答已完整)。`);
} else {
console.log(`\n ⚠️ 未遇到简答题,需要确认 shuffle 是否生效。`);
}
await page.screenshot({ path: 'assessment-10q-result.png', fullPage: true });
console.log(' 📸 截图已保存');
await browser.close();
console.log('\n=== 完成 ===');
}
run().catch(e => { console.error('\n❌', e.message); process.exit(1); });