// ==UserScript== // @name allure异常用例分析助手 // @namespace allure-ai-analyzer // @version 1.4.0 // @description 进入 Allure 报告自动预分析失败/异常用例,AI 根因分析,可拖拽可缩放浮动面板 // @author liuwubin // @match *://*/*/allure/* // @match *://*/*/allure/ // @include *://*/*/allure/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @connect dashscope.aliyuncs.com // @connect 172.16.0.67 // @connect * // @run-at document-idle // ==/UserScript== (function () { 'use strict'; // ================================================================ // ① 配置 // ================================================================ const AI_DEFAULTS = { apiUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', model: 'qwen-plus', maxTokens: 2048, temperature: 0.3, }; const FETCH_CONCURRENCY = 8; const TARGET_STATUSES = ['failed', 'broken']; // ================================================================ // ② 项目上下文(用户可直接编辑此段文本,补充常见失败模式) // ================================================================ const PROJECT_CONTEXT = ` ## 项目背景 ERP 进销存系统自动化测试,pytest + allure + Jenkins,覆盖销售、仓储、采购业务域。 ## 用例结构 前置部分(Set up)→ 测试步骤(Test body)→ 后置处理(Tear down) ## 根因定位规则 根据报错类型决定根因指向: ### 场景A:接口调用报错(不追溯) 接口返回 500、反序列化错误、参数类型不匹配等,根因就是该接口报错本身。 禁止猜测上游业务逻辑为什么没触发。 ### 场景B:查询/断言结果为空(需追溯) 查询步骤返回空结果、断言数据不存在,此时根因在上游创建/保存步骤,必须追溯哪个步骤没有正确创建数据。 ### 通用规则 - 禁止依赖用例标题判断用例目的,标题可能不准确 - 步骤展开后的中文描述(desc)是判断每一步做了什么的核心依据 - 结论用大白话,禁止技术术语堆砌 - 修复建议必须是可执行的具体动作 `; // ================================================================ // ③ 状态管理 // ================================================================ const S = { cache: new Map(), // uid → { raw, ai, aiStatus: 'pending'|'done'|'error' } total: 0, loaded: 0, phase: 'idle', // idle | fetching | ready currentUid: null, failedCases: [], // [{uid, name, status, suite}] panelVisible: false, panelMinimized: false, savedPos: null, // 最小化前保存的位置 }; // ================================================================ // ④ 样式 // ================================================================ GM_addStyle(` /* === 面板 === */ #az-panel{position:fixed;bottom:20px;right:20px;width:560px;height:580px;min-width:360px;min-height:280px; background:#1a1b26;color:#c0caf5;border-radius:12px;box-shadow:0 8px 40px rgba(0,0,0,.5); z-index:99999;font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;font-size:13px; line-height:1.6;display:flex;flex-direction:column;overflow:hidden;resize:both} #az-panel.az-min{width:52px!important;height:52px!important;min-width:52px!important;min-height:52px!important; border-radius:50%;cursor:pointer;resize:none} #az-panel.az-min .az-body,#az-panel.az-min .az-foot,#az-panel.az-min .az-prog, #az-panel.az-min .az-hd-info,#az-panel.az-min .az-hd-btns{display:none} #az-panel.az-min .az-hd{padding:14px;justify-content:center;border:none} /* 头部 */ .az-hd{display:flex;align-items:center;gap:8px;padding:10px 14px;background:#16161e; border-bottom:1px solid #292e42;cursor:move;user-select:none} .az-hd-icon{font-size:18px;flex-shrink:0} .az-hd-info{flex:1;min-width:0} .az-hd-title{font-weight:600;font-size:14px} .az-hd-sub{font-size:11px;color:#565f89;margin-top:1px} .az-hd-btns{display:flex;gap:4px} .az-hd-btns button{background:#292e42;border:none;color:#c0caf5;width:28px;height:28px;border-radius:6px; cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center} .az-hd-btns button:hover{background:#3b4261} /* 进度条 */ .az-prog{height:3px;background:#292e42} .az-prog-fill{height:100%;background:linear-gradient(90deg,#7aa2f7,#bb9af7);transition:width .3s} /* 内容区 */ .az-body{flex:1;overflow-y:auto;padding:12px 14px;scrollbar-width:thin;scrollbar-color:#3b4261 transparent} .az-body::-webkit-scrollbar{width:5px} .az-body::-webkit-scrollbar-thumb{background:#3b4261;border-radius:3px} /* 底部 */ .az-foot{padding:6px 14px;background:#16161e;border-top:1px solid #292e42;font-size:11px;color:#565f89; display:flex;justify-content:space-between} /* 标签 */ .az-badge{display:inline-block;padding:1px 8px;border-radius:4px;font-size:11px;font-weight:600;text-transform:uppercase} .az-badge-failed{background:rgba(247,118,142,.15);color:#f7768e} .az-badge-broken{background:rgba(255,158,100,.15);color:#ff9e64} /* 列表项 */ .az-item{display:flex;align-items:center;padding:8px 10px;border-radius:6px;cursor:pointer;gap:8px;transition:background .15s} .az-item:hover{background:#292e42} .az-item.az-active{background:#3b4261} .az-item-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .az-item-dur{font-size:11px;color:#565f89;flex-shrink:0} /* 详情卡片 */ .az-card{background:#16161e;border-radius:8px;padding:14px;border-left:3px solid #7aa2f7} .az-card-failed{border-left-color:#f7768e} .az-card-broken{border-left-color:#ff9e64} .az-root{font-size:16px;font-weight:700;color:#c0caf5;margin-bottom:10px} .az-row{display:flex;gap:8px;margin-bottom:6px;font-size:12px} .az-lbl{color:#565f89;min-width:72px;flex-shrink:0} .az-val{color:#c0caf5;word-break:break-all} .az-step{background:#11111b;border-radius:6px;padding:8px 10px;margin-top:6px;font-size:12px} .az-step-name{color:#e0af68;font-weight:500} .az-step-reason{color:#f7768e;margin-top:2px;font-size:11px} .az-fix{background:rgba(158,206,106,.1);border:1px solid rgba(158,206,106,.3);border-radius:6px; padding:10px 12px;margin-top:12px;font-size:12px;color:#9ece6a} .az-detail{background:#11111b;border-radius:6px;padding:10px 12px;margin-top:8px;font-size:12px; color:#a9b1d6;line-height:1.7;white-space:pre-wrap;word-break:break-all} /* 加载动画 */ .az-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;color:#7aa2f7} .az-spinner{width:28px;height:28px;border:3px solid #292e42;border-top-color:#7aa2f7;border-radius:50%; animation:az-spin .8s linear infinite;margin-bottom:10px} @keyframes az-spin{to{transform:rotate(360deg)}} /* 空状态 */ .az-empty{text-align:center;padding:40px 20px;color:#565f89} .az-empty-icon{font-size:36px;margin-bottom:8px} /* 设置弹窗 */ .az-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:100000; display:flex;align-items:center;justify-content:center} .az-dlg{background:#1a1b26;border-radius:12px;padding:24px;width:420px;box-shadow:0 8px 40px rgba(0,0,0,.5)} .az-dlg h3{margin:0 0 16px;font-size:16px} .az-field{margin-bottom:12px} .az-field label{display:block;font-size:12px;color:#565f89;margin-bottom:4px} .az-field input{width:100%;background:#16161e;border:1px solid #292e42;color:#c0caf5;padding:8px 12px; border-radius:6px;font-size:13px;box-sizing:border-box} .az-dlg-btns{display:flex;gap:8px;justify-content:flex-end;margin-top:16px} .az-dlg-btns button{padding:6px 16px;border-radius:6px;border:none;cursor:pointer;font-size:13px} .az-btn-pri{background:#7aa2f7;color:#1a1b26;font-weight:600} .az-btn-sec{background:#292e42;color:#c0caf5} `); // ================================================================ // 第1层:数据采集 // ================================================================ /** 获取 Allure 基础 URL */ function getBaseUrl() { const m = window.location.href.match(/^(https?:\/\/[^/]+\/.*?\/allure\/)/i); return m ? m[1].replace(/\/$/, '') : window.location.pathname.replace(/\/index\.html.*$/, ''); } /** 从页面 URL 或页面元素中提取 reportUuid */ function getReportUuid() { // 优先从 URL 参数中提取 const urlMatch = window.location.href.match(/[?&]reportUuid=([a-f0-9-]+)/i); if (urlMatch) return urlMatch[1]; // 从页面中搜索 reportUuid(Allure 页面会在加载时设置这个值) const scripts = document.querySelectorAll('script'); for (const s of scripts) { const m = s.textContent?.match(/reportUuid['":\s]+([a-f0-9-]+)/i); if (m) return m[1]; } return ''; } /** 构建带 reportUuid 的请求 URL */ function buildApiUrl(path) { const base = getBaseUrl(); const uuid = getReportUuid(); // 尝试 widgets 和 data 两种路径 return uuid ? base + path + '?reportUuid=' + uuid : base + path; } /** 通过 GM_xmlhttpRequest 获取 JSON(自动携带浏览器 Cookie,解决 Jenkins 登录问题) */ function fetchJSON(url) { return new Promise(resolve => { GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 15000, onload: resp => { if (resp.status < 200 || resp.status >= 400) { resolve(null); return; } const text = resp.responseText || ''; // 检测服务器返回 HTML(登录页/404)而非 JSON if (text.trimStart().startsWith('<') || text.trimStart().startsWith(' resolve(null), ontimeout: () => resolve(null), }); }); } /** 获取附件文本内容 */ function fetchText(url) { return new Promise(resolve => { GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 15000, onload: resp => { if (resp.status < 200 || resp.status >= 400) { resolve(null); return; } resolve(resp.responseText || ''); }, onerror: () => resolve(null), ontimeout: () => resolve(null), }); }); } /** 递归收集失败用例(含完整节点数据) */ function collectFailed(node, suite) { const list = []; if (!node) return list; const cur = suite || node.name || ''; if (TARGET_STATUSES.includes(node.status) && node.uid) { list.push({ uid: node.uid, name: node.name, status: node.status, suite: cur, node }); } (node.children || []).forEach(c => list.push(...collectFailed(c, cur))); return list; } /** 并发限流 */ async function parallelLimit(tasks, limit) { const results = []; const running = new Set(); for (const task of tasks) { const p = task().then(r => { running.delete(p); return r; }); running.add(p); results.push(p); if (running.size >= limit) await Promise.race(running); } return Promise.all(results); } /** 格式化耗时 */ function fmtDuration(ms) { if (!ms) return '-'; if (ms < 1000) return ms + 'ms'; const s = Math.floor(ms / 1000); return s >= 60 ? Math.floor(s / 60) + 'm ' + (s % 60) + 's' : s + 's'; } /** 从 behaviors.json 递归提取所有用例 */ function extractAllCases(nodes) { if (!nodes || !Array.isArray(nodes)) return []; const cases = []; for (const node of nodes) { if (node.uid && node.status && !node.children) cases.push(node); if (node.children) cases.push(...extractAllCases(node.children)); } return cases; } /** 从 behaviors.json 构建 uid → 功能路径 的映射 */ function buildBehaviorPathMap(nodes, path, map) { if (!nodes || !Array.isArray(nodes)) return; for (const n of nodes) { const curPath = path ? path + ' > ' + (n.name || '') : (n.name || ''); if (n.uid && (!n.children || n.children.length === 0)) { map.set(n.uid, curPath); } if (n.children) { buildBehaviorPathMap(n.children, curPath, map); } } } /** 后台预取全部失败用例 */ async function prefetchAll() { S.phase = 'fetching'; updatePanel(); const base = getBaseUrl(); let failedUids = new Set(); const categoryMap = new Map(); const behaviorPathMap = new Map(); // ★ 策略 1:categories.json const categories = await fetchJSON(base + '/data/categories.json'); const catList = Array.isArray(categories) ? categories : (categories?.children ? categories.children : null); if (catList) { function extractWithPath(nodes, path) { for (const n of (nodes || [])) { if (n.uid && (!n.children || n.children.length === 0)) { if (TARGET_STATUSES.includes(n.status)) { failedUids.add(n.uid); categoryMap.set(n.uid, path); } } else if (n.children) { const newPath = path ? path + ' > ' + n.name : n.name; extractWithPath(n.children, newPath); } } } for (const cat of catList) { extractWithPath(cat.children || [], cat.name || ''); } console.log('[Allure助手] categories.json 找到失败用例:', failedUids.size); } // 策略 2-5(备选) if (failedUids.size === 0) { const allCases = await fetchJSON(base + '/data/test-cases.json'); if (allCases && Array.isArray(allCases)) { for (const c of allCases) { if (TARGET_STATUSES.includes(c.status) && c.uid) failedUids.add(c.uid); } } } if (failedUids.size === 0) { const allCases = await fetchJSON(base + '/data/test-cases/index.json'); if (allCases && Array.isArray(allCases)) { for (const c of allCases) { if (TARGET_STATUSES.includes(c.status) && c.uid) failedUids.add(c.uid); } } } if (failedUids.size === 0) { const behaviors = await fetchJSON(base + '/data/behaviors.json'); if (behaviors) { const children = behaviors.children || (Array.isArray(behaviors) ? behaviors : null); if (children) { for (const leaf of extractAllCases(children)) { if (TARGET_STATUSES.includes(leaf.status) && leaf.uid) failedUids.add(leaf.uid); } } } } if (failedUids.size === 0) { const suites = await fetchJSON(base + '/widgets/suites.json') || await fetchJSON(buildApiUrl('/widgets/suites.json')); if (suites) { const children = suites.children || (Array.isArray(suites) ? suites : null); if (children) { for (const leaf of extractAllCases(children)) { if (TARGET_STATUSES.includes(leaf.status) && leaf.uid) failedUids.add(leaf.uid); } } } } if (failedUids.size === 0) { S.phase = 'idle'; updatePanel(); return; } // 从 behaviors.json 构建功能路径映射 const behaviors = await fetchJSON(base + '/data/behaviors.json'); if (behaviors) { const bChildren = behaviors.children || (Array.isArray(behaviors) ? behaviors : []); buildBehaviorPathMap(bChildren, '', behaviorPathMap); } // 构建失败用例列表 S.failedCases = [...failedUids].map(uid => ({ uid, name: uid, status: 'failed', suite: '', node: {}, categoryPath: categoryMap.get(uid) || '', behaviorPath: behaviorPathMap.get(uid) || '' })); S.total = S.failedCases.length; S.loaded = 0; console.log('[Allure助手] 失败用例总数:', S.total); // ★ 关键优化:立即进入 ready 状态,显示列表,后台继续预取 S.phase = 'ready'; S._backgroundFetching = true; updatePanel(); // 后台逐个获取用例详情,每完成一个就更新缓存和面板 const pendingAiUids = []; for (const fc of S.failedCases) { let raw = await fetchJSON(base + '/data/test-cases/' + fc.uid + '.json'); if (!raw) raw = await fetchJSON(buildApiUrl('/widgets/testcases/' + fc.uid + '.json')); if (raw) { fc.name = raw.name || fc.uid; fc.status = raw.status || raw.testStage?.status || 'failed'; fc.suite = raw.parentUid || ''; fc.node = raw; if (!fc.behaviorPath && raw.labels) { const feature = raw.labels.find(l => l.name === 'feature')?.value || ''; const story = raw.labels.find(l => l.name === 'story')?.value || ''; if (feature || story) fc.behaviorPath = [feature, story].filter(Boolean).join(' > '); } await fetchFailedStepAttachments(base, raw); } const data = raw || fc.node || {}; S.cache.set(fc.uid, { raw: data, ai: null, aiStatus: 'pending', fromSuites: !raw }); S.loaded++; pendingAiUids.push(fc.uid); // 如果用户正在查看这个用例,立刻刷新面板 if (S.currentUid === fc.uid) { updatePanel(); // 立即触发该用例的 AI 分析 analyzeCase(fc.uid).then(() => updatePanel()); } // 更新进度 updatePanel(); } S._backgroundFetching = false; console.log('[Allure助手] 预取完成:', S.loaded + '/' + S.total); updatePanel(); // 预取完成后,启动 AI 分析队列(跳过已由用户点击触发的用例) processAIQueue(); } // ================================================================ // 第2层:上下文构建 // ================================================================ /** 从用例详情中提取错误消息(兼容多种数据结构) */ function extractError(raw) { // 优先从顶层字段提取(Allure 2 标准结构) let msg = raw.statusMessage || ''; let trace = raw.statusTrace || ''; // 兆底:从 statusDetails 提取 if (!msg) { const sd = raw.statusDetails || raw.testStage?.statusDetails || {}; msg = sd.message || ''; trace = trace || sd.trace || ''; } // 兆底:从 testStage 提取 if (!msg && raw.testStage) { msg = raw.testStage.statusMessage || ''; trace = trace || raw.testStage.statusTrace || ''; } // 兆底:从步骤中提取 if (!msg) { const allSteps = [ ...(raw.beforeStages || []), ...(raw.beforeSteps || []), ...(raw.testStage?.steps || []), ...(raw.afterStages || []), ...(raw.afterSteps || []), ...(raw.steps || []), ]; for (const s of allSteps) { if (TARGET_STATUSES.includes(s.status)) { msg = msg || s.statusDetails?.message || s.statusMessage || ''; trace = trace || s.statusDetails?.trace || ''; } } } return { msg, trace }; } /** 扁平化步骤树,标注失败节点(兼容 suites 和 testcase 结构) */ function flattenSteps(steps, phase, depth) { if (!steps) return []; depth = depth || 0; phase = phase || 'Test body'; const out = []; for (const s of steps) { // 跳过非步骤节点(suites.json 中的 children 可能是子套件) if (s.children && !s.steps && !s.status) continue; const icon = s.status === 'failed' ? '❌' : s.status === 'broken' ? '⚠️' : '✅'; let line = ' '.repeat(depth) + icon + ' ' + (s.name || ''); if (s.time?.duration) line += ' (' + fmtDuration(s.time.duration) + ')'; // 提取附件中的 response/message if (s.attachments?.length > 0) { const att = s.attachments.find(a => /response|响应|result|请求/i.test(a.name || '')) || s.attachments[0]; if (att?._content) { const c = att._content; // 尝试解析文本格式附件(如 method:"POST" url:"..." response:{...}) const respMatch = c.match(/response:\s*(\{[\s\S]*\})/); if (respMatch) { try { const resp = JSON.parse(respMatch[1]); if (resp.code && resp.code !== '200') { line += '\n 接口响应: code=' + resp.code + ', message=' + (resp.message || '').substring(0, 300); } else if (resp.message) { line += '\n 接口响应: ' + (resp.message || '').substring(0, 300); } } catch { // 非标准 JSON,截取 response 部分 line += '\n 接口响应: ' + respMatch[1].substring(0, 500); } } else { // 无 response 字段,尝试提取 message const msgMatch = c.match(/"message"\s*:\s*"([^"]+)"/); if (msgMatch) { line += '\n 接口响应: ' + msgMatch[1]; } } } } out.push({ line, phase, failed: TARGET_STATUSES.includes(s.status), depth }); out.push(...flattenSteps(s.steps, phase, depth + 1)); } return out; } /** 拉取失败步骤的附件内容(response/request) */ async function fetchFailedStepAttachments(base, raw) { const stages = [raw.testStage, ...(raw.beforeStages || []), ...(raw.afterStages || [])]; const tasks = []; for (const stage of stages) { collectAttachmentTasks(stage?.steps, base, tasks); } if (tasks.length > 0) { await Promise.all(tasks); } } function collectAttachmentTasks(steps, base, tasks) { if (!steps) return; for (const s of steps) { if (s.attachments?.length > 0) { for (const att of s.attachments) { if (/response|request|响应|请求|result/i.test(att.name || '') && att.source && !att._content) { tasks.push( fetchText(base + '/data/attachments/' + att.source) .then(content => { att._content = content || ''; }) .catch(() => { /* 忽略附件拉取失败 */ }) ); } } } if (s.steps) collectAttachmentTasks(s.steps, base, tasks); } } /** 构建完整用例上下文(兼容两种数据源) */ function buildCaseContext(fc) { const cached = S.cache.get(fc.uid); if (!cached?.raw) return null; const raw = cached.raw; const { msg, trace } = extractError(raw); let all; // testcase 结构:有 beforeStages/beforeSteps/testStage/afterStages/afterSteps const hasBefore = raw.beforeStages || raw.beforeSteps; const hasAfter = raw.afterStages || raw.afterSteps; if (hasBefore || raw.testStage || hasAfter) { const before = flattenSteps(raw.beforeStages || raw.beforeSteps, 'Set up'); const test = flattenSteps(raw.testStage?.steps, 'Test body'); const after = flattenSteps(raw.afterStages || raw.afterSteps, 'Tear down'); all = [...before, ...test, ...after]; } else if (raw.steps) { // suites.json 节点结构:直接有 steps 数组 all = flattenSteps(raw.steps, 'Test body'); } else { all = []; } let stepsText = ''; let curPhase = ''; for (const s of all) { if (s.phase !== curPhase) { curPhase = s.phase; stepsText += '\n**[' + curPhase + ']**\n'; } stepsText += s.line + '\n'; } return { name: fc.name || raw.name || '', fullName: raw.fullName || '', duration: raw.time?.duration ? fmtDuration(raw.time.duration) : '', status: fc.status, errorMessage: msg || '无', trace: trace || '', stepsText: stepsText || '无步骤数据', categoryPath: fc.categoryPath || '', behaviorPath: fc.behaviorPath || '', }; } /** 组装完整 prompt */ function buildPrompt(ctx) { const hasSteps = ctx.stepsText && ctx.stepsText !== '无步骤数据'; const analysisHint = hasSteps ? '重要原则:禁止依赖用例标题判断用例目的,标题可能不准确,必须根据实际步骤和报错位置判断。\n' + '步骤后的中文描述是该步骤的核心说明,优先参考。\n' + '根因定位规则:\n' + '1. 接口调用报错(500、反序列化、参数类型不匹配)→ 根因就是该接口报错本身,禁止追溯上游业务逻辑\n' + '2. 查询/断言结果为空 → 向上追溯,找到哪个创建/保存步骤没有正确写入数据\n' + '3. 其他异常(如 AttributeError、TypeError)→ 根因在报错的代码位置,结合堆栈定位' : '重要原则:该用例未定义 allure 步骤,这是正常的,不要将“无步骤”作为问题来分析。\n' + '错误可能发生在前置阶段(fixture/setup),请完全基于错误消息和堆栈信息分析。\n' + '重点关注堆栈中的文件名、类名、行号,定位代码层面的根因。'; return '你是自动化测试失败分析专家。请分析以下失败用例,结合项目知识给出根因分析。\n\n' + analysisHint + '\n\n' + '---\n' + PROJECT_CONTEXT + '\n---\n\n' + '## 当前失败用例\n' + '- 名称:' + ctx.name + '\n' + '- 路径:' + ctx.fullName + '\n' + (ctx.categoryPath ? '- 分类:' + ctx.categoryPath + '\n' : '') + (ctx.behaviorPath ? '- 功能模块:' + ctx.behaviorPath + '\n' : '') + '- 耗时:' + ctx.duration + '\n' + '- 状态:' + ctx.status + '\n\n' + '## 错误消息\n```\n' + ctx.errorMessage + '\n```\n\n' + (hasSteps ? '## 执行步骤\n' + ctx.stepsText + '\n\n' : '') + (ctx.trace ? '## 错误堆栈\n```\n' + ctx.trace + '\n```\n\n' : '') + '## 请严格按以下 JSON 格式输出(不要输出其他内容):\n' + '```json\n{\n' + ' "rootCause": "一句话根因(大白话,15字以内)",\n' + ' "issueType": "code_issue 或 product_issue 或 param_change 或 environment 或 other",\n' + ' "issueTypeLabel": "代码问题 或 产品问题 或 参数变更 或 环境问题 或 其他",\n' + ' "failedStep": "' + (hasSteps ? '第一个出问题的步骤名称' : '前置阶段异常,写"无"') + '",\n' + ' "detail": "详细分析(2-3句话,禁止术语堆砌)",\n' + ' "fixAction": "具体可执行的修复动作(如果有参数类型不匹配,明确指出实际值和期望值)",\n' + ' "confidence": 8\n' + '}\n```\n\n' + '## 问题类型判断规则\n' + '- **产品问题**:接口响应包含 SQL 异常、Java 堆栈、反序列化错误(如 JSON parse error)、500 错误、枚举解析失败、业务逻辑返回错误结果\n' + '- **参数变更**:接口参数类型/格式发生变化,如字符串变数组、字段名变更、新增必填参数等。通常伴随反序列化错误或参数校验失败\n' + '- **代码问题**:测试代码本身有 bug,如 fixture 返回 None、断言期望值写错、导入缺失、参数错误。堆栈出错文件在 tests/、fixtures/、biz/、apis/ 目录下\n' + '- **环境问题**:401/403/502/503、服务不可用、登录失败、网络超时\n' + '\n## 参数类型不匹配分析规则\n' + '如果接口响应中包含参数类型错误(如 JSON parse error、Cannot deserialize),必须:\n' + '1. 从附件的请求参数中识别实际传入的值和类型\n' + '2. 从错误信息中识别接口期望的类型\n' + '3. 在 fixAction 中明确指出:实际传的是 XXX,接口期望的是 YYY,需要改成 ZZZ\n' + '\n## 禁止行为\n' + '- 接口报错场景:禁止猜测上游业务逻辑,如“自动绑定未触发导致接口报错”\n' + '- 查询为空场景:禁止只说“查询无结果”,必须追溯上游找到哪个创建步骤失败\n' + '\n## 正确示例\n' + '错误信息:AssertionError: 检查code,附件显示 queryProductlogList 接口返回 500,message 包含 Cannot deserialize\n' + '✅ 正确分析:根因=queryProductlogList接口报错,类型=参数变更,修复=opreateType传字符串"12"需改为数组[12]\n' + '❌ 错误分析:根因=自动绑定逻辑未触发,向前追溯到业务步骤...\n'; } // ================================================================ // 第3层:AI 调用 // ================================================================ /** 调用千问 API */ async function callAI(prompt) { const cfg = loadCfg(); if (!cfg.apiKey) return null; return new Promise(resolve => { GM_xmlhttpRequest({ method: 'POST', url: cfg.apiUrl || AI_DEFAULTS.apiUrl, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + cfg.apiKey, }, data: JSON.stringify({ model: cfg.model || AI_DEFAULTS.model, messages: [ { role: 'system', content: '你是自动化测试失败分析专家,严格按用户要求的JSON格式输出,不输出其他内容。' }, { role: 'user', content: prompt }, ], max_tokens: AI_DEFAULTS.maxTokens, temperature: AI_DEFAULTS.temperature, }), timeout: 60000, onload: resp => { try { const d = JSON.parse(resp.responseText); const text = d.choices?.[0]?.message?.content || ''; console.log('[Allure助手] AI原始响应:', text.substring(0, 500)); const m = text.match(/```json\s*([\s\S]*?)```/) || text.match(/\{[\s\S]*\}/); if (m) { const parsed = JSON.parse(m[1] || m[0]); console.log('[Allure助手] AI解析结果:', JSON.stringify(parsed)); resolve(parsed); return; } console.warn('[Allure助手] AI响应未匹配JSON格式'); resolve({ rootCause: text.substring(0, 200), detail: text }); } catch (e) { console.error('[Allure助手] AI响应解析失败:', e.message, resp.responseText?.substring(0, 300)); resolve({ rootCause: 'AI 响应解析失败', detail: '原始响应:' + resp.responseText?.substring(0, 300) }); } }, onerror: () => resolve(null), ontimeout: () => resolve({ rootCause: 'AI 请求超时', detail: '请检查网络连接或稍后重试' }), }); }); } /** AI 分析单个用例 */ async function analyzeCase(uid) { const fc = S.failedCases.find(c => c.uid === uid); if (!fc) return; const cached = S.cache.get(uid); if (!cached) return; // 防重复:如果已在加载或已完成,跳过 if (cached.aiStatus === 'loading' || cached.aiStatus === 'done') return; cached.aiStatus = 'loading'; const ctx = buildCaseContext(fc); if (!ctx) { cached.aiStatus = 'error'; return; } const prompt = buildPrompt(ctx); const result = await callAI(prompt); cached.ai = result; cached.aiStatus = result ? 'done' : 'error'; // 如果当前正在查看的用例,立刻更新面板 if (S.currentUid === uid) updatePanel(); } /** AI 分析队列(后台逐个处理) */ let aiRunning = false; async function processAIQueue() { if (aiRunning) return; aiRunning = true; for (const fc of S.failedCases) { const c = S.cache.get(fc.uid); if (c && c.aiStatus === 'pending') { await analyzeCase(fc.uid); } } aiRunning = false; } /** 优先分析当前查看的用例 */ function prioritizeAnalysis(uid) { const c = S.cache.get(uid); if (!c) { // 数据尚未预取到,显示加载中,等后台预取到此用例时自动渲染 updatePanel(); return; } // 已完成或在加载中,直接刷新面板,不重复触发 if (c.aiStatus === 'done' || c.aiStatus === 'loading') { updatePanel(); return; } // 立即分析 analyzeCase(uid).then(() => updatePanel()); } // ================================================================ // 第4层:展示层 // ================================================================ let panelEl, bodyEl, progEl, subEl, footEl; /** 创建面板 DOM */ function createPanel() { panelEl = document.createElement('div'); panelEl.id = 'az-panel'; panelEl.innerHTML = '' + '