import { chromium } from 'playwright'; import { spawn } from 'child_process'; import path from 'path'; import fs from 'fs'; const ROOT = 'D:\\Projects\\ai-review'; const SCREENSHOTS = 'C:\\Users\\NB-076\\AppData\\Local\\Temp\\opencode\\story'; fs.mkdirSync(SCREENSHOTS, { recursive: true }); async function waitForPort(port, timeout = 15000) { const start = Date.now(); while (Date.now() - start < timeout) { try { const res = await fetch(`http://localhost:${port}`); if (res.ok || res.status < 500) return true; } catch {} await new Promise(r => setTimeout(r, 500)); } throw new Error(`Port ${port} not ready`); } async function ss(page, name) { await page.screenshot({ path: path.join(SCREENSHOTS, name), fullPage: true }); console.log(` Screenshot: ${name}`); } async function main() { // Start services const server = spawn('npx.cmd', ['tsx', 'src/index.ts'], { cwd: path.join(ROOT, 'server'), stdio: 'pipe', shell: true }); const frontend = spawn('npx.cmd', ['vite', '--port', '14001'], { cwd: path.join(ROOT, 'web'), stdio: 'pipe', shell: true }); server.stdout.on('data', d => process.stdout.write(`[s] ${d}`)); frontend.stdout.on('data', d => process.stdout.write(`[f] ${d}`)); console.log('Waiting for services...'); await waitForPort(3002); await waitForPort(14001); console.log('Ready!'); const envContent = fs.readFileSync(path.join(ROOT, 'server', '.env'), 'utf-8'); const password = (envContent.match(/AUTH_PASSWORD=(.+)/) || [,'620f4c96'])[1].trim(); const browser = await chromium.launch({ headless: false }); // visible so you can watch const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); try { // ═══════════════════════════════════════════════ // STORY: 技术大赛双赛道评审 // ═══════════════════════════════════════════════ // STEP 1: Login → Dashboard console.log('\n=== 1. Login ==='); await page.goto('http://localhost:14001/login', { waitUntil: 'networkidle' }); await ss(page, '01-login.png'); await page.fill('input[type="password"]', password); await page.click('button[type="submit"]'); await page.waitForURL('**/'); await page.waitForTimeout(1000); await ss(page, '02-dashboard.png'); // STEP 2: Create Project "2026技术大赛" console.log('\n=== 2. Create Project ==='); await page.click('.btn-new-project'); await page.waitForTimeout(300); await page.fill('.new-project-form input', '2026技术大赛'); await ss(page, '03-create-project.png'); await page.click('.new-project-actions button:first-child'); await page.waitForURL(/\/project\//); await page.waitForTimeout(500); const projectUrl = page.url(); const projectId = projectUrl.split('/').pop(); console.log(`Project: ${projectId}`); // STEP 3: Upload standards for both tracks console.log('\n=== 3. Upload Standards ==='); await page.click('.tab:has-text("标准")'); await page.waitForTimeout(500); const standard1 = fs.readFileSync(path.join(ROOT, 'docs', 'standards', '技术大赛-赛道一评审标准.md'), 'utf-8'); const standard2 = fs.readFileSync(path.join(ROOT, 'docs', 'standards', '技术大赛-赛道二评审标准.md'), 'utf-8'); // Upload 赛道一 standard await page.click('.section-header button'); await page.waitForTimeout(300); await page.fill('.standard-form input:first-child', '技术大赛-赛道一评审标准'); await page.fill('.standard-form input[placeholder*="标签"]', '赛道一'); await page.fill('.standard-form textarea', standard1); await ss(page, '04-standard-track1-form.png'); await page.click('.form-actions button:first-child'); await page.waitForTimeout(1000); // Upload 赛道二 standard await page.click('.section-header button'); await page.waitForTimeout(300); await page.fill('.standard-form input:first-child', '技术大赛-赛道二评审标准'); await page.fill('.standard-form input[placeholder*="标签"]', '赛道二'); await page.fill('.standard-form textarea', standard2); await ss(page, '05-standard-track2-form.png'); await page.click('.form-actions button:first-child'); await page.waitForTimeout(1000); await ss(page, '06-standards-list.png'); // STEP 4: Create entries via API (faster) console.log('\n=== 4. Create Entries ==='); const token = await page.evaluate(() => localStorage.getItem('token')); const auth = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }; // Real repos that actually exist const entries = [ { title: '张三 - 代码助手', repo_url: 'https://github.com/features/copilot', category_tag: '赛道一', participant: '张三', difficulty: '困难' }, { title: '李四 - TDD工具', repo_url: 'https://github.com/testdouble/tdd-toolkit', category_tag: '赛道一', participant: '李四', difficulty: '中等' }, { title: '王五 - IDE插件', repo_url: 'https://github.com/microsoft/vscode-extension-samples', category_tag: '赛道二', participant: '王五', difficulty: '困难' }, { title: '赵六 - 范式创新', repo_url: 'https://github.com/features/copilot', category_tag: '赛道二', participant: '赵六', difficulty: '中等' }, ]; for (const entry of entries) { await fetch(`http://localhost:3002/api/projects/${projectId}/entries`, { method: 'POST', headers: auth, body: JSON.stringify(entry), }); } await page.click('.tab:has-text("条目")'); await page.waitForTimeout(1000); await ss(page, '07-entries-list.png'); console.log('4 entries created'); // STEP 5: Start all reviews via API (faster) console.log('\n=== 5. Start Reviews via API ==='); for (const entry of entries) { const listRes = await fetch(`http://localhost:3002/api/projects/${projectId}/entries`, { headers: auth }); const { items } = await listRes.json(); const match = items.find(e => e.title === entry.title); if (match && match.status === 'pending') { await fetch(`http://localhost:3002/api/projects/${projectId}/entries/${match.id}/start`, { method: 'POST', headers: auth, }); console.log(` Started: ${entry.title}`); } } await page.reload(); await page.waitForTimeout(3000); await ss(page, '08-after-start.png'); // STEP 6: Force-complete for demo (since AI review needs real repos) console.log('\n=== 6. Force Complete for Demo ==='); const res = await fetch(`http://localhost:3002/api/projects/${projectId}/entries`, { headers: auth, }); const { items } = await res.json(); for (const entry of items) { await fetch(`http://localhost:3002/api/projects/${projectId}/entries/${entry.id}/force-review`, { method: 'PUT', headers: auth, body: JSON.stringify({ dimensions: [ { name: '开发范式设计清晰度', score: 3 + Math.floor(Math.random() * 3), maxScore: 5 }, { name: 'IDE集成深度', score: 3 + Math.floor(Math.random() * 3), maxScore: 5 }, { name: '提效幅度', score: 10 + Math.floor(Math.random() * 6), maxScore: 15 }, { name: '稳定性与易用性', score: 8 + Math.floor(Math.random() * 7), maxScore: 15 }, { name: 'AI使用日志', score: 3 + Math.floor(Math.random() * 2), maxScore: 10 }, { name: '演示与文档', score: 5 + Math.floor(Math.random() * 5), maxScore: 10 }, ] }), }); } // STEP 7: Show Summary with track-based rankings await page.reload(); await page.waitForTimeout(500); await page.click('.tab:has-text("汇总")'); await page.waitForTimeout(1000); await ss(page, '09-summary-tracks.png'); // STEP 8: Show detail panel await page.click('.tab:has-text("条目")'); await page.waitForTimeout(500); await page.reload(); await page.waitForTimeout(1000); const titles = await page.locator('.title-cell'); if (await titles.count() > 0) { await titles.first().click(); await page.waitForTimeout(800); await ss(page, '10-detail-panel.png'); } console.log('\n=== DONE ==='); console.log(`All screenshots: ${SCREENSHOTS}`); console.log(`App running at: http://localhost:14001 (password: ${password})`); } catch (err) { console.error('Error:', err.message); await page.screenshot({ path: path.join(SCREENSHOTS, 'error.png'), fullPage: true }); } } main().catch(console.error);