mcpField
Q
qe-a11y-ally

qe-a11y-ally

github.com/proffesor-for-testing测试与质量51983 次安装今天更新
代码仓库

关于 qe-a11y-ally

面向 Claude 的无障碍审计技能,可扫描 URL,并结合自动化检查与基于上下文的 LLM 分析。它生成针对性的修复建议和报告;检测到缺少字幕的视频时,还会执行视频字幕流程。

主要功能

  1. 01使用 axe-core、pa11y 和 Lighthouse 扫描
  2. 02结合页面上下文分析问题
  3. 03生成针对性修复建议
  4. 04按影响和工作量排序修复项
  5. 05审计视频字幕

适用场景

  1. 01审计页面的无障碍问题
  2. 02并行检查多个页面
  3. 03爬取网站并进行无障碍检查
  4. 04生成修复与上线准备报告

MCP 相关

该技能可使用 Vibium 浏览器 MCP 工具访问并获取页面内容,也将 Claude Flow 浏览器工具列为备用方案。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/proffesor-for-testing/agentic-qe --skill qe-a11y-ally

安装后在 Claude Code 中输入/qe-a11y-ally确认已加载。

详细介绍

/a11y-ally - 全面无障碍审计

<default_to_action> 使用此技能时,如果提供了 URL,Claude 会自动执行所有步骤,不会在步骤之间等待用户提示。

这是一项由大模型驱动的技能

此技能的价值在于 Claude 的智能,而不仅仅是运行自动化工具:

自动化工具的功能Claude(此技能)的功能
标记“按钮没有名称”分析上下文:图标类别、父元素、附近文本 → 生成“添加到心愿单”
标记“图片缺少替代文本”使用 Vision 查看图片 → 描述实际内容
标记“视频没有字幕”下载视频、提取帧,使用 Vision 分析每一帧 → 生成实际字幕
输出通用模板生成针对具体上下文、可直接复制粘贴的修复方案

如果跳过大模型分析,此技能就毫无价值。


执行模式

CLAUDE 会不停下来执行所有步骤。

请勿在步骤之间等待用户提示。请执行完整流程:

  1. 数据收集:通过 Bash 运行多工具扫描(axe-core、pa11y、Lighthouse)
  2. 大模型分析:读取结果并分析每项违规的上下文
  3. Vision 流程:如果检测到视频 → 下载 → 提取帧 → 读取每一帧 → 进行描述
  4. 智能修复:运用推理生成针对具体上下文的修复方案
  5. 生成报告:将所有输出文件写入 docs/accessibility-scans/{page-slug}/

错误做法:

Claude: "I found 5 violations. Should I analyze them?"
User: "Yes"
Claude: "I see a video. Should I run the video pipeline?"
User: "Yes"

正确做法:

Claude: [Runs scan] → [Analyzes violations] → [Downloads video] → [Extracts frames] →
        [Reads each frame with Vision] → [Generates captions] → [Writes all files]
        "Audit complete. Generated 4 files in docs/accessibility-scans/example/"

步骤 1:浏览器自动化 - 获取内容

1.1:优先尝试 VIBIUM(主要方案)
ToolSearch("select:mcp__vibium__browser_launch")
ToolSearch("select:mcp__vibium__browser_navigate")
mcp__vibium__browser_launch({ headless: true })
mcp__vibium__browser_navigate({ url: "TARGET_URL" })

如果 Vibium 失败 → 转到步骤 1b

1b:尝试 AGENT-BROWSER 备用方案
ToolSearch("select:mcp__claude-flow_alpha__browser_open")
mcp__claude-flow_alpha__browser_open({ url: "TARGET_URL", waitUntil: "networkidle" })

如果 agent-browser 失败 → 转到步骤 1c

1c:PLAYWRIGHT + STEALTH(最终备用方案)
mkdir -p /tmp/a11y-work && cd /tmp/a11y-work
npm init -y 2>/dev/null
npm install playwright-extra puppeteer-extra-plugin-stealth @axe-core/playwright pa11y lighthouse chrome-launcher 2>/dev/null

创建并运行扫描脚本——完整的多工具扫描代码请参阅步骤 2。

1d:并行多页面审计(可选)

如需同时审计多个 URL,请使用并行执行:

// /tmp/a11y-work/parallel-audit.js
const { chromium } = require('playwright-extra');
const stealth = require('puppeteer-extra-plugin-stealth')();
const { AxeBuilder } = require('@axe-core/playwright');

chromium.use(stealth);

const MAX_CONCURRENT = 6;  // Maximum parallel auditors

async function auditUrl(browser, url) {
  const context = await browser.newContext();
  const page = await context.newPage();

  try {
    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
    await page.waitForTimeout(2000);

    const axeResults = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
      .analyze();

    return { url, success: true, violations: axeResults.violations };
  } catch (error) {
    return { url, success: false, error: error.message };
  } finally {
    await context.close();
  }
}

async function parallelAudit(urls) {
  const browser = await chromium.launch({ headless: true });
  const results = [];

  // Process in chunks of MAX_CONCURRENT
  for (let i = 0; i < urls.length; i += MAX_CONCURRENT) {
    const chunk = urls.slice(i, i + MAX_CONCURRENT);
    console.log(`Auditing batch ${Math.floor(i/MAX_CONCURRENT) + 1}: ${chunk.length} URLs`);

    const chunkResults = await Promise.all(
      chunk.map(url => auditUrl(browser, url))
    );
    results.push(...chunkResults);
  }

  await browser.close();
  return results;
}

// Usage: node parallel-audit.js url1 url2 url3 ...
const urls = process.argv.slice(2);
if (urls.length > 0) {
  parallelAudit(urls).then(results => {
    console.log(JSON.stringify(results, null, 2));
  });
}

多页面审计用法:

node parallel-audit.js https://example.com https://example.com/about https://example.com/contact
1e:站点爬取模式(可选)

如需全面审计站点,请爬取并审计所有页面:

// /tmp/a11y-work/crawl-audit.js
async function crawlAndAudit(startUrl, maxPages = 50) {
  const browser = await chromium.launch({ headless: true });
  const visited = new Set();
  const toVisit = [startUrl];
  const results = [];
  const baseUrl = new URL(startUrl).origin;

  while (toVisit.length > 0 && results.length < maxPages) {
    const url = toVisit.shift();
    if (visited.has(url)) continue;
    visited.add(url);

    console.log(`[${results.length + 1}/${maxPages}] Auditing: ${url}`);

    const context = await browser.newContext();
    const page = await context.newPage();

    try {
      await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });

      // Extract same-domain links for crawling
      const links = await page.evaluate((base) => {
        return [...document.querySelectorAll('a[href]')]
          .map(a => a.href)
          .filter(href => href.startsWith(base) && !href.includes('#'))
          .filter(href => !href.match(/\.(pdf|jpg|png|gif|css|js)$/i));
      }, baseUrl);

      // Add new links to queue
      links.forEach(link => {
        if (!visited.has(link) && !toVisit.includes(link)) {
          toVisit.push(link);
        }
      });

      // Run accessibility audit
      const axeResults = await new AxeBuilder({ page })
        .withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
        .analyze();

      results.push({ url, violations: axeResults.violations });
    } catch (e) {
      results.push({ url, error: e.message });
    }

    await context.close();
  }

  await browser.close();
  return { pagesAudited: results.length, results };
}

// Usage: node crawl-audit.js https://example.com 50
const [startUrl, maxPages] = process.argv.slice(2);
crawlAndAudit(startUrl, parseInt(maxPages) || 50).then(r => console.log(JSON.stringify(r, null, 2)));

步骤 2:全面 WCAG 扫描(多工具、并行、弹性)

**重要:**此步骤使用三种无障碍测试工具,以实现最大覆盖范围:

  • axe-core:行业标准,在 ARIA 和语义问题方面表现出色
  • pa11y:在对比度、链接和 HTML 验证方面表现出色
  • Lighthouse:Google 的无障碍评分,并与性能相关联

综合检测率比使用任意单一工具高约 15%。

2.0:弹性架构(v7.0 增强)

相较于 v6.0 的主要改进:

特性v6.0(旧)v7.0(新)
工具执行顺序执行并行执行(Promise.allSettled)
超时处理全局 60s按工具设置(60s/60s/90s)
故障模式全有或全无平稳降级
重试逻辑无指数退避(重试 3 次)
输出方式等待全部完成渐进式(就绪后立即输出)
最少工具数必须为 3 个3 个中有 1 个即可

按成功的工具数量计算的覆盖率:

  • 3/3 个工具:检测率约 95%(最佳)
  • 2/3 个工具:检测率约 85%(良好)
  • 1/3 个工具:检测率约 70%(可接受)
  • 0/3 个工具:失败——使用不同策略重试
2.1:运行多工具分析(并行 + 弹性)

创建并运行 /tmp/a11y-work/multi-tool-scan.js:

const { chromium } = require('playwright-extra');
const stealth = require('puppeteer-extra-plugin-stealth')();
const { AxeBuilder } = require('@axe-core/playwright');
const pa11y = require('pa11y');
const lighthouse = require('lighthouse').default || require('lighthouse');
const { launch: launchChrome } = require('chrome-launcher');
const fs = require('fs');

chromium.use(stealth);

const TARGET_URL = process.argv[2] || 'TARGET_URL';
const OUTPUT_FILE = '/tmp/a11y-work/scan-results.json';
const SYSTEM_CHROMIUM = '/usr/bin/chromium';

// ========== RESILIENCE UTILITIES ==========

// Timeout wrapper - wraps any promise with a timeout
function withTimeout(promise, ms, name) {
  return Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error(`${name} timed out after ${ms}ms`)), ms)
    )
  ]);
}

// Retry wrapper - retries with exponential backoff
async function withRetry(fn, name, maxRetries = 3, baseDelay = 2000) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      const isLastAttempt = attempt === maxRetries;
      console.log(`[${name}] Attempt ${attempt}/${maxRetries} failed: ${error.message}`);
      if (isLastAttempt) throw error;
      const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff
      console.log(`[${name}] Retrying in ${delay}ms...`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

// Sleep utility
const sleep = (ms) => new Promise(r => setTimeout(r, ms));

// Progressive output - append results as they arrive
function progressiveOutput(tool, data) {
  console.log(`\n=== ${tool.toUpperCase()} COMPLETE ===`);
  console.log(JSON.stringify(data, null, 2));

  // Append to results file for progressive access
  try {
    let results = {};
    if (fs.existsSync(OUTPUT_FILE)) {
      results = JSON.parse(fs.readFileSync(OUTPUT_FILE, 'utf8'));
    }
    results[tool] = data;
    results.lastUpdated = new Date().toISOString();
    fs.writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2));
  } catch (e) { /* ignore file errors */ }
}

// ========== TOOL RUNNERS ==========

// TOOL 1: Axe-core (with page info extraction)
async function runAxeCore(url) {
  console.log('[axe-core] Starting...');
  const browser = await chromium.launch({
    headless: true,
    executablePath: SYSTEM_CHROMIUM,
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-dev-shm-usage',
      '--disable-blink-features=AutomationControlled'
    ]
  });

  const context = await browser.newContext({
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    locale: 'en-US',
    viewport: { width: 1920, height: 1080 }
  });

  const page = await context.newPage();

  try {
    // Use domcontentloaded (faster, more reliable than networkidle)
    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });

    // Random delay to appear human
    await sleep(2000 + Math.random() * 2000);

    // Try to dismiss cookie banners
    try {
      const cookieSelectors = [
        'button:has-text("Accept")', 'button:has-text("Akzeptieren")',
        'button:has-text("Alle akzeptieren")', '[data-testid="cookie-accept"]',
        '#onetrust-accept-btn-handler', '.cookie-consent-accept'
      ];
      for (const selector of cookieSelectors) {
        const btn = await page.$(selector);
        if (btn) { await btn.click(); await sleep(500); break; }
      }
    } catch (e) { /* ignore cookie errors */ }

    // Run axe-core analysis
    const axeResults = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
      .analyze();

    // Extract comprehensive page info
    const pageInfo = await page.evaluate(() => ({
      title: document.title,
      url: window.location.href,
      lang: document.documentElement.lang,
      images: {
        total: document.querySelectorAll('img').length,
        withAlt: document.querySelectorAll('img[alt]').length,
        withoutAlt: document.querySelectorAll('img:not([alt])').length,
        emptyAlt: document.querySelectorAll('img[alt=""]').length
      },
      headings: {
        h1: Array.from(document.querySelectorAll('h1')).map(h => h.textContent.trim().slice(0,60)),
        h2: document.querySelectorAll('h2').length,
        h3: document.querySelectorAll('h3').length,
        total: document.querySelectorAll('h1,h2,h3,h4,h5,h6').length
      },
      forms: {
        total: document.querySelectorAll('form').length,
        inputs: document.querySelectorAll('input, select, textarea').length,
        buttons: document.querySelectorAll('button').length
      },
      links: { total: document.querySelectorAll('a').length },
      aria: {
        ariaLabels: document.querySelectorAll('[aria-label]').length,
        roles: document.querySelectorAll('[role]').length
      },
      landmarks: {
        main: document.querySelectorAll('main').length,
        nav: document.querySelectorAll('nav').length,
        header: document.querySelectorAll('header').length,
        footer: document.querySelectorAll('footer').length
      },
      media: {
        videos: document.querySelectorAll('video').length,
        iframes: document.querySelectorAll('iframe').length,
        videoUrls: Array.from(document.querySelectorAll('video')).map(v => {
          const src = v.src || (v.querySelector('source') ? v.querySelector('source').src : '');
          return {
            src: src,
            hasCaptions: !!v.querySelector('track[kind="captions"]')
          };
        })
      }
    }));

    const violations = axeResults.violations.map(v => ({
      tool: 'axe-core',
      id: v.id,
      impact: v.impact,
      description: v.description,
      help: v.help,
      helpUrl: v.helpUrl,
      tags: v.tags,
      nodeCount: v.nodes.length,
      nodes: v.nodes.slice(0, 5).map(n => ({
        html: n.html.slice(0, 200),
        target: n.target,
        failureSummary: n.failureSummary
      }))
    }));

    return {
      success: true,
      pageInfo,
      violations,
      passesCount: axeResults.passes.length
    };
  } finally {
    await context.close();
    await browser.close();
  }
}

// TOOL 2: Pa11y
async function runPa11y(url) {
  console.log('[pa11y] Starting...');
  const results = await pa11y(url, {
    standard: 'WCAG2AA',
    timeout: 45000,
    wait: 2000,
    chromeLaunchConfig: {
      executablePath: SYSTEM_CHROMIUM,
      args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage']
    }
  });

  const violations = results.issues.map(issue => ({
    tool: 'pa11y',
    id: issue.code,
    impact: issue.type === 'error' ? 'serious' : issue.type === 'warning' ? 'moderate' : 'minor',
    description: issue.message,
    selector: issue.selector,
    context: (issue.context || '').slice(0, 200)
  }));

  return { success: true, violations, total: results.issues.length };
}

// TOOL 3: Lighthouse
async function runLighthouse(url) {
  console.log('[lighthouse] Starting...');
  const chrome = await launchChrome({
    chromePath: SYSTEM_CHROMIUM,
    chromeFlags: ['--headless', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
  });

  try {
    const result = await lighthouse(url, {
      port: chrome.port,
      onlyCategories: ['accessibility'],
      output: 'json'
    });

    const lhr = result.lhr;
    const score = Math.round(lhr.categories.accessibility.score * 100);
    const violations = Object.values(lhr.audits)
      .filter(audit => audit.score !== null && audit.score < 1)
      .map(audit => ({
        tool: 'lighthouse',
        id: audit.id,
        impact: audit.score === 0 ? 'critical' : audit.score < 0.5 ? 'serious' : 'moderate',
        score: audit.score,
        description: audit.title
      }));

    return { success: true, score, violations };
  } finally {
    await chrome.kill();
  }
}

// ========== MAIN: PARALLEL EXECUTION WITH GRACEFUL DEGRADATION ==========

(async () => {
  console.log('=== MULTI-TOOL ACCESSIBILITY SCAN (v7.0 PARALLEL + RESILIENT) ===');
  console.log('Target:', TARGET_URL);
  console.log('Strategy: Promise.allSettled with per-tool timeouts\n');

  const startTime = Date.now();

  // Run ALL tools in PARALLEL with individual timeouts
  const [axeResult, pa11yResult, lighthouseResult] = await Promise.allSettled([
    withTimeout(
      withRetry(() => runAxeCore(TARGET_URL), 'axe-core', 2, 3000),
      60000, 'axe-core'
    ),
    withTimeout(
      withRetry(() => runPa11y(TARGET_URL), 'pa11y', 2, 3000),
      60000, 'pa11y'
    ),
    withTimeout(
      withRetry(() => runLighthouse(TARGET_URL), 'lighthouse', 2, 3000),
      90000, 'lighthouse'
    )
  ]);

  // ========== PROCESS RESULTS (Graceful Degradation) ==========
  const results = {
    url: TARGET_URL,
    timestamp: new Date().toISOString(),
    duration: `${((Date.now() - startTime) / 1000).toFixed(1)}s`,
    toolsSucceeded: 0,
    toolsFailed: 0,
    pageInfo: null,
    violations: [],
    byTool: {}
  };

  // Process axe-core results
  if (axeResult.status === 'fulfilled') {
    results.toolsSucceeded++;
    results.pageInfo = axeResult.value.pageInfo;
    results.violations.push(...axeResult.value.violations);
    results.byTool['axe-core'] = {
      success: true,
      count: axeResult.value.violations.length,
      passes: axeResult.value.passesCount
    };
    progressiveOutput('axe-core', axeResult.value);
  } else {
    results.toolsFailed++;
    results.byTool['axe-core'] = { success: false, error: axeResult.reason.message };
    console.log('\n[axe-core] FAILED:', axeResult.reason.message);
  }

  // Process pa11y results
  if (pa11yResult.status === 'fulfilled') {
    results.toolsSucceeded++;
    results.violations.push(...pa11yResult.value.violations);
    results.byTool['pa11y'] = {
      success: true,
      count: pa11yResult.value.violations.length
    };
    progressiveOutput('pa11y', pa11yResult.value);
  } else {
    results.toolsFailed++;
    results.byTool['pa11y'] = { success: false, error: pa11yResult.reason.message };
    console.log('\n[pa11y] FAILED:', pa11yResult.reason.message);
  }

  // Process lighthouse results
  if (lighthouseResult.status === 'fulfilled') {
    results.toolsSucceeded++;
    results.violations.push(...lighthouseResult.value.violations);
    results.byTool['lighthouse'] = {
      success: true,
      score: lighthouseResult.value.score,
      count: lighthouseResult.value.violations.length
    };
    progressiveOutput('lighthouse', lighthouseResult.value);
  } else {
    results.toolsFailed++;
    results.byTool['lighthouse'] = { success: false, error: lighthouseResult.reason.message };
    console.log('\n[lighthouse] FAILED:', lighthouseResult.reason.message);
  }

  // ========== DEDUPLICATE VIOLATIONS ==========
  const seen = new Set();
  const uniqueViolations = [];
  for (const v of results.violations) {
    const key = (v.description || '').toLowerCase().slice(0, 50);
    if (!seen.has(key)) {
      seen.add(key);
      uniqueViolations.push(v);
    }
  }
  results.uniqueViolations = uniqueViolations;
  results.totalUnique = uniqueViolations.length;

  // ========== FINAL OUTPUT ==========
  console.log('\n' + '='.repeat(60));
  console.log('=== SCAN COMPLETE ===');
  console.log('='.repeat(60));
  console.log(`Tools succeeded: ${results.toolsSucceeded}/3`);
  console.log(`Tools failed: ${results.toolsFailed}/3`);
  console.log(`Duration: ${results.duration}`);
  console.log(`Total unique violations: ${results.totalUnique}`);

  if (results.toolsSucceeded === 0) {
    console.log('\n⚠️  ALL TOOLS FAILED - Consider:');
    console.log('   1. Site may have strong bot protection');
    console.log('   2. Try Vibium MCP browser instead');
    console.log('   3. Check network connectivity');
  } else if (results.toolsSucceeded < 3) {
    console.log(`\n⚠️  Partial coverage (${results.toolsSucceeded}/3 tools) - Results still usable`);
  } else {
    console.log('\n✅ Full coverage achieved (3/3 tools)');
  }

  console.log('\n=== PAGE INFO ===');
  console.log(JSON.stringify(results.pageInfo, null, 2));

  console.log('\n=== VIOLATIONS BY TOOL ===');
  console.log(JSON.stringify(results.byTool, null, 2));

  console.log('\n=== UNIQUE VIOLATIONS ===');
  console.log(JSON.stringify(results.uniqueViolations, null, 2));

  // Save final results
  fs.writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2));
  console.log(`\nResults saved to: ${OUTPUT_FILE}`);
})();
2.2:读取扫描结果

运行扫描后,读取结果文件:

cat /tmp/a11y-work/scan-results.json

结果包括:

  • pageInfo:页面结构、图片、标题、媒体
  • violations:所有工具发现的违规项(已去重)
  • byTool:各工具的成功/失败状态
  • toolsSucceeded:已完成的工具数量(1-3)
2.3:优雅降级决策树
工具成功数操作
3/3✅ 覆盖完整 - 使用所有结果继续
2/3⚠️ 覆盖良好 - 在报告中注明失败的工具
1/3⚠️ 基本覆盖 - 继续处理,但标明置信度有限
0/3❌ 使用 Vibium MCP 重试,或记录失败
2.4:强制要求 - 检查视频并触发流水线

读取扫描结果后,检查 pageInfo.media.videoUrls:

// Check scan-results.json for videos
const results = JSON.parse(fs.readFileSync('/tmp/a11y-work/scan-results.json'));
if (results.pageInfo && results.pageInfo.media.videoUrls.length > 0) {
  console.log('=== VIDEOS DETECTED - TRIGGERING VIDEO PIPELINE ===');
  for (const video of results.pageInfo.media.videoUrls) {
    console.log(`Video: ${video.src}`);
    console.log(`  Has captions: ${video.hasCaptions}`);
  }
  // PROCEED TO STEP 7 IMMEDIATELY
}

如果检测到视频且 hasCaptions=false → 生成报告前必须执行 STEP 7。


STEP 3:基于上下文的修复(由大模型驱动)

这正是 CLAUDE 智能发挥作用的地方。

通用工具输出:aria-label="[DESCRIPTION]" 你的输出:aria-label="Add to shopping cart",因为你理解上下文。

3.1:上下文分析(运用推理能力)

对于每个违规项,Claude 必须:

  1. 读取 HTML 上下文 - 不要只看到 <button class="btn">,还要看到:

    <div class="product-card" data-product="Adidas Superstar">
      <img src="superstar.jpg" alt="White sneakers">
      <span class="price">$99</span>
      <button class="btn add-to-cart">  
        <svg class="icon-cart">...</svg>
      </button>
    </div>
    
  2. 根据以下信息推断用途:

    • 类名:add-to-cart、wishlist、menu-toggle
    • 父级上下文:位于包含产品数据的 .product-card 内
    • 图标类名:icon-cart、icon-heart、icon-search
    • 附近文本:产品名称、价格、“Add to bag”
    • 页面区域:页眉导航栏、产品网格或结账区域
  3. 生成具体修复方案:

    
    <button aria-label="[DESCRIPTION]">
    
    
    <button aria-label="Add Adidas Superstar to cart - $99">
    
3.2:置信度评分

为每项修复评定置信度:

  • 0.9+:上下文明确(class="add-to-cart" 位于产品名称附近)
  • 0.7-0.9:推断合理(仅有 icon-cart 类名)
  • <0.7:需要人工审核(上下文模糊)

在 remediation.md 中加入置信度:

### Button: `.product-card .btn` (Confidence: 0.95)
**Context:** Inside product card for "Adidas Superstar", has cart icon
**Fix:** `aria-label="Add Adidas Superstar to cart"`
3.2:按违规类型分类的修复模板

表单标签(WCAG 1.3.1、3.3.2、4.1.2)





<input type="text" name="cardNumber" placeholder="1234 5678 9012 3456">


<label for="card-number">Credit Card Number</label>
<input type="text"
       id="card-number"
       name="cardNumber"
       placeholder="1234 5678 9012 3456"
       aria-describedby="card-hint"
       autocomplete="cc-number"
       inputmode="numeric"
       pattern="[0-9\s]{13,19}">
<span id="card-hint" class="visually-hidden">Enter 16-digit card number</span>


- Visible label aids all users
- aria-describedby provides additional context
- autocomplete enables autofill
- inputmode shows numeric keyboard on mobile
- pattern enables browser validation

图标按钮(WCAG 4.1.2)





<button class="menu-toggle">
  <svg>...</svg>
</button>


<button class="menu-toggle"
        type="button"
        aria-expanded="false"
        aria-controls="main-menu"
        aria-label="Open navigation menu">
  <svg aria-hidden="true" focusable="false">...</svg>
</button>


- aria-label describes action, not icon
- aria-expanded communicates state
- aria-controls links to menu element
- SVG hidden from assistive tech (decorative)

颜色对比度(WCAG 1.4.3)






.low-contrast { color: #767676; background: #ffffff; }


.accessible { color: #757575; background: #ffffff; } /* 4.6:1 - PASSES AA */


.high-contrast { color: #595959; background: #ffffff; } /* 7.0:1 - PASSES AAA */


| Original | AA Pass | AAA Pass | Notes |
|----------|---------|----------|-------|
| #767676  | #757575 | #595959  | Gray text |
| #0066cc  | #0055b3 | #003d82  | Link blue |
| #cc0000  | #b30000 | #8b0000  | Error red |

标题层级(WCAG 1.3.1)




<h1>Welcome</h1>
<h1>Products</h1>      
<h4>Shoes</h4>         
<h1>Contact</h1>


<h1>Site Name - Main Page Title</h1>
<main>
  <section aria-labelledby="products-heading">
    <h2 id="products-heading">Products</h2>
    <h3>Shoes</h3>
    <h3>Clothing</h3>
  </section>
  <section aria-labelledby="contact-heading">
    <h2 id="contact-heading">Contact</h2>
  </section>
</main>


h1: Site Name - Main Page Title
├── h2: Products
│   ├── h3: Shoes
│   └── h3: Clothing
└── h2: Contact

跳过链接(WCAG 2.4.1)


<body>
  <a href="#main-content" class="skip-link">Skip to main content</a>
  <a href="#main-nav" class="skip-link">Skip to navigation</a>

  <header>
    <nav id="main-nav" aria-label="Main navigation">...</nav>
  </header>

  <main id="main-content" tabindex="-1">
    
  </main>
</body>

<style>
.skip-link {
  position: absolute;
  top: -100%;
  left: 16px;
  background: #000;
  color: #fff;
  padding: 12px 24px;
  z-index: 10000;
  text-decoration: none;
  font-weight: bold;
  border-radius: 0 0 4px 4px;
  transition: top 0.2s;
}
.skip-link:focus {
  top: 0;
  outline: 3px solid #ffcc00;
  outline-offset: 2px;
}
</style>

焦点指示器(WCAG 2.4.7)

/* NEVER do this */
*:focus { outline: none; } /* WCAG FAIL */

/* DO THIS - Custom focus styles */
:focus-visible {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
}

/* Remove outline only for mouse users */
:focus:not(:focus-visible) {
  outline: none;
}

/* High contrast for interactive elements */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[role="button"]:focus-visible {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
  box-shadow: 0 0 0 6px rgba(0, 95, 204, 0.2);
}

/* Dark backgrounds need light focus */
.dark-bg :focus-visible {
  outline-color: #ffffff;
  box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.3);
}

键盘导航(WCAG 2.1.1、2.1.2)




<div class="dropdown" onclick="toggleMenu()">
  Menu
</div>


<button type="button"
        class="dropdown-trigger"
        aria-expanded="false"
        aria-controls="dropdown-menu"
        onclick="toggleMenu()"
        onkeydown="handleKeydown(event)">
  Menu
</button>
<ul id="dropdown-menu" role="menu" hidden>
  <li role="none"><a role="menuitem" href="/page1">Page 1</a></li>
  <li role="none"><a role="menuitem" href="/page2">Page 2</a></li>
</ul>

<script>
function handleKeydown(event) {
  switch(event.key) {
    case 'Enter':
    case ' ':
      event.preventDefault();
      toggleMenu();
      break;
    case 'Escape':
      closeMenu();
      break;
    case 'ArrowDown':
      event.preventDefault();
      focusFirstMenuItem();
      break;
  }
}
</script>

模态框焦点陷阱(WCAG 2.4.3)

// Focus trap for modals - REQUIRED for WCAG compliance
function trapFocus(modal) {
  const focusable = modal.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  // Focus first element when modal opens
  first?.focus();

  modal.addEventListener('keydown', (e) => {
    if (e.key === 'Tab') {
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault();
        last.focus();
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
    if (e.key === 'Escape') {
      closeModal();
    }
  });
}

// Return focus when modal closes
function closeModal() {
  modal.hidden = true;
  triggerButton.focus(); // Return focus to trigger
}

iframe 标题(WCAG 4.1.2)


<iframe src="map.html" title="Store location map showing 5 nearby stores"></iframe>
<iframe src="video.html" title="Product demonstration video with captions"></iframe>
<iframe src="chat.html" title="Customer support chat window"></iframe>

STEP 4:用户影响分析

针对每项违规,计算用户影响:

4.1:受影响的用户群体
违规类型受影响群体用户占比
缺少 alt 文本盲人、低视力用户7-10%
缺少表单标签盲人、屏幕阅读器用户5-8%
颜色对比度低低视力、色盲用户8-12%
无法使用键盘访问运动障碍用户、进阶用户10-15%
缺少字幕耳聋、听力障碍用户5-7%
闪烁内容对癫痫发作敏感的用户0.5-1%
语言复杂认知障碍用户10-15%
4.2:影响严重程度分类
BLOCKS-USAGE: User cannot complete task at all
  - Missing form labels on required fields
  - Keyboard traps
  - Critical buttons without accessible names

IMPAIRS-USAGE: User can complete task with difficulty
  - Low contrast (can read with effort)
  - Missing skip links (tedious navigation)
  - Incorrect heading structure (confusing)

MINOR-INCONVENIENCE: Suboptimal but functional
  - Empty alt on decorative images
  - Redundant ARIA
  - Non-semantic HTML that works

STEP 5:基于投资回报率的优先级排序

为每项修复计算优先级:

5.1:优先级公式
PRIORITY_SCORE = (IMPACT_WEIGHT × USERS_AFFECTED) / EFFORT_HOURS

Where:
- IMPACT_WEIGHT: Critical=10, Serious=7, Moderate=4, Minor=1
- USERS_AFFECTED: Estimated % of users impacted
- EFFORT_HOURS: Estimated fix time (0.25 to 8 hours)
5.2:工作量估算指南
修复类型工作量复杂度
添加 aria-label0.25h微不足道
添加 alt 文本0.25h微不足道
添加表单标签0.5h简单
修复颜色对比度0.5h简单
添加跳过链接1h简单
修复标题结构2h中等
添加键盘导航4h高
实现焦点陷阱4h高
添加视频字幕8h高
5.3:优先级输出格式
| Rank | Violation | Impact | Users | Effort | ROI Score |
|------|-----------|--------|-------|--------|-----------|
| 1 | Form labels missing | Critical | 15% | 0.5h | 300 |
| 2 | Keyboard trap | Critical | 12% | 4h | 30 |
| 3 | Low contrast | Serious | 10% | 0.5h | 140 |
| 4 | Missing alt text | Serious | 8% | 0.25h | 224 |

STEP 6:生产就绪度评估

6.1:合规评分
COMPLIANCE_SCORE = (PASSED_CRITERIA / TOTAL_CRITERIA) × 100

Production Ready if:
✓ Score ≥ 85%
✓ Zero critical violations
✓ Fewer than 3 serious violations
✓ All user journeys keyboard accessible
6.2:POUR 分析(可感知、可操作、可理解、健壮)
| Principle | Guidelines | Pass | Fail | Score |
|-----------|-----------|------|------|-------|
| Perceivable | 1.1-1.4 | 12 | 3 | 80% |
| Operable | 2.1-2.5 | 18 | 2 | 90% |
| Understandable | 3.1-3.3 | 8 | 1 | 89% |
| Robust | 4.1 | 4 | 1 | 80% |
| **TOTAL** | | **42** | **7** | **86%** |

STEP 7:视频无障碍流水线

对页面上检测到的每个视频执行。

7.1:检测并提取视频 URL(强制要求)

此步骤必须集成到 STEP 2 的多工具扫描中。

将以下代码添加到多工具扫描中的 page.evaluate():

// In pageInfo extraction (STEP 2), add:
videos: {
  elements: [...document.querySelectorAll('video')].map(v => ({
    src: v.src || v.querySelector('source')?.src,
    fullUrl: new URL(v.src || v.querySelector('source')?.src || '', window.location.href).href,
    poster: v.poster,
    hasCaptions: v.querySelector('track[kind="captions"]') !== null,
    hasDescriptions: v.querySelector('track[kind="descriptions"]') !== null,
    duration: v.duration || 'unknown',
    autoplay: v.autoplay,
    muted: v.muted
  })),
  iframes: [...document.querySelectorAll('iframe')].map(iframe => {
    const src = iframe.src;
    const isVideo = /youtube|vimeo|dailymotion|wistia/.test(src);
    return isVideo ? { src, platform: src.match(/(youtube|vimeo|dailymotion|wistia)/)?.[1] } : null;
  }).filter(Boolean)
}

**强制输出:**记录找到的所有视频 URL:

=== VIDEOS DETECTED ===
Video 1: https://example.com/promo.mp4 (no captions, no descriptions)
YouTube iframe: https://youtube.com/embed/xxx
7.2:下载并提取帧(每个视频都必须执行)

对于在 7.1 中找到的每个视频 URL:

# Create output directory
mkdir -p /tmp/a11y-work/frames

# Download video (with retry and user-agent)
curl -L -A "Mozilla/5.0" --retry 3 -o /tmp/a11y-work/video.mp4 "FULL_VIDEO_URL"

# Verify download succeeded
if [ -f /tmp/a11y-work/video.mp4 ] && [ -s /tmp/a11y-work/video.mp4 ]; then
  echo "Video downloaded successfully"
  ffmpeg -i /tmp/a11y-work/video.mp4 -vf "fps=1/3" -frames:v 10 /tmp/a11y-work/frames/frame_%02d.jpg 2>/dev/null
  echo "Extracted $(ls /tmp/a11y-work/frames/*.jpg 2>/dev/null | wc -l) frames"
else
  echo "VIDEO DOWNLOAD FAILED - Document this in audit-summary.md"
fi

如果视频下载失败:

  1. 在 audit-summary.md 中记录失败原因
  2. 仍需在 violations.json 中创建 video-captions 违规项
  3. 添加修复说明,但不要生成字幕
  4. 将视频处理流程标记为“blocked”,而不是“skipped”
7.3:使用 Claude Vision 分析每一帧(必须执行)

对每张帧图像使用 Read 工具。

Claude Code 具备原生视觉能力。读取图像文件时,你就能看到它。

Read /tmp/a11y-work/frames/frame_01.jpg
Read /tmp/a11y-work/frames/frame_02.jpg
Read /tmp/a11y-work/frames/frame_03.jpg
... (continue for all frames)

对于每一帧,请描述:

  • 场景:环境、场所、光线、地点
  • 人物:出现的人物、他们在做什么、表情、服装
  • 产品:展示的物品(对于电商:产品名称、颜色、款式)
  • 文本:任何可见文本、徽标、标牌、价格
  • 动作:移动、转场、正在发生的事情

使用 Read 工具读取 frame_01.jpg 后的输出示例:

Frame 1 (0:00-0:03): A woman in white Adidas sneakers running on a forest trail.
Morning light filters through trees. She wears black athletic leggings and a
gray tank top. The Adidas three-stripe logo is visible on her shoes.

这就是大模型的价值。 通用工具会输出“[描述内容]”。 你能看到图像,因此可以输出实际描述。


备用方案:如果 Read 工具无法读取图像

尝试直接调用 Anthropic API:

const Anthropic = require('@anthropic-ai/sdk');
const fs = require('fs');
const client = new Anthropic();

const imageData = fs.readFileSync('/tmp/a11y-work/frames/frame_01.jpg').toString('base64');
const response = await client.messages.create({
  model: 'claude-sonnet-5',
  max_tokens: 500,
  messages: [{
    role: 'user',
    content: [
      { type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: imageData } },
      { type: 'text', text: 'Describe this video frame for accessibility captions.' }
    ]
  }]
});

最后手段:基于上下文推断(无视觉能力) 如果完全无法使用视觉能力,请根据以下信息推断:

  • 视频文件名:“product-demo.mp4” → 产品演示
  • 页面上下文:产品页面 → 产品展示
  • 周围文本:附近的标题和说明

对每一帧进行记录:

  • 场景:环境、场所、光线
  • 人物:人物、动作、表情、服装
  • 物体:产品、道具、设备
  • 文本:可见文本、徽标、标牌
  • 动作:移动、转场
  • 颜色:主色调、与无障碍相关的颜色
7.4:生成 WebVTT 字幕
WEBVTT
Kind: captions
Language: {detected-language}

00:00:00.000 --> 00:00:03.000
[Description from frame_01 analysis]

00:00:03.000 --> 00:00:06.000
[Description from frame_02 analysis]
7.5:生成音频描述
WEBVTT
Kind: descriptions
Language: en

00:00:00.000 --> 00:00:03.000
SCENE: [Detailed scene for blind users]
VISUAL: [What's on screen]
TEXT: [Any readable text]
ACTION: [What's happening]

第 8 步:生成完整报告

8.1:必需的输出文件

将所有文件保存到 docs/accessibility-scans/{page-slug}/:

文件内容
audit-summary.md执行摘要、评分、主要问题、对用户的影响
remediation.md所有可复制粘贴的代码修复及上下文
violations.json机器可读的违规数据
implementation.md视频集成指南(如有视频)
*.vtt字幕和音频描述文件
8.2:audit-summary.md 模板
# Accessibility Audit Report: {Site Name}

**URL:** {url}
**Date:** {date}
**Standard:** WCAG 2.2 Level AA

## Executive Summary

| Metric | Value |
|--------|-------|
| **Compliance Score** | {score}% |
| **Production Ready** | {Yes/No} |
| **Critical Issues** | {count} |
| **Total Violations** | {count} |
| **Estimated Fix Time** | {hours}h |

## POUR Analysis
{table}

## Top 10 Issues by Priority
{priority table with ROI scores}

## User Impact Summary
{affected user groups and percentages}

## Recommendations
{prioritized action items}
8.3:remediation.md 模板
# Accessibility Remediation Guide: {Site Name}

## Quick Wins (Copy-Paste Ready)

### 1. Form Labels ({count} issues)
{For EACH unlabeled input: context, before/after code, rationale, confidence}

### 2. Heading Structure ({count} issues)
{Current structure visualization, fixed structure, code changes}

### 3. Color Contrast ({count} issues)
{For EACH: current colors, ratio, suggested colors, CSS fixes}

### 4. Missing Alt Text ({count} issues)
{For EACH image: context-inferred alt text suggestions}

### 5. Keyboard Navigation ({count} issues)
{For EACH: element, issue, fix code, test instructions}

### 6. Focus Indicators
{Global CSS to add}

### 7. Skip Links
{Full HTML + CSS to add}

### 8. ARIA Fixes ({count} issues)
{For EACH: context, specific aria attributes to add}

### 9. iframe Titles ({count} issues)
{For EACH: suggested title based on content}

### 10. Video Accessibility ({count} videos)
{Links to generated VTT files, implementation code}

## Testing Checklist
- [ ] Tab through entire page - all interactive elements reachable
- [ ] Screen reader announces all content correctly
- [ ] Color contrast passes (use axe DevTools)
- [ ] Works without mouse
- [ ] Works at 200% zoom
- [ ] Video captions synchronized and accurate

第 9 步:学习协议(MCP 可用时)

与学习系统集成,持续改进。

9.1:审计前查询历史模式

检查之前是否审计过类似网站:

// Load MCP tools
ToolSearch("select:mcp__claude-flow_alpha__memory_retrieve")
ToolSearch("select:mcp__claude-flow_alpha__hooks_intelligence_pattern_search")

// Retrieve domain-specific patterns
mcp__claude-flow_alpha__memory_retrieve({
  key: `accessibility/patterns/${domain}`,
  namespace: "learning"
})

// Search for similar violation patterns
mcp__claude-flow_alpha__hooks_intelligence_pattern_search({
  query: "accessibility remediation",
  type: "accessibility-fix",
  limit: 10
})
9.2:审计后存储成功模式

存储有效模式,以便日后复用:

ToolSearch("select:mcp__claude-flow_alpha__memory_store")
ToolSearch("select:mcp__claude-flow_alpha__hooks_intelligence_pattern_store")

// Store audit outcome
mcp__claude-flow_alpha__memory_store({
  key: `accessibility-audit/${domain}-${Date.now()}`,
  namespace: "learning",
  value: {
    url: auditedUrl,
    timestamp: new Date().toISOString(),
    violationsFound: violations.length,
    criticalCount: violations.filter(v => v.impact === 'critical').length,
    toolsUsed: ['axe-core', 'pa11y', 'lighthouse'],
    patterns: {
      commonViolations: extractTopViolationTypes(violations),
      effectiveFixes: extractFixesThatWorked(remediations)
    }
  }
})

// Store reusable remediation patterns
mcp__claude-flow_alpha__hooks_intelligence_pattern_store({
  pattern: "form-label-contextual-fix",
  confidence: 0.92,
  type: "accessibility-remediation",
  metadata: {
    wcagCriteria: "1.3.1, 3.3.2, 4.1.2",
    violationType: "missing-form-label",
    codeTemplate: "<label for=\"{id}\">{inferredLabel}</label>",
    contextSignals: ["placeholder", "nearby-text", "field-name"]
  }
})
9.3:计算审计质量评分

自我评估审计完整性(用于学习反馈):

标准分值你的得分
使用多工具测试(3 种工具)20
检查所有 WCAG 2.2 AA 标准15
生成结合上下文的修复方案20
包含置信度评分10
计算 ROI 优先级10
完成视频处理流程(如适用)15
包含 EU 合规性映射10
总计100

质量等级:

  • 90-100:优秀(1.0 奖励)
  • 70-89:良好(0.8 奖励)
  • 50-69:合格(0.5 奖励)
  • <50:未完成(0.0 奖励——需要重做)

第 10 步:屏幕阅读器测试指南

手动测试说明(无法完全自动化):

10.1:NVDA(Windows - 免费)
1. Download: https://www.nvaccess.org/download/
2. Install and start NVDA (Ctrl+Alt+N)
3. Navigate to audited page

Key Commands:
- H: Jump through headings
- F: Jump through form fields
- B: Jump through buttons
- T: Jump through tables
- K: Jump through links
- D: Jump through landmarks
- Tab: Move through focusable elements

Verify:
- [ ] All headings announced with correct level
- [ ] Form fields announce labels
- [ ] Buttons announce purpose
- [ ] Images announce alt text or "decorative"
- [ ] Dynamic content changes announced (aria-live)
10.2:VoiceOver(macOS - 内置)
1. Enable: System Preferences → Accessibility → VoiceOver
2. Toggle: Cmd+F5
3. Navigate to audited page

Key Commands:
- VO+U: Open rotor (headings, links, forms, landmarks)
- VO+Space: Activate element
- VO+Right/Left: Move through content
- VO+Cmd+H: Jump to next heading

Verify:
- [ ] Rotor shows all headings hierarchically
- [ ] Forms are navigable and labels announced
- [ ] Focus order matches visual order
- [ ] All content is reachable
10.3:JAWS(Windows - 商用)
1. Trial: https://www.freedomscientific.com/products/software/jaws/
2. Start JAWS and navigate to page

Key Commands:
- H: Next heading
- F: Next form field
- B: Next button
- T: Next table
- Ins+F6: Heading list
- Ins+F7: Link list

Verify:
- [ ] Virtual cursor mode works correctly
- [ ] Forms mode activates in forms
- [ ] All ARIA roles announced properly
10.4:屏幕阅读器测试清单
测试NVDAVoiceOverJAWS
标题层级正确[ ][ ][ ]
表单标签已读出[ ][ ][ ]
按钮用途清晰[ ][ ][ ]
图片替代文本正确[ ][ ][ ]
链接已读出目标位置[ ][ ][ ]
可通过地标导航[ ][ ][ ]
焦点顺序符合逻辑[ ][ ][ ]
动态更新已读出[ ][ ][ ]
没有键盘陷阱[ ][ ][ ]
跳过链接正常工作[ ][ ][ ]

验证清单

完成前,请核实所有项目:

内容抓取(v7.0 弹性机制)
  • 已启动浏览器(Vibium/agent-browser/Playwright)
  • 页面已加载并分析
  • 已通过并行执行运行多工具扫描
  • 至少3 个工具中有 1 个成功(优雅降级)
  • 如果工具运行失败,已记录哪些工具失败及原因
  • 结果已保存至 /tmp/a11y-work/scan-results.json
违规项分析
  • 已提取所有违规项及其 WCAG 标准
  • 已分析每项违规的上下文
  • 已计算用户影响
修复方案生成
  • 提供了包含上下文和理由的表单标签修复方案
  • 提供了带可视化说明的标题层级修复方案
  • 提供了包含十六进制代码的颜色对比度修复方案
  • 提供了带置信度分数的替代文本建议
  • 提供了跳过链接代码(完整 HTML + CSS)
  • 提供了焦点指示器 CSS
  • 提供了包含具体属性的 ARIA 修复方案
  • 提供了键盘导航修复方案
  • 提供了 iframe 标题
视频无障碍(如果 pageInfo.media.videos > 0,则为必需)
  • 已提取视频 URL(完整 URL,而非相对路径)
  • 已尝试下载每个视频
  • 如果下载成功:已使用 ffmpeg 提取帧
  • 如果下载成功:已使用 Read 工具分析每一帧
  • 如果下载成功:已根据实际帧描述生成 captions.vtt
  • 如果下载成功:已生成 audiodesc.vtt
  • 如果下载失败:已在 audit-summary.md 中记录失败及原因
  • 如果下载失败:已在 remediation.md 中提供手动添加字幕的说明

**阻塞项:**尝试完成视频处理流程后,才能生成最终报告。

输出文件
  • audit-summary.md,包含评分、优先级和用户影响
  • remediation.md,包含所有可复制粘贴的代码修复方案
  • violations.json,包含违规数据
  • VTT 文件(如果有视频)
  • implementation.md(如果有视频)
质量检查
  • 已计算合规评分
  • 已评估生产就绪度
  • 已完成 ROI 优先级排序
  • 已包含 POUR 分析

如果任何复选框未勾选 = 任务未完成 </default_to_action>


快速参考卡

用法
/a11y-ally https://example.com
v7.0 弹性机制特性
特性描述
并行执行通过 Promise.allSettled 同时运行全部 3 个工具
单工具超时axe:60s,pa11y:60s,Lighthouse:90s
退避重试每个工具重试 2 次,并采用指数退避
优雅降级只要有 1 个或更多工具成功,就继续执行
渐进式输出工具完成后即流式输出结果
机器人防护隐身模式、随机延迟、关闭 Cookie 提示
预期输出结构
docs/accessibility-scans/{page-slug}/
├── audit-summary.md      # Executive summary with scores
├── remediation.md        # ALL copy-paste code fixes
├── violations.json       # Machine-readable data
├── implementation.md     # Video integration (if videos)
├── video-*-captions.vtt  # Captions (if videos)
└── video-*-audiodesc.vtt # Audio descriptions (if videos)
合规阈值
等级最低分数严重级高级
A70%0≤5
AA85%0≤3
AAA95%00
按成功情况划分的工具覆盖率
成功工具数检测率状态
3/3~95%✅ 最佳
2/3~85%⚠️ 良好
1/3~70%⚠️ 可接受
0/3—❌ 需要重试
ROI 公式
ROI = (Impact × Users%) / Effort_Hours

欧盟合规映射

WCAGEN 301 549《欧盟无障碍法案》
1.1.19.1.1.1EAA-I.1 可感知
1.4.39.1.4.3EAA-I.1 可感知
2.1.19.2.1.1EAA-I.2 可操作
2.4.79.2.4.7EAA-I.2 可操作
3.3.29.3.3.2EAA-I.3 可理解
4.1.29.4.1.2EAA-I.4 稳健

关键规则

执行规则(v7.0)
  1. 始终使用并行执行(Promise.allSettled)运行多工具扫描
  2. 始终在3 个工具中至少 1 个成功时继续执行(优雅降级)
  3. 始终在 audit-summary.md 中记录哪些工具失败以及失败原因
  4. 始终使用各工具独立的超时时间(60s/60s/90s),而非全局超时
  5. 始终在放弃前通过指数退避重试失败的工具
质量规则
  1. 始终在生成修复方案前分析上下文
  2. 始终为修复方案提供置信度评分
  3. 始终计算用户影响和 ROI
  4. 始终生成可直接复制粘贴的代码
  5. 绝不生成占位符或模板式修复方案
  6. 绝不在检测到视频时跳过视频处理流程
  7. 绝不在没有 remediation.md 的情况下完成
  8. 绝不仅因 1–2 个工具失败就判定审计失败(应使用优雅降级)

评论

支持 Markdown