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 Do | Claude (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 templates | Generate 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:
- Data Collection: Run multi-tool scan (axe-core, pa11y, Lighthouse) via Bash
- LLM Analysis: Read results and analyze context for each violation
- Vision Pipeline: If videos detected → download → extract frames → Read each frame → describe
- Intelligent Remediation: Generate context-specific fixes using your reasoning
- 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会不间断执行所有步骤。
不要在步骤之间等待用户提示。执行完整流程:
- 数据收集:通过Bash运行多工具扫描(axe-core、pa11y、Lighthouse)
- 大语言模型分析:读取结果并分析每个问题的上下文
- 视觉流程:如果检测到视频 → 下载 → 提取帧 → 读取每一帧 → 描述内容
- 智能修复:通过推理生成特定上下文的修复方案
- 生成报告:将所有输出文件写入
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/nullCreate 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/contact1e: 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:
| Feature | v6.0 (Old) | v7.0 (New) |
|---|---|---|
| Tool execution | Sequential | Parallel (Promise.allSettled) |
| Timeout handling | Global 60s | Per-tool (60s/60s/90s) |
| Failure mode | All-or-nothing | Graceful degradation |
| Retry logic | None | Exponential backoff (3 retries) |
| Output style | Wait for all | Progressive (stream as ready) |
| Minimum tools | 3 required | 1 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.jsjavascript
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.jsjavascript
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.jsonThe 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 Succeeded | Action |
|---|---|
| 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.videoUrlsjavascript
// 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.videoUrlsjavascript
// 检查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:
You output: because you understand context.
aria-label="[DESCRIPTION]"aria-label="Add to shopping cart"这是Claude智能的核心价值所在。
通用工具输出:
而你输出:,因为你理解上下文。
aria-label="[DESCRIPTION]"aria-label="加入购物车"3.1: Context Analysis (Use Your Reasoning)
3.1:上下文分析(运用你的推理能力)
For EACH violation, Claude must:
-
READ THE HTML CONTEXT - Don't just see, see:
<button class="btn">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> -
INFER PURPOSE from:
- Class names: ,
add-to-cart,wishlistmenu-toggle - Parent context: Inside with product data
.product-card - Icon classes: ,
icon-cart,icon-hearticon-search - Nearby text: Product name, price, "Add to bag"
- Page section: Header nav vs product grid vs checkout
- Class names:
-
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必须:
-
读取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> -
从以下信息推断用途:
- 类名:、
add-to-cart、wishlistmenu-toggle - 父上下文:位于包含产品数据的内
.product-card - 图标类:、
icon-cart、icon-hearticon-search - 附近文本:产品名称、价格、“加入购物袋”
- 页面区域:头部导航、产品网格、结账页
- 类名:
-
生成特定修复方案: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
undefinedButton: .product-card .btn
(Confidence: 0.95)
.product-card .btn按钮: .product-card .btn
(置信度: 0.95)
.product-card .btnContext: Inside product card for "Adidas Superstar", has cart icon
Fix:
aria-label="Add Adidas Superstar to cart"undefined上下文: 位于“Adidas Superstar”产品卡片内,包含购物车图标
修复:
aria-label="将Adidas Superstar加入购物车"undefined3.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 validationIcon 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: ContactSkip 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 Type | Affected Groups | % of Users |
|---|---|---|
| Missing alt text | Blind, low-vision | 7-10% |
| Missing form labels | Blind, screen reader users | 5-8% |
| Low color contrast | Low-vision, color blind | 8-12% |
| No keyboard access | Motor impaired, power users | 10-15% |
| Missing captions | Deaf, hard-of-hearing | 5-7% |
| Flashing content | Seizure sensitive | 0.5-1% |
| Complex language | Cognitive impairment | 10-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 worksBLOCKS-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 Type | Effort | Complexity |
|---|---|---|
| Add aria-label | 0.25h | Trivial |
| Add alt text | 0.25h | Trivial |
| Add form label | 0.5h | Simple |
| Fix color contrast | 0.5h | Simple |
| Add skip links | 1h | Simple |
| Fix heading structure | 2h | Medium |
| Add keyboard navigation | 4h | High |
| Implement focus trap | 4h | High |
| Add video captions | 8h | High |
| 修复类型 | 工作量 | 复杂度 |
|---|---|---|
| 添加aria-label | 0.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 accessibleCOMPLIANCE_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/xxx7.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
undefinedCreate 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}/| File | Contents |
|---|---|
| Executive summary, scores, top issues, user impact |
| ALL copy-paste code fixes with context |
| Machine-readable violation data |
| Video integration guide (if videos) |
| Caption and audio description files |
将所有文件保存到:
docs/accessibility-scans/{page-slug}/| 文件 | 内容 |
|---|---|
| 执行摘要、分数、主要问题、用户影响 |
| 所有可直接复制粘贴的代码修复方案及上下文 |
| 机器可读的问题数据 |
| 视频集成指南(如果有视频) |
| 字幕和音频描述文件 |
8.2: audit-summary.md Template
8.2:audit-summary.md模板
markdown
undefinedmarkdown
undefinedAccessibility Audit Report: {Site Name}
可访问性审计报告: {站点名称}
URL: {url}
Date: {date}
Standard: WCAG 2.2 Level AA
URL: {url}
日期: {date}
标准: WCAG 2.2 AA级
Executive Summary
执行摘要
| Metric | Value |
|---|---|
| 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{按优先级排序的行动项}
undefined8.3: remediation.md Template
8.3:remediation.md模板
markdown
undefinedmarkdown
undefinedAccessibility 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):
| Criteria | Points | Your Score |
|---|---|---|
| Multi-tool testing used (3 tools) | 20 | |
| All WCAG 2.2 AA criteria checked | 15 | |
| Context-aware fixes generated | 20 | |
| Confidence scores included | 10 | |
| ROI prioritization calculated | 10 | |
| Video pipeline completed (if applicable) | 15 | |
| EU compliance mapping included | 10 | |
| Total | 100 |
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 reachable1. 启用: 系统偏好设置 → 辅助功能 → 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 properly1. 试用版: 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:屏幕阅读器测试清单
| Test | NVDA | VoiceOver | JAWS |
|---|---|---|---|
| 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 | [ ] | [ ] | [ ] |
| 测试 | NVDA | VoiceOver | JAWS |
|---|---|---|---|
| 标题层级正确 | [ ] | [ ] | [ ] |
| 表单标签被播报 | [ ] | [ ] | [ ] |
| 按钮用途清晰 | [ ] | [ ] | [ ] |
| 图片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.comv7.0 Resilience Features
v7.0弹性特性
| Feature | Description |
|---|---|
| Parallel Execution | All 3 tools run simultaneously via Promise.allSettled |
| Per-Tool Timeouts | axe: 60s, pa11y: 60s, Lighthouse: 90s |
| Retry with Backoff | 2 retries per tool with exponential backoff |
| Graceful Degradation | Continue if 1+ tools succeed |
| Progressive Output | Results stream as tools complete |
| Bot Protection | Stealth 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
合规性阈值
| Level | Min Score | Critical | Serious |
|---|---|---|---|
| A | 70% | 0 | ≤5 |
| AA | 85% | 0 | ≤3 |
| AAA | 95% | 0 | 0 |
| 级别 | 最低分数 | 严重问题数 | 重要问题数 |
|---|---|---|---|
| A | 70% | 0 | ≤5 |
| AA | 85% | 0 | ≤3 |
| AAA | 95% | 0 | 0 |
Tool Coverage by Success
按工具成功数量的覆盖范围
| Tools Succeeded | Detection Rate | Status |
|---|---|---|
| 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_HoursROI = (影响 × 用户占比%) / 修复时间(小时)EU Compliance Mapping
欧盟合规映射
| WCAG | EN 301 549 | EU Accessibility Act |
|---|---|---|
| 1.1.1 | 9.1.1.1 | EAA-I.1 Perceivable |
| 1.4.3 | 9.1.4.3 | EAA-I.1 Perceivable |
| 2.1.1 | 9.2.1.1 | EAA-I.2 Operable |
| 2.4.7 | 9.2.4.7 | EAA-I.2 Operable |
| 3.3.2 | 9.3.3.2 | EAA-I.3 Understandable |
| 4.1.2 | 9.4.1.2 | EAA-I.4 Robust |
| WCAG | EN 301 549 | 欧盟可访问性法案 |
|---|---|---|
| 1.1.1 | 9.1.1.1 | EAA-I.1 可感知 |
| 1.4.3 | 9.1.4.3 | EAA-I.1 可感知 |
| 2.1.1 | 9.2.1.1 | EAA-I.2 可操作 |
| 2.4.7 | 9.2.4.7 | EAA-I.2 可操作 |
| 3.3.2 | 9.3.3.2 | EAA-I.3 可理解 |
| 4.1.2 | 9.4.1.2 | EAA-I.4 健壮 |
Critical Rules
关键规则
Execution Rules (v7.0)
执行规则(v7.0)
- ALWAYS run multi-tool scan with parallel execution (Promise.allSettled)
- ALWAYS continue if at least 1 of 3 tools succeeds (graceful degradation)
- ALWAYS document which tools failed and why in audit-summary.md
- ALWAYS use per-tool timeouts (60s/60s/90s) not global timeout
- ALWAYS retry failed tools with exponential backoff before giving up
- 始终使用并行执行(Promise.allSettled)运行多工具扫描
- 始终在至少3个工具中的1个成功时继续执行(优雅降级)
- 始终在audit-summary.md中记录哪些工具失败及原因
- 始终使用按工具设置的超时(60秒/60秒/90秒)而非全局超时
- 始终在放弃前对失败的工具采用指数退避重试
Quality Rules
质量规则
- ALWAYS analyze context before generating fixes
- ALWAYS include confidence scores with remediation
- ALWAYS calculate user impact and ROI
- ALWAYS generate copy-paste ready code
- NEVER generate placeholder/template fixes
- NEVER skip video pipeline if videos detected
- NEVER complete without remediation.md
- NEVER fail audit just because 1-2 tools failed (use graceful degradation)
- 始终在生成修复方案前分析上下文
- 始终在修复方案中包含置信度评分
- 始终计算用户影响和ROI
- 始终生成可直接复制粘贴的代码
- 绝不生成占位符/模板化修复方案
- 绝不在检测到视频时跳过视频流程
- 绝不在未生成remediation.md时完成任务
- 绝不因1-2个工具失败而判定审计失败(使用优雅降级)