import fs from 'node:fs' import path from 'node:path' const runId = (process.env.APE2E_RUN_ID || 'manual').replace(/[^A-Za-z0-9._-]+/g, '-').slice(0, 80) || 'manual' const runDirectory = path.resolve('artifacts', 'runs', runId) const outputFile = path.join(runDirectory, 'verification.json') const baseURL = (process.env.BASE_URL || 'http://127.0.0.1:8003').replace(/\/$/, '') const accountFile = path.resolve(process.env.E2E_ACCOUNT_FILE || '..', process.env.E2E_ACCOUNT_FILE ? '' : '账号.MD') function readCredentials() { const content = fs.readFileSync(accountFile, 'utf8') const username = content.match(/^\s*账号\s*[::]\s*(.+?)\s*$/m)?.[1]?.trim() const password = content.match(/^\s*密码\s*[::]\s*(.+?)\s*$/m)?.[1]?.trim() if (!username || !password) throw new Error('账号文件缺少“账号”或“密码”字段') return { username, password } } async function requestText(url, options = {}) { const response = await fetch(`${baseURL}${url}`, { ...options, signal: AbortSignal.timeout(30_000), }) return { response, text: await response.text() } } function walkFiles(directory) { if (!fs.existsSync(directory)) return [] const result = [] for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { const filename = path.join(directory, entry.name) if (entry.isDirectory()) result.push(...walkFiles(filename)) else if (entry.isFile()) result.push(filename) } return result } function occurrenceCount(value, pattern) { return [...value.matchAll(pattern)].length } async function main() { fs.mkdirSync(runDirectory, { recursive: true }) const { username, password } = readCredentials() const login = await requestText('/api/auth/v1/auth/login', { method: 'POST', headers: { 'content-type': 'application/json; charset=utf-8' }, body: JSON.stringify({ roleCode: 'admin', username, password, rememberMe: false }), }) if (!login.response.ok) throw new Error(`管理员后验收登录失败:HTTP ${login.response.status}`) const loginBody = JSON.parse(login.text) const token = loginBody?.data?.accessToken || loginBody?.data?.tokens?.accessToken if (!token) throw new Error('管理员后验收登录未返回 accessToken') const marker = 'APE2E' const keyword = encodeURIComponent(marker) const endpoints = [ `/api/auth/v1/users?keyword=${keyword}&page=1&size=100`, '/api/auth/v1/departments/tree', '/api/auth/v1/roles/all', '/api/auth/v1/menus/tree', '/api/auth/v1/system-config', ...['LLM', 'TTS', 'ASR', 'VOICE_CLONE', 'SCENE'].map((type) => `/api/v1/capabilities?capabilityType=${type}&keyword=${keyword}&page=1&pageSize=100`), `/api/v1/avatars?keyword=${keyword}&page=1&pageSize=100`, `/api/v1/knowledge/bases?keyword=${keyword}&page=1&pageSize=100`, `/api/v1/knowledge/documents?keyword=${keyword}&page=1&pageSize=100`, `/api/v1/agent-projects?keyword=${keyword}&page=1&pageSize=100`, `/api/v1/realtime-agents?keyword=${keyword}&page=1&pageSize=100`, `/api/v1/sensitive-words?keyword=${keyword}&page=1&pageSize=100`, `/api/v1/wake-words?keyword=${keyword}&page=1&pageSize=100`, `/api/v1/hot-words?keyword=${keyword}&page=1&pageSize=100`, `/api/v1/remote-terminals?keyword=${keyword}&page=1&pageSize=100`, `/api/v1/tools?keyword=${keyword}&page=1&pageSize=100`, `/api/v1/video-projects?keyword=${keyword}&page=1&pageSize=100`, `/api/v1/videos?keyword=${keyword}&page=1&pageSize=100`, '/api/v1/video-folders', ] const endpointResults = [] for (const endpoint of endpoints) { try { const result = await requestText(endpoint, { headers: { authorization: `Bearer ${token}` } }) endpointResults.push({ endpoint: endpoint.split('?')[0], status: result.response.status, markerOccurrences: occurrenceCount(result.text, /APE2E/gi), }) } catch (error) { endpointResults.push({ endpoint: endpoint.split('?')[0], status: 0, markerOccurrences: -1, error: String(error?.message || error).slice(0, 300) }) } } const textExtensions = new Set(['.md', '.json', '.html', '.txt']) const textFiles = walkFiles(runDirectory).filter((filename) => textExtensions.has(path.extname(filename).toLowerCase()) && filename !== outputFile) const artifactSafety = { scannedTextFiles: textFiles.length, passwordLiteralFiles: 0, passwordLiteralOccurrences: 0, bearerCandidates: 0, jwtCandidates: 0, generatedPasswordCandidates: 0, traceOrVideoFiles: walkFiles(runDirectory).filter((filename) => ['.zip', '.webm'].includes(path.extname(filename).toLowerCase())).length, } for (const filename of textFiles) { const value = fs.readFileSync(filename, 'utf8') const literalMatches = value.split(password).length - 1 if (literalMatches > 0) artifactSafety.passwordLiteralFiles += 1 artifactSafety.passwordLiteralOccurrences += literalMatches artifactSafety.bearerCandidates += occurrenceCount(value, /Bearer\s+[A-Za-z0-9._~-]{20,}/gi) artifactSafety.jwtCandidates += occurrenceCount(value, /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g) artifactSafety.generatedPasswordCandidates += occurrenceCount(value, /(?:Init|Final|Reset)@[A-Za-z0-9!@#$%^&*._~-]{4,}/g) } const endpointFailures = endpointResults.filter((item) => item.status !== 200 || item.markerOccurrences !== 0) const artifactLeaks = artifactSafety.passwordLiteralOccurrences + artifactSafety.bearerCandidates + artifactSafety.jwtCandidates + artifactSafety.generatedPasswordCandidates + artifactSafety.traceOrVideoFiles const verification = { runId, checkedAt: new Date().toISOString(), passed: endpointFailures.length === 0 && artifactLeaks === 0, artifactSafety, resourceSweep: { marker, checkedEndpoints: endpointResults.length, failedOrResidualEndpoints: endpointFailures.length, totalMarkerOccurrences: endpointResults.reduce((sum, item) => sum + Math.max(0, item.markerOccurrences), 0), endpoints: endpointResults, }, } fs.writeFileSync(outputFile, `${JSON.stringify(verification, null, 2)}\n`, 'utf8') console.log(`Post-run verification: ${verification.passed ? 'PASS' : 'FAIL'}; endpoints=${endpointResults.length}; residual=${verification.resourceSweep.totalMarkerOccurrences}; leaks=${artifactLeaks}`) if (!verification.passed) process.exitCode = 1 } main().catch((error) => { fs.mkdirSync(runDirectory, { recursive: true }) fs.writeFileSync(outputFile, `${JSON.stringify({ runId, passed: false, error: String(error?.message || error).slice(0, 500) }, null, 2)}\n`, 'utf8') console.error(`Post-run verification failed: ${String(error?.message || error)}`) process.exitCode = 1 })