a11y-ally

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

/a11y-ally - Comprehensive Accessibility Audit

/a11y-ally - 全面可访问性审计

<default_to_action> When this skill is invoked with a URL, Claude executes ALL steps automatically without waiting for user prompts between steps.
<default_to_action> 当通过URL调用此技能时,Claude会自动执行所有步骤,无需在步骤之间等待用户提示。

THIS IS AN LLM-POWERED SKILL

这是一款由大语言模型驱动的技能

The value of this skill is Claude's intelligence, not just running automated tools:
Automated Tools DoClaude (This Skill) Does
Flag "button has no name"Analyze context: icon class, parent element, nearby text → generate "Add to wishlist"
Flag "image missing alt"Use Vision to see the image → describe actual content
Flag "video has no captions"Download video, extract frames, analyze each frame with Vision → generate real captions
Output generic templatesGenerate context-specific, copy-paste ready fixes
IF YOU SKIP THE LLM ANALYSIS, THIS SKILL HAS NO VALUE.

本技能的核心价值在于Claude的智能分析能力,而不仅仅是运行自动化工具:
自动化工具的作用Claude(本技能)的作用
标记“按钮无名称”分析上下文:图标类、父元素、附近文本 → 生成“加入心愿单”
标记“图片缺少alt属性”利用视觉能力查看图片 → 描述实际内容
标记“视频无字幕”下载视频、提取帧、用视觉能力分析每一帧 → 生成真实字幕
输出通用模板生成特定上下文的、可直接复制粘贴的修复方案
如果跳过大语言模型分析,本技能将毫无价值。

EXECUTION MODEL

执行模型

CLAUDE EXECUTES ALL STEPS WITHOUT STOPPING.
Do NOT wait for user prompts between steps. Execute the full pipeline:
  1. Data Collection: Run multi-tool scan (axe-core, pa11y, Lighthouse) via Bash
  2. LLM Analysis: Read results and analyze context for each violation
  3. Vision Pipeline: If videos detected → download → extract frames → Read each frame → describe
  4. Intelligent Remediation: Generate context-specific fixes using your reasoning
  5. Generate Reports: Write all output files to
    docs/accessibility-scans/{page-slug}/
WRONG:
Claude: "I found 5 violations. Should I analyze them?"
User: "Yes"
Claude: "I see a video. Should I run the video pipeline?"
User: "Yes"
RIGHT:
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/"

Claude会不间断执行所有步骤。
不要在步骤之间等待用户提示。执行完整流程:
  1. 数据收集:通过Bash运行多工具扫描(axe-core、pa11y、Lighthouse)
  2. 大语言模型分析:读取结果并分析每个问题的上下文
  3. 视觉流程:如果检测到视频 → 下载 → 提取帧 → 读取每一帧 → 描述内容
  4. 智能修复:通过推理生成特定上下文的修复方案
  5. 生成报告:将所有输出文件写入
    docs/accessibility-scans/{page-slug}/
错误示例:
Claude: "我发现5个问题。需要我分析它们吗?"
User: "是的"
Claude: "我看到一个视频。需要我运行视频流程吗?"
User: "是的"
正确示例:
Claude: [运行扫描] → [分析问题] → [下载视频] → [提取帧] →
        [用视觉能力读取每一帧] → [生成字幕] → [写入所有文件]
        "审计完成。已在docs/accessibility-scans/example/生成4个文件"

STEP 1: BROWSER AUTOMATION - Content Fetching

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

1.1: Try VIBIUM First (Primary)

1.1:优先尝试VIBIUM(主方案)

javascript
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" })
If Vibium fails → Go to STEP 1b
javascript
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: Try AGENT-BROWSER Fallback

1b:尝试AGENT-BROWSER备用方案

javascript
ToolSearch("select:mcp__claude-flow_alpha__browser_open")
mcp__claude-flow_alpha__browser_open({ url: "TARGET_URL", waitUntil: "networkidle" })
If agent-browser fails → Go to STEP 1c
javascript
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 (Final Fallback)

1c:PLAYWRIGHT + STEALTH(最终备用方案)

bash
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
Create and run scan script - see STEP 2 for full multi-tool scan code.
bash
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: PARALLEL MULTI-PAGE AUDIT (Optional)

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

For auditing multiple URLs simultaneously, use parallel execution:
javascript
// /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));
  });
}
Usage for multi-page audit:
bash
node parallel-audit.js https://example.com https://example.com/about https://example.com/contact
如需同时审计多个URL,使用并行执行:
javascript
// /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;  // 最大并行审计数

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 = [];

  // 按MAX_CONCURRENT分批处理
  for (let i = 0; i < urls.length; i += MAX_CONCURRENT) {
    const chunk = urls.slice(i, i + MAX_CONCURRENT);
    console.log(`正在审计批次 ${Math.floor(i/MAX_CONCURRENT) + 1}: ${chunk.length} 个URL`);

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

  await browser.close();
  return results;
}

// 使用方式: 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));
  });
}
多页面审计使用方式:
bash
node parallel-audit.js https://example.com https://example.com/about https://example.com/contact

1e: SITE CRAWL MODE (Optional)

1e:站点爬取模式(可选)

For comprehensive site audits, crawl and audit all pages:
javascript
// /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)));

如需全面审计站点,爬取并审计所有页面:
javascript
// /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}] 正在审计: ${url}`);

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

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

      // 提取同域链接用于爬取
      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);

      // 将新链接加入队列
      links.forEach(link => {
        if (!visited.has(link) && !toVisit.includes(link)) {
          toVisit.push(link);
        }
      });

      // 运行可访问性审计
      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 };
}

// 使用方式: 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)));

STEP 2: COMPREHENSIVE WCAG SCAN (Multi-Tool, Parallel, Resilient)

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

IMPORTANT: This step uses THREE accessibility testing tools for maximum coverage:
  • axe-core: Industry standard, excellent for ARIA and semantic issues
  • pa11y: Strong on contrast, links, and HTML validation
  • Lighthouse: Google's accessibility scoring with performance correlation
Combined detection rate is ~15% higher than any single tool.
重要提示: 本步骤使用三种可访问性测试工具以实现最大覆盖范围:
  • axe-core:行业标准,在ARIA和语义问题上表现出色
  • pa11y:在对比度、链接和HTML验证方面表现突出
  • Lighthouse:谷歌的可访问性评分工具,可关联性能指标
组合检测率比任何单一工具高出约15%。

2.0: RESILIENCE ARCHITECTURE (v7.0 Enhancement)

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

Key improvements over v6.0:
Featurev6.0 (Old)v7.0 (New)
Tool executionSequentialParallel (Promise.allSettled)
Timeout handlingGlobal 60sPer-tool (60s/60s/90s)
Failure modeAll-or-nothingGraceful degradation
Retry logicNoneExponential backoff (3 retries)
Output styleWait for allProgressive (stream as ready)
Minimum tools3 required1 of 3 sufficient
Coverage by tools succeeded:
  • 3/3 tools: ~95% detection (optimal)
  • 2/3 tools: ~85% detection (good)
  • 1/3 tools: ~70% detection (acceptable)
  • 0/3 tools: FAIL - retry with different strategy
相较于v6.0的关键改进:
特性v6.0(旧版)v7.0(新版)
工具执行串行并行(Promise.allSettled)
超时处理全局60秒按工具设置(60秒/60秒/90秒)
失败模式全有或全无优雅降级
重试逻辑指数退避(3次重试)
输出方式等待所有工具完成渐进式(工具完成即输出)
最低工具要求需要3个工具全部成功3个工具中至少1个成功即可
按工具成功数量的覆盖范围:
  • 3/3工具成功:约95%的检测率(最优)
  • 2/3工具成功:约85%的检测率(良好)
  • 1/3工具成功:约70%的检测率(可接受)
  • 0/3工具成功:失败 - 用不同策略重试

2.1: Run Multi-Tool Analysis (PARALLEL + RESILIENT)

2.1:运行多工具分析(并行+弹性)

Create and run
/tmp/a11y-work/multi-tool-scan.js
:
javascript
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}`);
})();
创建并运行
/tmp/a11y-work/multi-tool-scan.js
javascript
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';

// ========== 弹性工具类 ==========

// 超时包装器 - 为任何Promise添加超时
function withTimeout(promise, ms, name) {
  return Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error(`${name} timed out after ${ms}ms`)), ms)
    )
  ]);
}

// 重试包装器 - 指数退避重试
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}/${maxRetries} 次尝试失败: ${error.message}`);
      if (isLastAttempt) throw error;
      const delay = baseDelay * Math.pow(2, attempt - 1); // 指数退避
      console.log(`[${name}] 将在 ${delay}ms 后重试...`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

// 睡眠工具函数
const sleep = (ms) => new Promise(r => setTimeout(r, ms));

// 渐进式输出 - 工具完成即追加结果
function progressiveOutput(tool, data) {
  console.log(`\n=== ${tool.toUpperCase()} 完成 ===`);
  console.log(JSON.stringify(data, null, 2));

  // 追加到结果文件以便渐进式访问
  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) { /* 忽略文件错误 */ }
}

// ========== 工具运行器 ==========

// 工具1:Axe-core(包含页面信息提取)
async function runAxeCore(url) {
  console.log('[axe-core] 开始...');
  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 {
    // 使用domcontentloaded(比networkidle更快、更可靠)
    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });

    // 随机延迟以模拟人类行为
    await sleep(2000 + Math.random() * 2000);

    // 尝试关闭Cookie横幅
    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) { /* 忽略Cookie错误 */ }

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

    // 提取全面的页面信息
    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();
  }
}

// 工具2:Pa11y
async function runPa11y(url) {
  console.log('[pa11y] 开始...');
  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 };
}

// 工具3:Lighthouse
async function runLighthouse(url) {
  console.log('[lighthouse] 开始...');
  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();
  }
}

// ========== 主程序:优雅降级的并行执行 ==========

(async () => {
  console.log('=== 多工具可访问性扫描(v7.0 并行+弹性) ===');
  console.log('目标:', TARGET_URL);
  console.log('策略: Promise.allSettled + 按工具设置超时\n');

  const startTime = Date.now();

  // 并行运行所有工具并设置单独超时
  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'
    )
  ]);

  // ========== 处理结果(优雅降级) ==========
  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: {}
  };

  // 处理axe-core结果
  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] 失败:', axeResult.reason.message);
  }

  // 处理pa11y结果
  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] 失败:', pa11yResult.reason.message);
  }

  // 处理lighthouse结果
  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] 失败:', lighthouseResult.reason.message);
  }

  // ========== 去重问题 ==========
  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;

  // ========== 最终输出 ==========
  console.log('\n' + '='.repeat(60));
  console.log('=== 扫描完成 ===');
  console.log('='.repeat(60));
  console.log(`成功的工具数: ${results.toolsSucceeded}/3`);
  console.log(`失败的工具数: ${results.toolsFailed}/3`);
  console.log(`耗时: ${results.duration}`);
  console.log(`唯一问题总数: ${results.totalUnique}`);

  if (results.toolsSucceeded === 0) {
    console.log('\n⚠️  所有工具均失败 - 请考虑:');
    console.log('   1. 站点可能有较强的反爬机制');
    console.log('   2. 尝试使用Vibium MCP浏览器');
    console.log('   3. 检查网络连接');
  } else if (results.toolsSucceeded < 3) {
    console.log(`\n⚠️  部分覆盖(${results.toolsSucceeded}/3个工具)- 结果仍可使用`);
  } else {
    console.log('\n✅ 已实现全面覆盖(3/3个工具)');
  }

  console.log('\n=== 页面信息 ===');
  console.log(JSON.stringify(results.pageInfo, null, 2));

  console.log('\n=== 按工具分类的问题 ===');
  console.log(JSON.stringify(results.byTool, null, 2));

  console.log('\n=== 唯一问题 ===');
  console.log(JSON.stringify(results.uniqueViolations, null, 2));

  // 保存最终结果
  fs.writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2));
  console.log(`\n结果已保存至: ${OUTPUT_FILE}`);
})();

2.2: Read Scan Results

2.2:读取扫描结果

After running the scan, read the results file:
bash
cat /tmp/a11y-work/scan-results.json
The results include:
  • pageInfo: Page structure, images, headings, media
  • violations: All violations from all tools (deduplicated)
  • byTool: Success/failure status per tool
  • toolsSucceeded: Number of tools that completed (1-3)
运行扫描后,读取结果文件:
bash
cat /tmp/a11y-work/scan-results.json
结果包含:
  • pageInfo:页面结构、图片、标题、媒体
  • violations:所有工具发现的所有问题(已去重)
  • byTool:每个工具的成功/失败状态
  • toolsSucceeded:完成的工具数量(1-3)

2.3: Graceful Degradation Decision Tree

2.3:优雅降级决策树

Tools SucceededAction
3/3✅ Full coverage - proceed with all results
2/3⚠️ Good coverage - note which tool failed in report
1/3⚠️ Basic coverage - proceed but flag limited confidence
0/3❌ Retry with Vibium MCP, or document failure
成功工具数操作
3/3✅ 全面覆盖 - 使用所有结果
2/3⚠️ 良好覆盖 - 在报告中记录失败的工具
1/3⚠️ 基础覆盖 - 继续处理但标记置信度有限
0/3❌ 用Vibium MCP重试,或记录失败

2.4: MANDATORY - Check for Videos and Trigger Pipeline

2.4:强制要求 - 检查视频并触发流程

After reading scan results, check
pageInfo.media.videoUrls
:
javascript
// 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
}
IF videos detected AND hasCaptions=false → STEP 7 is MANDATORY before generating reports.

读取扫描结果后,检查
pageInfo.media.videoUrls
javascript
// 检查scan-results.json中的视频
const results = JSON.parse(fs.readFileSync('/tmp/a11y-work/scan-results.json'));
if (results.pageInfo && results.pageInfo.media.videoUrls.length > 0) {
  console.log('=== 检测到视频 - 触发视频流程 ===');
  for (const video of results.pageInfo.media.videoUrls) {
    console.log(`视频: ${video.src}`);
    console.log(`  是否有字幕: ${video.hasCaptions}`);
  }
  // 立即进入步骤7
}
如果检测到视频且hasCaptions=false → 在生成报告前必须执行步骤7。

STEP 3: CONTEXT-AWARE REMEDIATION (LLM-POWERED)

步骤3:上下文感知修复(大语言模型驱动)

THIS IS WHERE CLAUDE'S INTELLIGENCE MATTERS.
Generic tools output:
aria-label="[DESCRIPTION]"
You output:
aria-label="Add to shopping cart"
because you understand context.
这是Claude智能的核心价值所在。
通用工具输出:
aria-label="[DESCRIPTION]"
而你输出:
aria-label="加入购物车"
,因为你理解上下文。

3.1: Context Analysis (Use Your Reasoning)

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

For EACH violation, Claude must:
  1. READ THE HTML CONTEXT - Don't just see
    <button class="btn">
    , see:
    html
    <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">  <!-- THIS IS THE VIOLATION -->
        <svg class="icon-cart">...</svg>
      </button>
    </div>
  2. INFER PURPOSE from:
    • Class names:
      add-to-cart
      ,
      wishlist
      ,
      menu-toggle
    • Parent context: Inside
      .product-card
      with product data
    • Icon classes:
      icon-cart
      ,
      icon-heart
      ,
      icon-search
    • Nearby text: Product name, price, "Add to bag"
    • Page section: Header nav vs product grid vs checkout
  3. GENERATE SPECIFIC FIX:
    html
    <!-- NOT THIS (generic template) -->
    <button aria-label="[DESCRIPTION]">
    
    <!-- THIS (context-aware) -->
    <button aria-label="Add Adidas Superstar to cart - $99">
对于每个问题,Claude必须:
  1. 读取HTML上下文 - 不要只看到
    <button class="btn">
    ,要看到:
    html
    <div class="product-card" data-product="Adidas Superstar">
      <img src="superstar.jpg" alt="白色运动鞋">
      <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
    • 附近文本:产品名称、价格、“加入购物袋”
    • 页面区域:头部导航、产品网格、结账页
  3. 生成特定修复方案
    html
    <!-- 不要这样(通用模板) -->
    <button aria-label="[DESCRIPTION]">
    
    <!-- 要这样(上下文感知) -->
    <button aria-label="将Adidas Superstar加入购物车 - 售价$99">

3.2: Confidence Scoring

3.2:置信度评分

Rate your confidence in each fix:
  • 0.9+: Clear context (class="add-to-cart" near product name)
  • 0.7-0.9: Reasonable inference (icon-cart class alone)
  • <0.7: Needs human review (ambiguous context)
Include confidence in remediation.md:
markdown
undefined
为每个修复方案评分:
  • 0.9+:上下文清晰(
    class="add-to-cart"
    紧邻产品名称)
  • 0.7-0.9:合理推断(仅
    icon-cart
    类)
  • <0.7:需要人工审核(上下文模糊)
在remediation.md中包含置信度:
markdown
undefined

Button:
.product-card .btn
(Confidence: 0.95)

按钮:
.product-card .btn
(置信度: 0.95)

Context: Inside product card for "Adidas Superstar", has cart icon Fix:
aria-label="Add Adidas Superstar to cart"
undefined
上下文: 位于“Adidas Superstar”产品卡片内,包含购物车图标 修复:
aria-label="将Adidas Superstar加入购物车"
undefined

3.2: Remediation Templates by Violation Type

3.2:按问题类型分类的修复模板

Form Labels (WCAG 1.3.1, 3.3.2, 4.1.2)
html
<!-- Context: Input inside payment form, near "Card Number" text -->
<!-- Confidence: 0.95 -->

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

<!-- AFTER -->
<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>

<!-- RATIONALE -->
- Visible label aids all users
- aria-describedby provides additional context
- autocomplete enables autofill
- inputmode shows numeric keyboard on mobile
- pattern enables browser validation
Icon Buttons (WCAG 4.1.2)
html
<!-- Context: Button with SVG inside nav, classes include "menu-toggle" -->
<!-- Confidence: 0.92 -->

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

<!-- AFTER -->
<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>

<!-- RATIONALE -->
- aria-label describes action, not icon
- aria-expanded communicates state
- aria-controls links to menu element
- SVG hidden from assistive tech (decorative)
Color Contrast (WCAG 1.4.3)
html
<!-- Context: Gray text (#767676) on white background -->
<!-- Current ratio: 4.48:1 (FAILS AA for normal text) -->
<!-- Required: 4.5:1 (AA) or 7:1 (AAA) -->

<!-- BEFORE -->
.low-contrast { color: #767676; background: #ffffff; }

<!-- AFTER (Option 1: Darken text - minimal change) -->
.accessible { color: #757575; background: #ffffff; } /* 4.6:1 - PASSES AA */

<!-- AFTER (Option 2: Higher contrast for AAA) -->
.high-contrast { color: #595959; background: #ffffff; } /* 7.0:1 - PASSES AAA */

<!-- COLOR ALTERNATIVES -->
| Original | AA Pass | AAA Pass | Notes |
|----------|---------|----------|-------|
| #767676  | #757575 | #595959  | Gray text |
| #0066cc  | #0055b3 | #003d82  | Link blue |
| #cc0000  | #b30000 | #8b0000  | Error red |
Heading Hierarchy (WCAG 1.3.1)
html
<!-- Context: Page has 10 H1 elements, skipped H2 levels -->

<!-- BEFORE (broken) -->
<h1>Welcome</h1>
<h1>Products</h1>      <!-- ERROR: Multiple H1s -->
<h4>Shoes</h4>         <!-- ERROR: Skipped H2, H3 -->
<h1>Contact</h1>

<!-- AFTER (correct) -->
<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>

<!-- HEADING STRUCTURE VISUALIZATION -->
h1: Site Name - Main Page Title
├── h2: Products
│   ├── h3: Shoes
│   └── h3: Clothing
└── h2: Contact
Skip Links (WCAG 2.4.1)
html
<!-- Add as FIRST element inside <body> -->
<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 content -->
  </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>
Focus Indicators (WCAG 2.4.7)
css
/* 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);
}
Keyboard Navigation (WCAG 2.1.1, 2.1.2)
html
<!-- Custom interactive element needs keyboard support -->

<!-- BEFORE (inaccessible) -->
<div class="dropdown" onclick="toggleMenu()">
  Menu
</div>

<!-- AFTER (accessible) -->
<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>
Modal Focus Trap (WCAG 2.4.3)
javascript
// 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 Titles (WCAG 4.1.2)
html
<!-- All iframes MUST have descriptive titles -->
<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>

表单标签(WCAG 1.3.1, 3.3.2, 4.1.2)
html
<!-- 上下文: 支付表单内的输入框,紧邻“卡号”文本 -->
<!-- 置信度: 0.95 -->

<!-- 修复前 -->
<input type="text" name="cardNumber" placeholder="1234 5678 9012 3456">

<!-- 修复后 -->
<label for="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">请输入16位卡号</span>

<!-- 理由 -->
- 可见标签帮助所有用户
- aria-describedby提供额外上下文
- autocomplete支持自动填充
- inputmode在移动端显示数字键盘
- pattern支持浏览器验证
图标按钮(WCAG 4.1.2)
html
<!-- 上下文: 导航内带SVG的按钮,类包含"menu-toggle" -->
<!-- 置信度: 0.92 -->

<!-- 修复前 -->
<button class="menu-toggle">
  <svg>...</svg>
</button>

<!-- 修复后 -->
<button class="menu-toggle"
        type="button"
        aria-expanded="false"
        aria-controls="main-menu"
        aria-label="打开导航菜单">
  <svg aria-hidden="true" focusable="false">...</svg>
</button>

<!-- 理由 -->
- aria-label描述操作而非图标
- aria-expanded传达状态
- aria-controls关联菜单元素
- SVG对辅助技术隐藏(装饰性)
颜色对比度(WCAG 1.4.3)
html
<!-- 上下文: 白色背景上的灰色文本 (#767676) -->
<!-- 当前对比度: 4.48:1(不符合普通文本AA标准) -->
<!-- 要求: 4.5:1(AA)或7:1(AAA) -->

<!-- 修复前 -->
.low-contrast { color: #767676; background: #ffffff; }

<!-- 修复后(方案1:加深文本 - 最小改动) -->
.accessible { color: #757575; background: #ffffff; } /* 4.6:1 - 符合AA标准 */

<!-- 修复后(方案2:更高对比度以符合AAA标准) -->
.high-contrast { color: #595959; background: #ffffff; } /* 7.0:1 - 符合AAA标准 */

<!-- 颜色替代方案 -->
| 原始颜色 | 符合AA标准的颜色 | 符合AAA标准的颜色 | 说明 |
|----------|---------|----------|-------|
| #767676  | #757575 | #595959  | 灰色文本 |
| #0066cc  | #0055b3 | #003d82  | 链接蓝色 |
| #cc0000  | #b30000 | #8b0000  | 错误红色 |
标题层级(WCAG 1.3.1)
html
<!-- 上下文: 页面有10个H1元素,跳过了H2层级 -->

<!-- 修复前(错误) -->
<h1>欢迎</h1>
<h1>产品</h1>      <!-- 错误:多个H1 -->
<h4>鞋子</h4>         <!-- 错误:跳过了H2、H3 -->
<h1>联系我们</h1>

<!-- 修复后(正确) -->
<h1>站点名称 - 主页标题</h1>
<main>
  <section aria-labelledby="products-heading">
    <h2 id="products-heading">产品</h2>
    <h3>鞋子</h3>
    <h3>服装</h3>
  </section>
  <section aria-labelledby="contact-heading">
    <h2 id="contact-heading">联系我们</h2>
  </section>
</main>

<!-- 标题结构可视化 -->
h1: 站点名称 - 主页标题
├── h2: 产品
│   ├── h3: 鞋子
│   └── h3: 服装
└── h2: 联系我们
跳过链接(WCAG 2.4.1)
html
<!-- 添加到<body>内的第一个元素位置 -->
<body>
  <a href="#main-content" class="skip-link">跳转到主要内容</a>
  <a href="#main-nav" class="skip-link">跳转到导航</a>

  <header>
    <nav id="main-nav" aria-label="主导航">...</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)
css
// 绝对不要这样做
*:focus { outline: none; } /* 不符合WCAG标准 */

// 正确做法 - 自定义焦点样式
:focus-visible {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
}

// 仅对鼠标用户隐藏轮廓
:focus:not(:focus-visible) {
  outline: none;
}

// 为交互元素提供高对比度
: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-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)
html
<!-- 自定义交互元素需要支持键盘操作 -->

<!-- 修复前(不可访问) -->
<div class="dropdown" onclick="toggleMenu()">
  菜单
</div>

<!-- 修复后(可访问) -->
<button type="button"
        class="dropdown-trigger"
        aria-expanded="false"
        aria-controls="dropdown-menu"
        onclick="toggleMenu()"
        onkeydown="handleKeydown(event)">
  菜单
</button>
<ul id="dropdown-menu" role="menu" hidden>
  <li role="none"><a role="menuitem" href="/page1">页面1</a></li>
  <li role="none"><a role="menuitem" href="/page2">页面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)
javascript
// 模态框焦点捕获 - WCAG合规必需
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];

  // 模态框打开时聚焦第一个元素
  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();
    }
  });
}

// 模态框关闭时恢复焦点
function closeModal() {
  modal.hidden = true;
  triggerButton.focus(); // 将焦点恢复到触发按钮
}
iframe标题(WCAG 4.1.2)
html
<!-- 所有iframe必须有描述性标题 -->
<iframe src="map.html" title="显示5家附近门店的店铺位置地图"></iframe>
<iframe src="video.html" title="带字幕的产品演示视频"></iframe>
<iframe src="chat.html" title="客户支持聊天窗口"></iframe>

STEP 4: USER IMPACT ANALYSIS

步骤4:用户影响分析

For each violation, calculate user impact:
对于每个问题,计算用户影响:

4.1: Affected User Groups

4.1:受影响的用户群体

Violation TypeAffected Groups% of Users
Missing alt textBlind, low-vision7-10%
Missing form labelsBlind, screen reader users5-8%
Low color contrastLow-vision, color blind8-12%
No keyboard accessMotor impaired, power users10-15%
Missing captionsDeaf, hard-of-hearing5-7%
Flashing contentSeizure sensitive0.5-1%
Complex languageCognitive impairment10-15%
问题类型受影响群体用户占比
缺少alt文本盲人群体、低视力群体7-10%
缺少表单标签盲人群体、屏幕阅读器用户5-8%
低颜色对比度低视力群体、色盲群体8-12%
无键盘访问行动障碍群体、高级用户10-15%
缺少字幕听障群体、重听群体5-7%
闪烁内容癫痫易感群体0.5-1%
复杂语言认知障碍群体10-15%

4.2: Impact Severity Classification

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

BLOCKS-USAGE: 用户完全无法完成任务
  - 必填字段缺少表单标签
  - 键盘陷阱
  - 关键按钮无可访问名称

IMPAIRS-USAGE: 用户可以完成任务但有困难
  - 低对比度(需要费力阅读)
  - 缺少跳过链接(导航繁琐)
  - 错误的标题结构(令人困惑)

MINOR-INCONVENIENCE: 次优但可用
  - 装饰性图片alt属性为空
  - 冗余ARIA
  - 非语义HTML但可正常工作

STEP 5: ROI-BASED PRIORITIZATION

步骤5:基于ROI的优先级排序

Calculate priority for each remediation:
计算每个修复方案的优先级:

5.1: Priority Formula

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)
PRIORITY_SCORE = (IMPACT_WEIGHT × USERS_AFFECTED) / EFFORT_HOURS

其中:
- IMPACT_WEIGHT: 严重=10, 重要=7, 中等=4, 次要=1
- USERS_AFFECTED: 受影响用户的估计百分比
- EFFORT_HOURS: 修复所需的估计时间(0.25至8小时)

5.2: Effort Estimation Guide

5.2:工作量估算指南

Fix TypeEffortComplexity
Add aria-label0.25hTrivial
Add alt text0.25hTrivial
Add form label0.5hSimple
Fix color contrast0.5hSimple
Add skip links1hSimple
Fix heading structure2hMedium
Add keyboard navigation4hHigh
Implement focus trap4hHigh
Add video captions8hHigh
修复类型工作量复杂度
添加aria-label0.25小时trivial
添加alt文本0.25小时trivial
添加表单标签0.5小时简单
修复颜色对比度0.5小时简单
添加跳过链接1小时简单
修复标题结构2小时中等
添加键盘导航4小时
实现焦点捕获4小时
添加视频字幕8小时

5.3: Priority Output Format

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 |

| 排名 | 问题 | 影响 | 用户占比 | 工作量 | ROI分数 |
|------|-----------|--------|-------|--------|-----------|
| 1 | 缺少表单标签 | 严重 | 15% | 0.5小时 | 300 |
| 2 | 键盘陷阱 | 严重 | 12% | 4小时 | 30 |
| 3 | 低对比度 | 重要 | 10% | 0.5小时 | 140 |
| 4 | 缺少alt文本 | 重要 | 8% | 0.25小时 | 224 |

STEP 6: PRODUCTION READINESS ASSESSMENT

步骤6:生产就绪性评估

6.1: Compliance Scoring

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
COMPLIANCE_SCORE = (PASSED_CRITERIA / TOTAL_CRITERIA) × 100

生产就绪的条件:
✓ 分数 ≥ 85%
✓ 无严重问题
✓ 重要问题少于3个
✓ 所有用户旅程支持键盘访问

6.2: POUR Analysis (Perceivable, Operable, Understandable, Robust)

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%** |

| 原则 | 指南 | 通过 | 失败 | 分数 |
|-----------|-----------|------|------|-------|
| 可感知 | 1.1-1.4 | 12 | 3 | 80% |
| 可操作 | 2.1-2.5 | 18 | 2 | 90% |
| 可理解 | 3.1-3.3 | 8 | 1 | 89% |
| 健壮 | 4.1 | 4 | 1 | 80% |
| **总计** | | **42** | **7** | **86%** |

STEP 7: VIDEO ACCESSIBILITY PIPELINE

步骤7:视频可访问性流程

Execute for EACH video detected on page.
为页面上检测到的每个视频执行此流程。

7.1: Detect and Extract Video URLs (MANDATORY)

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

This step MUST be integrated into STEP 2 multi-tool scan.
Add this to the page.evaluate() in the multi-tool scan:
javascript
// 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)
}
MANDATORY OUTPUT: Log all video URLs found:
=== VIDEOS DETECTED ===
Video 1: https://example.com/promo.mp4 (no captions, no descriptions)
YouTube iframe: https://youtube.com/embed/xxx
此步骤必须集成到步骤2的多工具扫描中。
在多工具扫描的page.evaluate()中添加以下代码:
javascript
// 在pageInfo提取中(步骤2)添加:
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:
=== 检测到视频 ===
视频1: https://example.com/promo.mp4(无字幕,无描述)
YouTube iframe: https://youtube.com/embed/xxx

7.2: Download and Extract Frames (MANDATORY for each video)

7.2:下载并提取帧(对每个视频强制要求)

For EACH video URL found in 7.1:
bash
undefined
针对7.1中发现的每个视频URL:
bash
undefined

Create output directory

创建输出目录

mkdir -p /tmp/a11y-work/frames
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"
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

**IF VIDEO DOWNLOAD FAILS:**
1. Document the failure reason in audit-summary.md
2. Still create video-captions violation in violations.json
3. Add remediation instructions WITHOUT generated captions
4. Mark video pipeline as "blocked" not "skipped"
if [ -f /tmp/a11y-work/video.mp4 ] && [ -s /tmp/a11y-work/video.mp4 ]; then echo "视频下载成功" 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 "已提取 $(ls /tmp/a11y-work/frames/*.jpg 2>/dev/null | wc -l) 帧" else echo "视频下载失败 - 在audit-summary.md中记录此情况" fi

**如果视频下载失败:**
1. 在audit-summary.md中记录失败原因
2. 仍在violations.json中创建video-captions问题
3. 在修复指南中添加无自动生成字幕的修复说明
4. 将视频流程标记为“受阻”而非“跳过”

7.3: Analyze Each Frame with Claude Vision (MANDATORY)

7.3:用Claude Vision分析每一帧(强制要求)

USE THE READ TOOL ON EACH FRAME IMAGE.
Claude Code has native vision capabilities. When you Read an image file, you SEE it.
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)
For EACH frame, describe:
  • SCENE: Setting, environment, lighting, location
  • PEOPLE: Who appears, what they're doing, expressions, clothing
  • PRODUCTS: Items shown (for e-commerce: product names, colors, styles)
  • TEXT: Any visible text, logos, signs, prices
  • ACTION: Movement, transitions, what's happening
Example output after reading 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.
THIS IS THE LLM VALUE. Generic tools output "[DESCRIBE CONTENT]". You output actual descriptions because you can SEE the image.

FALLBACK: If Read tool fails on images
Try Anthropic API directly:
javascript
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-4-20250514',
  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.' }
    ]
  }]
});
LAST RESORT: Context-Based Inference (No Vision) If vision completely unavailable, infer from:
  • Video filename: "product-demo.mp4" → product demonstration
  • Page context: product page → product showcase
  • Surrounding text: nearby headings and descriptions
Document for each frame:
  • SCENE: Setting, environment, lighting
  • PEOPLE: Who, actions, expressions, clothing
  • OBJECTS: Products, props, equipment
  • TEXT: Visible text, logos, signs
  • ACTION: Movement, transitions
  • COLORS: Dominant colors, accessibility-relevant
对每一帧图片使用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
...(继续处理所有帧)
对每一帧,描述:
  • 场景:环境、地点、光线、位置
  • 人物:出现的人物、动作、表情、服装
  • 产品:展示的物品(对于电商:产品名称、颜色、款式)
  • 文本:任何可见的文本、标志、价格
  • 动作:运动、过渡、正在发生的事情
读取frame_01.jpg后的示例输出:
帧1(0:00-0:03):一名穿着白色Adidas运动鞋的女性在森林小径上跑步。
晨光透过树林洒下。她穿着黑色运动紧身裤和灰色背心。鞋子上可见Adidas三条纹标志。
这是大语言模型的价值所在。 通用工具输出“[描述内容]”。 而你输出实际描述,因为你可以“看到”图片。

备用方案:如果Read工具无法读取图片
尝试直接调用Anthropic API:
javascript
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-4-20250514',
  max_tokens: 500,
  messages: [{
    role: 'user',
    content: [
      { type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: imageData } },
      { type: 'text', text: '为可访问性字幕描述此视频帧。' }
    ]
  }]
});
最后手段:基于上下文的推断(无视觉能力) 如果完全无法使用视觉能力,从以下信息推断:
  • 视频文件名:"product-demo.mp4" → 产品演示
  • 页面上下文:产品页面 → 产品展示
  • 周围文本:附近的标题和描述
为每一帧记录:
  • 场景:环境、地点、光线
  • 人物:人物、动作、表情、服装
  • 物品:产品、道具、设备
  • 文本:可见的文本、标志
  • 动作:运动、过渡
  • 颜色:主导颜色、与可访问性相关的颜色

7.4: Generate WebVTT Captions

7.4:生成WebVTT字幕

vtt
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]
vtt
WEBVTT
Kind: captions
Language: {检测到的语言}

00:00:00.000 --> 00:00:03.000
[来自frame_01分析的描述]

00:00:03.000 --> 00:00:06.000
[来自frame_02分析的描述]

7.5: Generate Audio Descriptions

7.5:生成音频描述

vtt
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]

vtt
WEBVTT
Kind: descriptions
Language: en

00:00:00.000 --> 00:00:03.000
场景: [为盲人群体提供的详细场景描述]
视觉: [屏幕上的内容]
文本: [任何可读文本]
动作: [正在发生的事情]

STEP 8: GENERATE COMPREHENSIVE REPORTS

步骤8:生成全面报告

8.1: Required Output Files

8.1:必需的输出文件

Save ALL files to
docs/accessibility-scans/{page-slug}/
:
FileContents
audit-summary.md
Executive summary, scores, top issues, user impact
remediation.md
ALL copy-paste code fixes with context
violations.json
Machine-readable violation data
implementation.md
Video integration guide (if videos)
*.vtt
Caption and audio description files
将所有文件保存到
docs/accessibility-scans/{page-slug}/
文件内容
audit-summary.md
执行摘要、分数、主要问题、用户影响
remediation.md
所有可直接复制粘贴的代码修复方案及上下文
violations.json
机器可读的问题数据
implementation.md
视频集成指南(如果有视频)
*.vtt
字幕和音频描述文件

8.2: audit-summary.md Template

8.2:audit-summary.md模板

markdown
undefined
markdown
undefined

Accessibility Audit Report: {Site Name}

可访问性审计报告: {站点名称}

URL: {url} Date: {date} Standard: WCAG 2.2 Level AA
URL: {url} 日期: {date} 标准: WCAG 2.2 AA级

Executive Summary

执行摘要

MetricValue
Compliance Score{score}%
Production Ready{Yes/No}
Critical Issues{count}
Total Violations{count}
Estimated Fix Time{hours}h
指标数值
合规性分数{score}%
生产就绪{是/否}
严重问题数{count}
问题总数{count}
估计修复时间{hours}小时

POUR Analysis

POUR分析

{table}
{表格}

Top 10 Issues by Priority

按优先级排序的前10个问题

{priority table with ROI scores}
{带ROI分数的优先级表格}

User Impact Summary

用户影响摘要

{affected user groups and percentages}
{受影响的用户群体及占比}

Recommendations

建议

{prioritized action items}
undefined
{按优先级排序的行动项}
undefined

8.3: remediation.md Template

8.3:remediation.md模板

markdown
undefined
markdown
undefined

Accessibility Remediation Guide: {Site Name}

可访问性修复指南: {站点名称}

Quick Wins (Copy-Paste Ready)

快速修复(可直接复制粘贴)

1. Form Labels ({count} issues)

1. 表单标签({count}个问题)

{For EACH unlabeled input: context, before/after code, rationale, confidence}
{对每个无标签输入:上下文、修复前后代码、理由、置信度}

2. Heading Structure ({count} issues)

2. 标题结构({count}个问题)

{Current structure visualization, fixed structure, code changes}
{当前结构可视化、修复后的结构、代码改动}

3. Color Contrast ({count} issues)

3. 颜色对比度({count}个问题)

{For EACH: current colors, ratio, suggested colors, CSS fixes}
{对每个问题:当前颜色、对比度、建议颜色、CSS修复方案}

4. Missing Alt Text ({count} issues)

4. 缺少alt文本({count}个问题)

{For EACH image: context-inferred alt text suggestions}
{对每个图片:基于上下文推断的alt文本建议}

5. Keyboard Navigation ({count} issues)

5. 键盘导航({count}个问题)

{For EACH: element, issue, fix code, test instructions}
{对每个问题:元素、问题、修复代码、测试说明}

6. Focus Indicators

6. 焦点指示器

{Global CSS to add}
{要添加的全局CSS}

7. Skip Links

7. 跳过链接

{Full HTML + CSS to add}
{完整的HTML + CSS代码}

8. ARIA Fixes ({count} issues)

8. ARIA修复({count}个问题)

{For EACH: context, specific aria attributes to add}
{对每个问题:上下文、要添加的特定ARIA属性}

9. iframe Titles ({count} issues)

9. iframe标题({count}个问题)

{For EACH: suggested title based on content}
{对每个iframe:基于内容的建议标题}

10. Video Accessibility ({count} videos)

10. 视频可访问性({count}个视频)

{Links to generated VTT files, implementation code}
{生成的VTT文件链接、实现代码}

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

---
  • 按Tab键遍历整个页面 - 所有交互元素均可访问
  • 屏幕阅读器正确播报所有内容
  • 颜色对比度通过(使用axe DevTools)
  • 无需鼠标即可操作
  • 在200%缩放比例下正常工作
  • 视频字幕同步且准确

---

STEP 9: LEARNING PROTOCOL (When MCP Available)

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

Integrate with the learning system to improve over time.
与学习系统集成以随时间改进。

9.1: Query Previous Patterns BEFORE Audit

9.1:审计前查询以往模式

Check if similar sites were audited before:
javascript
// 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
})
检查是否曾审计过类似站点:
javascript
// 加载MCP工具
ToolSearch("select:mcp__claude-flow_alpha__memory_retrieve")
ToolSearch("select:mcp__claude-flow_alpha__hooks_intelligence_pattern_search")

// 检索特定域名的模式
mcp__claude-flow_alpha__memory_retrieve({
  key: `accessibility/patterns/${domain}`,
  namespace: "learning"
})

// 搜索类似的问题模式
mcp__claude-flow_alpha__hooks_intelligence_pattern_search({
  query: "accessibility remediation",
  type: "accessibility-fix",
  limit: 10
})

9.2: Store Successful Patterns AFTER Audit

9.2:审计后存储成功模式

Store patterns that worked for future reuse:
javascript
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"]
  }
})
存储有效的模式以备将来复用:
javascript
ToolSearch("select:mcp__claude-flow_alpha__memory_store")
ToolSearch("select:mcp__claude-flow_alpha__hooks_intelligence_pattern_store")

// 存储审计结果
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)
    }
  }
})

// 存储可复用的修复模式
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: Calculate Audit Quality Score

9.3:计算审计质量分数

Self-assess audit completeness (for learning feedback):
CriteriaPointsYour Score
Multi-tool testing used (3 tools)20
All WCAG 2.2 AA criteria checked15
Context-aware fixes generated20
Confidence scores included10
ROI prioritization calculated10
Video pipeline completed (if applicable)15
EU compliance mapping included10
Total100
Quality Levels:
  • 90-100: Excellent (1.0 reward)
  • 70-89: Good (0.8 reward)
  • 50-69: Acceptable (0.5 reward)
  • <50: Incomplete (0.0 reward - redo required)

自我评估审计的完整性(用于学习反馈):
标准分值你的分数
使用多工具测试(3个工具)20
检查所有WCAG 2.2 AA标准15
生成上下文感知的修复方案20
包含置信度评分10
完成ROI优先级排序10
完成视频流程(如适用)15
包含欧盟合规映射10
总计100
质量等级:
  • 90-100:优秀(1.0奖励)
  • 70-89:良好(0.8奖励)
  • 50-69:可接受(0.5奖励)
  • <50:不完整(0.0奖励 - 需要重新执行)

STEP 10: SCREEN READER TESTING GUIDE

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

Manual testing instructions (cannot be fully automated):
手动测试说明(无法完全自动化):

10.1: NVDA (Windows - Free)

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)
1. 下载: https://www.nvaccess.org/download/
2. 安装并启动NVDA(Ctrl+Alt+N)
3. 导航到被审计页面

快捷键:
- H: 跳转到下一个标题
- F: 跳转到下一个表单字段
- B: 跳转到下一个按钮
- T: 跳转到下一个表格
- K: 跳转到下一个链接
- D: 跳转到下一个地标
- Tab: 移动到下一个可聚焦元素

验证:
- [ ] 所有标题均以正确级别播报
- [ ] 表单字段播报标签
- [ ] 按钮播报用途
- [ ] 图片播报alt文本或“装饰性”
- [ ] 动态内容变化被播报(aria-live)

10.2: VoiceOver (macOS - Built-in)

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
1. 启用: 系统偏好设置 → 辅助功能 → VoiceOver
2. 切换: Cmd+F5
3. 导航到被审计页面

快捷键:
- VO+U: 打开转子(标题、链接、表单、地标)
- VO+Space: 激活元素
- VO+右/左箭头: 浏览内容
- VO+Cmd+H: 跳转到下一个标题

验证:
- [ ] 转子按层级显示所有标题
- [ ] 表单可导航且标签被播报
- [ ] 焦点顺序与视觉顺序一致
- [ ] 所有内容均可访问

10.3: JAWS (Windows - Commercial)

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
1. 试用版: https://www.freedomscientific.com/products/software/jaws/
2. 启动JAWS并导航到页面

快捷键:
- H: 下一个标题
- F: 下一个表单字段
- B: 下一个按钮
- T: 下一个表格
- Ins+F6: 标题列表
- Ins+F7: 链接列表

验证:
- [ ] 虚拟光标模式正常工作
- [ ] 在表单中自动激活表单模式
- [ ] 所有ARIA角色被正确播报

10.4: Screen Reader Testing Checklist

10.4:屏幕阅读器测试清单

TestNVDAVoiceOverJAWS
Headings hierarchy correct[ ][ ][ ]
Form labels announced[ ][ ][ ]
Button purposes clear[ ][ ][ ]
Image alt text correct[ ][ ][ ]
Links announce destination[ ][ ][ ]
Landmarks navigable[ ][ ][ ]
Focus order logical[ ][ ][ ]
Dynamic updates announced[ ][ ][ ]
No keyboard traps[ ][ ][ ]
Skip links work[ ][ ][ ]

测试NVDAVoiceOverJAWS
标题层级正确[ ][ ][ ]
表单标签被播报[ ][ ][ ]
按钮用途清晰[ ][ ][ ]
图片alt文本正确[ ][ ][ ]
链接播报目标[ ][ ][ ]
地标可导航[ ][ ][ ]
焦点顺序合理[ ][ ][ ]
动态更新被播报[ ][ ][ ]
无键盘陷阱[ ][ ][ ]
跳过链接正常工作[ ][ ][ ]

VALIDATION CHECKLIST

验证清单

Before completing, verify ALL items:
完成前,验证所有项:

Content Fetching (v7.0 Resilient)

内容获取(v7.0弹性)

  • Browser launched (Vibium/agent-browser/Playwright)
  • Page loaded and analyzed
  • Multi-tool scan ran with parallel execution
  • At least 1 of 3 tools succeeded (graceful degradation)
  • If tools failed, documented which tools and why
  • Results saved to
    /tmp/a11y-work/scan-results.json
  • 已启动浏览器(Vibium/agent-browser/Playwright)
  • 已加载并分析页面
  • 已运行多工具扫描并采用并行执行
  • 至少3个工具中的1个成功(优雅降级)
  • 如果工具失败,已记录哪些工具失败及原因
  • 结果已保存到
    /tmp/a11y-work/scan-results.json

Violation Analysis

问题分析

  • All violations extracted with WCAG criteria
  • Context analyzed for each violation
  • User impact calculated
  • 已提取所有问题及对应的WCAG标准
  • 已分析每个问题的上下文
  • 已计算用户影响

Remediation Generation

修复方案生成

  • Form label fixes with context and rationale
  • Heading hierarchy fix with visualization
  • Color contrast fixes with hex codes
  • Alt text suggestions with confidence scores
  • Skip link code (full HTML + CSS)
  • Focus indicator CSS
  • ARIA fixes with specific attributes
  • Keyboard navigation fixes
  • iframe titles
  • 已生成带上下文和理由的表单标签修复方案
  • 已生成带可视化的标题层级修复方案
  • 已生成带十六进制颜色代码的颜色对比度修复方案
  • 已生成带置信度评分的alt文本建议
  • 已生成跳过链接代码(完整HTML + CSS)
  • 已生成焦点指示器CSS
  • 已生成带特定属性的ARIA修复方案
  • 已生成键盘导航修复方案
  • 已生成iframe标题

Video Accessibility (MANDATORY if pageInfo.media.videos > 0)

视频可访问性(如果pageInfo.media.videos > 0则强制要求)

  • Video URLs extracted (full URLs, not relative)
  • Download attempted for EACH video
  • IF download succeeded: Frames extracted with ffmpeg
  • IF download succeeded: Each frame analyzed with Read tool
  • IF download succeeded: captions.vtt generated from ACTUAL frame descriptions
  • IF download succeeded: audiodesc.vtt generated
  • IF download FAILED: Failure documented in audit-summary.md with reason
  • IF download FAILED: Manual captioning instructions in remediation.md
BLOCKING: Cannot generate final reports until video pipeline attempted.
  • 已提取视频URL(完整URL,而非相对路径)
  • 已尝试下载每个视频
  • 如果下载成功:已用ffmpeg提取帧
  • 如果下载成功:已用Read工具分析每一帧
  • 如果下载成功:已根据实际帧描述生成captions.vtt
  • 如果下载成功:已生成audiodesc.vtt
  • 如果下载失败:已在audit-summary.md中记录失败原因
  • 如果下载失败:已在remediation.md中添加手动字幕说明
阻塞项: 在尝试视频流程前无法生成最终报告。

Output Files

输出文件

  • audit-summary.md with scores, priorities, and user impact
  • remediation.md with ALL copy-paste code fixes
  • violations.json with violation data
  • VTT files (if videos)
  • implementation.md (if videos)
  • 已生成带分数、优先级和用户影响的audit-summary.md
  • 已生成带所有可直接复制粘贴代码修复方案的remediation.md
  • 已生成带问题数据的violations.json
  • 已生成VTT文件(如果有视频)
  • 已生成implementation.md(如果有视频)

Quality Checks

质量检查

  • Compliance score calculated
  • Production readiness assessed
  • ROI prioritization completed
  • POUR analysis included
IF ANY CHECKBOX IS NO = TASK INCOMPLETE </default_to_action>

  • 已计算合规性分数
  • 已评估生产就绪性
  • 已完成ROI优先级排序
  • 已包含POUR分析
如果任何复选框为否 = 任务未完成 </default_to_action>

Quick Reference Card

快速参考卡

Usage

使用方式

/a11y-ally https://example.com
/a11y-ally https://example.com

v7.0 Resilience Features

v7.0弹性特性

FeatureDescription
Parallel ExecutionAll 3 tools run simultaneously via Promise.allSettled
Per-Tool Timeoutsaxe: 60s, pa11y: 60s, Lighthouse: 90s
Retry with Backoff2 retries per tool with exponential backoff
Graceful DegradationContinue if 1+ tools succeed
Progressive OutputResults stream as tools complete
Bot ProtectionStealth mode, random delays, cookie dismissal
特性描述
并行执行所有3个工具通过Promise.allSettled同时运行
按工具设置超时axe: 60秒, pa11y: 60秒, Lighthouse: 90秒
退避重试每个工具最多重试2次,采用指数退避
优雅降级如果至少1个工具成功则继续执行
渐进式输出工具完成即输出结果
反爬机制规避隐身模式、随机延迟、Cookie横幅关闭

Expected Output Structure

预期输出结构

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)
docs/accessibility-scans/{page-slug}/
├── audit-summary.md      # 带分数的执行摘要
├── remediation.md        # 所有可直接复制粘贴的代码修复方案
├── violations.json       # 机器可读数据
├── implementation.md     # 视频集成(如果有视频)
├── video-*-captions.vtt  # 字幕(如果有视频)
└── video-*-audiodesc.vtt # 音频描述(如果有视频)

Compliance Thresholds

合规性阈值

LevelMin ScoreCriticalSerious
A70%0≤5
AA85%0≤3
AAA95%00
级别最低分数严重问题数重要问题数
A70%0≤5
AA85%0≤3
AAA95%00

Tool Coverage by Success

按工具成功数量的覆盖范围

Tools SucceededDetection RateStatus
3/3~95%✅ Optimal
2/3~85%⚠️ Good
1/3~70%⚠️ Acceptable
0/3❌ Retry needed
成功工具数检测率状态
3/3~95%✅ 最优
2/3~85%⚠️ 良好
1/3~70%⚠️ 可接受
0/3❌ 需要重试

ROI Formula

ROI公式

ROI = (Impact × Users%) / Effort_Hours

ROI = (影响 × 用户占比%) / 修复时间(小时)

EU Compliance Mapping

欧盟合规映射

WCAGEN 301 549EU Accessibility Act
1.1.19.1.1.1EAA-I.1 Perceivable
1.4.39.1.4.3EAA-I.1 Perceivable
2.1.19.2.1.1EAA-I.2 Operable
2.4.79.2.4.7EAA-I.2 Operable
3.3.29.3.3.2EAA-I.3 Understandable
4.1.29.4.1.2EAA-I.4 Robust

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 健壮

Critical Rules

关键规则

Execution Rules (v7.0)

执行规则(v7.0)

  1. ALWAYS run multi-tool scan with parallel execution (Promise.allSettled)
  2. ALWAYS continue if at least 1 of 3 tools succeeds (graceful degradation)
  3. ALWAYS document which tools failed and why in audit-summary.md
  4. ALWAYS use per-tool timeouts (60s/60s/90s) not global timeout
  5. ALWAYS retry failed tools with exponential backoff before giving up
  1. 始终使用并行执行(Promise.allSettled)运行多工具扫描
  2. 始终在至少3个工具中的1个成功时继续执行(优雅降级)
  3. 始终在audit-summary.md中记录哪些工具失败及原因
  4. 始终使用按工具设置的超时(60秒/60秒/90秒)而非全局超时
  5. 始终在放弃前对失败的工具采用指数退避重试

Quality Rules

质量规则

  1. ALWAYS analyze context before generating fixes
  2. ALWAYS include confidence scores with remediation
  3. ALWAYS calculate user impact and ROI
  4. ALWAYS generate copy-paste ready code
  5. NEVER generate placeholder/template fixes
  6. NEVER skip video pipeline if videos detected
  7. NEVER complete without remediation.md
  8. NEVER fail audit just because 1-2 tools failed (use graceful degradation)
  1. 始终在生成修复方案前分析上下文
  2. 始终在修复方案中包含置信度评分
  3. 始终计算用户影响和ROI
  4. 始终生成可直接复制粘贴的代码
  5. 绝不生成占位符/模板化修复方案
  6. 绝不在检测到视频时跳过视频流程
  7. 绝不在未生成remediation.md时完成任务
  8. 绝不因1-2个工具失败而判定审计失败(使用优雅降级)