Token导航 LogoToken导航TokenDH.com
开发操作浏览器github未标认证来源可访问许可证需确认审计通过

app-studio-demo-capture应用工作室演示捕获

Agent Skill

app-studio-demo-capture 用于处理 GitHub 仓库、Issue、Pull Request 和代码协作信息,适合在 Codex、Claude、Cursor、Gemini CLI 中需要围绕仓库状态、代码变更或协作事项进行整理时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

总安装

760

周安装

32

GitHub Stars

14

下载量

266
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:app-studio-demo-capture(应用工作室演示捕获)
来源仓库:https://github.com/stahura/domo-ai-vibe-rules
仓库路径:skills/app-studio-demo-capture
安装命令:
npx skills add https://github.com/stahura/domo-ai-vibe-rules --skill app-studio-demo-capture
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/stahura/domo-ai-vibe-rules --skill app-studio-demo-capture

简介

用于录制已部署的 Domo App Studio 应用的真实演示视频,捕获完整用户体验流程。

  • 基于 Playwright 实现浏览器自动化,记录导航、滚动与交互过程。
  • 输出包含左侧导航、原生卡片与主题样式的生产级画面。
  • 非 Remotion 类组件渲染方案,不适用于合成数据动画场景。
  • 需在应用部署后运行,并确保认证状态正常以访问目标页面。

SKILL.md

App Studio Demo Capture

What this is: A Playwright-based pipeline that opens a real Domo App Studio app in an authenticated headless browser, navigates through pages, scrolls, and records a polished demo video. Captures the full production experience — left-nav, native cards, pro-code iframes, applied theme, live data. What this is NOT: This is not Remotion. It does not render React components in Node.js or use sample data. If you need component-level rendering with synthetic data and animated cursors, use basic-custom-app-build-w-video instead.

When to Use

  • After deploying an App Studio app (via app-studio + app-studio-pro-code pipeline) and you want a walkthrough video
  • When the user says "demo", "walkthrough", "animated preview", "record the app", or "show me what it looks like"
  • When generating eval screenshots for comparison or documentation
  • When producing content for pitch decks, stakeholder reviews, or marketing

Prerequisites

  1. App is deployed — the App Studio app exists on the Domo instance with all pages, cards, themes, and data
  2. Playwright installednpm install playwright in the project directory
  3. Domo authentication — a valid session ID obtainable via domo login or the upload_bridge.get_sid() helper
  4. ffmpeg installed (for post-processing) — brew install ffmpeg on macOS

Pipeline Overview

┌─────────────┐     ┌──────────────┐     ┌───────────────┐     ┌──────────────┐
│ Authenticate │ ──▶ │ Capture      │ ──▶ │ Post-process  │ ──▶ │ Output       │
│ (get SID)    │     │ (Playwright) │     │ (ffmpeg)      │     │ (.mp4/.gif)  │
└─────────────┘     └──────────────┘     └───────────────┘     └──────────────┘

Three capture modes, in order of complexity:

ModeMethodOutputBest for
Screenshotspage.screenshot() per pagePNGsStatic eval, deck slides, documentation
Scroll videoPlaywright recordVideo + scripted scrollRaw MP4 per pageSmooth page walkthroughs
Full walkthroughPlaywright recordVideo + nav + scroll across all pagesSingle MP4Polished demo videos

Mode 1: Multi-Page Screenshots (Proven Pattern)

This is the existing screenshot.js pattern — battle-tested across dozens of App Studio apps.

Usage

node screenshot.js app <appId> '<json-pageIds>' [outputDir]

Example

node screenshot.js app 396231467 '{"Overview":"1942764543","Sales":"1942764544","Inventory":"1942764545","Performance":"1942764546"}' ./demo-output/screenshots

Integration in Python build scripts

import subprocess, json

page_ids = {"Overview": str(overview_id), "Sales": str(sales_id), ...}
subprocess.run([
    "node", "screenshot.js", "app", str(app_id),
    json.dumps(page_ids), f"./{app_name}/screenshots"
], check=True)

Key implementation details (from proven screenshot.js)

  • Viewport: 1440x900 with deviceScaleFactor: 2 (produces 2880x1800 retina PNGs)
  • Auth: Inject X-Domo-Authentication header via page.route('**/*') interception
  • Wait strategy: waitUntil: 'networkidle' + additional waitForTimeout(8000) for pro-code iframes to render
  • Full page: fullPage: true captures content below the fold
  • Error recovery: On navigation failure, capture an _error.png for debugging

Also capture supporting assets

# Dataset details pages
node screenshot.js datasets '{"sales":"guid1","inventory":"guid2"}' ./demo-output/datasets

# Magic ETL dataflow graph
node screenshot.js etl <dataflowId> ./demo-output/etl

# Arbitrary Domo URLs
node screenshot.js urls '{"landing":"https://modocorp.domo.com/page/123"}' ./demo-output/urls

Mode 2: Per-Page Scroll Video

Record a smooth scroll-through of each page individually. Produces one MP4 per page.

Script: demo-capture.js

const { chromium } = require('playwright');
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');

const INSTANCE = 'modocorp';

function getSid() {
  const result = execSync(
    `cd "${path.join(__dirname, 'domo_data_generator')}" && python3 -c "from upload_bridge import get_sid; print(get_sid('${INSTANCE}'));"`,
    { encoding: 'utf-8' }
  ).trim();
  return result.split('\n').pop().trim();
}

async function capturePageVideo(browser, sid, appId, pageName, pageId, outputDir, options = {}) {
  const {
    scrollDistance = 600,
    scrollStepPx = 2,
    scrollIntervalMs = 16,
    holdTopMs = 3000,
    holdBottomMs = 2000,
    loadWaitMs = 8000,
  } = options;

  const videoDir = path.join(outputDir, '_raw_video');
  fs.mkdirSync(videoDir, { recursive: true });

  const context = await browser.newContext({
    viewport: { width: 1440, height: 900 },
    recordVideo: { dir: videoDir, size: { width: 1440, height: 900 } },
  });

  const page = await context.newPage();

  await page.route('**/*', (route, request) => {
    const url = request.url();
    if (url.includes('.domo.com') || url.includes('domoapps.')) {
      route.continue({ headers: { ...request.headers(), 'X-Domo-Authentication': sid } });
    } else {
      route.continue();
    }
  });

  const url = `https://${INSTANCE}.domo.com/app-studio/${appId}/pages/${pageId}`;
  console.log(`  Recording ${pageName}: ${url}`);

  await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 });
  await page.waitForTimeout(loadWaitMs);

  // Hold at top
  await page.waitForTimeout(holdTopMs);

  // Smooth scroll down
  const scrollSteps = Math.ceil(scrollDistance / scrollStepPx);
  for (let i = 0; i < scrollSteps; i++) {
    await page.evaluate((px) => window.scrollBy(0, px), scrollStepPx);
    await page.waitForTimeout(scrollIntervalMs);
  }

  // Hold at bottom
  await page.waitForTimeout(holdBottomMs);

  // Close context to finalize video
  const videoPath = await page.video().path();
  await context.close();

  // Move to final location
  const finalPath = path.join(outputDir, `${pageName}.webm`);
  fs.renameSync(videoPath, finalPath);
  console.log(`  Saved: ${finalPath}`);

  return finalPath;
}

async function captureAllPages(appId, pageIds, outputDir, options = {}) {
  fs.mkdirSync(outputDir, { recursive: true });
  const sid = getSid();
  console.log(`SID obtained (${sid.substring(0, 12)}...)`);

  const browser = await chromium.launch({ headless: true });
  const videoPaths = [];

  for (const [pageName, pageId] of Object.entries(pageIds)) {
    const vp = await capturePageVideo(browser, sid, appId, pageName, pageId, outputDir, options);
    videoPaths.push(vp);
  }

  await browser.close();
  console.log('All pages recorded.');
  return videoPaths;
}

// CLI
const args = process.argv.slice(2);
if (args.length < 2) {
  console.log('Usage: node demo-capture.js <appId> <json-pageIds> [outputDir] [scrollDistance]');
  process.exit(1);
}

const appId = args[0];
const pageIds = JSON.parse(args[1]);
const outputDir = args[2] || path.join(__dirname, 'demo-output', `app-${appId}`);
const scrollDistance = args[3] ? parseInt(args[3]) : 600;

captureAllPages(appId, pageIds, outputDir, { scrollDistance })
  .catch(err => { console.error('Fatal:', err); process.exit(1); });

Usage

node demo-capture.js 396231467 '{"Overview":"1942764543","Sales":"1942764544"}' ./demo-output 800

Mode 3: Full Walkthrough Video

Record a single continuous video that navigates through all pages — simulating a user clicking through the left-nav. This produces the most polished output.

Script: demo-walkthrough.js

const { chromium } = require('playwright');
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');

const INSTANCE = 'modocorp';

function getSid() {
  const result = execSync(
    `cd "${path.join(__dirname, 'domo_data_generator')}" && python3 -c "from upload_bridge import get_sid; print(get_sid('${INSTANCE}'));"`,
    { encoding: 'utf-8' }
  ).trim();
  return result.split('\n').pop().trim();
}

async function recordWalkthrough(appId, pageIds, outputDir, options = {}) {
  const {
    scrollDistance = 600,
    scrollStepPx = 2,
    scrollIntervalMs = 16,
    holdPageMs = 3000,
    loadWaitMs = 8000,
    transitionPauseMs = 2000,
  } = options;

  fs.mkdirSync(outputDir, { recursive: true });
  const videoDir = path.join(outputDir, '_raw_video');
  fs.mkdirSync(videoDir, { recursive: true });

  const sid = getSid();
  console.log(`SID obtained (${sid.substring(0, 12)}...)`);

  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    viewport: { width: 1440, height: 900 },
    recordVideo: { dir: videoDir, size: { width: 1440, height: 900 } },
  });

  const page = await context.newPage();
  await page.route('**/*', (route, request) => {
    const url = request.url();
    if (url.includes('.domo.com') || url.includes('domoapps.')) {
      route.continue({ headers: { ...request.headers(), 'X-Domo-Authentication': sid } });
    } else {
      route.continue();
    }
  });

  const entries = Object.entries(pageIds);
  console.log(`Recording walkthrough: ${entries.length} pages`);

  for (let i = 0; i < entries.length; i++) {
    const [pageName, pageId] = entries[i];
    const url = `https://${INSTANCE}.domo.com/app-studio/${appId}/pages/${pageId}`;
    console.log(`  Page ${i + 1}/${entries.length}: ${pageName}`);

    await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 });
    await page.waitForTimeout(i === 0 ? loadWaitMs : loadWaitMs / 2);

    // Hold at top
    await page.waitForTimeout(holdPageMs);

    // Smooth scroll
    const scrollSteps = Math.ceil(scrollDistance / scrollStepPx);
    for (let s = 0; s < scrollSteps; s++) {
      await page.evaluate((px) => window.scrollBy(0, px), scrollStepPx);
      await page.waitForTimeout(scrollIntervalMs);
    }

    // Hold at scroll position
    await page.waitForTimeout(holdPageMs / 2);

    // Scroll back to top before navigating to next page
    await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' }));
    await page.waitForTimeout(transitionPauseMs);
  }

  const videoPath = await page.video().path();
  await context.close();
  await browser.close();

  // Move raw video to output
  const rawPath = path.join(outputDir, 'walkthrough_raw.webm');
  fs.renameSync(videoPath, rawPath);
  console.log(`Raw walkthrough: ${rawPath}`);

  return rawPath;
}

// CLI
const args = process.argv.slice(2);
if (args.length < 2) {
  console.log('Usage: node demo-walkthrough.js <appId> <json-pageIds> [outputDir]');
  process.exit(1);
}

recordWalkthrough(args[0], JSON.parse(args[1]), args[2] || `./demo-output/app-${args[0]}`)
  .catch(err => { console.error('Fatal:', err); process.exit(1); });

Usage

node demo-walkthrough.js 396231467 \
  '{"Overview":"1942764543","Sales":"1942764544","Inventory":"1942764545","Performance":"1942764546"}' \
  ./demo-output

Post-Processing with ffmpeg

Convert WebM to MP4

Playwright records in WebM (VP8). Convert to MP4 (H.264) for universal compatibility:

ffmpeg -i walkthrough_raw.webm -c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p -movflags +faststart walkthrough.mp4

Stitch per-page videos into one

If using Mode 2 (per-page videos), concatenate them:

# Create concat list
echo "file 'Overview.webm'" > concat.txt
echo "file 'Sales.webm'" >> concat.txt
echo "file 'Inventory.webm'" >> concat.txt
echo "file 'Performance.webm'" >> concat.txt

# Concatenate and convert
ffmpeg -f concat -safe 0 -i concat.txt -c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p demo.mp4

Add crossfade transitions between pages

For a polished result with 0.5s crossfades between page clips:

ffmpeg \
  -i Overview.webm -i Sales.webm -i Inventory.webm -i Performance.webm \
  -filter_complex "\
    [0:v]setpts=PTS-STARTPTS[v0]; \
    [1:v]setpts=PTS-STARTPTS[v1]; \
    [2:v]setpts=PTS-STARTPTS[v2]; \
    [3:v]setpts=PTS-STARTPTS[v3]; \
    [v0][v1]xfade=transition=fade:duration=0.5:offset=4[x01]; \
    [x01][v2]xfade=transition=fade:duration=0.5:offset=8[x02]; \
    [x02][v3]xfade=transition=fade:duration=0.5:offset=12[out]" \
  -map "[out]" -c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p demo.mp4

Adjust offset values based on each clip's duration. The offset is the timestamp (in seconds) where the crossfade begins.

Trim to exact duration

ffmpeg -i demo.mp4 -t 30 -c copy demo_30s.mp4

Add intro/outro title cards

Create a 2-second title card from a PNG:

# Generate title card video from image
ffmpeg -loop 1 -i title.png -c:v libx264 -t 2 -pix_fmt yuv420p -vf "scale=1440:900" title.mp4

# Prepend to walkthrough
ffmpeg -f concat -safe 0 -i <(echo -e "file 'title.mp4'\nfile 'walkthrough.mp4'") -c copy final.mp4

Generate animated GIF (for Slack/docs)

ffmpeg -i walkthrough.mp4 -vf "fps=12,scale=720:-1:flags=lanczos" -loop 0 demo.gif

Timing Recommendations

Page contentholdPageMsscrollDistanceloadWaitMs
KPI row + chart (typical)30004008000
Dense dashboard (many cards)400080010000
Single chart (pro-code)25002008000
Data table30006006000
First page (cold load)300060010000

Total video duration estimates

PagesPer-page holdScroll timeTransitionsTotal
4~5s each~3s each~2s each~38s
4 (trimmed)~4s each~2s each~1s each~28s
6~4s each~2s each~1s each~42s

Target 25-35 seconds for a polished demo. Under 20s feels rushed; over 45s loses attention.


Integration with Build Pipeline

After build_app.py completes (Steps 1-8 from app-build-process-summary), add a capture step:

import subprocess, json

# Step 9: Capture demo
page_ids = {
    "Overview": str(overview_page_id),
    "Sales": str(sales_page_id),
    "Inventory": str(inventory_page_id),
    "Performance": str(performance_page_id),
}

# Screenshots (always — for eval and deck slides)
subprocess.run([
    "node", "screenshot.js", "app", str(app_id),
    json.dumps(page_ids), f"./{app_name}/screenshots"
], check=True)

# Animated walkthrough video
subprocess.run([
    "node", "demo-walkthrough.js", str(app_id),
    json.dumps(page_ids), f"./{app_name}/demo"
], check=True)

# Post-process to MP4
subprocess.run([
    "ffmpeg", "-i", f"./{app_name}/demo/walkthrough_raw.webm",
    "-c:v", "libx264", "-preset", "slow", "-crf", "18",
    "-pix_fmt", "yuv420p", "-movflags", "+faststart",
    f"./{app_name}/demo/walkthrough.mp4"
], check=True)

Comparison with basic-custom-app-build-w-video

Aspectapp-studio-demo-capture (this skill)basic-custom-app-build-w-video
CapturesReal deployed App Studio app in browserReact components in Remotion
DataLive Domo dataSynthetic TypeScript arrays
ChromeFull App Studio (nav, layout, cards, theme)Component only (no nav/layout)
AuthDomo session ID requiredNo auth needed
ToolingPlaywright + ffmpegRemotion + @remotion/cli
Code changesNone — captures any deployed appRequires Remotion-safe styling constraints
Cursor animationNot included (real browser cursor)AnimatedCursor component
Best forApp Studio apps, eval screenshots, stakeholder demosStandalone custom apps, product marketing

Use both together: basic-custom-app-build-w-video for component-level hero shots with animated cursor, app-studio-demo-capture for the full-chrome production walkthrough. Stitch both into a final video with ffmpeg.


Troubleshooting

SymptomCauseFix
Pro-code cards show blank/whiteiframes haven't loaded yetIncrease loadWaitMs to 10000-12000
Charts missing dataData query still in flightIncrease loadWaitMs; add page.waitForSelector('.recharts-wrapper')
Left-nav collapsed/hiddenViewport too narrowEnsure viewport width >= 1280
Auth redirect to login pageExpired SIDRe-run domo login; re-obtain SID
Video is choppy/laggyHeadless rendering under loadUse --disable-gpu flag; reduce scrollStepPx to 1
WebM won't playPlayer doesn't support VP8Convert to MP4 with ffmpeg
Screenshots too largedeviceScaleFactor: 2 + fullPage: trueUse deviceScaleFactor: 1 for video; keep 2x for screenshots

Output Checklist

After running the pipeline, verify:

  • Screenshots exist for every page (no _error.png files)
  • Pro-code banners and charts are visible (not blank)
  • Theme is applied (correct colors, fonts, nav styling)
  • Video is smooth (no frame drops or loading spinners captured)
  • Video duration is 25-35s
  • MP4 plays in QuickTime/VLC (H.264 baseline profile)
  • Left-nav is visible and shows correct page names/icons
  • Data is populated (charts have lines/bars, KPIs have values)

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Codex

35.3%
按下载量换算94

Claude

29.27%
按下载量换算78

Cursor

19.07%
按下载量换算51

Gemini CLI

10.19%
按下载量换算27

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

操作浏览器

该 Skill 可能涉及浏览器控制能力,使用时可能读取或操作网页内容,需要在受控环境中确认权限边界。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills