|
- 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
- })
|