25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 

142 satır
6.6 KiB

  1. import fs from 'node:fs'
  2. import path from 'node:path'
  3. const runId = (process.env.APE2E_RUN_ID || 'manual').replace(/[^A-Za-z0-9._-]+/g, '-').slice(0, 80) || 'manual'
  4. const runDirectory = path.resolve('artifacts', 'runs', runId)
  5. const outputFile = path.join(runDirectory, 'verification.json')
  6. const baseURL = (process.env.BASE_URL || 'http://127.0.0.1:8003').replace(/\/$/, '')
  7. const accountFile = path.resolve(process.env.E2E_ACCOUNT_FILE || '..', process.env.E2E_ACCOUNT_FILE ? '' : '账号.MD')
  8. function readCredentials() {
  9. const content = fs.readFileSync(accountFile, 'utf8')
  10. const username = content.match(/^\s*账号\s*[::]\s*(.+?)\s*$/m)?.[1]?.trim()
  11. const password = content.match(/^\s*密码\s*[::]\s*(.+?)\s*$/m)?.[1]?.trim()
  12. if (!username || !password) throw new Error('账号文件缺少“账号”或“密码”字段')
  13. return { username, password }
  14. }
  15. async function requestText(url, options = {}) {
  16. const response = await fetch(`${baseURL}${url}`, {
  17. ...options,
  18. signal: AbortSignal.timeout(30_000),
  19. })
  20. return { response, text: await response.text() }
  21. }
  22. function walkFiles(directory) {
  23. if (!fs.existsSync(directory)) return []
  24. const result = []
  25. for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
  26. const filename = path.join(directory, entry.name)
  27. if (entry.isDirectory()) result.push(...walkFiles(filename))
  28. else if (entry.isFile()) result.push(filename)
  29. }
  30. return result
  31. }
  32. function occurrenceCount(value, pattern) {
  33. return [...value.matchAll(pattern)].length
  34. }
  35. async function main() {
  36. fs.mkdirSync(runDirectory, { recursive: true })
  37. const { username, password } = readCredentials()
  38. const login = await requestText('/api/auth/v1/auth/login', {
  39. method: 'POST',
  40. headers: { 'content-type': 'application/json; charset=utf-8' },
  41. body: JSON.stringify({ roleCode: 'admin', username, password, rememberMe: false }),
  42. })
  43. if (!login.response.ok) throw new Error(`管理员后验收登录失败:HTTP ${login.response.status}`)
  44. const loginBody = JSON.parse(login.text)
  45. const token = loginBody?.data?.accessToken || loginBody?.data?.tokens?.accessToken
  46. if (!token) throw new Error('管理员后验收登录未返回 accessToken')
  47. const marker = 'APE2E'
  48. const keyword = encodeURIComponent(marker)
  49. const endpoints = [
  50. `/api/auth/v1/users?keyword=${keyword}&page=1&size=100`,
  51. '/api/auth/v1/departments/tree',
  52. '/api/auth/v1/roles/all',
  53. '/api/auth/v1/menus/tree',
  54. '/api/auth/v1/system-config',
  55. ...['LLM', 'TTS', 'ASR', 'VOICE_CLONE', 'SCENE'].map((type) => `/api/v1/capabilities?capabilityType=${type}&keyword=${keyword}&page=1&pageSize=100`),
  56. `/api/v1/avatars?keyword=${keyword}&page=1&pageSize=100`,
  57. `/api/v1/knowledge/bases?keyword=${keyword}&page=1&pageSize=100`,
  58. `/api/v1/knowledge/documents?keyword=${keyword}&page=1&pageSize=100`,
  59. `/api/v1/agent-projects?keyword=${keyword}&page=1&pageSize=100`,
  60. `/api/v1/realtime-agents?keyword=${keyword}&page=1&pageSize=100`,
  61. `/api/v1/sensitive-words?keyword=${keyword}&page=1&pageSize=100`,
  62. `/api/v1/wake-words?keyword=${keyword}&page=1&pageSize=100`,
  63. `/api/v1/hot-words?keyword=${keyword}&page=1&pageSize=100`,
  64. `/api/v1/remote-terminals?keyword=${keyword}&page=1&pageSize=100`,
  65. `/api/v1/tools?keyword=${keyword}&page=1&pageSize=100`,
  66. `/api/v1/video-projects?keyword=${keyword}&page=1&pageSize=100`,
  67. `/api/v1/videos?keyword=${keyword}&page=1&pageSize=100`,
  68. '/api/v1/video-folders',
  69. ]
  70. const endpointResults = []
  71. for (const endpoint of endpoints) {
  72. try {
  73. const result = await requestText(endpoint, { headers: { authorization: `Bearer ${token}` } })
  74. endpointResults.push({
  75. endpoint: endpoint.split('?')[0],
  76. status: result.response.status,
  77. markerOccurrences: occurrenceCount(result.text, /APE2E/gi),
  78. })
  79. } catch (error) {
  80. endpointResults.push({ endpoint: endpoint.split('?')[0], status: 0, markerOccurrences: -1, error: String(error?.message || error).slice(0, 300) })
  81. }
  82. }
  83. const textExtensions = new Set(['.md', '.json', '.html', '.txt'])
  84. const textFiles = walkFiles(runDirectory).filter((filename) => textExtensions.has(path.extname(filename).toLowerCase()) && filename !== outputFile)
  85. const artifactSafety = {
  86. scannedTextFiles: textFiles.length,
  87. passwordLiteralFiles: 0,
  88. passwordLiteralOccurrences: 0,
  89. bearerCandidates: 0,
  90. jwtCandidates: 0,
  91. generatedPasswordCandidates: 0,
  92. traceOrVideoFiles: walkFiles(runDirectory).filter((filename) => ['.zip', '.webm'].includes(path.extname(filename).toLowerCase())).length,
  93. }
  94. for (const filename of textFiles) {
  95. const value = fs.readFileSync(filename, 'utf8')
  96. const literalMatches = value.split(password).length - 1
  97. if (literalMatches > 0) artifactSafety.passwordLiteralFiles += 1
  98. artifactSafety.passwordLiteralOccurrences += literalMatches
  99. artifactSafety.bearerCandidates += occurrenceCount(value, /Bearer\s+[A-Za-z0-9._~-]{20,}/gi)
  100. artifactSafety.jwtCandidates += occurrenceCount(value, /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g)
  101. artifactSafety.generatedPasswordCandidates += occurrenceCount(value, /(?:Init|Final|Reset)@[A-Za-z0-9!@#$%^&*._~-]{4,}/g)
  102. }
  103. const endpointFailures = endpointResults.filter((item) => item.status !== 200 || item.markerOccurrences !== 0)
  104. const artifactLeaks = artifactSafety.passwordLiteralOccurrences
  105. + artifactSafety.bearerCandidates
  106. + artifactSafety.jwtCandidates
  107. + artifactSafety.generatedPasswordCandidates
  108. + artifactSafety.traceOrVideoFiles
  109. const verification = {
  110. runId,
  111. checkedAt: new Date().toISOString(),
  112. passed: endpointFailures.length === 0 && artifactLeaks === 0,
  113. artifactSafety,
  114. resourceSweep: {
  115. marker,
  116. checkedEndpoints: endpointResults.length,
  117. failedOrResidualEndpoints: endpointFailures.length,
  118. totalMarkerOccurrences: endpointResults.reduce((sum, item) => sum + Math.max(0, item.markerOccurrences), 0),
  119. endpoints: endpointResults,
  120. },
  121. }
  122. fs.writeFileSync(outputFile, `${JSON.stringify(verification, null, 2)}\n`, 'utf8')
  123. console.log(`Post-run verification: ${verification.passed ? 'PASS' : 'FAIL'}; endpoints=${endpointResults.length}; residual=${verification.resourceSweep.totalMarkerOccurrences}; leaks=${artifactLeaks}`)
  124. if (!verification.passed) process.exitCode = 1
  125. }
  126. main().catch((error) => {
  127. fs.mkdirSync(runDirectory, { recursive: true })
  128. fs.writeFileSync(outputFile, `${JSON.stringify({ runId, passed: false, error: String(error?.message || error).slice(0, 500) }, null, 2)}\n`, 'utf8')
  129. console.error(`Post-run verification failed: ${String(error?.message || error)}`)
  130. process.exitCode = 1
  131. })