forked from hangshuo652/aurak
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>
This commit is contained in:
+113
-38
@@ -1,7 +1,18 @@
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const BASE = 'http://localhost:13001';
|
||||
const SA_REPLIES = ['需要审查代码质量和安全性', '还要检查逻辑正确性和性能问题'];
|
||||
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) {
|
||||
@@ -24,14 +35,19 @@ async function clickSendButton(page) {
|
||||
}, { timeout: 15000 });
|
||||
await page.locator('button:has(svg.lucide-send)').last().click({ timeout: 5000 });
|
||||
} catch {
|
||||
// Regular click failed (stale/detached element); wait briefly and force
|
||||
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 考核多轮对话测试 ===\n');
|
||||
console.log('=== AuraK 10题考核多轮对话测试 ===\n');
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
||||
|
||||
@@ -62,40 +78,67 @@ async function run() {
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
}
|
||||
console.log(' ✅ 第 1 题已出现');
|
||||
|
||||
// Wait spinner to finish
|
||||
await page.waitForFunction(() => !document.querySelector('.animate-spin'), { timeout: 60000 }).catch(() => {});
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
await waitForIdle(page);
|
||||
|
||||
// Answer questions
|
||||
let qIdx = 1;
|
||||
let saCount = 0, followUpCount = 0;
|
||||
const totalQs = 4;
|
||||
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 choiceBtns = Array.from(document.querySelectorAll('button'))
|
||||
.filter(b => /^[A-D]/.test(b.textContent || '') && (b.textContent || '').length > 5)
|
||||
.filter(b => !(b.textContent || '').startsWith('AuraK'))
|
||||
.filter(b => !(b.textContent || '').startsWith('Admin'));
|
||||
|
||||
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: choiceBtns.length,
|
||||
choiceCount: optionBtns.length,
|
||||
hasTextarea: ta !== null && ta.offsetParent !== null,
|
||||
firstChoice: choiceBtns[0]?.textContent || '',
|
||||
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) {
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
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 ──
|
||||
console.log(`\n 🟦 第 ${qIdx}/${totalQs} 题 (选择) "${state.firstChoice.substring(0, 30)}..."`);
|
||||
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();
|
||||
@@ -104,12 +147,14 @@ async function run() {
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
|
||||
const confirm = page.locator('button:has-text("确认答案")');
|
||||
if (await confirm.isVisible().catch(() => false)) {
|
||||
await confirm.click();
|
||||
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++;
|
||||
@@ -123,17 +168,15 @@ async function run() {
|
||||
console.log(` ✅ 已提交`);
|
||||
|
||||
// Wait for grading
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
await page.waitForFunction(() => !document.querySelector('.animate-spin'), { timeout: 60000 }).catch(() => {});
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
await waitForIdle(page);
|
||||
|
||||
// Check for follow-up: textarea visible again at same question position
|
||||
// Check for follow-up question
|
||||
const stillTA = await page.evaluate(() => {
|
||||
const ta = document.querySelector('textarea');
|
||||
return ta !== null && ta.offsetParent !== null;
|
||||
});
|
||||
|
||||
if (stillTA && followUpCount < SA_REPLIES.length - 1) {
|
||||
if (stillTA) {
|
||||
followUpCount++;
|
||||
const fReply = SA_REPLIES[Math.min(followUpCount, SA_REPLIES.length - 1)];
|
||||
console.log(` 🔄 AI 追问 #${followUpCount} 已触发!`);
|
||||
@@ -144,33 +187,62 @@ async function run() {
|
||||
await clickSendButton(page);
|
||||
console.log(` ✅ 追问已提交`);
|
||||
|
||||
await page.waitForFunction(() => !document.querySelector('.animate-spin'), { timeout: 60000 }).catch(() => {});
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
await waitForIdle(page);
|
||||
}
|
||||
|
||||
qIdx++;
|
||||
|
||||
} else {
|
||||
// Wait for anything to appear
|
||||
console.log(` ⏳ 等待第 ${qIdx} 题...`);
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
// ── 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 next question
|
||||
await page.waitForFunction(() => !document.querySelector('.animate-spin'), { timeout: 30000 }).catch(() => {});
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
}
|
||||
|
||||
// Results
|
||||
await new Promise(r => setTimeout(r, 4000));
|
||||
// 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 📊 结果:`);
|
||||
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(` AI追问: ${followUpCount}次`);
|
||||
console.log(` 分数: ${scores ? scores.join(', ') : '无'}`);
|
||||
console.log(` 等级: ${level}`);
|
||||
console.log(` ${passed ? '🎉 合格!' : '😅 未合格'}`);
|
||||
|
||||
if (followUpCount > 0) {
|
||||
console.log(`\n 🎉 多轮对话正常工作!`);
|
||||
@@ -180,6 +252,9 @@ async function run() {
|
||||
console.log(`\n ⚠️ 未遇到简答题,需要确认 shuffle 是否生效。`);
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'assessment-10q-result.png', fullPage: true });
|
||||
console.log(' 📸 截图已保存');
|
||||
|
||||
await browser.close();
|
||||
console.log('\n=== 完成 ===');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user