// ==UserScript== // @name Allure Report 智能分析器 // @namespace http://tampermonkey.net/ // @version 3.4.0 // @description 自动分析 Allure 测试报告中的失败用例,全链路追溯根因,提供错误原因分析和修改建议 // @author AI Assistant // @match http://*/*/allure/* // @match https://*/*/allure/* // @match http://*/allure/* // @match https://*/allure/* // @match http://localhost:*/allure/* // @match http://127.0.0.1:*/allure/* // @match http://*/*/job/*/allure/* // @match https://*/*/job/*/allure/* // @match http://*/*/view/*/job/*/allure/* // @match https://*/*/view/*/job/*/allure/* // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @connect * // @run-at document-end // ==/UserScript== (function() { 'use strict'; // ==================== 配置 ==================== const CONFIG = { enableAutoAnalysis: true, analysisDelay: 1500, showStatistics: true, showDetailedStack: true, maxSuggestions: 5, retryInterval: 2000, maxRetries: 5, // AI 分析配置 enableAIAnalysis: true, // 是否启用 AI 分析(规则匹配失败时) aiProvider: 'custom', // AI 提供商:'doubao' | 'deepseek' | 'openai' | 'custom' aiApiKey: 'sk-7c7151b32d6e4d80a89fcea76e4493df', // API Key(留空则跳过 AI 分析) aiApiUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', // API 地址(阿里云 DashScope) aiModel: 'qwen-plus' // 模型名称 }; // ==================== 错误模式定义 ==================== const ERROR_PATTERNS = { assertion: { name: '断言失败', icon: '\u274C', color: '#fd6056', patterns: [ /AssertionError/i, /assert\s/i, /expect.*fail/i, /should.*but/i, /does not contain/i, /is not.*equal/i, /Failed assertion/i ], suggestions: [ '检查预期值与实际值的差异,确认断言条件是否符合当前业务场景', '验证测试数据的正确性,确保测试前置数据未过期或被修改', '确认断言逻辑是否与最新需求一致,需求变更后需同步更新断言', '检查是否存在并发或时序问题导致中间状态与预期不符' ] }, element: { name: '元素未找到', icon: '\uD83D\uDD0D', color: '#ff8800', patterns: [ /NoSuchElement/i, /ElementNotVisible/i, /element.*not.*found/i, /element.*not.*interactable/i, /no such element/i, /Unable to locate/i, /StaleElementReference/i ], suggestions: [ '检查元素选择器是否正确(CSS/XPath)', '增加显式等待时间,确保元素加载完成后再操作', '确认页面是否已完全加载,检查是否有异步渲染', '检查元素是否在 iframe 或 shadow DOM 中' ] }, timeout: { name: '超时错误', icon: '\u23F0', color: '#ffaa00', patterns: [ /Timeout/i, /Timed?\s*out/i, /timeout.*exceeded/i, /waitFor.*timeout/i, /request.*timeout/i ], suggestions: [ '增加超时时间配置,适配当前网络/服务响应速度', '优化等待策略,使用显式等待代替固定等待', '检查被测服务端是否响应正常(日志、监控)', '考虑添加重试机制处理偶发超时' ] }, network: { name: '网络错误', icon: '\uD83C\uDF10', color: '#4488ff', patterns: [ /ConnectionError/i, /NetworkError/i, /Connection refused/i, /ERR_.*_FAILED/i, /fetch.*failed/i, /HTTP.*5\d{2}/i, /HTTP.*4\d{2}/i ], suggestions: [ '检查网络连接和服务端可用性', '验证 API 接口地址和请求参数是否正确', '检查认证令牌(Token)是否过期', '添加请求重试和错误处理机制' ] }, null: { name: '空值错误', icon: '\u26A0\uFE0F', color: '#ff6600', patterns: [ /NullPointer/i, /NoneType/i, /undefined.*is not/i, /null.*reference/i, /Cannot read propert/i, /of undefined/i, /of null/i, /AttributeError.*None/i ], suggestions: [ '检查变量初始化是否正确,确认数据源返回了预期数据', '添加空值检查(if obj is not None / if not obj)', '使用安全访问方式(如 Python 的 getattr 或 dict.get)', '添加默认值处理,避免链式调用中出现空值' ] }, type: { name: '类型错误', icon: '\uD83D\uDCDD', color: '#9944ff', patterns: [ /TypeError/i, /is not a function/i, /is not iterable/i, /cannot.*convert/i, /unsupported operand/i, /TypeError.*NoneType/i ], suggestions: [ '检查数据类型是否符合预期', '添加类型转换或验证逻辑', '确认函数参数类型和返回值正确', '使用类型检查工具(如 mypy)进行静态分析' ] }, permission: { name: '权限错误', icon: '\uD83D\uDD12', color: '#cc0000', patterns: [ /PermissionDenied/i, /AccessDenied/i, /unauthorized/i, /forbidden/i, /403/i ], suggestions: [ '检查用户权限配置和认证状态', '验证认证令牌(Token)是否有效', '确认角色和权限分配正确' ] }, database: { name: '数据库错误', icon: '\uD83D\uDCBE', color: '#0088cc', patterns: [ /DatabaseError/i, /IntegrityError/i, /SQL.*error/i, /duplicate.*key/i, /constraint.*violation/i, /table.*doesn'?t exist/i, /OperationalError/i ], suggestions: [ '检查数据库连接配置和 SQL 语句语法', '确认表结构和字段是否存在', '检查事务处理和回滚逻辑', '查看数据库日志获取详细错误信息' ] }, file: { name: '文件错误', icon: '\uD83D\uDCC1', color: '#888888', patterns: [ /FileNotFoundError/i, /PermissionError/i, /ENOENT/i, /cannot open file/i, /No such file/i ], suggestions: [ '检查文件路径是否正确', '确认文件是否存在于指定位置', '验证文件读写权限设置', '添加文件存在性检查后再操作' ] }, api_version: { name: '接口版本不兼容', icon: '\uD83D\uDD04', color: '#e67e22', patterns: [ /接口.*不存在|不存在.*接口|api.*not.*found|endpoint.*not.*found/i, /版本.*不匹配|不匹配.*版本|version.*mismatch|incompatible.*version/i, /高低版本|高版本|低版本|版本.*差异/i, /接口.*废弃|废弃.*接口|deprecated.*api|api.*deprecated/i, /接口.*移除|移除.*接口|removed.*api|api.*removed/i, /接口.*升级|升级.*接口|api.*upgrade|upgrade.*api/i, /404.*接口|接口.*404/i, /接口.*变更|变更.*接口|接口.*调整|调整.*接口/i, /api.*version|version.*api/i, /接口.*不支持|不支持.*接口/i, /接口.*返回.*为空|接口.*无数据/i ], suggestions: [ '检查当前测试环境的服务版本是否与接口定义匹配,高/低版本可能导致接口不存在或参数变更', '对比不同版本的服务端文档,确认接口在当前版本是否已被废弃或移除', '检查接口请求的版本号参数(如 api-version、v 参数)是否正确', '如果接口确实不存在于当前版本,建议将用例标记为跳过或适配新版接口', '检查测试环境是否切换到了正确的分支/版本,避免高低版本混用导致接口缺失' ] }, config_intercept: { name: '配置拦截', icon: '\u2699\uFE0F', color: '#e67e22', patterns: [ /配置.*拦截|拦截.*配置|被配置拦截|配置导致|配置变更/i, /开关.*开启|开启.*开关|功能开关|系统配置|系统设置/i, /低于.*不允许|高于.*不允许|超出.*限制|限制.*条件|不满足.*条件/i, /配置.*生效|生效.*配置|配置项|参数设置|参数配置/i, /规则.*触发|触发.*规则|校验.*规则|规则校验|前置.*校验/i, /配置原因|配置问题|配置冲突|配置不匹配/i, /因为.*配置|由于.*配置|配置导致.*失败/i, /未满足.*配置条件|配置条件.*不满足|条件.*拦截/i ], suggestions: [ '《配置开关已开启》某个系统配置/开关开启后触发了前置校验,导致业务流程走了拦截分支,在用例前置步骤中关闭该开关', '《用例缺少前置配置》用例缺少设置系统配置的前置步骤,在 setUp 阶段加入关闭相关配置开关的操作', '《断言逻辑未跟进配置变更》配置变更后业务流程已改变,断言仍按配置关闭时的逻辑编写,按当前配置下的实际返回值更新断言', '《环境配置状态不一致》测试环境与预期配置不一致,核对并还原正确的系统配置后重新执行用例' ] }, index_error: { name: '数据未就绪', icon: '\uD83D\uDCED', color: '#e74c3c', patterns: [ /list index out of range/i, /index.*out.*of.*range/i, /IndexError/i, /list.*empty|empty.*list/i, /sequence.*out.*of.*range/i, /index.*error/i ], suggestions: [ '《前置接口未成功》查看当前步骤之前的接口(新增/创建类)是否正常返回,列表为空通常表示前置数据未创建成功', '《查询时数据尚未入库》新增操作后立即查询可能取到空列表,需要在查询前添加等待时间或轮询确认数据已持久化', '《断言条件过于严格》确认过滤条件是否过严,导致实际匹配的数据条数为0', '《环境数据未初始化》检查测试环境预设数据是否缺失,导致查询时列表为空' ] } }; // ==================== Allure API 客户端 ==================== const AllureAPI = { _baseUrl: null, // 获取 Allure 报告的基路径 getBaseUrl() { if (this._baseUrl) return this._baseUrl; const href = window.location.href; // 匹配 .../allure/... 的路径 const match = href.match(/^(https?:\/\/[^/]+\/.*?\/allure\/)/i); if (match) { this._baseUrl = match[1].replace(/\/$/, ''); } else { // 退回当前路径 this._baseUrl = window.location.origin + window.location.pathname.replace(/\/index\.html.*$/, '').replace(/\/$/, ''); } return this._baseUrl; }, // 构建完整 URL buildUrl(path) { return this.getBaseUrl() + '/' + path; }, // 通过 GM_xmlhttpRequest 获取纯文本内容(用于附件) async fetchText(path, retries = 2) { const url = this.buildUrl(path); console.log(`[Allure Analyzer] fetchText 请求:${url} (剩余重试:${retries})`); // 优先尝试 GM_xmlhttpRequest if (typeof GM_xmlhttpRequest === 'function') { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 30000, onload: (response) => { if (response.status >= 400) { console.error(`[Allure Analyzer] GM_xmlhttpRequest HTTP错误:${response.status}`); if (retries > 0) { setTimeout(() => resolve(this.fetchText(path, retries - 1)), 1000); } else { resolve(response.responseText || null); } return; } console.log('[Allure Analyzer] GM_xmlhttpRequest 响应:', response.status, '长度:', response.responseText?.length || 0); resolve(response.responseText !== undefined ? response.responseText : null); }, onerror: (err) => { console.error('[Allure Analyzer] GM_xmlhttpRequest 错误:', err); if (retries > 0) { console.log(`[Allure Analyzer] 网络错误,1秒后重试...`); setTimeout(() => resolve(this.fetchText(path, retries - 1)), 1000); } else { // GM_xmlhttpRequest 彻底失败,降级到 fetch console.log('[Allure Analyzer] GM_xmlhttpRequest 失败,降级使用 fetch'); this._fetchWithFetch(url).then(resolve); } }, ontimeout: () => { console.error('[Allure Analyzer] GM_xmlhttpRequest 超时:', url); if (retries > 0) { console.log(`[Allure Analyzer] 超时,1秒后重试...`); setTimeout(() => resolve(this.fetchText(path, retries - 1)), 1000); } else { // 超时,降级到 fetch console.log('[Allure Analyzer] GM_xmlhttpRequest 超时,降级使用 fetch'); this._fetchWithFetch(url).then(resolve); } } }); }); } else { // GM_xmlhttpRequest 不可用,直接使用 fetch console.log('[Allure Analyzer] GM_xmlhttpRequest 不可用,使用 fetch'); return this._fetchWithFetch(url); } }, // 使用原生 fetch 获取文本(兜底方案) async _fetchWithFetch(url) { return fetch(url).then(r => { console.log('[Allure Analyzer] fetch 响应状态:', r.status); if (!r.ok) { console.error(`[Allure Analyzer] fetch HTTP错误:${r.status}`); return null; } return r.text(); }).then(text => { if (text !== null && text !== undefined) { console.log('[Allure Analyzer] fetch 内容长度:', text.length); } return text !== null && text !== undefined ? text : null; }).catch(err => { console.error('[Allure Analyzer] fetch 彻底失败:', err); return null; }); }, // 通过 GM_xmlhttpRequest 获取数据 async fetchJSON(path) { const url = this.buildUrl(path); return new Promise((resolve) => { if (typeof GM_xmlhttpRequest !== 'function') { console.warn('[Allure Analyzer] GM_xmlhttpRequest 不可用,使用原生 fetch'); // 降级到原生 fetch fetch(url).then(r => r.json()).then(resolve).catch(() => resolve(null)); return; } GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 10000, onload: (response) => { try { const data = JSON.parse(response.responseText); console.log(`[Allure Analyzer] 获取成功: ${path}`, data); resolve(data); } catch (e) { console.warn('[Allure Analyzer] JSON 解析失败:', url, e); resolve(null); } }, onerror: (err) => { console.warn('[Allure Analyzer] 请求失败:', url, err); resolve(null); }, ontimeout: () => { console.warn('[Allure Analyzer] 请求超时:', url); resolve(null); } }); }); }, // 获取所有测试用例的结果 async fetchTestResults() { // 首先获取 summary 了解总体情况 const summary = await this.fetchJSON('widgets/summary.json'); if (summary && summary.statistic) { console.log('[Allure Analyzer] Summary:', summary.statistic); } // 策略1:尝试 data/test-cases.json(标准路径) console.log('[Allure Analyzer] 尝试策略1: data/test-cases.json...'); let testCases = await this.fetchJSON('data/test-cases.json'); if (testCases && Array.isArray(testCases)) { console.log('[Allure Analyzer] 策略1成功,获取到', testCases.length, '个用例'); return testCases; } console.log('[Allure Analyzer] 策略1失败'); // 策略2:尝试 data/test-cases/index.json console.log('[Allure Analyzer] 尝试策略2: data/test-cases/index.json...'); testCases = await this.fetchJSON('data/test-cases/index.json'); if (testCases && Array.isArray(testCases)) { console.log('[Allure Analyzer] 策略2成功,获取到', testCases.length, '个用例'); return testCases; } console.log('[Allure Analyzer] 策略2失败'); // 策略3:从 data/behaviors.json 递归提取所有用例 console.log('[Allure Analyzer] 尝试策略3: data/behaviors.json...'); const behaviors = await this.fetchJSON('data/behaviors.json'); console.log('[Allure Analyzer] behaviors.json 数据:', behaviors); if (behaviors && behaviors.children) { console.log('[Allure Analyzer] 策略3开始提取用例...'); const allCases = this.extractAllCases(behaviors.children); console.log(`[Allure Analyzer] 策略3成功,提取到 ${allCases.length} 个用例`); return allCases; } console.log('[Allure Analyzer] 策略3失败,behaviors:', behaviors); console.log('[Allure Analyzer] 所有策略都失败,返回空数组'); return []; }, // 递归提取 behaviors 中的所有用例 extractAllCases(nodes) { const cases = []; if (!nodes || !Array.isArray(nodes)) return cases; for (const node of nodes) { // 如果是叶子节点(用例),且包含 uid 和 status if (node.uid && node.status && !node.children) { cases.push(node); } // 如果有 children,递归提取 if (node.children && Array.isArray(node.children)) { cases.push(...this.extractAllCases(node.children)); } } return cases; }, // 获取单个测试用例的详细数据 async fetchTestCaseDetail(uid) { if (!uid) return null; console.log(`[Allure Analyzer] 正在获取用例详情: ${uid}`); const detail = await this.fetchJSON(`data/test-cases/${uid}.json`); if (detail) { console.log(`[Allure Analyzer] 用例 ${uid} 详情结构:`, Object.keys(detail)); console.log(`[Allure Analyzer] 用例 ${uid} detail.testStage:`, detail.testStage ? '存在' : '不存在'); console.log(`[Allure Analyzer] 用例 ${uid} detail.beforeStages:`, detail.beforeStages?.length); console.log(`[Allure Analyzer] 用例 ${uid} detail.afterStages:`, detail.afterStages?.length); // 辅助函数:递归提取所有步骤(支持嵌套) const extractAllSteps = (stepsArray) => { if (!stepsArray || stepsArray.length === 0) return []; const allSteps = []; stepsArray.forEach(step => { // 添加当前步骤 allSteps.push(step); // 递归提取子步骤 if (step.steps && step.steps.length > 0) { allSteps.push(...extractAllSteps(step.steps)); } }); return allSteps; }; // Allure JSON 结构中,steps 分布在 testStage、beforeStages 和 afterStages 中 // 按阶段组织:Set up → Test body → Tear down,并在步骤上打 _phase 标记 let allSteps = []; const tagAndExtract = (stepsArray, phase) => { const flat = extractAllSteps(stepsArray); flat.forEach(s => { s._phase = phase; }); return flat; }; // 1. Set up:beforeStages(优先放最前面) if (detail.beforeStages) { detail.beforeStages.forEach(stage => { if (stage.steps && stage.steps.length > 0) { const extracted = tagAndExtract(stage.steps, 'Set up'); allSteps = allSteps.concat(extracted); } else if (stage.attachments && stage.attachments.length > 0) { const stageAsStep = { name: stage.name, status: stage.status || 'passed', statusMessage: stage.statusMessage || '', attachments: stage.attachments, _phase: 'Set up' }; allSteps.push(stageAsStep); } }); } // 2. Test body:testStage if (detail.testStage && detail.testStage.steps && detail.testStage.steps.length > 0) { const extracted = tagAndExtract(detail.testStage.steps, 'Test body'); allSteps = allSteps.concat(extracted); console.log(`[Allure Analyzer] Test body 提取 ${extracted.length} 步`); } // 3. Tear down:afterStages if (detail.afterStages) { detail.afterStages.forEach(stage => { if (stage.steps && stage.steps.length > 0) { const extracted = tagAndExtract(stage.steps, 'Tear down'); allSteps = allSteps.concat(extracted); } else if (stage.attachments && stage.attachments.length > 0) { const stageAsStep = { name: stage.name, status: stage.status || 'passed', statusMessage: stage.statusMessage || '', attachments: stage.attachments, _phase: 'Tear down' }; allSteps.push(stageAsStep); } }); } // 4. 兼容顶层 steps(旧结构) if (detail.steps && detail.steps.length > 0) { const extracted = tagAndExtract(detail.steps, 'Test body'); allSteps = allSteps.concat(extracted); console.log(`[Allure Analyzer] 顶层 steps 提取 ${extracted.length} 步`); } // 将合并后的 steps 赋值给 detail.steps detail.steps = allSteps; // 异步加载步骤附件内容:失败/异常步骤 + login步骤 + 所有有附件的步骤 const attLoadTasks = []; detail.steps.forEach(step => { if (step.attachments && step.attachments.length > 0) { // 加载所有附件(不管步骤状态) step.attachments.forEach(att => { if (att && att.source) { console.log(`[Allure Analyzer] 准备加载附件: [${step.name}] ${att.name} | source=${att.source}`); attLoadTasks.push( this.fetchText(`data/attachments/${att.source}`).then(content => { // 保存完整内容(不截取),在构建 prompt 时再截取 att._content = content !== null && content !== undefined ? content : null; console.log(`[Allure Analyzer] 附件加载结果:[${step.name}] ${att.name} | 内容长度=${content?.length || 0}`); }).catch(err => { console.error(`[Allure Analyzer] 附件加载失败:[${step.name}] ${att.name} |`, err); }) ); } else { console.warn(`[Allure Analyzer] 附件无source字段: [${step.name}] ${att.name}`); } }); } else { console.log(`[Allure Analyzer] 步骤无附件: [${step.name}]`); } }); if (attLoadTasks.length > 0) { await Promise.all(attLoadTasks); } } else { console.log(`[Allure Analyzer] 用例 ${uid} 详情获取失败,返回 null`); } return detail; }, // 获取 categories 数据 async fetchCategories() { return await this.fetchJSON('widgets/categories.json'); }, // 获取 behaviors 数据 async fetchBehaviors() { return await this.fetchJSON('widgets/behaviors.json'); }, // 获取 suites 数据 async fetchSuites() { return await this.fetchJSON('widgets/suites.json'); } }; // ==================== 错误分析引擎 ==================== const ErrorAnalyzer = { classifyError(errorMessage, stackTrace) { const fullText = `${errorMessage}\n${stackTrace || ''}`; for (const [type, config] of Object.entries(ERROR_PATTERNS)) { for (const pattern of config.patterns) { if (pattern.test(fullText)) { return { type, ...config }; } } } return { type: 'unknown', name: '未知错误', icon: '\u2753', color: '#888888', suggestions: [ '查看详细错误日志,搜索相关错误信息', '检查测试环境和依赖是否正常', '对比最近通过和失败的差异,定位变更点', '联系开发团队协助排查' ] }; }, analyzeStackTrace(stackTrace) { if (!stackTrace) return { location: null, lines: [] }; const lines = stackTrace.split('\n'); const projectFiles = []; const frameworkPatterns = [ /site-packages/, /lib\/python/, /node_modules/, /org\/junit/, /org\/testng/, /com\/sun/, /java\.lang\./, /pytest[_\/]/, /pluggy/, /_pytest/, /conftest/ ]; for (const line of lines) { const isFramework = frameworkPatterns.some(p => p.test(line)); if (!isFramework && (line.includes('.py') || line.includes('.js') || line.includes('.java') || line.includes('.ts'))) { const location = this.extractLocation(line); if (location) { projectFiles.push({ line: line.trim(), ...location }); } } } return { location: projectFiles[0] || null, lines: projectFiles }; }, extractLocation(line) { // Python: File "path/to/file.py", line 123, in method_name const pythonMatch = line.match(/File\s+"([^"]+)",\s*line\s+(\d+)(?:,\s*in\s+(\w+))?/); if (pythonMatch) { return { file: pythonMatch[1], line: parseInt(pythonMatch[2]), method: pythonMatch[3] || null }; } // Python pytest: path/to/file.py:123:AssertionError const pytestMatch = line.match(/^([\w./\\-]+\.py):(\d+)/m); if (pytestMatch) { return { file: pytestMatch[1], line: parseInt(pytestMatch[2]) }; } // JavaScript: at Object.method (path/to/file.js:123:45) const jsMatch = line.match(/at\s+(?:[\w.]+\s+)?\(?(.+?):(\d+):(\d+)\)?/); if (jsMatch) { return { file: jsMatch[1], line: parseInt(jsMatch[2]), column: parseInt(jsMatch[3]) }; } // Java: at com.example.Class.method(Class.java:123) const javaMatch = line.match(/at\s+[\w.$]+\.([\w]+)\(([\w.]+\.java):(\d+)\)/); if (javaMatch) { return { file: javaMatch[2], line: parseInt(javaMatch[3]), method: javaMatch[1] }; } return null; }, analyze(errorData) { const { message, trace, name, fullName, steps } = errorData; const errorType = this.classifyError(message, trace); const stackAnalysis = this.analyzeStackTrace(trace); // 调用 AssertionAnalyzer 进行断言子类型深度分析 const deepAnalysis = AssertionAnalyzer.deepAnalyze(message, trace, steps || []); // 如果断言深度分析有更具体的建议,优先使用 let finalSuggestions = errorType.suggestions.slice(0, CONFIG.maxSuggestions); if (deepAnalysis.specificSuggestions && deepAnalysis.specificSuggestions.length > 0) { finalSuggestions = deepAnalysis.specificSuggestions.slice(0, CONFIG.maxSuggestions); } return { errorType, stackAnalysis, message: message || '无错误消息', trace: trace || '', name: name || '', fullName: fullName || '', suggestions: finalSuggestions, stepAnalysis: [], // 规则分析无逐条分析,AI 分析才会填充 conclusion: '', // 规则分析无结论,AI 分析才会填充 // 新增:断言深度分析 assertionDeep: deepAnalysis }; }, // 直接 AI 分析模式(跳过规则匹配) async analyzeWithFallback(errorData) { // 打印当前配置状态 console.log('[Allure Analyzer] 配置状态:', { enableAIAnalysis: CONFIG.enableAIAnalysis, hasApiKey: !!CONFIG.aiApiKey, apiKeyPrefix: CONFIG.aiApiKey ? CONFIG.aiApiKey.substring(0, 10) + '...' : '空' }); console.log('[Allure Analyzer] errorData.steps:', errorData?.steps); console.log('[Allure Analyzer] errorData.steps.length:', errorData?.steps?.length); // 直接调用 AI 获取完整分析结果(包含 stepAnalysis + conclusion) if (CONFIG.enableAIAnalysis && CONFIG.aiApiKey) { console.log('[Allure Analyzer] 开始 AI 分析...'); try { const aiResult = await AIAnalyzer.analyze(errorData); if (aiResult) { console.log('[Allure Analyzer] AI 分析成功:', { errorType: aiResult.errorType?.name, stepAnalysisCount: aiResult.stepAnalysis?.length, conclusion: aiResult.conclusion?.substring(0, 100) }); return aiResult; } else { console.warn('[Allure Analyzer] AI 返回空结果'); } } catch (error) { console.error('[Allure Analyzer] AI 分析失败:', error); } } else { console.log('[Allure Analyzer] AI 未启用或未配置 API Key'); } // AI 不可用时,返回基础结构(避免崩溃) return { errorType: { type: 'unknown', name: '待分析', icon: '\u2753', color: '#888888' }, stepAnalysis: [], conclusion: '', suggestions: ['等待 AI 分析...'], message: errorData?.message || '', name: errorData?.name || '', fullName: errorData?.fullName || '', trace: errorData?.trace || '' }; } }; // ==================== AI 智能分析引擎 ==================== const AIAnalyzer = { // 搜索知识库中的相似案例 async searchKnowledge(errorData) { try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: 'http://172.16.0.68:5000/api/knowledge/search', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ errorTypeCode: errorData?.errorType?.type || '', issueType: errorData?.issueType || '', conclusion: errorData?.conclusion || '', message: errorData?.message || '' }), timeout: 5000, onload: (response) => { if (response.status === 200) { try { resolve(JSON.parse(response.responseText)); } catch (e) { reject(e); } } else { reject(new Error(`HTTP ${response.status}`)); } }, onerror: reject, ontimeout: reject }); }); if (response.success && response.results && response.results.length > 0) { console.log('[知识库] 找到', response.count, '条相似案例'); return response.results; } else { console.log('[知识库] 无匹配案例'); return []; } } catch (error) { console.warn('[知识库] 搜索失败:', error); return []; } }, // 构建 AI 分析提示词 buildPrompt(errorData, knowledgeResults = []) { const { message, trace, name, fullName, steps } = errorData; // 按 Set up / Test body / Tear down 分阶段构建步骤上下文 // 优先使用预加载的 _content(实际附件内容),而非无效的 value 文件名 let apiDataContext = ''; if (steps && steps.length > 0) { console.log('[Allure Analyzer] 步骤数量:', steps.length); const phases = ['Set up', 'Test body', 'Tear down']; const grouped = {}; phases.forEach(p => { grouped[p] = []; }); steps.forEach(s => { const ph = s._phase || 'Test body'; if (!grouped[ph]) grouped[ph] = []; grouped[ph].push(s); }); const phaseLines = []; phases.forEach(phase => { const phaseSteps = grouped[phase]; if (!phaseSteps || phaseSteps.length === 0) return; const lines = phaseSteps.map((s, i) => { const isBroken = s.status === 'broken'; const isFailed = s.status === 'failed'; const statusIcon = isFailed ? '失败' : isBroken ? '️异常(黄色)' : s.status === 'passed' ? '✅成功' : '跳过'; const stepMsg = s.statusMessage || (s.statusDetails && s.statusDetails.message) || ''; let line = ` ${i+1}. [${statusIcon}] ${s.name || ''}`; if (stepMsg) line += `\n 错误消息: ${stepMsg.substring(0, 200)}`; // 使用预加载的 _content(真实附件内容),优先取 response/响应 相关附件 const isLoginStep = /login|登录|token|auth|profile/i.test(s.name || ''); if (s.attachments && s.attachments.length > 0) { const respAtt = s.attachments.find(a => /response|响应|result|结果|body|返回|request/i.test(a.name || '') ) || s.attachments[0]; const content = respAtt && respAtt._content; if (content) { console.log(`[Allure Analyzer] 步骤"${s.name}"附件内容长度: ${content.length}`); // 提取附件关键信息:json(实际数据) + response(wrapper) let excerpt = ''; const jsonIdx = content.indexOf('"json"'); const responseIdx = content.indexOf('"response"'); const messageIdx = content.indexOf('"message":'); // 优先提取 json 字段(包含实际业务数据如 id、stopped 等) if (jsonIdx !== -1) { const jsonEnd = Math.min(jsonIdx + 800, content.length); excerpt += content.substring(jsonIdx, jsonEnd) + '\n'; } // 再提取 response/message 部分 if (messageIdx !== -1) { const msgStart = Math.max(messageIdx - 50, 0); // 往前50字符保留上下文 excerpt += content.substring(msgStart, msgStart + 1500); } else if (responseIdx !== -1) { excerpt += content.substring(responseIdx, responseIdx + 1500); } else { // 兜底:取最后 2000 字符 excerpt += content.length > 2000 ? content.substring(content.length - 2000) : content; } line += `\n 附件内容(${respAtt.name || 'attachment'}): ${excerpt}`; // 自动检测分页问题:total > pageSize 时,目标数据可能在后续页面 const totalMatch = content.match(/"total"\s*:\s*(\d+)/); const pageSizeMatch = content.match(/"pageSize"\s*:\s*(\d+)/) || content.match(/"count"\s*:\s*(\d+)/); const pageIndexMatch = content.match(/"pageIndex"\s*:\s*(\d+)/) || content.match(/"page"\s*:\s*(\d+)/); if (totalMatch && pageSizeMatch) { const total = parseInt(totalMatch[1]); const pageSize = parseInt(pageSizeMatch[1]); const pageIndex = pageIndexMatch ? parseInt(pageIndexMatch[1]) : 1; if (total > pageSize) { const totalPages = Math.ceil(total / pageSize); const currentPage = pageIndex || 1; line += `\n ️【分页检测】total=${total}, pageSize=${pageSize}, 当前第${currentPage}页, 共${totalPages}页`; if (currentPage < totalPages) { line += ` — 目标数据可能在第${currentPage + 1}页!请求第${currentPage}页仅返回前${pageSize}条,但总共有${total}条`; } } } } else if (isBroken || isFailed || isLoginStep) { // 异常/失败/登录步骤但附件内容为空:触发回溯分析 console.log(`[Allure Analyzer] 步骤"${s.name}"附件内容为空,attachments数量: ${s.attachments.length}`); // 即使附件空,也传步骤名称+参数信息给AI,而非仅[无法加载] let fallbackCtx = `[附件未加载]`; if (s.name) fallbackCtx += ` 步骤名称: ${s.name.replace(/\n/g, '; ')}`; if (s.parameters && s.parameters.length > 0) { fallbackCtx += ` 参数: ` + s.parameters.map(p => `${p.name}=${JSON.stringify(p.value)}`).join(', '); } fallbackCtx += ` ⚠️【强制约束】附件未加载,你无法看到该步骤的实际请求参数。\n → 禁止用其他步骤的数据(如"步骤22有stopped参数且返回了数据")来推断该步骤缺少某参数\n → 可能原因:(1)该步骤也有此参数但附件没加载 (2)参数值不正确 (3)其他筛选条件导致无数据 (4)分页问题\n → 正确做法:列出所有可能原因,建议用户检查该步骤的实际请求参数`; line += `\n 附件内容: ${fallbackCtx}`; } } else if (isBroken || isFailed || isLoginStep) { // 没有附件字段:同样触发回溯,传步骤名称+参数给AI console.log(`[Allure Analyzer] 步骤"${s.name}"无附件字段`); let fallbackCtx = `[无附件]`; if (s.name) fallbackCtx += ` 步骤名称: ${s.name.replace(/\n/g, '; ')}`; if (s.parameters && s.parameters.length > 0) { fallbackCtx += ` 参数: ` + s.parameters.map(p => `${p.name}=${JSON.stringify(p.value)}`).join(', '); } fallbackCtx += ` ️【强制约束】无附件数据,你无法看到该步骤的实际请求参数。\n → 禁止用其他步骤的数据(如"步骤22有stopped参数且返回了数据")来推断该步骤缺少某参数\n → 可能原因:(1)该步骤也有此参数但无附件 (2)参数值不正确 (3)其他筛选条件导致无数据 (4)分页问题\n → 正确做法:列出所有可能原因,建议用户检查该步骤的实际请求参数`; line += `\n 附件内容: ${fallbackCtx}`; } return line; }).join('\n'); phaseLines.push(`\n**[阶段: ${phase}]**\n${lines}`); }); apiDataContext = `\n\n**测试执行步骤(Set up → Test body → Tear down)**` + phaseLines.join('\n'); console.log('[Allure Analyzer] 传递给 AI 的 apiDataContext 前800字符:', apiDataContext.substring(0, 800)); } else { console.log('[Allure Analyzer] 没有步骤数据'); } return `你是一个业务自动化测试失败分析专家。以下是测试执行三个阶段的数据(⚠️黄色箭头=broken异常,❌红色箭头=failed失败)。 **分析规则(按优先级依次检查)**: 🔍 **第一优先级:登录/认证步骤(任何阶段)** - **必须检查所有阶段中名称含 login/登录/token/auth/profile 的步骤**,无论其状态(✅/⚠️/❌) - 查看这些步骤的「附件内容」字段(即真实的 request & response 数据) - 如果附件内容里的 message 字段包含错误信息(如"账号或密码错误"、"认证失败"、"401"等) → **直接以该 message 作为根因输出,不再分析后续步骤** → **问题类型=test_case_issue,错误类型=permission** 🔍 **第二优先级:Test body 阶段中有 ⚠️异常(黄色) 或 ❌失败(红色) 的步骤**(仅在 Set up 全部正常时) - 查看错误消息和附件内容 - **🚨 附件数据结构规则(强制规则)**:附件内容可能包含两层数据: - 'json: {...}' — **实际业务数据**(如 id、stopped、billNumber 等字段),这是接口真正返回的数据 - 'response: {code:200, message:..., data:null}' — **框架包装层**(data 可能为 null 或空) - **关键判断**:如果 response.data 为 null 但 json 字段中有具体的业务字段(如 id=xxx, stopped=true) → **接口已成功返回数据**,不要因为 response.data 为 null 就断言"未返回数据" → 应该从 json 字段中提取实际的返回值进行分析 - **禁止**:只看 response.data 不看 json 字段就下结论 - **🚨 停用/删除/作废操作识别规则(强制规则)**: - 停用(disable/stop)、删除(delete)、作废(cancel) 类操作的 ID 是从请求参数中传入的,不是从返回值中获取的 - **不要在 response 中寻找这些操作的 ID**,ID 在 json 请求体字段中(如 id、billId、billNumber、docId 等) - 如果步骤名称包含"停用"、"删除"、"作废"、"关闭"、"撤回"、"作废"等关键词,这是变更操作而非查询操作 - **判断停用/删除是否成功的标准**:看 response.code 是否为 200/0、response.message 是否有错误提示,而不是看 response 是否返回 ID - **禁止**:对停用/删除类步骤说"未返回 ID"或"未返回单据ID供后续使用",因为这类步骤的设计目的不是返回 ID(ID 本身就是从前面步骤拿到的输入参数) - **🚨 筛选参数存在性 vs 正确性规则(强制规则)**: - **存在性判断**:只要请求参数的 json 字段中出现了筛选参数(如 stopped、validityFlag、offerFlag 等),就说明该步骤已经对该维度做了筛选 → **禁止说"未筛选 XXX"或"缺少 XXX 筛选条件"** - **正确性判断**:参数存在 ≠ 参数值正确,需结合步骤名称/上下文验证: - 如果步骤名称含"停用"、"查看停用记录"且 stopped=1 → 值正确 - 如果步骤名称含"停用"但 stopped=0 → 值可能错误 - 如果上下文无法确认预期值 → **不能断言参数值错误** - **不确定时的修复建议**:不要凭空断言值对或错,改为建议用户自查,如"确认 stopped 参数的值是否符合预期(1=已停用,0=未停用)"或"确认 validityFlag/offerFlag 等筛选参数的值是否正确" - **禁止**:参数存在时还说"未筛选";禁止在无法确认意图时断言参数值一定错误 - **🚨 附件为空时的对比推断禁止规则(强制规则)**: - 当某步骤附件为空且返回数据为 null/None/空时,**禁止用"后续步骤有某参数且返回了数据"来断言该步骤缺少该参数** - 典型错误逻辑:"步骤22有 stopped 参数且返回16条 → 所以步骤21缺少 stopped 参数" → **这是错误的**,因为: - 步骤21可能也有 stopped 参数但附件没加载出来 - 步骤21的 stopped 参数值可能不正确 - 步骤21可能其他筛选条件(时间范围、ptypeIds、validityFlag 等)导致无数据 - 步骤21可能是分页问题(数据在第2页而不是第1页) - **正确做法**:列出所有可能的原因(至少3种),建议用户检查该步骤的实际请求参数 - **禁止**:只挑"缺少 stopped 参数"这一种可能并作为唯一结论 - **🚨 强制规则:必须优先提取 response.message 中的中文错误信息**: - **第一步**:在附件内容中搜索 "message:" 字段 - **第二步**:如果 message 字段包含中文(如"服务调用超时,请重试"、"当前单位名称与物流公司明称重复"等) → **必须直接引用该 message 作为根因**,在 rootCauseDetail 中完整复制该 message 内容 → **禁止忽略或概括**,必须原样引用 → **问题类型=product_defect**(接口返回业务错误)或 **test_case_issue**(测试数据问题) - **第三步**:如果 message 字段没有中文(如只有 code 或英文提示) → 继续检查其他步骤的附件内容 → 向上追溯请求参数和返回数据 - **必须对比 request 和 response 数据,分析关联性问题**: - **🚨 查询参数逐一分析规则(强制规则)**:当分析查询接口失败时,必须按以下顺序逐一检查每个参数 → **分析顺序**(必须按此顺序): 1. **时间范围**:检查 startTime/endTime 是否包含单据创建时间(billDate) 2. **分类ID**:检查 ptypeIds 是否匹配前面步骤创建的数据(productTypeId 或 ptypeId) 3. **分类名称**:检查 ptypeFullName 是否匹配 4. **来源编号**:检查 sourceNumber/sourceNum 是否匹配 5. **停用状态**:检查 stoped/stoped 参数值是否正确(0=未停用,1=已停用) 6. **有效标志**:检查 validityFlag 参数值是否正确 7. **报价标志**:检查 offerFlag 参数值是否正确 8. **分页参数**:检查 pageSize/pageIndex 是否能查询到目标数据 → **分析输出格式**:对每个参数给出"✅ 匹配"或"❌ 不匹配 + 具体原因" → **重要**:即使前几步发现问题,也要继续分析后续参数,确保完整诊断 → **禁止**:只分析一个参数就下结论 - **🚨 分页查询问题(强制规则)**:如果 request 中 pageSize=X, page=1(或pageIndex=1),但 response 中 total>pageSize → **第一步**:计算 total / pageSize 的商和余数,确定总页数 → **第二步**:如果 response 返回的是 list 数组,检查 list.length 是否等于 pageSize → **第三步**:如果 list.length < pageSize 或 list.length < total,说明有数据在下一页 → **第四步**:在 briefRootCause 中明确指出"请求 page=1 只返回X条,total=Y,目标数据在后续页面" → **第五步**:在 fixAction 中**优先**给出最简单的方案:"修改'查询XXX'请求,将 pageSize 从 X 改成 Y 或更大,一次性查询所有数据" → **禁止**:说"数据不存在"或"缺少筛选条件",除非能确认数据确实不存在 - **查询条件不匹配**:如果 request 中查询条件与 response 返回数据不匹配 → 指出具体字段差异(如"请求参数中的单据ID=XXX,但 response 中没有返回该 ID 的数据") - **数据不存在**:如果 request 参数正确但 response 返回空数据 → 检查上游步骤是否正确创建了数据 - 断言失败:指出预期值和实际值的差异 - 接口错误:指出附件内容中的具体错误字段(如 response.message、response.code) 🔍 **第二优先级(增强):逐个分析失败步骤的 JSON 请求内容,向前追溯结果** - **对每个 ⚠️异常 或 ❌失败 的步骤,按以下顺序逐一分析**: 1. **提取该步骤的 request 参数**:从附件内容中找到请求的完整 JSON 2. **提取该步骤的 response 数据**:从附件内容中找到返回的完整 JSON 3. **分析 request 中的关键参数**: - 查询条件(如 ID、编号、时间范围、分页参数等) - 这些参数的值是否合理?是否与上游步骤的 response 匹配? 4. **向前追溯**:从当前失败步骤开始,逐个检查前面的步骤: - 检查前一个步骤的 response 是否返回了正确的数据 - 如果前一个步骤的 response 中有目标数据(如 ID、编号),但当前步骤的 request 没有使用 → **根因是参数传递问题** - 如果前一个步骤的 response 中也没有目标数据 → 继续往前追溯 5. **追溯终止条件**: - 找到某个步骤的 response 中有错误信息 → **该步骤是根因** - 找到某个步骤的 response 中有目标数据,但后续步骤没有正确使用 → **根因是参数传递问题** - 追溯到 Set up 阶段仍无异常 → 结合步骤名称和失败模式推断 - **输出格式**:对每个失败步骤,给出: - 该步骤的 request 关键参数 - 该步骤的 response 关键数据 - 向前追溯的路径和发现 - 最终根因判断 🔍 **第三优先级:Tear down 阶段**(仅在前两阶段均正常时才分析) **问题类型判断**: - product_defect:系统功能bug(接口返回错误、计算错误、状态异常) - test_case_issue:用例问题(断言错误、数据过时、步骤有误、账号密码失效、模板文件错误) - environment_data:环境问题(网络超时、数据库连接失败、数据污染) **【特殊错误类型判断规则】**: - **模板错误**:如果错误消息包含"模板错误"、"模板不正确"、"模板格式错误"等,且测试步骤涉及上传模板文件 → **必须判断为 test_case_issue(用例问题)**,因为是测试用例中使用的模板文件本身有错误 → **禁止判断为 product_defect**,除非能确认是系统前端下载的模板和后端校验不一致 → **修复建议**:检查测试用例中使用的模板文件,确认为最新版本 - **🚨 时间范围查询问题(强制规则)**:如果创建步骤返回 billDate 或 createTime,但查询步骤返回空数据 → **第一步**:对比创建步骤的 billDate/createTime 和查询步骤的 startTime/endTime → **第二步**:如果 billDate 不在 [startTime, endTime] 范围内,确认为时间范围问题 → **第三步**:注意边界情况:如果 billDate == endTime,需要检查是否使用 < 或 <= 比较 → **第四步**:在 rootCauseDetail 中明确指出"单据创建时间为 XXX,但查询时间范围为 YYY,未包含该创建时间" → **第五步**:在 fixAction 中指出"扩大查询时间范围,如将 endTime 延后一天" → **禁止**:说"数据不存在"或"查询条件错误",如果是时间范围导致的 **【修复动作编写规则】** - **必须使用明确的动作指令**,禁止使用"请确认"、"如果"、"可能需要"、"建议检查"等不确定词语 - **必须使用步骤名称,禁止使用步骤编号**: - ❌ 错误写法:"检查第20步接口" - ❌ 错误写法:"修改第21步的查询条件" - ✅ 正确写法:"检查'停用成本预测单'接口的response数据,确认返回的单据ID" - ✅ 正确写法:"修改'查询成本预测单列表'请求,添加 stopped=true 参数" - **直接指出具体操作步骤**,如: - ❌ 错误写法:"请确认停用操作是否成功执行" - ❌ 错误写法:"检查第20步'停用成本预测单'接口的response数据" - ✅ 正确写法:"检查'停用成本预测单'接口的response数据,确认返回的单据ID和状态字段" - ❌ 错误写法:"如果第21步是模拟手动查询..." - ✅ 正确写法:"'查询成本预测单列表'缺少 stopped=true 参数,导致查询不到停用记录" - ✅ 正确写法:“修改第21步的查询条件,使用第20步返回的单据ID作为查询参数” **【附件内容为空时的回溯分析规则】** 如果某步骤显示为 ⚠️异常(黄色) 或 ❌失败(红色),但「附件内容」字段为空或无有效数据: → **从该步骤开始,逐个向前一个步骤追溯**(当前步骤 → 上一个步骤 → 上上一个步骤 → ...) → **对每个回溯步骤,检查其附件内容中的 request 和 response**: - 如果某步骤的 response 中有业务错误(如"账号或密码错误"、"认证失败"、"401"、"数据不存在"等) → **该步骤就是根因所在**,输出该错误并停止回溯 - 如果某步骤的请求参数有误(如传了错误的ID、编号、时间范围) → **该步骤就是根因所在**,输出参数问题并停止回溯 - 如果该步骤正常 → 继续往前一个步骤追溯 → 如果一直追溯到 Set up 阶段仍无异常 → 结合步骤名称和后续步骤失败模式推断(如 login 步骤失败但无响应,推断为账号/密码/网络问题) **用例信息** - 用例名称: ${name || '未知'} - 完整路径: ${fullName || '未知'} - 顶层错误: ${message || '无'} ${apiDataContext} ${knowledgeResults.length > 0 ? ` **📚 历史相似案例(供参考)** ${knowledgeResults.map((item, i) => { const sim = Math.round(item.similarity * 100); return `${i+1}. [${sim}%相似] ${item.caseName || '未知'} - 根因:${item.rootCause || item.conclusion || '-'} - 结论:${item.conclusion || '-'} - 建议:${(item.suggestions || []).join('; ') || '-'}`; }).join('\n')} **注意**:以上历史案例仅供参考,请结合当前附件数据验证是否适用,不要直接套用。 ` : ''} **输出 JSON(严格遵循以下格式)**: \`\`\`json { "issueType": "product_defect 或 test_case_issue 或 environment_data", "issueTypeLabel": "产品缺陷 或 用例问题 或 环境数据问题", "briefRootCause": "一句话概括根本原因(如:'请求 page=1 只返回10条,total=16,目标在第2页,需调整分页')", "dataFlow": [ {"step": 1, "phase": "Set up", "action": "步骤名称", "output": "附件内容关键数据或空", "status": "broken", "reason": "具体错误原因,如果附件有 response.message 且有中文错误描述,必须直接引用该 message(如:'当前单位名称与物流公司明称重复');如果是分页/查询问题,必须指出具体的参数差异(如:'请求参数 pageSize=10, pageIndex=1 只能查询前10条,但需要查询的数据是第11条')"} ], "firstFailureStep": 1, "rootCauseDetail": "基于实际附件内容或错误消息的具体分析结果,如果附件有 response.message 且有中文错误描述,必须直接引用该 message(如:'当前单位名称与物流公司明称重复');如果是分页/查询问题,必须明确指出请求参数和响应数据的具体差异(如:'请求参数 pageIndex=1 查询第1页(前10条),但需要查询的数据在 total=11 的第2页')", "fixAction": "具体修复动作(必须用步骤名称,禁止用步骤编号)", "timeFormatIssue": false, "timeIssueDetail": "", "conclusion": "最终结论(一句话,15字以内)", "errorType": "错误类型名称", "errorTypeCode": "assertion|data_not_ready|timeout|network|null|type|config|permission|business|time_format|utc_offset|unknown", "rootCauses": ["具体原因1"], "suggestions": ["具体建议1(用步骤名称)"], "confidence": 0.85 } \`\`\` 请只输出 JSON,不要其他内容。`; }, // 调用 AI API async callAI(prompt) { if (!CONFIG.aiApiKey) { console.warn('[AI Analyzer] 未配置 API Key,跳过 AI 分析'); return null; } try { // 判断 API 格式:/responses 用 input 格式,/chat/completions 用 messages 格式 const isResponsesAPI = CONFIG.aiApiUrl.includes('/responses'); let requestData; if (isResponsesAPI) { // DeepSeek-V4-pro 响应式 API 格式 requestData = { model: CONFIG.aiModel, input: [ { role: 'system', content: [ { type: 'input_text', text: '你是一个专业的测试失败分析助手,擅长从错误日志中定位问题根因。' } ] }, { role: 'user', content: [ { type: 'input_text', text: prompt } ] } ], temperature: 0.3 // 注意:/responses 接口不支持 max_tokens 参数 }; } else { // 标准 OpenAI 兼容格式 requestData = { model: CONFIG.aiModel, messages: [ { role: 'system', content: '你是一个专业的测试失败分析助手,擅长从错误日志中定位问题根因。' }, { role: 'user', content: prompt } ], temperature: 0.3, max_tokens: 1000 }; } // 使用 GM_xmlhttpRequest 绕过 CORS return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: CONFIG.aiApiUrl, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONFIG.aiApiKey}` }, data: JSON.stringify(requestData), onload: function(response) { console.log('[AI Analyzer] API 响应状态:', response.status); console.log('[AI Analyzer] API 响应内容前100字符:', response.responseText.substring(0, 100)); // 处理 HTTP 错误状态码 if (response.status !== 200) { let errorMessage = `HTTP ${response.status}`; try { const errorData = JSON.parse(response.responseText); if (errorData.error?.message) { errorMessage = errorData.error.message; } else if (errorData.error?.code) { errorMessage = `${errorData.error.code}: ${errorData.error.message || 'Unknown error'}`; } } catch (e) { // 解析失败,使用默认错误信息 if (response.responseText.length > 0) { errorMessage = response.responseText.substring(0, 200); } } console.error('[AI Analyzer] API 调用失败:', errorMessage); reject(new Error(`API错误: ${errorMessage}`)); return; } // 检查是否返回了 HTML(通常是错误页面) if (response.responseText.trim().startsWith('<') && !response.responseText.trim().startsWith('{')) { console.error('[AI Analyzer] API 返回了 HTML 而不是 JSON,可能是认证失败:', response.responseText.substring(0, 200)); reject(new Error('API认证失败,请检查API Key是否正确')); return; } try { const data = JSON.parse(response.responseText); // 适配两种响应格式: // 1. 标准 OpenAI 格式:data.choices[0].message.content // 2. DeepSeek-V4-pro /responses 格式:data.output 或 data.content let content = null; if (data.choices?.[0]?.message?.content) { // 标准 OpenAI 格式 content = data.choices[0].message.content; } else if (data.output) { // DeepSeek-V4-pro 格式 content = data.output; } else if (data.content) { // 其他格式 content = data.content; } if (content) { console.log('[AI Analyzer] 完整 AI 响应内容:', content); resolve(content); } else { console.error('[AI Analyzer] AI 响应内容为空:', data); resolve(null); } } catch (e) { console.error('[AI Analyzer] 解析 AI 响应失败:', e, response.responseText.substring(0, 200)); reject(e); } }, onerror: function(error) { console.error('[AI Analyzer] 请求失败:', error); reject(error); } }); }); } catch (error) { console.error('[AI Analyzer] API 调用失败:', error); return null; } }, // 解析 AI 返回的 JSON parseAIResponse(aiContent) { try { // 清理内容:去掉首尾空白 let cleanContent = aiContent.trim(); // 1. 先尝试提取 ```json ... ``` 代码块 let jsonStr = null; const codeBlockMatch = cleanContent.match(/```json\s*([\s\S]*?)```/); if (codeBlockMatch) { jsonStr = codeBlockMatch[1].trim(); } // 2. 尝试提取 ``` ... ``` 代码块(无语言标识) if (!jsonStr) { const plainBlockMatch = cleanContent.match(/```\s*([\s\S]*?)```/); if (plainBlockMatch) { jsonStr = plainBlockMatch[1].trim(); } } // 3. 尝试找第一个 { 到最后一个 } 的内容 if (!jsonStr) { const firstBrace = cleanContent.indexOf('{'); const lastBrace = cleanContent.lastIndexOf('}'); if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) { jsonStr = cleanContent.slice(firstBrace, lastBrace + 1); } } // 4. 还不行就直接用全文 if (!jsonStr) { jsonStr = cleanContent; } // 修复 AI 输出的 JSON 格式问题(用状态机处理) // 1. 字符串值内的真实换行 → 转义为 \n // 2. JSON 结构层面的中文逗号 → 英文逗号 // 3. JSON 结构层面的中文冒号 → 英文冒号 jsonStr = (() => { let inString = false, escaped = false, result = ''; for (let i = 0; i < jsonStr.length; i++) { const ch = jsonStr[i]; if (escaped) { result += ch; escaped = false; continue; } if (ch === '\\') { result += ch; escaped = true; continue; } if (ch === '"') { inString = !inString; result += ch; continue; } if (!inString && ch === ',') { result += ','; continue; } if (!inString && ch === ':') { result += ':'; continue; } if (inString && ch === '\n') { result += '\\n'; continue; } if (inString && ch === '\r') { result += '\\r'; continue; } result += ch; } return result; })(); const parsed = JSON.parse(jsonStr); // 映射到标准格式 const typeMap = { 'assertion': { name: '断言失败', icon: '\u274C', color: '#fd6056' }, 'element': { name: '元素未找到', icon: '\uD83D\uDD0D', color: '#ff8800' }, 'timeout': { name: '超时错误', icon: '\u23F0', color: '#ffaa00' }, 'network': { name: '网络错误', icon: '\uD83C\uDF10', color: '#4488ff' }, 'null': { name: '空值错误', icon: '\u26A0\uFE0F', color: '#ff6600' }, 'type': { name: '类型错误', icon: '\uD83D\uDCDD', color: '#9944ff' }, 'config': { name: '配置错误', icon: '\u2699\uFE0F', color: '#888888' }, 'environment': { name: '环境问题', icon: '\uD83D\uDDA5\uFE0F', color: '#66aa88' }, 'business': { name: '业务逻辑错误', icon: '\uD83D\uDCCB', color: '#aa66cc' }, 'unknown': { name: '未知错误', icon: '\u2753', color: '#888888' } }; const typeInfo = typeMap[parsed.errorTypeCode] || typeMap['unknown']; return { errorType: { type: parsed.errorTypeCode || 'unknown', name: parsed.errorType || typeInfo.name, icon: parsed.icon || typeInfo.icon, color: parsed.color || typeInfo.color, suggestions: parsed.suggestions || [] }, dataFlow: parsed.dataFlow || [], // 数据流追踪 firstFailureStep: parsed.firstFailureStep || null, // 第一个失败步骤 rootCauseDetail: parsed.rootCauseDetail || '', // 根本原因详情 fixAction: parsed.fixAction || '', // 具体修复动作 stepAnalysis: parsed.stepAnalysis || [], // AI 逐条步骤分析(兼容旧格式) conclusion: parsed.conclusion || '', // AI 最终结论 rootCauses: parsed.rootCauses || [], suggestions: parsed.suggestions || [], confidence: parsed.confidence || 0.5, issueType: parsed.issueType || '', // 问题类型:product_defect/test_case_issue/environment_data issueTypeLabel: parsed.issueTypeLabel || '', // 问题类型标签 timeFormatIssue: parsed.timeFormatIssue || false, timeIssueDetail: parsed.timeIssueDetail || '' }; } catch (error) { console.error('[AI Analyzer] 解析 AI 响应失败:', error); return null; } }, // 主分析方法 async analyze(errorData) { // 先搜索知识库中的相似案例 console.log('[AI Analyzer] 搜索知识库...'); const knowledgeResults = await this.searchKnowledge(errorData); // 构建 prompt(包含历史经验) const prompt = this.buildPrompt(errorData, knowledgeResults); const aiContent = await this.callAI(prompt); if (!aiContent) { return null; } const result = this.parseAIResponse(aiContent); if (result) { // 补充其他字段 result.stackAnalysis = ErrorAnalyzer.analyzeStackTrace(errorData.trace); result.message = errorData.message || '无错误消息'; result.trace = errorData.trace || ''; result.name = errorData.name || ''; result.fullName = errorData.fullName || ''; result.assertionDeep = { hasDeepAnalysis: false }; // 传递 steps 给 UI 渲染用 result.steps = errorData.steps || []; } return result; } }; // ==================== 步骤分析器 ==================== const StepAnalyzer = { // 分析单个步骤 analyzeStep(step) { const name = step.name || '未知步骤'; const status = step.status || 'unknown'; const statusDetails = step.statusDetails || {}; const message = statusDetails.message || ''; const trace = statusDetails.trace || ''; // 判断是否成功 const isSuccess = status === 'passed'; const isFailure = status === 'failed' || status === 'broken'; // 提取接口信息(从步骤名称中) const apiInfo = this.extractAPIInfo(name, message); return { name, status, isSuccess, isFailure, message, trace, apiInfo, // 如果有子步骤,递归分析 children: (step.steps || []).map(child => this.analyzeStep(child)) }; }, // 从步骤名称和消息中提取接口信息 extractAPIInfo(stepName, errorMessage) { const info = { apiName: '', apiType: '', // http, rpc, db, etc. endpoint: '', method: '' }; // 尝试从步骤名称中提取 HTTP 方法 const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']; for (const method of httpMethods) { if (stepName.toUpperCase().includes(method)) { info.method = method; info.apiType = 'http'; break; } } // 尝试提取 URL/endpoint const urlMatch = stepName.match(/(https?:\/\/[^\s]+)/) || errorMessage.match(/(https?:\/\/[^\s]+)/); if (urlMatch) { info.endpoint = urlMatch[1]; } // 尝试提取接口名称 const apiMatch = stepName.match(/接口[::]\s*(.+)/) || stepName.match(/调用[::]\s*(.+)/) || stepName.match(/请求[::]\s*(.+)/); if (apiMatch) { info.apiName = apiMatch[1].trim(); } else { info.apiName = stepName; } return info; }, // 分析所有步骤 analyzeAllSteps(steps) { if (!steps || !Array.isArray(steps)) return []; return steps.map(step => this.analyzeStep(step)); }, // 生成步骤分析报告 generateStepReport(analyzedSteps) { if (!analyzedSteps || analyzedSteps.length === 0) { return { total: 0, passed: 0, failed: 0, steps: [] }; } let total = 0; let passed = 0; let failed = 0; const stepReports = []; const processStep = (step, level = 0) => { total++; if (step.isSuccess) passed++; if (step.isFailure) failed++; stepReports.push({ ...step, level, order: stepReports.length + 1 }); // 处理子步骤 if (step.children && step.children.length > 0) { step.children.forEach(child => processStep(child, level + 1)); } }; analyzedSteps.forEach(step => processStep(step)); return { total, passed, failed, steps: stepReports }; } }; // ==================== 断言子类型识别器 ==================== const AssertionAnalyzer = { // 断言子类型定义 SUB_TYPES: { count_mismatch: { name: '数量不等', icon: '\uD83D\uDD22', color: '#ff6b35', patterns: [ /assert.*\d+.*(?:==|!=|>|<|>=|<=).*\d+/i, /expected.*\d+.*(?:but|got|was|actual).*\d+/i, /期望.*\d+.*(?:实际|但).*\d+/i, /count.*(?:mismatch|diff|not equal)/i, /length.*(?:not|diff|mismatch)/i, /数量.*(?:不等|不一致|不匹配)/i, /len\(.*\)\s*!=?\s*\d+/i, /assert\s+\d+\s*==\s*\d+/i, /assert.*\d+\s*!=\s*\d+/i, /\[\d+\]\s*!=\s*\[\d+\]/i, /Expected:\s*\d+.*Actual:\s*\d+/i, /预期.*\d+.*实际.*\d+/i ], description: '断言两侧数量不一致,通常因上游数据缺失或状态不正确导致', suggestions: [ '数量不等通常因为上游单据未全部生成或状态不正确,需检查上游操作是否完整执行', '检查上游创建的单据是否处于正确状态(已审核/已过账),草稿或未审核状态的单据不会被统计', '对比断言预期数量与实际数量,差额即缺失的数据条数,有助于定位具体缺失项' ] }, status_mismatch: { name: '状态不匹配', icon: '\uD83D\uDD04', color: '#e67e22', patterns: [ /status.*(?:not|diff|mismatch|expect)/i, /状态.*(?:不|错误|异常|不符)/i, /(?:草稿|未审核|未过账|draft|pending|unapproved)/i, /expected.*(?:已审核|已过账|approved|posted)/i, /but.*(?:草稿|draft|pending|未审核)/i, /单据状态/i, /bill.*status/i ], description: '单据状态与预期不符,如草稿未过账、未审核通过等', suggestions: [ '单据状态不正确通常因为上游审核/过账操作未成功执行', '检查上游是否有审核或过账步骤,该步骤是否执行成功', '草稿/未审核状态的单据在下游查询中不会被统计,需确保上游操作完整' ] }, value_mismatch: { name: '值不匹配', icon: '\u2194\uFE0F', color: '#9b59b6', patterns: [ /expected.*but/i, /期望.*(?:但|实际|为)/i, /(?:is|was)\s+not\s+/i, /does not (?:contain|match|equal)/i, /不等于|不匹配|不符合/i, /value.*(?:mismatch|diff)/i ], description: '断言预期值与实际值不匹配', suggestions: [ '对比断言预期值与实际值的差异,确认差异是否因上游数据变更导致', '检查测试数据是否被其他用例修改,存在数据竞争或依赖问题' ] }, not_found: { name: '数据不存在', icon: '\u2753', color: '#e74c3c', patterns: [ /not found|不存在|未找到|no such/i, /empty|为空|没有数据|no data/i, /返回.*\[\]|\[\].*返回/i, /0 items|0 条|0条/i, /is None|is null|undefined/i ], description: '预期数据不存在,可能因上游创建失败或数据被清除', suggestions: [ '数据不存在通常因为上游创建/生成操作失败,检查上游操作是否成功', '检查数据是否被其他用例的清理操作(如删除/作废)移除' ] }, api_not_found: { name: '接口不存在', icon: '\u274C', color: '#e74c3c', patterns: [ /接口.*不存在|不存在.*接口|api.*not.*found|endpoint.*not.*found/i, /接口.*废弃|废弃.*接口|deprecated.*api|api.*deprecated/i, /接口.*移除|移除.*接口|removed.*api|api.*removed/i, /接口.*升级|升级.*接口|api.*upgrade|upgrade.*api/i, /404.*接口|接口.*404/i, /接口.*不支持|不支持.*接口/i, /接口.*未定义|未定义.*接口/i, /接口.*变更|变更.*接口|接口.*调整|调整.*接口/i, /api.*version|version.*api/i ], description: '接口在当前版本不存在,可能因高低版本差异导致接口被移除或参数变更', suggestions: [ '检查当前测试环境的服务版本是否与接口定义匹配,高/低版本可能导致接口不存在', '对比不同版本的服务端文档,确认接口是否已被废弃或移除', '检查接口请求的版本号参数(如 api-version)是否正确', '如果接口确实不存在,建议标记用例为跳过或适配新版接口' ] }, version_mismatch: { name: '版本不匹配', icon: '\uD83D\uDD04', color: '#e67e22', patterns: [ /版本.*不匹配|不匹配.*版本|version.*mismatch|incompatible.*version/i, /高低版本|高版本|低版本|版本.*差异/i, /版本.*不一致|不一致.*版本/i, /接口.*版本|版本.*接口/i, /version.*not.*support|不支持.*版本/i, /api.*version|version.*api/i ], description: '服务端版本与客户端期望版本不一致,导致接口行为异常或参数不兼容', suggestions: [ '检查测试环境是否部署了正确的服务版本,避免高低版本混用', '确认接口在新旧版本中的参数差异,更新请求参数以适配当前版本', '如果版本差异导致大量用例失败,建议统一测试环境版本' ] }, config_intercepted: { name: '配置拦截', icon: '\u2699\uFE0F', color: '#e67e22', patterns: [ /配置.*拦截|拦截.*配置|被配置拦截|配置导致|配置变更/i, /开关.*开启|开启.*开关|功能开关|系统配置|系统设置/i, /低于.*不允许|高于.*不允许|超出.*限制|限制.*条件|不满足.*条件/i, /配置.*生效|生效.*配置|配置项|参数设置|参数配置/i, /规则.*触发|触发.*规则|校验.*规则|规则校验|前置.*校验/i, /配置原因|配置问题|配置冲突|配置不匹配/i, /因为.*配置|由于.*配置|配置导致.*失败/i, /未满足.*配置条件|配置条件.*不满足|条件.*拦截/i, /开启.*后|打开.*后|启用.*后|关闭.*后/i, /配置.*开关|开关.*配置|参数.*开关|开关.*参数/i, /前置.*条件|条件.*不满足|条件.*限制|限制条件/i ], description: '系统配置或开关开启后,业务流程被前置校验拦截,未走到预期的断言逻辑', suggestions: [ '检查系统配置/开关状态是否变更,某些配置开启后会改变业务流程或增加前置校验', '确认测试环境配置是否与用例期望一致,避免配置变更导致业务流程被截胡', '检查用例执行前是否已设置正确的系统配置(如:关闭相关开关以跳过前置拦截)', '对比配置变更前后的业务流程差异,确认当前配置下预期的断言逻辑是否仍然适用', '如果业务逻辑因配置变更而改变,需要同步更新用例的断言逻辑或前置配置步骤' ] } }, // 分析断言子类型 analyzeAssertionSubType(errorMessage, stackTrace) { const fullText = `${errorMessage}\n${stackTrace || ''}`; const matchedTypes = []; for (const [type, config] of Object.entries(this.SUB_TYPES)) { for (const pattern of config.patterns) { if (pattern.test(fullText)) { matchedTypes.push({ type, ...config }); break; // 每种类型只匹配一次 } } } // 返回匹配度最高的 // 优先级:config_intercepted > api_not_found > version_mismatch > count_mismatch > status_mismatch > value_mismatch > not_found const priority = ['config_intercepted', 'api_not_found', 'version_mismatch', 'count_mismatch', 'status_mismatch', 'value_mismatch', 'not_found']; for (const p of priority) { const found = matchedTypes.find(m => m.type === p); if (found) return found; } return null; // 无法识别子类型 }, // 从错误消息中提取断言差异(预期值 vs 实际值) extractAssertionDiff(errorMessage) { if (!errorMessage) return null; // 模式1: Expected X but was Y / Expected X, got Y let match = errorMessage.match(/Expected[:\s]+(.+?)[,\s]+(?:but|got|was|actual)[:\s]+(.+)/i); if (match) { return { expected: match[1].trim(), actual: match[2].trim() }; } // 模式2: assert X == Y / assert X != Y match = errorMessage.match(/assert\s+(\d+)\s*(?:==|!=)\s*(\d+)/i); if (match) { return { expected: match[1], actual: match[2] }; } // 模式3: 期望 X 但 Y / 预期 X 实际 Y match = errorMessage.match(/(?:期望|预期)[:\s]+(.+?)(?:但|实际|,)[:\s]+(.+)/i); if (match) { return { expected: match[1].trim(), actual: match[2].trim() }; } // 模式4: len(X) != Y match = errorMessage.match(/len\([^)]*\)\s*!=?\s*(\d+)/i); if (match) { const actualLen = errorMessage.match(/len\(([^)]*)\)\s*==?\s*(\d+)/); return { expected: match[1] + ' 项', actual: (actualLen ? actualLen[2] : '?') + ' 项' }; } return null; }, // 从步骤中提取上下文信息(单据状态 + 配置开关) extractContextFromSteps(steps) { const result = { statusInfo: [], configInfo: [] }; if (!steps || !Array.isArray(steps)) return result; // 单据状态关键词 const statusKeywords = [ { pattern: /草稿|draft/i, status: '草稿', isAbnormal: true }, { pattern: /未审核|unapproved|pending/i, status: '未审核', isAbnormal: true }, { pattern: /未过账|unposted/i, status: '未过账', isAbnormal: true }, { pattern: /已审核|approved|reviewed/i, status: '已审核', isAbnormal: false }, { pattern: /已过账|posted/i, status: '已过账', isAbnormal: false }, { pattern: /已确认|confirmed/i, status: '已确认', isAbnormal: false }, { pattern: /已作废|voided|cancelled/i, status: '已作废', isAbnormal: true }, { pattern: /已删除|deleted/i, status: '已删除', isAbnormal: true } ]; // 配置/开关关键词(用于识别系统配置状态) const configKeywords = [ { pattern: /配置.*开启|开启.*配置|开关.*开启|开启.*开关/i, config: '开关已开启', isIntercept: true }, { pattern: /配置.*关闭|关闭.*配置|开关.*关闭|关闭.*开关/i, config: '开关已关闭', isIntercept: false }, { pattern: /启用|enable|enabled|开启功能/i, config: '功能已启用', isIntercept: true }, { pattern: /禁用|disable|disabled|关闭功能/i, config: '功能已禁用', isIntercept: false }, { pattern: /系统配置|系统设置|配置项|参数设置|参数配置/i, config: '系统配置变更', isIntercept: true }, { pattern: /低于.*不允许|高于.*不允许|超出.*限制|限制.*条件/i, config: '限制条件触发', isIntercept: true }, { pattern: /规则.*生效|生效.*规则|校验.*生效|生效.*校验/i, config: '规则已生效', isIntercept: true }, { pattern: /前置.*校验|校验.*拦截|拦截.*校验/i, config: '前置校验拦截', isIntercept: true }, { pattern: /配置.*变更|变更.*配置|修改.*配置|配置.*修改/i, config: '配置已变更', isIntercept: true }, { pattern: /参数.*变更|变更.*参数|修改.*参数|参数.*修改/i, config: '参数已变更', isIntercept: true } ]; const extractFromStep = (step, path = '') => { const stepName = step.name || ''; const stepStatus = step.status || ''; // 提取单据状态 for (const sk of statusKeywords) { if (sk.pattern.test(stepName)) { result.statusInfo.push({ stepName, stepPath: path, status: sk.status, isAbnormal: sk.isAbnormal, stepStatus: stepStatus }); } } // 提取配置/开关信息 for (const ck of configKeywords) { if (ck.pattern.test(stepName)) { result.configInfo.push({ stepName, stepPath: path, config: ck.config, isIntercept: ck.isIntercept, stepStatus: stepStatus }); } } // 检查步骤的附件中的状态字段 if (step.attachments && Array.isArray(step.attachments)) { for (const att of step.attachments) { if (att.name && /status|状态/i.test(att.name)) { result.statusInfo.push({ stepName: `附件[${att.name}]`, stepPath: path, status: '(见附件内容)', isAbnormal: false, stepStatus: stepStatus }); } if (att.name && /config|配置|setting|设置|switch|开关/i.test(att.name)) { result.configInfo.push({ stepName: `附件[${att.name}]`, stepPath: path, config: '(配置内容见附件)', isIntercept: true, stepStatus: stepStatus }); } } } // 递归子步骤 if (step.steps && Array.isArray(step.steps)) { step.steps.forEach((sub, i) => { extractFromStep(sub, `${path} > ${stepName || `step${i}`}`); }); } }; steps.forEach((step, i) => extractFromStep(step, `step${i}`)); return result; }, // 兼容旧方法名:从步骤中提取单据状态信息 extractStatusFromSteps(steps) { return this.extractContextFromSteps(steps).statusInfo; }, // 综合分析:结合断言子类型和步骤状态/配置信息生成深度分析 deepAnalyze(errorMessage, stackTrace, steps) { const subType = this.analyzeAssertionSubType(errorMessage, stackTrace); const diff = this.extractAssertionDiff(errorMessage); const context = this.extractContextFromSteps(steps); const statusInfo = context.statusInfo; const configInfo = context.configInfo; // 检查是否存在异常状态的单据 const abnormalStatuses = statusInfo.filter(s => s.isAbnormal); const hasAbnormalStatus = abnormalStatuses.length > 0; // 检查是否存在配置拦截 const interceptedConfigs = configInfo.filter(c => c.isIntercept); const hasConfigIntercept = interceptedConfigs.length > 0; // 综合判定 const result = { subType, diff, statusInfo, configInfo, abnormalStatuses, hasAbnormalStatus, interceptedConfigs, hasConfigIntercept, isCountMismatchWithStatusIssue: false, isConfigInterceptedIssue: false, diagnosis: '', specificSuggestions: [] }; // 特殊场景:数量不等 + 单据状态异常 if (subType && subType.type === 'count_mismatch' && hasAbnormalStatus) { result.isCountMismatchWithStatusIssue = true; const statusNames = [...new Set(abnormalStatuses.map(s => s.status))].join('、'); result.diagnosis = `断言数量不等,且检测到上游单据存在异常状态(${statusNames}),这些状态的单据在下游统计中不会被计入`; // 只输出实际检测到的数据,不输出模板化建议 if (abnormalStatuses.length > 0) { result.specificSuggestions = [ `上游单据状态为"${statusNames}",未完成审核/过账流程,这些状态在下游统计中不会被计入` ]; } } // 数量不等但未检测到状态异常 else if (subType && subType.type === 'count_mismatch' && !hasAbnormalStatus) { if (diff) { result.diagnosis = `断言数量不等(预期: ${diff.expected}, 实际: ${diff.actual})`; } else { result.diagnosis = '断言数量不等'; } // 不输出模板化建议,让上层使用规则建议或AI } // 状态不匹配 else if (subType && subType.type === 'status_mismatch') { result.diagnosis = '断言单据状态与预期不符,上游操作未将单据状态推进到预期状态'; // 不输出模板化建议 } // 接口不存在(高低版本导致) else if (subType && subType.type === 'api_not_found') { result.diagnosis = '接口在当前版本不存在,可能因高低版本差异导致接口被移除、废弃或参数变更'; // 不输出模板化建议 } // 版本不匹配 else if (subType && subType.type === 'version_mismatch') { result.diagnosis = '服务端版本与客户端期望版本不一致,导致接口行为异常或参数不兼容'; // 不输出模板化建议 } // 配置拦截(系统配置/开关导致业务流程被截胡) else if (subType && subType.type === 'config_intercepted') { result.isConfigInterceptedIssue = true; const configNames = interceptedConfigs.length > 0 ? [...new Set(interceptedConfigs.map(c => c.config))].join('、') : '未知配置'; result.diagnosis = `系统配置或开关导致业务流程被拦截(${configNames}),预期逻辑未执行`; // 只有真实检测到具体配置拦截时才输出建议,避免模板化套话 if (interceptedConfigs.length > 0) { result.specificSuggestions = [ `检测到${configNames}在保存/提交步骤触发了前置校验,业务流程被拦截` ]; } } // 通用建议 else if (subType) { result.specificSuggestions = subType.suggestions || []; } return result; } }; // ==================== 用例依赖关系分析器 ==================== const DependencyAnalyzer = { // 业务关键词映射:定义用例名称中的关键词与业务操作的关系 // 用于推断用例之间的上下游依赖 BUSINESS_KEYWORDS: { // 新增类操作(上游/数据准备) create: { patterns: [/新增|创建|添加|录入|新建|create|add|insert|save/i], type: 'create', label: '数据创建' }, // 查询类操作(下游/依赖上游数据) query: { patterns: [/查询|搜索|获取|列表|query|search|list|get|find|fetch/i], type: 'query', label: '数据查询' }, // 修改/编辑类操作 update: { patterns: [/修改|编辑|更新|update|edit|modify|change/i], type: 'update', label: '数据修改' }, // 删除类操作 delete: { patterns: [/删除|移除|作废|delete|remove|cancel|void/i], type: 'delete', label: '数据删除' }, // 审核/审批类操作 approve: { patterns: [/审核|审批|确认|approve|review|confirm|check/i], type: 'approve', label: '审核审批' }, // 提交/下发类操作 submit: { patterns: [/提交|下发|推送|submit|push|send|dispatch/i], type: 'submit', label: '提交下发' }, // 过账/记账类操作(状态变更:草稿→已过账) post: { patterns: [/过账|记账|入账|结转|核算|post|posting|book|booking|accounting|settle|settlement/i], type: 'post', label: '过账记账' }, // 状态变更类操作 statusChange: { patterns: [/状态变更|状态流转|变更状态|change.*status|status.*change|transition|流转/i], type: 'statusChange', label: '状态变更' } }, // 业务依赖规则:下游操作依赖上游操作的数据 // key: 下游操作类型, value: 依赖的上游操作类型列表 DEPENDENCY_RULES: { query: ['create', 'update', 'submit', 'approve', 'post', 'statusChange'], // 查询依赖创建/修改/提交/审批/过账/状态变更的数据 update: ['create', 'submit', 'approve', 'post'], // 修改依赖创建/提交/审批/过账的数据 delete: ['create', 'submit', 'approve', 'post'], // 删除依赖创建/提交/审批/过账的数据 approve: ['create', 'submit', 'update'], // 审批依赖创建/提交/修改的数据 submit: ['create', 'update'], // 提交依赖创建/修改的数据 post: ['create', 'submit', 'approve'], // 过账依赖创建/提交/审批的数据 statusChange: ['create', 'submit', 'approve'], // 状态变更依赖创建/提交/审批的数据 }, // 识别用例的业务操作类型 identifyOperationType(testName) { if (!testName) return { type: 'unknown', label: '未知操作' }; for (const [key, config] of Object.entries(this.BUSINESS_KEYWORDS)) { for (const pattern of config.patterns) { if (pattern.test(testName)) { return { type: config.type, label: config.label }; } } } return { type: 'unknown', label: '未知操作' }; }, // 提取用例名称中的业务实体关键词 // 例如 "52324——待开票数据查询" 提取出 "待开票" 或 "发票" extractBusinessEntity(testName) { if (!testName) return ''; // 常见业务实体关键词 const entities = [ '发票', '订单', '单据', '退货', '调拨', '库存', '入库', '出库', '采购', '销售', '客户', '供应商', '商品', '物料', '仓库', '账单', '结算', '付款', '收款', '合同', '报价', 'bill', 'order', 'invoice', 'stock', 'inventory', 'purchase', 'sale', 'return', 'transfer' ]; for (const entity of entities) { if (testName.includes(entity)) { return entity; } } // 提取中文关键词(2-4个中文字符的词组) const chineseMatch = testName.match(/[\u4e00-\u9fa5]{2,6}/g); if (chineseMatch && chineseMatch.length > 0) { // 过滤常见非实体词 const stopWords = ['测试', '用例', '数据', '接口', '功能', '验证', '检查', '断言', '测试用']; const filtered = chineseMatch.filter(w => !stopWords.includes(w)); if (filtered.length > 0) { return filtered[filtered.length - 1]; // 返回最后一个(通常更具体) } } return ''; }, // 从 Allure 测试用例的 steps 中提取关键业务数据 // 例如提取步骤中的单据号、ID 等关键信息 extractKeyDataFromSteps(steps) { const keyData = []; if (!steps || !Array.isArray(steps)) return keyData; for (const step of steps) { const stepName = step.name || ''; // 提取单据号模式:PXX-20260512-00450, ORD-xxx 等 const billNoMatch = stepName.match(/[A-Z]{2,4}-\d{6,10}-\d{3,5}/g); if (billNoMatch) { keyData.push(...billNoMatch.map(no => ({ type: 'billNo', value: no, step: stepName }))); } // 提取 ID 模式 const idMatch = stepName.match(/id[=:]\s*(\d+)/i); if (idMatch) { keyData.push({ type: 'id', value: idMatch[1], step: stepName }); } // 递归检查子步骤 if (step.steps && step.steps.length > 0) { keyData.push(...this.extractKeyDataFromSteps(step.steps)); } } return keyData; }, // 判断两个用例是否属于同一业务实体 isSameBusinessEntity(test1, test2) { const entity1 = this.extractBusinessEntity(test1.name || ''); const entity2 = this.extractBusinessEntity(test2.name || ''); if (!entity1 || !entity2) return false; return entity1 === entity2; }, // 判断两个用例是否在同一个 feature/story 分组下 isSameGroup(test1, test2) { const getLabels = (test) => { const labels = test.detail?.labels || []; const result = {}; for (const label of labels) { if (['epic', 'feature', 'story', 'suite', 'subSuite'].includes(label.name)) { result[label.name] = label.value; } } return result; }; const labels1 = getLabels(test1); const labels2 = getLabels(test2); // 如果 feature 相同,认为在同一业务分组 if (labels1.feature && labels1.feature === labels2.feature) return true; if (labels1.story && labels1.story === labels2.story) return true; if (labels1.suite && labels1.suite === labels2.suite) return true; return false; }, // 分析两个用例之间的依赖关系 // 返回: 'upstream' (test2 是 test1 的上游), 'downstream' (test2 是 test1 的下游), null (无直接依赖) analyzeDependency(test1, test2) { const op1 = this.identifyOperationType(test1.name || ''); const op2 = this.identifyOperationType(test2.name || ''); // 如果操作类型相同,无依赖 if (op1.type === op2.type || op1.type === 'unknown' || op2.type === 'unknown') { // 仍可通过业务实体关联 if (this.isSameBusinessEntity(test1, test2) || this.isSameGroup(test1, test2)) { // 同一实体的相同操作,可能是参数化不同场景,暂不建立依赖 return null; } return null; } // 检查 test1 是否依赖 test2 的操作类型 const requiredUpstreams = this.DEPENDENCY_RULES[op1.type]; if (requiredUpstreams && requiredUpstreams.includes(op2.type)) { // test1 的操作依赖 test2 的操作类型 // 还需验证它们是否在同一个业务实体/分组下 if (this.isSameBusinessEntity(test1, test2) || this.isSameGroup(test1, test2)) { return 'upstream'; // test2 是 test1 的上游 } } // 检查反向依赖 const requiredUpstreams2 = this.DEPENDENCY_RULES[op2.type]; if (requiredUpstreams2 && requiredUpstreams2.includes(op1.type)) { if (this.isSameBusinessEntity(test1, test2) || this.isSameGroup(test1, test2)) { return 'downstream'; // test2 是 test1 的下游 } } return null; }, // 追溯根因:对给定的失败用例,查找其上游依赖的失败用例 // v3.1: 所有失败用例都执行全链路追溯,不再限制仅查询类操作 traceRootCause(failedCase, allFailedCases, visited = new Set(), depth = 0) { const result = { rootCauses: [], // 根因用例列表 chain: [], // 依赖链路 isDirectCause: true, // 是否直接原因(无上游依赖失败) depth: 0, // 追溯深度 relatedUpstreamFailures: [] // 同业务域的所有上游失败 }; const caseUid = failedCase.uid || failedCase.name; if (visited.has(caseUid)) return result; // 防止循环 visited.add(caseUid); const op = this.identifyOperationType(failedCase.name || ''); const entity = this.extractBusinessEntity(failedCase.name || ''); // ====== 策略1:基于业务操作类型的依赖规则追溯 ====== // 对所有操作类型都检查其上游依赖 const requiredUpstreamTypes = this.DEPENDENCY_RULES[op.type] || []; for (const otherCase of allFailedCases) { if (otherCase.uid === failedCase.uid || otherCase.name === failedCase.name) continue; const otherOp = this.identifyOperationType(otherCase.name || ''); // 检查 otherCase 是否是当前用例的上游 const dep = this.analyzeDependency(failedCase, otherCase); if (dep === 'upstream') { // otherCase 是 failedCase 的上游依赖 result.rootCauses.push({ case: otherCase, relation: `${otherOp.label}失败导致${op.label}异常`, entity: entity, depth: depth + 1, matchType: 'dependency_rule' }); // 递归追溯上游的上游 const deeperResult = this.traceRootCause(otherCase, allFailedCases, new Set(visited), depth + 1); if (deeperResult.rootCauses.length > 0) { result.rootCauses.push(...deeperResult.rootCauses); result.depth = Math.max(result.depth, deeperResult.depth + 1); } } } // ====== 策略2:基于同业务域 + 上游操作类型的模糊追溯 ====== // 即使没有精确的依赖规则匹配,如果同一业务实体/分组下有上游操作失败,也追溯 if (result.rootCauses.length === 0) { for (const otherCase of allFailedCases) { if (otherCase.uid === failedCase.uid || otherCase.name === failedCase.name) continue; const otherOp = this.identifyOperationType(otherCase.name || ''); const sameEntity = this.isSameBusinessEntity(failedCase, otherCase); const sameGroup = this.isSameGroup(failedCase, otherCase); if (!sameEntity && !sameGroup) continue; // 判断 otherCase 是否可能是上游操作 const isLikelyUpstream = this.isLikelyUpstreamOperation(otherOp.type, op.type); if (isLikelyUpstream) { result.relatedUpstreamFailures.push({ case: otherCase, operation: otherOp.label, entity: entity || this.extractBusinessEntity(otherCase.name || ''), sameEntity, sameGroup }); result.rootCauses.push({ case: otherCase, relation: `${otherOp.label}失败可能影响${op.label}`, entity: entity || this.extractBusinessEntity(otherCase.name || ''), depth: depth + 1, matchType: 'same_domain' }); // 递归追溯 const deeperResult = this.traceRootCause(otherCase, allFailedCases, new Set(visited), depth + 1); if (deeperResult.rootCauses.length > 0) { result.rootCauses.push(...deeperResult.rootCauses); result.depth = Math.max(result.depth, deeperResult.depth + 1); } } } } // ====== 策略3:断言结果为空时的增强追溯 ====== const errorMsg = (failedCase.analysis?.message || '').toLowerCase(); const isEmptyResult = /空|empty|none|null|不存在|未找到|not found|no data|返回.*\[\]|\[\]|0 items|0 条/i.test(errorMsg); if (isEmptyResult && result.rootCauses.length === 0) { // 结果为空,强制检查同域内所有创建/提交/审批类失败 for (const otherCase of allFailedCases) { if (otherCase.uid === failedCase.uid || otherCase.name === failedCase.name) continue; if (result.rootCauses.some(rc => rc.case.uid === otherCase.uid || rc.case.name === otherCase.name)) continue; const sameGroup = this.isSameGroup(failedCase, otherCase) || this.isSameBusinessEntity(failedCase, otherCase); const otherOp = this.identifyOperationType(otherCase.name || ''); if (sameGroup && ['create', 'submit', 'approve', 'update'].includes(otherOp.type)) { const otherEntity = this.extractBusinessEntity(otherCase.name || ''); result.rootCauses.push({ case: otherCase, relation: `${otherOp.label}失败导致后续操作无数据`, entity: entity || otherEntity, depth: depth + 1, matchType: 'empty_result' }); // 递归 const deeperResult = this.traceRootCause(otherCase, allFailedCases, new Set(visited), depth + 1); if (deeperResult.rootCauses.length > 0) { result.rootCauses.push(...deeperResult.rootCauses); result.depth = Math.max(result.depth, deeperResult.depth + 1); } } } } // ====== 策略4:基于步骤中关键数据的追溯 ====== // 从用例的 steps 中提取单据号等关键数据,在其他失败用例中搜索 const steps = failedCase.detail?.steps || []; const keyData = this.extractKeyDataFromSteps(steps); if (keyData.length > 0) { for (const otherCase of allFailedCases) { if (otherCase.uid === failedCase.uid || otherCase.name === failedCase.name) continue; if (result.rootCauses.some(rc => rc.case.uid === otherCase.uid || rc.case.name === otherCase.name)) continue; const otherSteps = otherCase.detail?.steps || []; const otherKeyData = this.extractKeyDataFromSteps(otherSteps); // 检查是否有共同的关键数据(如相同的单据号) for (const kd1 of keyData) { for (const kd2 of otherKeyData) { if (kd1.type === kd2.type && kd1.value === kd2.value) { const otherOp = this.identifyOperationType(otherCase.name || ''); result.rootCauses.push({ case: otherCase, relation: `${otherOp.label}失败(共享数据: ${kd1.value})导致当前用例异常`, entity: entity, depth: depth + 1, matchType: 'shared_data' }); // 递归 const deeperResult = this.traceRootCause(otherCase, allFailedCases, new Set(visited), depth + 1); if (deeperResult.rootCauses.length > 0) { result.rootCauses.push(...deeperResult.rootCauses); result.depth = Math.max(result.depth, deeperResult.depth + 1); } break; } } } } } // ====== 策略5:基于执行顺序的追溯 ====== // 在同一个 suite 下,按执行时间排序,前面的失败用例可能是后面失败的原因 if (result.rootCauses.length === 0 && failedCase.detail) { const currentStart = failedCase.detail.start || 0; const currentSuite = (failedCase.detail.labels || []).find(l => l.name === 'suite')?.value; if (currentSuite && currentStart) { for (const otherCase of allFailedCases) { if (otherCase.uid === failedCase.uid || otherCase.name === failedCase.name) continue; if (result.rootCauses.some(rc => rc.case.uid === otherCase.uid || rc.case.name === otherCase.name)) continue; const otherStart = otherCase.detail?.start || 0; const otherSuite = (otherCase.detail?.labels || []).find(l => l.name === 'suite')?.value; // 同一个 suite,且执行时间在当前用例之前(上游) if (otherSuite === currentSuite && otherStart > 0 && otherStart < currentStart) { const otherOp = this.identifyOperationType(otherCase.name || ''); // 只有上游操作类型是数据准备类时才追溯 if (['create', 'submit', 'approve', 'update'].includes(otherOp.type)) { const otherEntity = this.extractBusinessEntity(otherCase.name || ''); result.rootCauses.push({ case: otherCase, relation: `${otherOp.label}(先执行)失败可能影响后续用例`, entity: entity || otherEntity, depth: depth + 1, matchType: 'execution_order' }); // 递归 const deeperResult = this.traceRootCause(otherCase, allFailedCases, new Set(visited), depth + 1); if (deeperResult.rootCauses.length > 0) { result.rootCauses.push(...deeperResult.rootCauses); result.depth = Math.max(result.depth, deeperResult.depth + 1); } } } } } } // ====== 构建链路 ====== if (result.rootCauses.length > 0) { result.isDirectCause = false; // 去重:同一个用例可能被多个策略匹配到 const seenUids = new Set(); result.rootCauses = result.rootCauses.filter(rc => { const uid = rc.case.uid || rc.case.name; if (seenUids.has(uid)) return false; seenUids.add(uid); return true; }); // 按深度排序,最深(最上游)的在前 result.rootCauses.sort((a, b) => b.depth - a.depth); // 构建链路 result.chain.push({ name: failedCase.name, operation: this.identifyOperationType(failedCase.name || '').label, status: failedCase.status, isCurrent: true }); for (const rc of result.rootCauses) { result.chain.push({ name: rc.case.name, operation: this.identifyOperationType(rc.case.name || '').label, relation: rc.relation, entity: rc.entity, status: rc.case.status, matchType: rc.matchType, isRootCause: rc.depth === Math.max(...result.rootCauses.map(r => r.depth)) }); } } return result; }, // 判断 otherOpType 是否可能是 currentOpType 的上游操作 isLikelyUpstreamOperation(otherOpType, currentOpType) { // 定义操作的上下游层级 const levels = { create: 1, // 最上游:数据创建 submit: 2, // 提交 approve: 3, // 审批 post: 4, // 过账/记账 statusChange: 4, // 状态变更 update: 2, // 修改 delete: 5, // 删除(通常在中下游) query: 6 // 最下游:查询/统计 }; const otherLevel = levels[otherOpType] || 99; const currentLevel = levels[currentOpType] || 99; // 如果 otherOp 层级更靠上游,则认为是可能的上下游关系 return otherLevel < currentLevel; }, // 批量追溯所有失败用例的根因 traceAllRootCauses(allFailedCases) { const results = []; for (const failedCase of allFailedCases) { const traceResult = this.traceRootCause(failedCase, allFailedCases); results.push({ case: failedCase, trace: traceResult }); } return results; }, // 生成根因摘要 generateRootCauseSummary(traceResult) { if (traceResult.isDirectCause) { return null; // 直接原因,无需追溯 } const rootCauses = traceResult.rootCauses.filter(rc => rc.isRootCause || rc.depth === Math.max(...traceResult.rootCauses.map(r => r.depth))); if (rootCauses.length === 0) return null; const rootCause = rootCauses[0]; const rootOp = this.identifyOperationType(rootCause.case.name || ''); const currentOp = this.identifyOperationType(traceResult.chain[0]?.name || ''); return { rootName: rootCause.case.name, rootOperation: rootOp.label, rootMessage: rootCause.case.analysis?.message || '', rootLocation: rootCause.case.analysis?.stackAnalysis?.location || null, relation: rootCause.relation, entity: rootCause.entity, chainLength: traceResult.chain.length, suggestion: this.generateRootCauseSuggestion(rootCause, currentOp) }; }, // 生成针对根因的建议 generateRootCauseSuggestion(rootCause, currentOp) { const rootOp = this.identifyOperationType(rootCause.case.name || ''); const entity = rootCause.entity || '数据'; const suggestions = []; const rootMsg = (rootCause.case.analysis?.message || '').toLowerCase(); // 根据根因的具体错误类型生成建议 if (/负库存|库存不足|insufficient.*stock|negative.*stock/i.test(rootMsg)) { suggestions.push(`根因定位:${rootOp.label}${entity}时因库存不足无法保存,需先确保库存充足`); suggestions.push(`建议:检查${entity}关联的库存数据,确认库存是否满足创建条件`); suggestions.push(`建议:在${rootOp.label}前添加库存校验,库存不足时跳过或准备库存数据`); } else if (/重复|duplicate|already.*exist/i.test(rootMsg)) { suggestions.push(`根因定位:${rootOp.label}${entity}时因数据重复无法保存`); suggestions.push(`建议:检查测试数据是否已存在,清理旧数据或使用唯一标识`); } else if (/权限|permission|forbidden|unauthorized/i.test(rootMsg)) { suggestions.push(`根因定位:${rootOp.label}${entity}时因权限不足无法执行`); suggestions.push(`建议:检查测试账号的权限配置`); } else if (/超时|timeout/i.test(rootMsg)) { suggestions.push(`根因定位:${rootOp.label}${entity}时因服务超时无法完成`); suggestions.push(`建议:检查服务端性能和网络状况`); } // ====== 新增:单据状态问题 ====== else if (/草稿|draft|未审核|unapproved|未过账|unposted|未确认/i.test(rootMsg)) { suggestions.push(`根因定位:${rootOp.label}${entity}后单据状态未流转(草稿/未审核/未过账),导致下游统计数量不足`); suggestions.push(`建议:检查审核/过账步骤是否成功执行,确保单据状态正确流转`); suggestions.push(`建议:在创建${entity}后立即执行审核/过账操作,确保单据处于"已审核/已过账"状态`); suggestions.push(`建议:下游数量断言前增加单据状态校验,排除状态不正确的单据`); } // ====== 新增:过账/记账失败 ====== else if (/过账|记账|入账.*失败|post.*fail/i.test(rootMsg)) { suggestions.push(`根因定位:${entity}过账/记账操作失败,单据停留在未过账状态`); suggestions.push(`建议:检查过账前置条件是否满足(如:是否已审核、金额是否平衡)`); suggestions.push(`建议:检查过账接口返回的错误信息,定位具体的过账失败原因`); } // ====== 新增:审核/审批被拒绝 ====== else if (/审核.*拒绝|审批.*拒绝|reject|refused|denied/i.test(rootMsg)) { suggestions.push(`根因定位:${entity}审核/审批被拒绝,单据无法流转到下一步`); suggestions.push(`建议:检查审核拒绝的原因(如:数据不完整、金额超出限制等)`); suggestions.push(`建议:确保提交审核的数据满足审核条件`); } // ====== 新增:接口不存在/版本不兼容 ====== else if (/接口.*不存在|不存在.*接口|api.*not.*found|endpoint.*not.*found/i.test(rootMsg)) { suggestions.push(`根因定位:${rootOp.label}操作调用的接口在当前版本不存在,可能因高低版本差异导致接口被移除`); suggestions.push(`建议:确认当前测试环境的服务版本,对比接口文档确认该接口是否存在`); suggestions.push(`建议:检查接口请求是否包含正确的版本号参数`); suggestions.push(`建议:如果接口已被移除,更新用例以调用新版替代接口`); } else if (/版本.*不匹配|不匹配.*版本|version.*mismatch|incompatible.*version|高低版本/i.test(rootMsg)) { suggestions.push(`根因定位:测试环境版本与用例期望版本不一致,导致${rootOp.label}操作失败`); suggestions.push(`建议:检查测试环境部署的服务版本,确保与用例期望版本一致`); suggestions.push(`建议:对比接口在新旧版本中的参数差异,更新请求参数`); suggestions.push(`建议:如果大量用例因版本差异失败,建议统一测试环境至目标版本`); } // ====== 新增:配置拦截/开关导致 ====== else if (/配置.*拦截|拦截.*配置|开关.*开启|开启.*开关|系统配置|配置导致|配置变更|功能开关|参数配置/i.test(rootMsg)) { suggestions.push(`根因定位:${rootOp.label}操作被系统配置/开关拦截,业务流程未按预期执行`); suggestions.push(`建议:检查测试环境的系统配置/开关状态,确认是否与用例期望一致`); suggestions.push(`建议:如果配置变更导致业务流程改变,在用例前置步骤中设置正确的配置(如关闭相关开关)`); suggestions.push(`建议:对比配置开启前后的业务流程差异,更新用例以适配当前配置下的业务逻辑`); } else { suggestions.push(`根因定位:${rootOp.label}${entity}失败(${rootCause.case.analysis?.errorType?.name || '未知类型'}),导致后续${currentOp.label}无可用数据`); suggestions.push(`建议:优先修复上游用例"${rootCause.case.name}"的错误,下游用例可能随之通过`); suggestions.push(`建议:如果上游修复困难,考虑为下游用例独立准备测试数据`); } return suggestions; } }; // ==================== 工具函数 ==================== const Utils = { addStyles() { const styles = ` .allure-analyzer-overlay { position: fixed; top: 0; right: 0; width: 780px; min-width: 320px; max-width: 900px; height: 100vh; background: #1a1a2e; color: #eee; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Microsoft YaHei', sans-serif; z-index: 99999; overflow: hidden; display: flex; flex-direction: column; } /* 左侧拖拽调整宽度 */ .allure-analyzer-overlay::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 6px; cursor: ew-resize; background: transparent; transition: background 0.2s; z-index: 10; } .allure-analyzer-overlay::before:hover { background: rgba(102, 126, 234, 0.3); } /* 底部拖拽调整高度 */ .allure-analyzer-overlay::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 6px; cursor: ns-resize; background: transparent; transition: background 0.2s; z-index: 10; } .allure-analyzer-overlay::after:hover { background: rgba(102, 126, 234, 0.3); } /* 右上角全屏按钒 */ .analyzer-fullscreen-btn { background: rgba(255,255,255,0.2); border: none; color: white; width: 32px; height: 32px; border-radius: 50%; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; transition: background 0.2s; margin-right: 8px; } .analyzer-fullscreen-btn:hover { background: rgba(255,255,255,0.4); } /* 侧边栏内容区:左列表 + 右详情,左右布局 */ .analyzer-overlay-content { flex: 1; display: flex; overflow: hidden; position: relative; } /* 左侧用例列表区 */ .analyzer-list-pane { width: 40%; min-width: 180px; flex-shrink: 0; display: flex; flex-direction: column; border-right: 1px solid #2a2a4a; overflow: hidden; } /* 左侧小统计栏 */ .analyzer-stats-section { padding: 8px 10px; border-bottom: 1px solid #2a2a4a; flex-shrink: 0; } /* 左侧用例滚动列表 */ .analyzer-case-list { flex: 1; overflow-y: auto; overflow-x: hidden; } /* 右侧详情区 */ .analyzer-detail-panel { flex: 1; display: flex; flex-direction: column; background: #13131f; overflow: hidden; } /* 右侧详情头部 */ .analyzer-detail-header { padding: 12px 16px; border-bottom: 1px solid #2a2a4a; font-size: 13px; font-weight: 600; color: #90caf9; flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; } /* 右侧详情内容 */ .analyzer-detail-content { flex: 1; overflow-y: auto; padding: 16px; background: #13131f; color: #eee; } /* 详情面板右上角返回按钮 */ .analyzer-detail-close { background: transparent; border: 1px solid #444; color: #999; width: 24px; height: 24px; border-radius: 4px; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; } .analyzer-detail-close:hover { background: #333; color: #fff; } /* 右侧默认占位提示 */ .analyzer-detail-placeholder { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #444; font-size: 13px; gap: 10px; } .analyzer-detail-placeholder .ph-icon { font-size: 40px; } @keyframes analyzerFadeIn { from { opacity: 0; } to { opacity: 1; } } .analyzer-overlay-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 16px 20px; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; } .analyzer-overlay-title { font-size: 18px; font-weight: 700; color: white; } .analyzer-overlay-close { background: rgba(255,255,255,0.2); border: none; color: white; width: 32px; height: 32px; border-radius: 50%; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; transition: background 0.2s; } .analyzer-overlay-close:hover { background: rgba(255,255,255,0.4); } .analyzer-stats-section { padding: 16px 20px; border-bottom: 1px solid #2a2a4a; flex-shrink: 0; } .analyzer-stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 12px; } .analyzer-stat-card { background: #252547; border-radius: 8px; padding: 12px; text-align: center; } .analyzer-stat-card .stat-number { font-size: 28px; font-weight: 700; line-height: 1.2; } .analyzer-stat-card .stat-label { font-size: 11px; color: #aaa; margin-top: 4px; } .analyzer-stat-failed { color: #fd6056; } .analyzer-stat-broken { color: #ffd151; } .analyzer-stat-type { color: #667eea; } .analyzer-distribution { margin-top: 12px; } .analyzer-dist-item { display: flex; align-items: center; padding: 6px 0; gap: 10px; font-size: 13px; } .analyzer-dist-label { min-width: 80px; color: #ccc; } .analyzer-dist-bar { flex: 1; height: 6px; background: #252547; border-radius: 3px; overflow: hidden; } .analyzer-dist-bar-fill { height: 100%; border-radius: 3px; transition: width 0.5s ease; } .analyzer-dist-count { min-width: 40px; text-align: right; color: #aaa; font-size: 12px; } .analyzer-case-item { padding: 8px 12px; border-bottom: 1px solid #1e1e35; cursor: pointer; transition: background 0.15s; overflow: hidden; } .aci-header { display: flex; align-items: center; gap: 6px; overflow: hidden; } .aci-status-icon { flex-shrink: 0; font-size: 12px; } .aci-name { font-size: 12px; color: #ddd; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; } .analyzer-case-item:hover { background: #252547; } .analyzer-case-item.active { background: #1e3a5f; border-left: 3px solid #90caf9; padding-left: 9px; } .analyzer-case-item.expanded { background: #1e1e3a; } .analyzer-case-header { display: flex; align-items: flex-start; gap: 10px; } .analyzer-case-status { flex-shrink: 0; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; color: white; margin-top: 2px; } .analyzer-case-info { flex: 1; min-width: 0; } .analyzer-case-name { font-size: 14px; font-weight: 600; color: #eee; word-break: break-all; line-height: 1.4; } .analyzer-case-fullname { font-size: 11px; color: #888; margin-top: 3px; word-break: break-all; } .analyzer-case-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 10px; color: white; font-size: 11px; font-weight: 500; margin-top: 6px; } .analyzer-case-expand { margin-top: 12px; display: none; } .analyzer-case-item.expanded .analyzer-case-expand { display: block; } .analyzer-error-box { background: #0d0d1a; border: 1px solid #3a3a5a; border-radius: 6px; padding: 12px; margin-bottom: 10px; font-size: 12px; } .analyzer-error-box pre { margin: 0; white-space: pre-wrap; word-wrap: break-word; font-family: 'Cascadia Code', 'Fira Code', 'Courier New', monospace; font-size: 11px; line-height: 1.5; color: #fd6056; } .analyzer-error-box .error-label { color: #888; font-size: 11px; margin-bottom: 6px; display: block; } .analyzer-location-box { background: #1a1a00; border: 1px solid #554400; border-radius: 6px; padding: 10px 12px; margin-bottom: 10px; font-size: 12px; font-family: 'Cascadia Code', 'Fira Code', 'Courier New', monospace; } .analyzer-location-box .loc-icon { color: #ffd151; margin-right: 6px; } .analyzer-location-box .loc-file { color: #667eea; } .analyzer-location-box .loc-line { color: #ffd151; } .analyzer-suggestion-list { list-style: none; padding: 0; margin: 0; } .analyzer-suggestion-list li { padding: 8px 10px; margin-bottom: 5px; background: #252547; border-radius: 6px; border-left: 3px solid #667eea; font-size: 12px; color: #ccc; line-height: 1.5; } .analyzer-suggestion-label { font-size: 12px; color: #ffd151; margin-bottom: 8px; font-weight: 600; } .analyzer-case-actions { display: flex; gap: 6px; margin-top: 10px; } .analyzer-btn-sm { padding: 4px 10px; border: none; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: 500; transition: all 0.15s; color: white; } .analyzer-btn-copy { background: #667eea; } .analyzer-btn-copy:hover { background: #5568d3; } .analyzer-btn-sugg { background: #444466; } .analyzer-btn-sugg:hover { background: #555577; } /* 浮动触发按钮 */ .allure-analyzer-fab { position: fixed; bottom: 24px; right: 24px; width: 56px; height: 56px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; cursor: pointer; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); font-size: 24px; display: flex; align-items: center; justify-content: center; z-index: 99998; transition: transform 0.2s, box-shadow 0.2s; } .allure-analyzer-fab:hover { transform: scale(1.1); box-shadow: 0 6px 16px rgba(102, 126, 234, 0.6); } .allure-analyzer-fab .fab-badge { position: absolute; top: -4px; right: -4px; background: #fd6056; color: white; font-size: 12px; font-weight: 700; min-width: 20px; height: 20px; border-radius: 10px; display: flex; align-items: center; justify-content: center; padding: 0 4px; } /* Toast 提示 */ .analyzer-toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: #333; color: white; padding: 10px 20px; border-radius: 6px; font-size: 14px; z-index: 100000; animation: analyzerFadeInOut 2s ease; } @keyframes analyzerFadeInOut { 0% { opacity: 0; transform: translateX(-50%) translateY(20px); } 10% { opacity: 1; transform: translateX(-50%) translateY(0); } 90% { opacity: 1; transform: translateX(-50%) translateY(0); } 100% { opacity: 0; transform: translateX(-50%) translateY(20px); } } /* 页面内联分析标记 - 在失败用例列表项旁显示 */ .allure-analyzer-inline-badge { display: inline-flex; align-items: center; gap: 3px; padding: 1px 6px; border-radius: 8px; color: white; font-size: 10px; font-weight: 600; margin-left: 6px; vertical-align: middle; cursor: pointer; transition: transform 0.15s; } .allure-analyzer-inline-badge:hover { transform: scale(1.1); } /* 根因追溯链路样式 */ .analyzer-rootcause-section { margin-bottom: 10px; } .analyzer-rootcause-alert { background: linear-gradient(135deg, #3a1a00 0%, #2a1500 100%); border: 1px solid #884400; border-radius: 8px; padding: 12px; margin-bottom: 8px; } .analyzer-rootcause-alert .rc-header { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 700; color: #ff8800; margin-bottom: 8px; } .analyzer-rootcause-alert .rc-header .rc-icon { font-size: 16px; } .analyzer-chain-visual { display: flex; flex-direction: column; gap: 0; margin: 10px 0; padding-left: 12px; border-left: 2px dashed #666; } .analyzer-chain-node { position: relative; padding: 6px 0 6px 16px; font-size: 12px; color: #ccc; } .analyzer-chain-node::before { content: ''; position: absolute; left: -13px; top: 12px; width: 10px; height: 10px; border-radius: 50%; border: 2px solid; } .analyzer-chain-node.chain-current::before { background: #fd6056; border-color: #fd6056; } .analyzer-chain-node.chain-upstream::before { background: #ffd151; border-color: #ffd151; } .analyzer-chain-node.chain-root::before { background: #ff4444; border-color: #ff4444; box-shadow: 0 0 8px rgba(255, 68, 68, 0.6); } .analyzer-chain-node .chain-label { font-size: 10px; padding: 1px 6px; border-radius: 8px; margin-right: 4px; font-weight: 600; } .analyzer-chain-node.chain-current .chain-label { background: #fd6056; color: white; } .analyzer-chain-node.chain-upstream .chain-label { background: #ffd151; color: #333; } .analyzer-chain-node.chain-root .chain-label { background: #ff4444; color: white; } .analyzer-chain-node .chain-name { color: #eee; font-weight: 500; } .analyzer-chain-node .chain-arrow { color: #888; font-size: 10px; margin-left: 6px; } .analyzer-chain-node .chain-relation { color: #ff8800; font-size: 11px; margin-top: 3px; padding-left: 2px; } .analyzer-rootcause-detail { background: #1a0a00; border: 1px solid #663300; border-radius: 6px; padding: 10px; margin-top: 8px; } .analyzer-rootcause-detail .rc-name { color: #ff4444; font-weight: 600; font-size: 13px; margin-bottom: 6px; } .analyzer-rootcause-detail .rc-msg { color: #ff8866; font-size: 11px; font-family: 'Cascadia Code', 'Fira Code', 'Courier New', monospace; margin-bottom: 6px; word-break: break-all; } .analyzer-rootcause-detail .rc-location { color: #ffd151; font-size: 11px; font-family: monospace; } .analyzer-rootcause-suggestions { margin-top: 8px; } .analyzer-rootcause-suggestions .rc-sugg-title { color: #ffd151; font-size: 12px; font-weight: 600; margin-bottom: 6px; } .analyzer-rootcause-suggestions .rc-sugg-item { padding: 6px 10px; margin-bottom: 4px; background: #2a1a00; border-radius: 6px; border-left: 3px solid #ff8800; font-size: 12px; color: #ddd; line-height: 1.5; } .analyzer-no-rootcause { background: #0d1a0d; border: 1px solid #225522; border-radius: 6px; padding: 10px 12px; font-size: 12px; color: #88cc88; display: flex; align-items: center; gap: 6px; } /* 断言深度分析样式 */ .analyzer-assertion-deep { background: #1a0a2a; border: 1px solid #6633aa; border-radius: 8px; padding: 12px; margin-bottom: 10px; } .analyzer-assertion-deep .ad-header { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 700; color: #bb88ff; margin-bottom: 8px; } .analyzer-assertion-deep .ad-subtype-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 10px; color: white; font-size: 11px; font-weight: 600; } .analyzer-assertion-deep .ad-diff { background: #0d0520; border: 1px solid #553399; border-radius: 6px; padding: 10px; margin-bottom: 8px; display: flex; gap: 16px; align-items: center; justify-content: center; } .ad-diff-item { text-align: center; } .ad-diff-item .ad-diff-label { font-size: 10px; color: #888; margin-bottom: 4px; } .ad-diff-item .ad-diff-value { font-size: 20px; font-weight: 700; font-family: 'Cascadia Code', monospace; } .ad-diff-item.ad-expected .ad-diff-value { color: #88cc88; } .ad-diff-item.ad-actual .ad-diff-value { color: #ff6666; } .ad-diff-arrow { color: #ff8800; font-size: 18px; font-weight: 700; } .analyzer-assertion-deep .ad-diagnosis { background: #2a1030; border-left: 3px solid #bb88ff; border-radius: 4px; padding: 8px 10px; font-size: 12px; color: #ddaaff; margin-bottom: 8px; line-height: 1.5; } .analyzer-status-list { margin-top: 8px; } .analyzer-status-list .as-title { font-size: 11px; color: #888; margin-bottom: 4px; } .analyzer-status-item { display: flex; align-items: center; gap: 6px; padding: 4px 8px; margin-bottom: 3px; border-radius: 4px; font-size: 11px; font-family: monospace; } .analyzer-status-item.as-abnormal { background: #3a1a00; border-left: 3px solid #ff4444; color: #ff8866; } .analyzer-status-item.as-normal { background: #0a2a0a; border-left: 3px solid #44aa44; color: #88cc88; } .as-status-icon { font-size: 12px; } /* AI 配置面板样式 */ .analyzer-settings-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 500px; max-height: 80vh; background: #1a1a1a; border: 1px solid #333; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.6); z-index: 10001; display: flex; flex-direction: column; } .analyzer-settings-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid #333; } .analyzer-settings-title { font-size: 16px; font-weight: 700; color: #fff; } .analyzer-settings-close { background: transparent; border: none; color: #999; font-size: 20px; cursor: pointer; padding: 4px 8px; border-radius: 4px; } .analyzer-settings-close:hover { background: #333; color: #fff; } .analyzer-settings-body { padding: 20px; overflow-y: auto; } .analyzer-setting-item { margin-bottom: 20px; } .analyzer-setting-label { display: block; color: #ddd; font-size: 14px; font-weight: 600; margin-bottom: 8px; cursor: pointer; } .analyzer-setting-label input[type="checkbox"] { margin-right: 8px; } .analyzer-setting-desc { color: #888; font-size: 12px; margin-top: 4px; } .analyzer-setting-select, .analyzer-setting-input { width: 100%; padding: 10px 12px; background: #2a2a2a; border: 1px solid #444; border-radius: 6px; color: #fff; font-size: 14px; font-family: inherit; } .analyzer-setting-select:focus, .analyzer-setting-input:focus { outline: none; border-color: #0078d4; } .analyzer-setting-actions { display: flex; gap: 12px; margin-top: 24px; } .analyzer-btn-primary, .analyzer-btn-secondary { padding: 10px 20px; border: none; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; } .analyzer-btn-primary { background: #0078d4; color: white; flex: 1; } .analyzer-btn-primary:hover { background: #106ebe; } .analyzer-btn-secondary { background: #333; color: #ddd; } .analyzer-btn-secondary:hover { background: #444; } .analyzer-settings-status { padding: 12px; border-radius: 6px; font-size: 13px; margin-top: 12px; display: none; } .analyzer-settings-status.success { display: block; background: #0a2a0a; color: #88cc88; border: 1px solid #225522; } .analyzer-settings-status.error { display: block; background: #2a0a0a; color: #ff6666; border: 1px solid #552222; } .analyzer-settings-status.loading { display: block; background: #1a1a0a; color: #ffcc00; border: 1px solid #555522; } .analyzer-settings-btn { background: transparent; border: none; font-size: 18px; cursor: pointer; padding: 4px; border-radius: 4px; transition: all 0.2s; } .analyzer-settings-btn:hover { background: rgba(255,255,255,0.1); } /* 步骤分析样式 */ .step-analysis-section { background: #0f1923; border: 1px solid #1e3a5f; border-radius: 8px; padding: 16px; margin: 16px 0; } .step-analysis-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #1e3a5f; } .step-analysis-title { font-size: 14px; font-weight: 700; color: #64b5f6; } .step-analysis-stats { display: flex; gap: 12px; font-size: 12px; } .step-stat { padding: 4px 10px; border-radius: 12px; font-weight: 600; } .step-stat.step-pass { background: #0a2a0a; color: #66bb6a; border: 1px solid #225522; } .step-stat.step-fail { background: #2a0a0a; color: #ef5350; border: 1px solid #552222; } .step-analysis-list { display: flex; flex-direction: column; gap: 8px; } .step-item { background: #1a2332; border: 1px solid #2a3a4a; border-radius: 6px; padding: 12px; transition: all 0.2s; } .step-item:hover { background: #1e2a3a; border-color: #3a4a5a; } .step-item.step-passed { border-left: 3px solid #66bb6a; } .step-item.step-failed { border-left: 3px solid #ef5350; background: #1a0a0a; } .step-item.step-warning { border-left: 3px solid #ffa726; } .step-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; } .step-order { background: #2a3a4a; color: #90a4ae; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; min-width: 32px; text-align: center; } .step-status-icon { font-size: 16px; font-weight: 700; } .step-passed .step-status-icon { color: #66bb6a; } .step-failed .step-status-icon { color: #ef5350; } .step-warning .step-status-icon { color: #ffa726; } .step-name { flex: 1; color: #e0e0e0; font-size: 13px; font-weight: 600; } .step-api-info { display: flex; align-items: center; gap: 8px; margin-top: 6px; padding: 6px 10px; background: #0f1923; border-radius: 4px; font-size: 12px; } .step-method { padding: 2px 8px; border-radius: 4px; font-weight: 700; font-size: 11px; font-family: 'Cascadia Code', monospace; } .step-method.get { background: #1b5e20; color: #81c784; } .step-method.post { background: #e65100; color: #ffb74d; } .step-method.put { background: #0d47a1; color: #64b5f6; } .step-method.delete { background: #b71c1c; color: #ef5350; } .step-method.patch { background: #4a148c; color: #ce93d8; } .step-endpoint { color: #90a4ae; font-family: 'Cascadia Code', monospace; word-break: break-all; } .step-error-msg { margin-top: 6px; padding: 8px 10px; background: #2a0a0a; border-left: 2px solid #ef5350; border-radius: 4px; color: #ef9a9a; font-size: 12px; font-family: 'Cascadia Code', monospace; line-height: 1.5; } `; GM_addStyle(styles); }, showToast(message) { const toast = document.createElement('div'); toast.className = 'analyzer-toast'; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => toast.remove(), 2000); }, copyToClipboard(text) { if (typeof GM_setClipboard === 'function') { GM_setClipboard(text); } else if (navigator.clipboard) { navigator.clipboard.writeText(text); } this.showToast('已复制到剪贴板'); }, escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } }; // ==================== UI 渲染器 ==================== const UIRenderer = { // 创建浮动触发按钮 createFAB(failedCount) { const fab = document.createElement('button'); fab.className = 'allure-analyzer-fab'; fab.id = 'allure-analyzer-fab'; fab.innerHTML = `\uD83D\uDCCA${failedCount > 0 ? `${failedCount}` : ''}`; fab.title = `Allure 智能分析 - ${failedCount} 个失败用例`; return fab; }, // 创建侧边分析面板 createOverlay(analyses, analyzer) { const overlay = document.createElement('div'); overlay.className = 'allure-analyzer-overlay'; overlay.id = 'allure-analyzer-overlay'; // 初始统计:只统计状态(错误类型等分析完成后刷新) const failedCount = analyses.filter(a => a.status === 'failed').length; const brokenCount = analyses.filter(a => a.status === 'broken').length; overlay.innerHTML = `
\uD83D\uDCCA 智能分析报告
${failedCount}
Failed
${brokenCount}
Broken
0
错误类型
${analyses.map((a, idx) => { try { return this.renderCaseItem(a, idx); } catch(e) { console.error('[Allure Analyzer] renderCaseItem失败 idx='+idx, e); return `
${idx+1}. 渲染失败
`; } }).join('')}
点击左侧用例查看详情
🔍 选择左侧用例即可展示详情
`; return overlay; }, // 渲染单个用例 renderCaseItem(caseData, index) { const { analysis, status, name, fullName, rootCauseSummary, traceResult } = caseData; // 如果还没分析完成,展示占位状态 if (!analysis) { const safeName = typeof name === 'string' ? name : String(name || '未知用例'); const statusColor = status === 'failed' ? '#fd6056' : '#ffd151'; const statusIcon = status === 'failed' ? '\u2716' : '\u26A0'; return `
${statusIcon} 等待分析 ${Utils.escapeHtml(safeName)}
`; } const { errorType, stackAnalysis, suggestions, message, trace, assertionDeep } = analysis; const statusColor = status === 'failed' ? '#fd6056' : '#ffd151'; const statusIcon = status === 'failed' ? '\u2716' : '\u26A0'; // 获取错误类型颜色 const errorPattern = Object.values(ERROR_PATTERNS).find(p => p.name === errorType); const typeColor = errorPattern ? errorPattern.color : '#888'; // 安全提取用例名称(处理对象类型) const safeName = typeof name === 'string' ? name : (name && name.name ? name.name : (name && name.text ? name.text : String(name || '未知用例'))); // 安全提取错误消息 const safeMessage = typeof message === 'string' ? message : String(message || ''); // 只在有意义的错误类型时才显示标签(过滤掉"未知") const showBadge = typeof errorType === 'string' && errorType !== '\u672a\u77e5' && errorType !== 'unknown' && errorType.trim() !== ''; return `
${statusIcon} ${showBadge ? `${errorType}` : ''} ${rootCauseSummary ? '\uD83D\uDD0D' : ''} ${Utils.escapeHtml(safeName)}
`; }, // 渲染单个用例的详情面板内容 renderCaseDetail(caseData, index = -1) { const { analysis, status, name, fullName, rootCauseSummary, traceResult, errorData } = caseData; const { errorType, stackAnalysis, suggestions, message, stepAnalysis, conclusion, dataFlow, firstFailureStep, rootCauseDetail, fixAction, timeFormatIssue, timeIssueDetail } = analysis || {}; // 安全提取用例名称 const safeName = typeof name === 'string' ? name : (name && name.name ? name.name : (name && name.text ? name.text : String(name || '未知用例'))); const statusColor = status === "failed" ? "#fd6056" : "#ffd151"; const statusIcon = status === "failed" ? "X" : "!"; // 安全提取错误消息 const safeMessage = typeof message === 'string' ? message : String(message || ''); let rootCauseHtml = ''; // 直接原因:用 AI 分析结论或 errorType 替代原始错误消息 const aiConclusion = conclusion || errorType?.name || ''; if (rootCauseSummary && traceResult) { rootCauseHtml = "
根因追溯
"; traceResult.chain.forEach(node => { const label = node.isCurrent ? "当前用例" : (node.isRootCause ? "根因" : "上游"); rootCauseHtml += "
" + label + ": " + Utils.escapeHtml(node.name) + "
"; }); rootCauseHtml += "
" + Utils.escapeHtml(rootCauseSummary.rootName) + "
"; } else { rootCauseHtml = `
${aiConclusion ? Utils.escapeHtml(aiConclusion) : '等待分析...'}
`; } // 步骤分析:全量展示所有步骤 + 结论 + 深度分析 let stepAnalysisHtml = ''; let deepAnalysisHtml = ''; // 提前声明,供后续使用 if (errorData && errorData.steps && errorData.steps.length > 0) { const analyzedSteps = StepAnalyzer.analyzeAllSteps(errorData.steps); const stepReport = StepAnalyzer.generateStepReport(analyzedSteps); // 只展示顶层步骤(level=0),避免子步骤重复 const topSteps = stepReport.steps.filter(s => s.level === 0); // firstFailed 只从顶层取,保证 indexOf 一定能找到 const firstFailed = topSteps.find(s => s.isFailure) || null; if (topSteps.length > 0) { // 1. 全量步骤列表 const allStepsHtml = topSteps.map((step, idx) => { const stepNum = idx + 1; const stepName = Utils.escapeHtml(step.name || ''); const isPass = step.isSuccess; const isFail = step.isFailure; const icon = isPass ? '✅' : (isFail ? '❌' : '⚠️'); const rowBg = isFail ? '#1a0a0a' : (isPass ? '#0a1a0a' : '#1a1a00'); const borderColor = isFail ? '#ef5350' : (isPass ? '#4caf50' : '#ffc107'); const nameColor = isFail ? '#ef9a9a' : (isPass ? '#a5d6a7' : '#fff176'); // 失败步骤显示错误摘要 const errMsg = isFail && step.message ? `:${Utils.escapeHtml(step.message.substring(0, 80))}${step.message.length > 80 ? '…' : ''}` : ''; // 成功步骤显示简短结果 const successHint = isPass ? `→ 成功` : ''; return `
${stepNum}. ${icon} ${stepName}${errMsg}${successHint}
`; }).join(''); // 2. 结论行 - 优先用 AI 结论,否则自动推断 let conclusionHtml = ''; const hasAIAnalysis = conclusion && stepAnalysis && stepAnalysis.length > 0; if (hasAIAnalysis) { // AI 已分析,直接用 AI 结论,不重复显示自动推断 conclusionHtml = ''; } else if (firstFailed) { const firstFailedIdx = topSteps.indexOf(firstFailed); const affectedAfter = topSteps.slice(firstFailedIdx + 1).filter(s => s.isFailure || (!s.isSuccess)); let conclusionText = `因步骤 ${firstFailedIdx + 1}【${Utils.escapeHtml(firstFailed.name || '')}】失败`; if (affectedAfter.length > 0) { const affectedNums = affectedAfter.map(s => topSteps.indexOf(s) + 1).join('、'); conclusionText += `,导致步骤 ${affectedNums} 异常/无数据`; } conclusionHtml = `
📌 结论:${conclusionText}
`; } // 3. 深度分析块(显示 AI 数据流追踪 + 根本原因 + 修复动作) // 优先用 analysis 里的 dataFlow/rootCauseDetail/fixAction(来自 AI 分析结果) const dataFlowData = dataFlow || []; const aiConclusionText = conclusion || ''; const rootCauseText = rootCauseDetail || ''; const fixActionText = fixAction || ''; if (dataFlowData.length > 0 || aiConclusionText || rootCauseText) { // 数据流追踪 const dataFlowHtml = dataFlowData.length > 0 ? dataFlowData.map((df, di) => { const isSuccess = df.status === 'passed'; const isFailed = df.status === 'failed'; const itemBg = isFailed ? '#1a0a0a' : (isSuccess ? '#0a1a0a' : '#1a1a00'); const itemBorder = isFailed ? '#ef5350' : (isSuccess ? '#4caf50' : '#ffc107'); const itemColor = isFailed ? '#ef9a9a' : (isSuccess ? '#a5d6a7' : '#fff176'); const icon = isFailed ? '❌' : (isSuccess ? '✅' : '⚠️'); const action = Utils.escapeHtml(df.action || ''); const output = Utils.escapeHtml(df.output || ''); const reason = Utils.escapeHtml(df.reason || ''); return `
${icon} 步骤${df.step || (di+1)} ${action}
${output}
${reason ? `
原因: ${reason}
` : ''}
`; }).join('') : ''; // 根本原因 const rootCauseBlock = rootCauseText ? `
🔴 根本原因:${Utils.escapeHtml(rootCauseText)}
` : ''; // 修复动作 const fixActionBlock = fixActionText ? `
修复动作:${Utils.escapeHtml(fixActionText)}
` : ''; // AI 结论 const conclusionBlock = aiConclusionText ? `
📌 AI 结论:${Utils.escapeHtml(aiConclusionText)}
` : ''; deepAnalysisHtml = `
🔬 AI 深度分析
${dataFlowHtml} ${rootCauseBlock} ${fixActionBlock} ${conclusionBlock}
`; } // 移除接口执行链路,左侧已有 stepAnalysisHtml = ''; } } // 有步骤分析时,建议已内嵌在深度分析块里,不重复展示 // 无步骤时才展示独立修复建议 let suggestionsHtml = ""; if (!stepAnalysisHtml && suggestions && suggestions.length > 0) { suggestionsHtml = "
修复建议
"; } // 有步骤分析就展示步骤链路;没有步骤则退回显示原始错误消息 const mainContent = stepAnalysisHtml || `
${Utils.escapeHtml(safeMessage)}
`; // 深度分析直接显示(不包在步骤链路里) const fullContent = mainContent + (deepAnalysisHtml || ''); // 问题类型判断 const issueType = analysis?.issueType || ''; const issueTypeLabel = analysis?.issueTypeLabel || ''; let issueTypeBadge = ''; if (issueType) { const typeColors = { 'product_defect': { bg: '#e53935', label: '产品缺陷' }, 'test_case_issue': { bg: '#fb8c00', label: '用例问题' }, 'environment_data': { bg: '#1e88e5', label: '环境数据问题' } }; const typeConfig = typeColors[issueType] || { bg: '#78909c', label: issueTypeLabel || '未知' }; issueTypeBadge = `${typeConfig.label}`; } // 反馈入口 const feedbackHtml = `
📝 分析反馈
`; return "
" + "
" + Utils.escapeHtml(safeName) + "
" + issueTypeBadge + fullContent + rootCauseHtml + suggestionsHtml + feedbackHtml + "
"; }, // 渲染单个步骤 renderStepItem(step) { const statusIcon = step.isSuccess ? '\u2713' : (step.isFailure ? '\u2717' : '\u26A0'); const statusClass = step.isSuccess ? 'step-passed' : (step.isFailure ? 'step-failed' : 'step-warning'); const indent = step.level > 0 ? `padding-left: ${step.level * 20}px;` : ''; let apiInfoHtml = ''; if (step.apiInfo) { const { method, endpoint, apiName } = step.apiInfo; if (method || endpoint) { apiInfoHtml = '
'; if (method) { apiInfoHtml += `${method}`; } if (endpoint) { apiInfoHtml += `${Utils.escapeHtml(endpoint)}`; } apiInfoHtml += '
'; } } let errorMsgHtml = ''; if (step.isFailure && step.message) { errorMsgHtml = `
${Utils.escapeHtml(step.message.substring(0, 150))}${step.message.length > 150 ? '...' : ''}
`; } return `
#${step.order} ${statusIcon} ${Utils.escapeHtml(step.name)}
${apiInfoHtml} ${errorMsgHtml}
`; }, }; // ==================== 主控制器 ==================== const Analyzer = { analyses: [], overlayVisible: false, fabElement: null, overlayElement: null, retryCount: 0, observer: null, // 初始化 init() { console.log('[Allure Analyzer] 初始化 v3.4...'); // 加载保存的 AI 配置 try { const savedConfig = GM_getValue('analyzer_ai_config'); if (savedConfig) { const config = JSON.parse(savedConfig); Object.assign(CONFIG, config); console.log('[Allure Analyzer] AI 配置已加载'); } } catch (e) { console.warn('[Allure Analyzer] 加载 AI 配置失败:', e); } Utils.addStyles(); window.allureAnalyzer = this; // 等待页面加载完成后开始分析 this.waitForPage().then(() => { this.startAnalysis(); }); // 监听 SPA 路由变化 this.setupNavigationListener(); }, // 等待页面关键元素加载 async waitForPage() { return new Promise((resolve) => { const check = () => { // Allure SPA 的侧边栏 const sidebar = document.querySelector('[class*="sidebar"]') || document.querySelector('nav') || document.querySelector('[class*="app"]'); if (sidebar || this.retryCount > 3) { resolve(); } else { this.retryCount++; setTimeout(check, 1000); } }; setTimeout(check, 500); }); }, // 监听导航变化(Allure SPA 使用 hash 路由) setupNavigationListener() { let lastHash = window.location.hash; const checkHashChange = () => { const currentHash = window.location.hash; if (currentHash !== lastHash) { lastHash = currentHash; console.log('[Allure Analyzer] 页面导航变化:', currentHash); // 尝试从 hash 中提取用例 UID(格式: #behaviors/suiteid/uid 或 #suites/suiteid/uid) const uidMatch = currentHash.match(/\/([0-9a-f]{16,})\/?$/); if (uidMatch) { const uid = uidMatch[1]; this._locateCaseByUid(uid); } else { // 不是用例页,路由切换才重新分析 setTimeout(() => this.startAnalysis(), 1000); } } }; setInterval(checkHashChange, 300); // 监听 Allure 原生用例列表点击事件 setTimeout(() => { const behaviorsList = document.querySelector('.behaviors-tree, .test-result-tree, [class*="tree"]'); if (behaviorsList) { behaviorsList.addEventListener('click', (e) => { // 查找点击的用例项 const caseItem = e.target.closest('.tree-item, [class*="tree-item"], [data-uid]'); if (caseItem) { // 尝试从用例项中获取 UID const uid = caseItem.getAttribute('data-uid') || caseItem.querySelector('[data-uid]')?.getAttribute('data-uid'); if (uid) { console.log('[Allure Analyzer] 检测到用例点击, UID:', uid); this._locateCaseByUid(uid); } else { // 等待 500ms URL 变化 setTimeout(() => { const hash = window.location.hash; const uidMatch = hash.match(/\/([0-9a-f]{16,})\/?$/); if (uidMatch) { this._locateCaseByUid(uidMatch[1]); } }, 500); } } }, true); // 捕获阶段 console.log('[Allure Analyzer] 已绑定 Allure 用例列表点击监听'); } }, 2000); }, // 根据 uid 在面板左侧列表定位并高亮对应用例 _locateCaseByUid(uid) { if (!this.analyses || !this.overlayVisible || !this.overlayElement) return; const idx = this.analyses.findIndex(a => a.uid === uid); if (idx < 0) return; const caseList = this.overlayElement.querySelector('#analyzer-case-list'); const detailContent = this.overlayElement.querySelector('#analyzer-detail-content'); const detailTitle = this.overlayElement.querySelector('#analyzer-detail-title'); if (!caseList) return; // 高亮选中 caseList.querySelectorAll('.analyzer-case-item').forEach(el => el.classList.remove('active')); const targetItem = caseList.querySelector(`.analyzer-case-item[data-index="${idx}"]`); if (targetItem) { targetItem.classList.add('active'); // 平滑滚动到可视区域 targetItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } // 同时更新右侧详情 const caseData = this.analyses[idx]; if (detailTitle) { const safeName = typeof caseData.name === 'string' ? caseData.name : String(caseData.name || '未知用例'); detailTitle.textContent = safeName; } if (detailContent) { if (!caseData.analysis) { detailContent.innerHTML = `
🔄
正在优先分析...
已插队到队头,稍候即可展示
`; this.priorityAnalyze(idx, detailContent); } else { detailContent.innerHTML = UIRenderer.renderCaseDetail(caseData, idx); } } }, // 主分析流程 async startAnalysis() { if (!CONFIG.enableAutoAnalysis) return; console.log('[Allure Analyzer] 开始分析...'); // 策略 1: 通过 Allure JSON API 获取数据(优先) let failedCases = await this.analyzeViaAPI(); // 策略 2: 如果 API 无法获取,从 DOM 提取 if (failedCases.length === 0) { console.log('[Allure Analyzer] API 方式未获取到数据,尝试 DOM 分析...'); failedCases = await this.analyzeViaDOM(); } if (failedCases.length === 0) { console.log('[Allure Analyzer] 未找到失败用例'); this.updateFAB(0); return; } // 先显示 FAB,让用户知道脚本在运行 this.updateFAB(failedCases.length); // 创建进度提示 const progressDiv = document.createElement('div'); progressDiv.id = 'allure-analyzer-progress'; progressDiv.style.cssText = ` position: fixed; bottom: 100px; right: 24px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 16px 24px; border-radius: 12px; font-size: 14px; z-index: 99998; box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4); min-width: 200px; text-align: center; `; progressDiv.innerHTML = `
🔄
准备分析 ${failedCases.length} 个失败用例...
`; document.body.appendChild(progressDiv); const updateProgress = (text, percent) => { const textEl = document.getElementById('analyzer-progress-text'); const fillEl = document.getElementById('analyzer-progress-fill'); if (textEl) textEl.textContent = text; if (fillEl) fillEl.style.width = percent + '%'; }; // 分批获取详情,每次只发5个请求 const BATCH_SIZE = 5; const total = failedCases.length; updateProgress('分批获取用例详情...', 5); for (let i = 0; i < failedCases.length; i += BATCH_SIZE) { const batch = failedCases.slice(i, i + BATCH_SIZE); // 并行获取这一批详情 const promises = batch.map((fc, idx) => { const realIdx = i + idx; const uid = fc.uid; if (!uid) return Promise.resolve(); return AllureAPI.fetchTestCaseDetail(uid).then(detail => { if (detail) { failedCases[realIdx].detail = detail; // 调试:打印 Allure JSON 结构 if (realIdx === 0) { console.log(`[Allure Analyzer] 第一个用例详情 JSON 顶层字段:`, Object.keys(detail)); console.log(`[Allure Analyzer] detail.steps:`, detail.steps); console.log(`[Allure Analyzer] detail.containers:`, detail.containers); console.log(`[Allure Analyzer] detail.children:`, detail.children); console.log(`[Allure Analyzer] detail JSON 前800字符:`, JSON.stringify(detail).substring(0, 800)); } // 提取错误消息:优先用例本身的消息,如果没有则从失败的子步骤获取 let errorMsg = detail.statusMessage || detail.statusDetails?.message || ''; if (!errorMsg && detail.steps && detail.steps.length > 0) { // 从失败的子步骤中提取第一个错误消息 const failedStep = detail.steps.find(s => s.status === 'failed' || s.status === 'broken'); if (failedStep) { errorMsg = failedStep.statusMessage || failedStep.statusDetails?.message || ''; } } failedCases[realIdx].errorData = { message: errorMsg, trace: detail.statusTrace || detail.statusDetails?.trace || '', name: detail.name || fc.name || '', fullName: detail.fullName || '', steps: detail.steps || [] }; // 调试:打印 steps 数据 console.log(`[Allure Analyzer] 构建 errorData, steps 数量: ${failedCases[realIdx].errorData.steps.length}`); if (failedCases[realIdx].errorData.steps.length > 0) { console.log(`[Allure Analyzer] 第一个 step 名称:`, failedCases[realIdx].errorData.steps[0].name); } console.log(`[Allure Analyzer] 构建 errorData, steps 数量: ${failedCases[realIdx].errorData.steps.length}`); console.log(`[Allure Analyzer] detail.steps:`, detail.steps); } updateProgress(`获取详情: ${realIdx + 1}/${total}`, 5 + (realIdx / total) * 50); }).catch(() => {}); }); await Promise.all(promises); // 让出主线程 await new Promise(r => setTimeout(r, 30)); } updateProgress('加载列表完成!', 100); // 先显示列表,详情后台慢慢加载 this.analyses = failedCases; // 如果面板已经打开(用户在看加载中屏),自动刷新 if (this.overlayVisible && this.overlayElement) { const listEl = this.overlayElement.querySelector('#analyzer-case-list'); if (!listEl) { // 还在显示加载屏,重新应用面板 this.hideOverlay(); this.showOverlay(); } } // 渲染 UI this.updateFAB(failedCases.length); this.injectInlineBadges(failedCases); // 3秒后移除进度条 setTimeout(() => { const progressDiv = document.getElementById('allure-analyzer-progress'); if (progressDiv) progressDiv.remove(); }, 3000); console.log(`[Allure Analyzer] 列表加载完成,共 ${failedCases.length} 个失败/异常用例`); console.log(`[Allure Analyzer] 详情数据已缓存,点击即用`); // 后台慢慢分析根因(不阻塞页面) this.analyzeInBackground(failedCases, total); // 暴露给全局,方便调试 window.analyzerFailedCases = failedCases; }, // 分析单个用例(公共方法) async _analyzeSingle(fc) { if (!fc || !fc.errorData || fc.analysis) return; fc.analysis = await ErrorAnalyzer.analyzeWithFallback(fc.errorData); // 根因追溯:暂时跳过,后续单独分析 // 注意:DependencyAnalyzer 需要所有失败用例才能追溯,这里单个用例无法追溯 // const traceResult = DependencyAnalyzer.traceRootCause(fc, this._failedCases || []); // if (traceResult && traceResult.chain && traceResult.chain.length > 1) { // fc.traceResult = traceResult; // fc.rootCauseSummary = DependencyAnalyzer.generateRootCauseSummary(traceResult); // } // 分析完成后立即刷新左侧列表对应项 this._refreshCaseItem(fc); }, // 刷新左侧列表中对应的用例条目 _refreshCaseItem(fc) { if (!this.overlayElement || !this._failedCases) return; const idx = this._failedCases.indexOf(fc); if (idx < 0) return; const caseList = this.overlayElement.querySelector('#analyzer-case-list'); if (!caseList) return; const oldItem = caseList.querySelector(`.analyzer-case-item[data-index="${idx}"]`); if (oldItem) { const isActive = oldItem.classList.contains('active'); const newHtml = UIRenderer.renderCaseItem(fc, idx); const temp = document.createElement('div'); temp.innerHTML = newHtml; const newItem = temp.firstElementChild; if (isActive) newItem.classList.add('active'); oldItem.replaceWith(newItem); // 如果这是当前正在显示的详情,也刷新详情面板 if (isActive && this._pendingDetailIdx === idx && this._pendingDetailEl) { this._pendingDetailEl.innerHTML = UIRenderer.renderCaseDetail(fc, idx); this.bindFeedbackEvents(); console.log(`[Allure Analyzer] 后台分析完成,已刷新详情 #${idx}`); } } }, // 后台顺序分析,支持优先插队 async analyzeInBackground(failedCases, total) { const BATCH_SIZE = 3; let currentIdx = 0; this._priorityQueue = []; this._failedCases = failedCases; const analyzeBatch = async () => { // 1. 优先处理插队的用例(每次循环都检查) while (this._priorityQueue.length > 0) { const pidx = this._priorityQueue.shift(); console.log(`[Allure Analyzer] 处理插队用例 #${pidx}`); await this._analyzeSingleWithCallback(failedCases[pidx], pidx); // 插队用例处理完后,继续检查是否还有插队 } // 2. 按顺序处理剩余(每次只处理 1 个,然后检查插队) if (currentIdx >= failedCases.length) { console.log('[Allure Analyzer] 后台分析全部完成!'); this._analysisRunning = false; if (this.overlayVisible) this.refreshOverlayStats(); return; } // 每次只处理 1 个用例,然后检查插队 const fc = failedCases[currentIdx]; if (!fc.analysis) { console.log(`[Allure Analyzer] 分析用例 #${currentIdx}: ${fc.name}`); await this._analyzeSingleWithCallback(fc, currentIdx); } currentIdx++; // 立即检查插队,而不是等批次完成 setTimeout(analyzeBatch, 0); }; this._analysisRunning = true; console.log(`[Allure Analyzer] 开始后台分析,共 ${failedCases.length} 个用例(纯 AI 模式)`); analyzeBatch(); }, // 优先分析某个用例(点击时调用) priorityAnalyze(idx, detailContent) { if (!this._priorityQueue) this._priorityQueue = []; const fc = this._failedCases && this._failedCases[idx]; if (!fc) return; if (fc.analysis) { // 已分析完,直接渲染 if (detailContent) { detailContent.innerHTML = UIRenderer.renderCaseDetail(fc, idx); this.bindFeedbackEvents(); } return; } // 记录待刷新的详情面板 this._pendingDetailIdx = idx; this._pendingDetailEl = detailContent; // 插队到优先队列头部 if (!this._priorityQueue.includes(idx)) { this._priorityQueue.unshift(idx); console.log(`[Allure Analyzer] 用例 ${idx} 插队优先分析`); } }, // 刷新待显示的详情面板 _flushPendingDetail() { const idx = this._pendingDetailIdx; const el = this._pendingDetailEl; if (idx === undefined || !el) return; const fc = this._failedCases && this._failedCases[idx]; if (fc && fc.analysis) { el.innerHTML = UIRenderer.renderCaseDetail(fc, idx); this.bindFeedbackEvents(); this._pendingDetailIdx = undefined; this._pendingDetailEl = null; console.log(`[Allure Analyzer] 优先分析完成,详情已刷新`); } }, // 分析单个用例(带详情刷新检查) async _analyzeSingleWithCallback(fc, idx) { await this._analyzeSingle(fc); // 分析完成后,检查是否需要刷新详情面板 if (this._pendingDetailIdx === idx && this._pendingDetailEl) { this._flushPendingDetail(); } // 也刷新列表项 this._refreshCaseItem(fc); }, // 分析全部完成后刷新面板统计 refreshOverlayStats() { const analyses = this.analyses; if (!analyses || !this.overlayElement) return; const total = analyses.length; const withRootCause = analyses.filter(a => a.rootCauseSummary).length; const directCause = total - withRootCause; // 统计错误类型 const errorTypeCount = {}; analyses.forEach(a => { const type = a.analysis?.errorType?.name; if (type) errorTypeCount[type] = (errorTypeCount[type] || 0) + 1; }); const typeCount = Object.keys(errorTypeCount).length; const sortedTypes = Object.entries(errorTypeCount).sort((a, b) => b[1] - a[1]); // 更新卡片数字 const setEl = (id, val) => { const el = this.overlayElement.querySelector('#' + id); if (el) el.textContent = val; }; setEl('stat-type-count', typeCount); setEl('stat-root', withRootCause); setEl('stat-direct', directCause); // 渲染分布图 const distContainer = this.overlayElement.querySelector('#analyzer-dist-container'); if (distContainer && sortedTypes.length > 0) { distContainer.innerHTML = `
${ sortedTypes.map(([type, count]) => { const pct = (count / total * 100).toFixed(0); const pattern = Object.values(ERROR_PATTERNS).find(p => p.name === type); const color = pattern ? pattern.color : '#888'; return `
${type}
${count}
`; }).join('') }
`; } // 刷新用例列表中尚未分析完成的项 analyses.forEach((fc, idx) => { const item = this.overlayElement.querySelector(`.analyzer-case-item[data-index="${idx}"]`); if (item && fc.analysis) { item.outerHTML = UIRenderer.renderCaseItem(fc, idx); } }); console.log('[Allure Analyzer] 面板统计已刷新'); }, // 通过 JSON API 分析 async analyzeViaAPI() { const results = []; // 尝试获取 summary 数据 const summary = await AllureAPI.fetchJSON('widgets/summary.json'); if (summary && summary.statistic) { const stat = summary.statistic; console.log(`[Allure Analyzer] Summary: total=${stat.total}, failed=${stat.failed}, broken=${stat.broken}`); } // 尝试直接获取所有失败/异常的测试结果 const categories = await AllureAPI.fetchCategories(); if (categories && Array.isArray(categories)) { for (const category of categories) { if (category.children && Array.isArray(category.children)) { for (const child of category.children) { if (child.status === 'failed' || child.status === 'broken') { const uid = child.uid || child.parentUid; const detail = uid ? await AllureAPI.fetchTestCaseDetail(uid) : null; const errorData = detail ? { message: detail.statusMessage || detail.statusDetails?.message || '', trace: detail.statusTrace || detail.statusDetails?.trace || '', name: detail.name || child.name || '', fullName: detail.fullName || '', steps: detail.steps || [] } : { message: child.statusMessage || child.statusDetails?.message || '', trace: child.statusTrace || child.statusDetails?.trace || '', name: child.name || '', fullName: '', steps: [] }; // 注意:不在此处做规则分析,让后台 AI 分析完成 results.push({ status: child.status, name: errorData.name || child.name || '未知用例', fullName: errorData.fullName, uid: uid, analysis: null, // 由后台 AI 分析填充 errorData: errorData, // 保存原始错误数据供分析用 detail }); } } } } } // 如果 categories 没有数据,尝试获取 behaviors if (results.length === 0) { // 优先尝试 data/behaviors.json(包含完整用例树) console.log('[Allure Analyzer] 尝试获取 data/behaviors.json...'); const dataBehaviors = await AllureAPI.fetchJSON('data/behaviors.json'); if (dataBehaviors && dataBehaviors.children) { console.log('[Allure Analyzer] 获取到 data/behaviors.json,开始递归提取用例...'); // 递归提取所有用例 function extractAllCasesFromBehaviors(nodes) { const cases = []; if (!nodes || !Array.isArray(nodes)) return cases; for (const node of nodes) { if (node.uid && node.status && !node.children) { cases.push(node); } if (node.children && Array.isArray(node.children)) { cases.push(...extractAllCasesFromBehaviors(node.children)); } } return cases; } const allCases = extractAllCasesFromBehaviors(dataBehaviors.children); console.log(`[Allure Analyzer] 从 data/behaviors.json 提取到 ${allCases.length} 个用例`); // 筛选失败用例 const failedCases = allCases.filter(c => c.status === 'failed' || c.status === 'broken'); console.log(`[Allure Analyzer] 其中 ${failedCases.length} 个失败/异常`); for (const child of failedCases) { const uid = child.uid; const detail = uid ? await AllureAPI.fetchTestCaseDetail(uid) : null; const errorData = detail ? { message: detail.statusMessage || detail.statusDetails?.message || '', trace: detail.statusTrace || detail.statusDetails?.trace || '', name: detail.name || child.name || '', fullName: detail.fullName || '', steps: detail.steps || [] } : { message: child.statusMessage || child.statusDetails?.message || '', trace: child.statusTrace || child.statusDetails?.trace || '', name: child.name || '', fullName: '', steps: [] }; // 注意:不在此处做规则分析,让后台 AI 分析完成 results.push({ status: child.status, name: errorData.name || child.name || '未知用例', fullName: errorData.fullName, uid: uid, analysis: null, // 由后台 AI 分析填充 errorData: errorData, // 保存原始错误数据供分析用 detail }); } console.log('[Allure Analyzer] data/behaviors.json 处理完成,results.length:', results.length); return results; } // 如果 data/behaviors.json 失败,尝试 widgets/behaviors.json console.log('[Allure Analyzer] data/behaviors.json 无数据,尝试 widgets/behaviors.json...'); const behaviors = await AllureAPI.fetchBehaviors(); // widgets/behaviors.json 返回 { total, items } 格式 const behaviorItems = behaviors?.items || (Array.isArray(behaviors) ? behaviors : null); if (behaviorItems && Array.isArray(behaviorItems)) { console.log('[Allure Analyzer] widgets/behaviors.items 数量:', behaviorItems.length); for (const behavior of behaviorItems) { // 尝试获取 behavior 下的 children const children = behavior.children || []; if (children.length > 0) { for (const child of children) { if (child.status === 'failed' || child.status === 'broken') { const uid = child.uid || child.parentUid; const detail = uid ? await AllureAPI.fetchTestCaseDetail(uid) : null; const errorData = detail ? { message: detail.statusMessage || detail.statusDetails?.message || '', trace: detail.statusTrace || detail.statusDetails?.trace || '', name: detail.name || child.name || '', fullName: detail.fullName || '', steps: detail.steps || [] } : { message: child.statusMessage || child.statusDetails?.message || '', trace: child.statusTrace || child.statusDetails?.trace || '', name: child.name || '', fullName: '', steps: [] }; // 注意:不在此处做规则分析,让后台 AI 分析完成 results.push({ status: child.status, name: errorData.name || child.name || '未知用例', fullName: errorData.fullName, uid: uid, analysis: null, // 由后台 AI 分析填充 errorData: errorData, // 保存原始错误数据供分析用 detail }); } } } // 如果 behavior 本身就是用例(有 uid 和 status) if (behavior.uid && behavior.status && (behavior.status === 'failed' || behavior.status === 'broken')) { const uid = behavior.uid; const detail = uid ? await AllureAPI.fetchTestCaseDetail(uid) : null; const errorData = detail ? { message: detail.statusMessage || detail.statusDetails?.message || '', trace: detail.statusTrace || detail.statusDetails?.trace || '', name: detail.name || behavior.name || '', fullName: detail.fullName || '', steps: detail.steps || [] } : { message: behavior.statusMessage || behavior.statusDetails?.message || '', trace: behavior.statusTrace || behavior.statusDetails?.trace || '', name: behavior.name || '', fullName: '', steps: [] }; // 注意:不在此处做规则分析,让后台 AI 分析完成 results.push({ status: behavior.status, name: errorData.name || behavior.name || '未知用例', fullName: errorData.fullName, uid: uid, analysis: null, // 由后台 AI 分析填充 errorData: errorData, // 保存原始错误数据供分析用 detail }); } } } console.log('[Allure Analyzer] behaviors 处理完成,results.length:', results.length); } // 如果 behaviors 也没有,尝试 suites if (results.length === 0) { const suites = await AllureAPI.fetchSuites(); if (suites && Array.isArray(suites)) { for (const suite of suites) { if (suite.children && Array.isArray(suite.children)) { for (const child of suite.children) { if (child.status === 'failed' || child.status === 'broken') { const uid = child.uid || child.parentUid; const detail = uid ? await AllureAPI.fetchTestCaseDetail(uid) : null; const errorData = detail ? { message: detail.statusMessage || detail.statusDetails?.message || '', trace: detail.statusTrace || detail.statusDetails?.trace || '', name: detail.name || child.name || '', fullName: detail.fullName || '', steps: detail.steps || [] } : { message: child.statusMessage || child.statusDetails?.message || '', trace: child.statusTrace || child.statusDetails?.trace || '', name: child.name || '', fullName: '', steps: [] }; // 注意:不在此处做规则分析,让后台 AI 分析完成 results.push({ status: child.status, name: errorData.name || child.name || '未知用例', fullName: errorData.fullName, uid: uid, analysis: null, // 由后台 AI 分析填充 errorData: errorData, // 保存原始错误数据供分析用 detail }); } } } } } } return results; }, // 通过 DOM 分析 async analyzeViaDOM() { const results = []; // 等待 Allure SPA 渲染完成 await this.delay(CONFIG.analysisDelay); // 查找所有失败/异常的用例行 const allTestRows = document.querySelectorAll( '[class*="tree__body"] [class*="tree__row"], ' + '[class*="testcase"], ' + '[class*="test-row"], ' + 'tr[class*="failed"], ' + 'tr[class*="broken"]' ); allTestRows.forEach(row => { const statusEl = row.querySelector('[class*="status"], [class*="failed"], [class*="broken"]'); const nameEl = row.querySelector('[class*="name"], [class*="title"], td:nth-child(2)'); if (statusEl) { const statusText = statusEl.className.includes('failed') ? 'failed' : statusEl.className.includes('broken') ? 'broken' : null; if (statusText) { const name = nameEl ? nameEl.textContent.trim() : '未知用例'; const messageText = row.textContent.trim().substring(0, 500); // 注意:不在此处做规则分析,让后台 AI 分析完成 results.push({ status: statusText, name, fullName: '', analysis: null, // 由后台 AI 分析填充 errorData: { message: messageText, trace: '', name, fullName: '', steps: [] } // 保存原始错误数据供分析用 }); } } }); return results; }, // 更新浮动按钮 updateFAB(count) { console.log('[Allure Analyzer] updateFAB 被调用,count:', count); console.log('[Allure Analyzer] UIRenderer.createFAB:', typeof UIRenderer.createFAB); try { const existing = document.getElementById('allure-analyzer-fab'); if (existing) existing.remove(); this.fabElement = UIRenderer.createFAB(count); console.log('[Allure Analyzer] FAB 创建成功:', this.fabElement); this.fabElement.addEventListener('click', () => this.toggleOverlay()); document.body.appendChild(this.fabElement); console.log('[Allure Analyzer] FAB 已添加到页面'); } catch (e) { console.error('[Allure Analyzer] updateFAB 错误:', e); } }, // 切换侧边面板 toggleOverlay() { if (this.overlayVisible) { this.hideOverlay(); } else { this.showOverlay(); } }, showOverlay() { if (this.overlayElement) this.overlayElement.remove(); // 如果数据还没准备好,展示加载中并轮询 if (!this.analyses || this.analyses.length === 0) { const overlay = document.createElement('div'); overlay.className = 'allure-analyzer-overlay'; overlay.id = 'allure-analyzer-overlay'; overlay.innerHTML = `
📊 智能分析报告
🔄
数据加载中...
等等数据准备好后自动刷新
`; document.body.appendChild(overlay); this.overlayElement = overlay; this.overlayVisible = true; document.getElementById('analyzer-overlay-close').addEventListener('click', () => this.hideOverlay()); // 轮询等待数据就绪 const waitForData = setInterval(() => { if (this.analyses && this.analyses.length > 0) { clearInterval(waitForData); // 数据到位,重新打开面板 this.overlayVisible = false; this.showOverlay(); } }, 300); return; } this.overlayElement = UIRenderer.createOverlay(this.analyses, this); document.body.appendChild(this.overlayElement); this.overlayVisible = true; // 打开弹窗时,如果有分析已完成但未刷新的,立即刷新 this.refreshOverlayStats(); // 添加拖拽调整宽度功能 this._bindResizeEvents(); // 绑定关闭按钮 document.getElementById('analyzer-overlay-close').addEventListener('click', () => this.hideOverlay()); // 绑定全屏按钮 const fullscreenBtn = document.getElementById('analyzer-fullscreen-btn'); if (fullscreenBtn) { fullscreenBtn.addEventListener('click', () => { this.toggleFullscreen(); }); } // 绑定设置按钮 const settingsBtn = document.getElementById('analyzer-settings-btn'); if (settingsBtn) { settingsBtn.addEventListener('click', () => { this.showSettingsPanel(); }); } // 绑定用例点击事件 const caseList = document.getElementById('analyzer-case-list'); const detailContent = document.getElementById('analyzer-detail-content'); const detailTitle = document.getElementById('analyzer-detail-title'); caseList.addEventListener('click', (e) => { const caseItem = e.target.closest('.analyzer-case-item'); // 复制按钒不触发详情 if (e.target.closest('.analyzer-btn-sm')) { const btn = e.target.closest('.analyzer-btn-sm'); const action = btn.dataset.action; const idx = parseInt(btn.dataset.index); const caseData = this.analyses[idx]; if (caseData) { if (action === 'copy-analysis') this.copyAnalysis(caseData); else if (action === 'copy-suggestions') this.copySuggestions(caseData); } return; } // 点击用例:右侧直接展示详情 if (caseItem) { // 高亮当前选中项 caseList.querySelectorAll('.analyzer-case-item').forEach(el => el.classList.remove('active')); caseItem.classList.add('active'); const idx = parseInt(caseItem.dataset.index); const caseData = this.analyses[idx]; if (!caseData || !detailContent) return; // 更新右侧标题为用例名称 const safeName = typeof caseData.name === 'string' ? caseData.name : String(caseData.name || '未知用例'); if (detailTitle) detailTitle.textContent = safeName; if (!caseData.analysis) { console.log(`[Allure Analyzer] 点击未分析用例 #${idx},触发优先分析`); detailContent.innerHTML = `
🔄
正在优先分析...
已插队到队头,稍候即可展示
`; this.priorityAnalyze(idx, detailContent); } else { console.log(`[Allure Analyzer] 点击已分析用例 #${idx},直接展示`); detailContent.innerHTML = UIRenderer.renderCaseDetail(caseData, idx); // 绑定反馈事件 this.bindFeedbackEvents(); } } }); }, // 提交反馈到内网服务 submitFeedback(idx, rating, comment) { const caseData = this.analyses[idx]; if (!caseData) return; const feedbackData = { caseName: caseData.name || '', caseFullName: caseData.fullName || '', uid: caseData.uid || '', status: caseData.status || '', rating: rating, comment: comment || '', issueType: caseData.analysis?.issueType || '', issueTypeLabel: caseData.analysis?.issueTypeLabel || '', conclusion: caseData.analysis?.conclusion || '', suggestions: caseData.analysis?.suggestions || [], timestamp: new Date().toISOString() }; const feedbackUrl = 'http://172.16.0.68:5000/feedback'; console.log('[反馈] 准备提交:', feedbackData); GM_xmlhttpRequest({ method: 'POST', url: feedbackUrl, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(feedbackData), onload: (response) => { console.log('[反馈] 提交成功:', response.responseText); const statusEl = document.querySelector(`.feedback-status[data-idx="${idx}"]`); if (statusEl) { statusEl.textContent = '✅ 已提交'; statusEl.style.color = '#4caf50'; } // 写入知识库:有反馈内容 OR 点了有帮助(无内容时保留根本原因+修复动作+结论) if (rating === 'helpful' || (comment && comment.trim())) { this.writeToKnowledgeBase(idx, rating, comment); } }, onerror: (error) => { console.error('[反馈] 提交失败:', error); const statusEl = document.querySelector(`.feedback-status[data-idx="${idx}"]`); if (statusEl) { statusEl.textContent = '❌ 提交失败'; statusEl.style.color = '#f44336'; } } }); }, // 写入知识库 writeToKnowledgeBase(idx, rating, comment) { const caseData = this.analyses[idx]; if (!caseData) return; const knowledgeData = { caseName: caseData.name || '', caseFullName: caseData.fullName || '', issueType: caseData.analysis?.issueType || '', issueTypeLabel: caseData.analysis?.issueTypeLabel || '', errorType: caseData.analysis?.errorType || '', errorTypeCode: caseData.analysis?.errorTypeCode || '', timeFormatIssue: caseData.analysis?.timeFormatIssue || false, timeIssueDetail: caseData.analysis?.timeIssueDetail || '', conclusion: caseData.analysis?.conclusion || '', suggestions: caseData.analysis?.suggestions || [], message: caseData.message || '', rootCause: caseData.analysis?.rootCauseDetail || caseData.analysis?.rootCauses?.[0] || '', rating: rating, // 记录评分(helpful/not_helpful) userComment: comment || '' // 用户输入的反馈内容 }; console.log('[知识库] 准备写入:', knowledgeData); GM_xmlhttpRequest({ method: 'POST', url: 'http://172.16.0.68:5000/api/knowledge/add', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(knowledgeData), onload: (r) => { console.log('[知识库] 响应状态:', r.status, r.responseText); if (r.status === 200) { try { const result = JSON.parse(r.responseText); console.log('[知识库] ✅ 写入成功:', result); // 显示成功提示 const statusEl = document.querySelector(`.feedback-status[data-idx="${idx}"]`); if (statusEl) { statusEl.textContent = '✅ 已提交并写入知识库'; } } catch(e) { console.error('[知识库] 响应解析失败:', e, r.responseText); } } else { console.error('[知识库] 写入失败,HTTP 状态:', r.status, r.responseText); alert('知识库写入失败:' + r.responseText); } }, onerror: (e) => { console.error('[知识库] ❌ 请求失败:', e); alert('知识库写入失败,请检查服务是否正常运行'); } }); }, // 绑定反馈按钮事件 bindFeedbackEvents() { // 延迟绑定,确保 DOM 已渲染 setTimeout(() => { // 👍 有帮助按钮 document.querySelectorAll('.thumbs-up').forEach(btn => { btn.addEventListener('click', (e) => { const idx = parseInt(e.target.dataset.idx); const resultDiv = document.querySelector(`.feedback-result[data-idx="${idx}"]`); if (resultDiv) resultDiv.style.display = 'block'; // 记录评分 e.target.dataset.rating = 'helpful'; document.querySelectorAll(`.feedback-btn[data-idx="${idx}"]`).forEach(b => b.style.opacity = '0.5'); e.target.style.opacity = '1'; }); }); // 👎 没帮助按钮 document.querySelectorAll('.thumbs-down').forEach(btn => { btn.addEventListener('click', (e) => { const idx = parseInt(e.target.dataset.idx); const resultDiv = document.querySelector(`.feedback-result[data-idx="${idx}"]`); if (resultDiv) resultDiv.style.display = 'block'; // 记录评分 e.target.dataset.rating = 'not_helpful'; document.querySelectorAll(`.feedback-btn[data-idx="${idx}"]`).forEach(b => b.style.opacity = '0.5'); e.target.style.opacity = '1'; }); }); // 提交反馈按钮 document.querySelectorAll('.submit-feedback').forEach(btn => { btn.addEventListener('click', (e) => { const idx = parseInt(e.target.dataset.idx); const textEl = document.querySelector(`.feedback-text[data-idx="${idx}"]`); const comment = textEl ? textEl.value : ''; // 查找用户实际点击的评分按钮 const thumbsUp = document.querySelector(`.thumbs-up[data-idx="${idx}"]`); const thumbsDown = document.querySelector(`.thumbs-down[data-idx="${idx}"]`); let rating = 'not_helpful'; // 默认值 if (thumbsUp && thumbsUp.style.opacity === '1') { rating = 'helpful'; } else if (thumbsDown && thumbsDown.style.opacity === '1') { rating = 'not_helpful'; } console.log('[反馈] 提交评分:', rating, 'idx:', idx); this.submitFeedback(idx, rating, comment); }); }); }, 100); }, hideOverlay() { if (this.overlayElement) { this.overlayElement.remove(); this.overlayElement = null; } this.overlayVisible = false; }, // 绑定拖拽调整宽度事件 _bindResizeEvents() { if (!this.overlayElement) return; const overlay = this.overlayElement; const listPane = overlay.querySelector('.analyzer-list-pane'); const detailPane = overlay.querySelector('.analyzer-detail-panel'); if (!listPane || !detailPane) { console.log('[Allure Analyzer] 无法找到面板元素,分隔线未创建'); return; } // 创建可拖拽分隔线 const resizer = document.createElement('div'); resizer.className = 'analyzer-resizer'; resizer.style.cssText = ` position: absolute; top: 0; bottom: 0; width: 10px; cursor: col-resize; background: rgba(102, 126, 234, 0.5); z-index: 10000; transition: background 0.2s; `; // 插入到 content 中,而不是 listPane 后面 const content = overlay.querySelector('.analyzer-overlay-content'); if (content) { content.style.position = 'relative'; // 设置初始位置为 listPane 的右边(包括 border) const initialLeft = listPane.offsetLeft + listPane.offsetWidth; resizer.style.left = initialLeft + 'px'; content.appendChild(resizer); console.log('[Allure Analyzer] ✅ 分隔线已创建,位置:', initialLeft, 'px'); } let isResizing = false; let startX = 0; let startLeftWidth = 0; // 鼠标按下 resizer.addEventListener('mousedown', (e) => { console.log('[Allure Analyzer] ✅ 分隔线 mousedown 触发'); isResizing = true; startX = e.clientX; startLeftWidth = listPane.offsetWidth; resizer.style.background = 'rgba(102, 126, 234, 1)'; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; // 防止拖拽时选中文本 e.preventDefault(); e.stopPropagation(); }); // 鼠标移动 - 绑定到 document const handleMouseMove = (e) => { if (!isResizing) return; console.log('[Allure Analyzer] 🔄 mousemove:', e.clientX, 'diff:', e.clientX - startX); const diff = e.clientX - startX; const newWidth = startLeftWidth + diff; const minWidth = 180; const maxWidth = content.offsetWidth - 320; // 右侧最小 320px const clampedWidth = Math.max(minWidth, Math.min(maxWidth, newWidth)); // 更新 listPane 宽度 listPane.style.width = clampedWidth + 'px'; listPane.style.flexShrink = '0'; // 确保不被压缩 // 更新分隔线位置 resizer.style.left = (listPane.offsetLeft + clampedWidth) + 'px'; // detailPane 会自动填充剩余空间(flex: 1) }; document.addEventListener('mousemove', handleMouseMove); // 鼠标释放 const handleMouseUp = () => { if (isResizing) { console.log('[Allure Analyzer] ✅ mouseup 触发,拖拽结束'); isResizing = false; resizer.style.background = 'rgba(102, 126, 234, 0.5)'; document.body.style.cursor = ''; document.body.style.userSelect = ''; } }; document.addEventListener('mouseup', handleMouseUp); // 悬停效果 resizer.addEventListener('mouseenter', () => { if (!isResizing) resizer.style.background = 'rgba(102, 126, 234, 0.8)'; }); resizer.addEventListener('mouseleave', () => { if (!isResizing) resizer.style.background = 'rgba(102, 126, 234, 0.5)'; }); console.log('[Allure Analyzer] ✅ 分隔线事件已绑定'); }, // 显示 AI 配置面板 showSettingsPanel() { // 检查是否已存在 const existing = document.getElementById('analyzer-settings-panel'); if (existing) { existing.remove(); return; } const panel = document.createElement('div'); panel.id = 'analyzer-settings-panel'; panel.className = 'analyzer-settings-panel'; panel.innerHTML = `
\uD83E\uDD16 AI 分析配置
当规则无法识别错误类型时,自动调用 AI 进行智能分析
你的 API Key 仅保存在本地浏览器中
`; document.body.appendChild(panel); // 绑定事件 document.getElementById('analyzer-settings-close').addEventListener('click', () => { panel.remove(); }); document.getElementById('analyzer-settings-save').addEventListener('click', () => { CONFIG.enableAIAnalysis = document.getElementById('analyzer-enable-ai').checked; CONFIG.aiProvider = document.getElementById('analyzer-ai-provider').value; CONFIG.aiApiKey = document.getElementById('analyzer-ai-apikey').value; CONFIG.aiApiUrl = document.getElementById('analyzer-ai-apiurl').value; CONFIG.aiModel = document.getElementById('analyzer-ai-model').value; // 保存到 localStorage GM_setValue('analyzer_ai_config', JSON.stringify({ enableAIAnalysis: CONFIG.enableAIAnalysis, aiProvider: CONFIG.aiProvider, aiApiKey: CONFIG.aiApiKey, aiApiUrl: CONFIG.aiApiUrl, aiModel: CONFIG.aiModel })); const status = document.getElementById('analyzer-settings-status'); status.textContent = '✅ 配置已保存!'; status.className = 'analyzer-settings-status success'; setTimeout(() => { panel.remove(); }, 1000); }); document.getElementById('analyzer-settings-test').addEventListener('click', async () => { const apiKey = document.getElementById('analyzer-ai-apikey').value; const apiUrl = document.getElementById('analyzer-ai-apiurl').value; const model = document.getElementById('analyzer-ai-model').value; const status = document.getElementById('analyzer-settings-status'); if (!apiKey) { status.textContent = '❌ 请先输入 API Key'; status.className = 'analyzer-settings-status error'; return; } status.textContent = '⏳ 正在测试连接...'; status.className = 'analyzer-settings-status loading'; try { const testResult = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: apiUrl, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: model, messages: [{ role: 'user', content: '你好' }], max_tokens: 10 }), onload: function(response) { if (response.status === 200) { resolve('success'); } else { reject(new Error(`HTTP ${response.status}`)); } }, onerror: reject }); }); status.textContent = '✅ 连接成功!AI 分析可用'; status.className = 'analyzer-settings-status success'; } catch (error) { status.textContent = `❌ 连接失败: ${error.message}`; status.className = 'analyzer-settings-status error'; } }); }, // 全屏/退出全屏切换 isFullscreen: false, toggleFullscreen() { this.isFullscreen = !this.isFullscreen; const overlay = this.overlayElement; if (!overlay) return; if (this.isFullscreen) { // 全屏模式 overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.top = '0'; overlay.style.right = '0'; overlay.style.left = '0'; overlay.style.bottom = '0'; } else { // 恢复正常 overlay.style.width = '480px'; overlay.style.height = '100vh'; overlay.style.top = '0'; overlay.style.right = '0'; overlay.style.left = ''; overlay.style.bottom = ''; } }, // 在 Allure 原生失败用例行上注入内联标记 injectInlineBadges(analyses) { // 为每个分析结果创建映射 analyses.forEach(caseData => { const badge = document.createElement('span'); badge.className = 'allure-analyzer-inline-badge'; badge.style.background = caseData.analysis?.errorType?.color || '#555'; badge.textContent = caseData.analysis?.errorType?.name || '等待分析'; badge.title = `点击查看分析: ${caseData.analysis?.errorType?.name || '等待分析'}`; badge.addEventListener('click', (e) => { e.stopPropagation(); this.showOverlay(); }); // 如果有根因,添加额外标记 let rootBadge = null; if (caseData.rootCauseSummary) { rootBadge = document.createElement('span'); rootBadge.className = 'allure-analyzer-inline-badge'; rootBadge.style.background = '#ff8800'; rootBadge.textContent = '上游根因'; rootBadge.title = `根因: ${caseData.rootCauseSummary.rootName}`; rootBadge.addEventListener('click', (e) => { e.stopPropagation(); this.showOverlay(); }); } // 尝试找到对应的 DOM 元素并插入标记 const testRows = document.querySelectorAll( `[class*="tree__row"], [class*="testcase"], [class*="test-row"]` ); testRows.forEach(row => { const nameEl = row.querySelector('[class*="name"], [class*="title"]'); if (nameEl && nameEl.textContent.includes(caseData.name)) { const existingBadge = nameEl.querySelector('.allure-analyzer-inline-badge'); if (!existingBadge) { nameEl.appendChild(badge); if (rootBadge) nameEl.appendChild(rootBadge); } } }); }); }, // 复制分析报告 copyAnalysis(caseData) { const { analysis, name, fullName, status, rootCauseSummary, traceResult } = caseData; let text = `=== Allure 智能分析报告 ===\n`; text += `用例名称: ${name}\n`; if (fullName) text += `完整路径: ${fullName}\n`; text += `状态: ${status}\n`; text += `错误类型: ${analysis.errorType.name}\n`; // 根因追溯信息 if (rootCauseSummary && traceResult) { text += `\n--- 根因追溯 ---\n`; text += `此用例失败由上游操作引起,链路如下:\n`; traceResult.chain.forEach((node, i) => { const label = node.isCurrent ? '[当前失败]' : node.isRootCause ? '[根因]' : '[上游失败]'; text += ` ${label} ${node.name}\n`; if (node.relation) text += ` \u2191 ${node.relation}\n`; }); text += `\n根因用例: ${rootCauseSummary.rootName}\n`; text += `根因操作: ${rootCauseSummary.rootOperation}\n`; text += `根因消息: ${rootCauseSummary.rootMessage}\n`; if (rootCauseSummary.rootLocation) { text += `根因位置: ${rootCauseSummary.rootLocation.file} line ${rootCauseSummary.rootLocation.line}\n`; } if (rootCauseSummary.suggestion && rootCauseSummary.suggestion.length > 0) { text += `\n根因修复建议(优先处理):\n`; rootCauseSummary.suggestion.forEach((s, i) => { text += ` ${i + 1}. ${s}\n`; }); } } else { text += `\n此用例失败为直接原因,无上游依赖问题\n`; } text += `\n--- 错误消息 ---\n${analysis.message}\n`; if (analysis.stackAnalysis.location) { text += `\n--- 错误位置 ---\n`; text += `文件: ${analysis.stackAnalysis.location.file}\n`; text += `行号: ${analysis.stackAnalysis.location.line}\n`; } text += `\n--- 建议修改点 ---\n`; analysis.suggestions.forEach((s, i) => { text += `${i + 1}. ${s}\n`; }); Utils.copyToClipboard(text); }, // 复制建议 copySuggestions(caseData) { const text = caseData.analysis.suggestions .map((s, i) => `${i + 1}. ${s}`) .join('\n'); Utils.copyToClipboard(text); }, delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } }; // ==================== 启动 ==================== if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => Analyzer.init()); } else { Analyzer.init(); } // 全局调试函数:查看 Allure JSON 结构 window.debugAllureJSON = async function(uid) { if (!uid) { const allFailedCases = window.analyzerFailedCases || []; if (allFailedCases.length > 0) { uid = allFailedCases[0].uid; console.log('使用第一个失败用例的 UID:', uid); } } if (!uid) { console.log('请提供用例 UID,例如: debugAllureJSON("your-uid-here")'); return; } console.log('正在获取用例详情:', uid); const detail = await AllureAPI.fetchTestCaseDetail(uid); console.log('=== 用例详情 JSON 结构 ==='); console.log('顶层字段:', Object.keys(detail || {})); console.log('detail.steps:', detail?.steps); console.log('detail.containers:', detail?.containers); console.log('detail.children:', detail?.children); console.log('=== JSON 前1000字符 ==='); console.log(JSON.stringify(detail, null, 2).substring(0, 1000)); return detail; }; console.log('[Allure Analyzer] 调试函数已加载: window.debugAllureJSON()'); // 暴露 AllureAPI 到全局,方便调试 window.AllureAPI = AllureAPI; console.log('[Allure Analyzer] AllureAPI 已暴露到全局: window.AllureAPI'); // 暴露 Analyzer 到全局,方便调试 window.Analyzer = Analyzer; console.log('[Allure Analyzer] Analyzer 已暴露到全局: window.Analyzer'); })();