Communitygithub.com

UI 演示视频录制

使用 Playwright 录制精美打磨的 UI 演示视频。当用户要求创建网页应用的演示、导览、屏幕录制或教程视频时使用。生成带有可见光标、自然节奏和专业风格的 WebM 视频。

UI 演示视频录制 是什么?

此技能提供了一种结构化的三阶段方法(发现 -> 排练 -> 录制),用于创建专业、叙事流畅的浏览器演示视频。它基于 Playwright,通过在录制前系统地发现和验证 DOM 状态来确保录制稳定性,从而避免因选择器错误或假定的 UI 结构而导致的静默失败。核心工作流包括:在发现阶段交互式地转储页面内容以构建准确的选择器映射;在录制前对所有选择器进行严格的可见性断言进行排练;以及一个封装了平滑光标动画、仿人类输入速度、自然滚动和可配置字幕叠加层的录制引擎。该技能在 1280x720 分辨率的 WebM 输出中,生成带有显式光标的视频,适用于文档、用户引导或利益相关者演示,并可在 Claude Code、Cursor 和 Codex 等主流 AI 代理平台中使用。

兼容平台~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/ui-demo

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

UI 演示视频录制器

使用 Playwright 的视频录制功能,结合注入的光标叠加层、自然节奏和叙事流程,录制精美的网页应用演示视频。

何时使用

  • 用户要求制作“演示视频”、“屏幕录制”、“导览”或“教程”
  • 用户希望以视觉方式展示某个功能或工作流程
  • 用户需要用于文档、用户引导或利益相关者演示的视频

三阶段流程

每个演示都遵循三个阶段:发现 -> 排练 -> 录制。切勿直接跳到录制阶段。


阶段 1:发现

在编写任何脚本之前,先探索目标页面以了解实际内容。

为什么

你没有见过的东西无法编写脚本。字段可能是 <input> 而不是 <textarea>,下拉菜单可能是自定义组件而不是 <select>,评论框可能支持 @提及 或 #标签。错误的假设会静默地破坏录制。

如何操作

导航到流程中的每个页面并转储其交互元素:

// 在编写演示脚本之前,对流程中的每个页面运行此操作
const fields = await page.evaluate(() => {
  const els = [];
  document.querySelectorAll('input, select, textarea, button, [contenteditable]').forEach(el => {
    if (el.offsetParent !== null) {
      els.push({
        tag: el.tagName,
        type: el.type || '',
        name: el.name || '',
        placeholder: el.placeholder || '',
        text: el.textContent?.trim().substring(0, 40) || '',
        contentEditable: el.contentEditable === 'true',
        role: el.getAttribute('role') || '',
      });
    }
  });
  return els;
});
console.log(JSON.stringify(fields, null, 2));

查找什么

  • 表单字段:它们是 <select>、<input>、自定义下拉菜单还是组合框?
  • 选择选项:转储选项的值 AND 文本。占位符通常具有 value="0" 或 value="",看起来非空。使用 Array.from(el.options).map(o => ({ value: o.value, text: o.text }))。跳过文本包含“选择”或值为 "0" 的选项。
  • 富文本:评论框是否支持 @提及、#标签、markdown 或 emoji?检查占位符文本。
  • 必填字段:哪些字段会阻止表单提交?检查 required、标签中的 *,并尝试提交空表单以查看验证错误。
  • 动态内容:字段是否在其他字段填写后出现?
  • 按钮标签:确切的文本,如 "提交"、"提交请求" 或 "发送"。
  • 表格列标题:对于基于表格的模态框,将每个 input[type="number"] 映射到其列标题,而不是假设所有数字输入都意味着同一件事。

输出

每个页面的字段映射,用于在脚本中编写正确的选择器。示例:

/purchase-requests/new:
  - 预算代码: <select> (页面上的第一个下拉菜单,4 个选项)
  - 期望交付日期: <input type="date">
  - 上下文: <textarea> (不是 input)
  - BOM 表: 内联可编辑单元格,具有 span.cursor-pointer -> input 模式
  - 提交: <button> text="提交"

/purchase-requests/N (详情):
  - 评论: <input placeholder="输入消息..."> 支持 @user 和 #PR 标签
  - 发送: <button> text="发送" (在输入有内容之前禁用)

阶段 2:排练

在不录制的情况下运行所有步骤。验证每个选择器是否都能解析。

为什么

静默的选择器失败是演示录制失败的主要原因。排练可以在浪费录制时间之前捕获它们。

如何操作

使用 ensureVisible,它记录日志并大声失败:

async function ensureVisible(page, locator, label) {
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    const msg = `排练失败: "${label}" 未找到 - 选择器: ${typeof locator === 'string' ? locator : '(locator object)'}`;
    console.error(msg);
    const found = await page.evaluate(() => {
      return Array.from(document.querySelectorAll('button, input, select, textarea, a'))
        .filter(el => el.offsetParent !== null)
        .map(el => `${el.tagName}[${el.type || ''}] "${el.textContent?.trim().substring(0, 30)}"`)
        .join('\n  ');
    });
    console.error('  可见元素:\n  ' + found);
    return false;
  }
  console.log(`排练成功: "${label}"`);
  return true;
}

排练脚本结构

const steps = [
  { label: '登录邮箱字段', selector: '#email' },
  { label: '登录提交', selector: 'button[type="submit"]' },
  { label: '新建请求按钮', selector: 'button:has-text("新建请求")' },
  { label: '预算代码下拉菜单', selector: 'select' },
  { label: '交付日期', selector: 'input[type="date"]:visible' },
  { label: '描述字段', selector: 'textarea:visible' },
  { label: '添加项目按钮', selector: 'button:has-text("添加项目")' },
  { label: '提交按钮', selector: 'button:has-text("提交")' },
];

let allOk = true;
for (const step of steps) {
  if (!await ensureVisible(page, step.selector, step.label)) {
    allOk = false;
  }
}
if (!allOk) {
  console.error('排练失败 - 在录制前修复选择器');
  process.exit(1);
}
console.log('排练通过 - 所有选择器已验证');

排练失败时

  1. 读取可见元素转储。
  2. 找到正确的选择器。
  3. 更新脚本。
  4. 重新运行排练。
  5. 仅在每个选择器都通过时才继续。

阶段 3:录制

仅在发现和排练通过后,才创建录制。

录制原则

1. 叙事流程

将视频规划为一个故事。遵循用户指定的顺序,或使用此默认顺序:

  • 入口:登录或导航到起点
  • 上下文:扫视周围区域,以便观众定位
  • 操作:执行主要工作流步骤
  • 变化:展示一个次要功能,如设置、主题或本地化
  • 结果:展示结果、确认或新状态

2. 节奏

  • 登录后: 4s
  • 导航后: 3s
  • 点击按钮后: 2s
  • 主要步骤之间: 1.5-2s
  • 最终操作后: 3s
  • 打字延迟: 25-40ms 每字符

3. 光标叠加层

注入一个跟随鼠标移动的 SVG 箭头光标:

async function injectCursor(page) {
  await page.evaluate(() => {
    if (document.getElementById('demo-cursor')) return;
    const cursor = document.createElement('div');
    cursor.id = 'demo-cursor';
    cursor.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M5 3L19 12L12 13L9 20L5 3Z" fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
    </svg>`;
    cursor.style.cssText = `
      position: fixed; z-index: 999999; pointer-events: none;
      width: 24px; height: 24px;
      transition: left 0.1s, top 0.1s;
      filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.3));
    `;
    cursor.style.left = '0px';
    cursor.style.top = '0px';
    document.body.appendChild(cursor);
    document.addEventListener('mousemove', (e) => {
      cursor.style.left = e.clientX + 'px';
      cursor.style.top = e.clientY + 'px';
    });
  });
}

在每次页面导航后调用 injectCursor(page),因为叠加层在导航时会被销毁。

4. 鼠标移动

切勿瞬移光标。在点击之前移动到目标:

async function moveAndClick(page, locator, label, opts = {}) {
  const { postClickDelay = 800, ...clickOpts } = opts;
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    console.error(`警告: moveAndClick 跳过 - "${label}" 不可见`);
    return false;
  }
  try {
    await el.scrollIntoViewIfNeeded();
    await page.waitForTimeout(300);
    const box = await el.boundingBox();
    if (box) {
      await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 10 });
      await page.waitForTimeout(400);
    }
    await el.click(clickOpts);
  } catch (e) {
    console.error(`警告: moveAndClick 在 "${label}" 上失败: ${e.message}`);
    return false;
  }
  await page.waitForTimeout(postClickDelay);
  return true;
}

每次调用都应包含一个描述性的 label,以便调试。

5. 打字

可视化输入,而不是即时填充:

async function typeSlowly(page, locator, text, label, charDelay = 35) {
  const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
  const visible = await el.isVisible().catch(() => false);
  if (!visible) {
    console.error(`警告: typeSlowly 跳过 - "${label}" 不可见`);
    return false;
  }
  await moveAndClick(page, el, label);
  await el.fill('');
  await el.pressSequentially(text, { delay: charDelay });
  await page.waitForTimeout(500);
  return true;
}

6. 滚动

使用平滑滚动而不是跳跃:

await page.evaluate(() => window.scrollTo({ top: 400, behavior: 'smooth' }));
await page.waitForTimeout(1500);

7. 仪表盘扫视

在展示仪表盘或概览页面时,将光标移动到关键元素上:

async function panElements(page, selector, maxCount = 6) {
  const elements = await page.locator(selector).all();
  for (let i = 0; i < Math.min(elements.length, maxCount); i++) {
    try {
      const box = await elements[i].boundingBox();
      if (box && box.y < 700) {
        await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 8 });
        await page.waitForTimeout(600);
      }
    } catch (e) {
      console.warn(`警告: panElements 跳过元素 ${i} (选择器: "${selector}"): ${e.message}`);
    }
  }
}

8. 字幕

在视口底部注入一个字幕栏:

async function injectSubtitleBar(page) {
  await page.evaluate(() => {
    if (document.getElementById('demo-subtitle')) return;
    const bar = document.createElement('div');
    bar.id = 'demo-subtitle';
    bar.style.cssText = `
      position: fixed; bottom: 0; left: 0; right: 0; z-index: 999998;
      text-align: center; padding: 12px 24px;
      background: rgba(0, 0, 0, 0.75);
      color: white; font-family: -apple-system, "Segoe UI", sans-serif;
      font-size: 16px; font-weight: 500; letter-spacing: 0.3px;
      transition: opacity 0.3s;
      pointer-events: none;
    `;
    bar.textContent = '';
    bar.style.opacity = '0';
    document.body.appendChild(bar);
  });
}

async function showSubtitle(page, text) {
  await page.evaluate((t) => {
    const bar = document.getElementById('demo-subtitle');
    if (!bar) return;
    if (t) {
      bar.textContent = t;
      bar.style.opacity = '1';
    } else {
      bar.style.opacity = '0';
    }
  }, text);
  if (text) await page.waitForTimeout(800);
}

在每次导航后,与 injectCursor(page) 一起调用 injectSubtitleBar(page)。

使用模式:

await showSubtitle(page, '步骤 1 - 登录中');
await showSubtitle(page, '步骤 2 - 仪表盘概览');
await showSubtitle(page, '');

指南:

  • 保持字幕文本简短,理想情况下少于 60 个字符。
  • 使用 步骤 N - 操作 格式以保持一致性。
  • 在长时间暂停且界面可以自我说明时,清除字幕。

脚本模板

'use strict';
const { chromium } = require('playwright');
const path = require('path');
const fs = require('fs');

const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:3000';
const VIDEO_DIR = path.join(__dirname, 'screenshots');
const OUTPUT_NAME = 'demo-功能.webm';
const REHEARSAL = process.argv.includes('--rehearse');

// 在此处粘贴 injectCursor、injectSubtitleBar、showSubtitle、moveAndClick、
// typeSlowly、ensureVisible 和 panElements。

(async () => {
  const browser = await chromium.launch({ headless: true });

  if (REHEARSAL) {
    const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
    const page = await context.newPage();
    // 遍历流程并为每个选择器运行 ensureVisible。
    await browser.close();
    return;
  }

  const context = await browser.newContext({
    recordVideo: { dir: VIDEO_DIR, size: { width: 1280, height: 720 } },
    viewport: { width: 1280, height: 720 }
  });
  const page = await context.newPage();

  try {
    await injectCursor(page);
    await injectSubtitleBar(page);

    await showSubtitle(page, '步骤 1 - 登录中');
    // 登录操作

    await page.goto(`${BASE_URL}/dashboard`);
    await injectCursor(page);
    await injectSubtitleBar(page);
    await showSubtitle(page, '步骤 2 - 仪表盘概览');
    // 扫视仪表盘

    await showSubtitle(page, '步骤 3 - 主要工作流');
    // 操作序列

    await showSubtitle(page, '步骤 4 - 结果');
    // 最终展示
    await showSubtitle(page, '');
  } catch (err) {
    console.error('演示错误:', err.message);
  } finally {
    await context.close();
    const video = page.video();
    if (video) {
      const src = await video.path();
      const dest = path.join(VIDEO_DIR, OUTPUT_NAME);
      try {
        fs.copyFileSync(src, dest);
        console.log('视频已保存:', dest);
      } catch (e) {
        console.error('错误: 复制视频失败:', e.message);
        console.error('  源文件:', src);
        console.error('  目标文件:', dest);
      }
    }
    await browser.close();
  }
})();

用法:

# 阶段 2: 排练
node demo-脚本.cjs --rehearse

# 阶段 3: 录制
node demo-脚本.cjs

录制前检查清单

  • 发现阶段已完成
  • 排练通过,所有选择器正常
  • 已启用无头模式
  • 分辨率设置为 1280x720
  • 每次导航后重新注入光标和字幕叠加层
  • 在主要过渡时使用 showSubtitle(page, '步骤 N - ...')
  • 所有点击均使用带有描述性标签的 moveAndClick
  • 使用 typeSlowly 进行可视输入
  • 没有静默的 catch;

…

[译文截断]

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

相关技能