mcpField
A
a11y-ally

a11y-ally

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

关于 a11y-ally

a11y-ally 是面向 Claude 的无障碍审计技能,可扫描网页、分析问题并生成针对性的修复建议和报告。它结合自动化检测与 LLM 的上下文判断,并可处理缺少字幕的视频。

主要功能

  1. 01并行运行多种无障碍检测工具
  2. 02结合页面上下文分析违规项
  3. 03生成具体修复建议及置信度
  4. 04评估用户影响并排列修复优先级
  5. 05为无字幕视频生成字幕内容

适用场景

  1. 01审计单个网页的 WCAG 问题
  2. 02并行检查多个页面或爬取整站
  3. 03为检测到的问题编写修复建议
  4. 04识别无字幕视频并生成字幕

MCP 相关

可优先使用已注册的 Vibium MCP 工具获取网页内容;否则通过命令行调用 vibium。

安装

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

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

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

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
a11y-ally.yaml
schemas/
output.json
scripts/
validate-config.json

详细介绍

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

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

这是一项由 LLM 驱动的技能

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

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

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


执行模式

CLAUDE 会不停顿地执行所有步骤。

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

  1. 数据收集:通过 Bash 运行多工具扫描(axe-core、pa11y、Lighthouse)
  2. LLM 分析:读取结果并分析每项违规的上下文
  3. 视觉处理流程:如果检测到视频 → 下载 → 提取帧 → 逐帧读取 → 描述
  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:浏览器自动化 - 获取内容

使用 qe-browser fleet 技能作为浏览器引擎。qe-browser 封装了 Vibium(WebDriver BiDi,10MB Go 二进制文件),并提供我们依赖的 QE 原语。参见 .claude/skills/qe-browser/SKILL.md。

1.1:主要方式 — 通过 Vibium CLI 使用 qe-browser
# Navigate
vibium go "$TARGET_URL"
vibium wait load

# Capture accessibility tree without visual render
vibium a11y-tree --json > /tmp/a11y-work/tree.json

# Screenshot for Vision pipeline
vibium screenshot -o /tmp/a11y-work/page.png --full-page

如果已注册 Vibium MCP 工具(mcp__vibium__*),优先使用它们;否则,调用 aqe init 安装的 vibium 二进制文件。

1.2:通过 qe-browser 运行 axe-core + WCAG 断言
# Inject axe-core via vibium eval and collect violations
vibium eval --stdin <<'EOF'
const s = document.createElement('script');
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.10.2/axe.min.js';
document.head.appendChild(s);
await new Promise(r => s.onload = r);
const results = await axe.run();
JSON.stringify({ violations: results.violations.length, issues: results.violations });
EOF

# Enforce: no critical a11y violations + no failed network requests
node .claude/skills/qe-browser/scripts/assert.js --checks '[
  {"kind": "no_console_errors"},
  {"kind": "no_failed_requests"},
  {"kind": "selector_visible", "selector": "main, [role=main]"}
]'
1.3:备用方式 — pa11y + Lighthouse(仅在 axe 不足时)
# Only use when you need the extra rulesets, not as the primary path
pa11y "$TARGET_URL" --reporter json > /tmp/a11y-work/pa11y.json
lighthouse "$TARGET_URL" --only-categories=accessibility --output=json --output-path=/tmp/a11y-work/lighthouse.json --chrome-flags="--headless"

从主要路径中移除 playwright-extra + puppeteer-extra-plugin-stealth 的原因:

  • 相比 Vibium 的 10MB 二进制文件,需要 300MB 以上的 Node 依赖
  • 功能重复:Vibium 使用 WebDriver BiDi,相比原始 CDP 更难被指纹识别
  • 更简单:只需一种工具,而非一连串工具
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:情境感知修复(由 LLM 驱动)

这正是 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:受影响的用户群体
违规类型受影响群体用户占比
缺少替代文本盲人、低视力用户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:基于 ROI 的优先级排序

为每项修复计算优先级:

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极低
添加替代文本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(必需)

必须将此步骤集成到步骤 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)

针对每一帧,描述:

  • 场景:环境、周围环境、光线、地点
  • 人物:出镜者、他们的行为、表情、服装
  • 产品:展示的物品(对于电商内容:产品名称、颜色、款式)
  • 文字:任何可见文字、标志、标牌、价格
  • 动作:移动、转场、正在发生的事情

读取 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
包含欧盟合规性映射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 549EU Accessibility Act
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 个工具失败就判定审计失败(采用优雅降级)

注意事项

  • axe-core 只能检测约 30% 的 WCAG 问题——自动化工具无法发现键盘导航、阅读顺序和认知方面的问题
  • Agent 仅运行 Lighthouse 并报告“无障碍”——仅使用 Lighthouse 并不足够,始终也要运行 axe-core + pa11y
  • 屏幕阅读器测试需要实际使用屏幕阅读器交互,而不仅仅是检查 ARIA 属性
  • 视频无障碍(字幕、音频描述)经常被跳过——检查每个 <video> 元素
  • 颜色对比度工具对渐变和透明度的结果可能不一致——使用实际的低视力模拟进行测试
  • Playwright+Stealth 可能会被某些网站拦截——应优雅降级,不要跳过审计

评论

支持 Markdown