Demo Browser
演示用浏览器。基于 Electron + Chromium,把网页演示拆成 Stage(给观众看)和 Recorder(固定规格录制),不 fork 浏览器内核; 演示流程用场景脚本编排,配 Director 控制台与本地控制 API。
当前仓库只做技术验证:无头录制、CDP 物理交互、覆盖层、1080p30 出片、证据文件、
最小 .app。
快速开始
前提:Node ≥ 22.12(不含 23 行;本仓在 Node 24 上开发与测试)。
package.json 的 engines 已声明同一范围,版本不满足时包管理器在安装阶段就给出
明确提示(yarn 直接失败;npm / pnpm 默认打印 EBADENGINE 警告、engine-strict
下失败),不会拖到 vite / vitest 深处才炸出难懂的错误。
npm install
npm run build
# 起 fixture 静态服务(另开一个终端)
python3 -m http.server 4788 --bind 127.0.0.1 --directory out/fixture
# 默认:无头后台录制,10 秒
npx electron . --mode=record --seconds=10 --out=evidence --label=hidden
默认录制不显示窗口、不抢焦点、不占 Dock(macOS 下 setActivationPolicy('accessory')),
可以一边录一边继续用电脑。
命令
| 命令 | 作用 |
|---|---|
npm run build |
编译主进程(tsc)和 fixture(vite) |
npm start |
打开 Director 控制台 + Stage(Stage 以 inactive 显示,不抢焦点) |
npm run start:control |
同上,并启动本地控制 API(http://127.0.0.1:7317) |
npx electron . --mode=stage |
可见 Stage,手动体验页面 |
npx electron . --mode=record --seconds=10 |
无头录制(默认) |
npx electron . --mode=record --window-mode=inactive --seconds=10 |
可见但不抢焦点录制 |
npx electron . --mode=record --plain --seconds=10 |
不做交互,只录静态页(对照用) |
npx electron . --mode=app --autorun=report-review --scenes=fixtures/scenes |
无头跑完整场景并出片 |
npx electron . --mode=app --director-check --scenes=fixtures/scenes |
隐形校验 Director 与 preload 桥接 |
npx electron . --mode=app --headless=true --control-api=true |
全无窗口后台实例,只留控制 API 与录制 |
npm test |
单元测试(Scene / Runner / Security / Timeline) |
npm run package |
打 dist/mac-arm64/Demo Browser.app |
./scripts/run-phase0.sh |
跑 Phase 0 主证据(hidden + inactive) |
常用参数:--url、--width、--height、--out、--label、--keep-frames、
--window-mode=hidden|inactive|offscreen|ghost|focused、--focus-emulation=false、
--control-api=true、--control-port、--headless=true。
本地控制 API 只绑回环地址,启动时生成随机 token 并写到 <out>/control.json(0600),
接口与 curl 示例见 docs/control-api.md。
窗口模式(Phase 0 实测)
| 模式 | 行为 | 可录制 | 可交互 |
|---|---|---|---|
hidden(默认,带 focus emulation) |
不显示窗口 | 是 | 是 |
hidden + --focus-emulation=false |
不显示窗口 | 是 | 否,点击会被丢弃 |
inactive |
显示窗口但不抢焦点 | 是 | 是 |
offscreen |
显示到屏幕外 | 是 | 是 |
ghost |
显示但透明度 0 | 是 | 是 |
无头模式必须配合 Emulation.setFocusEmulationEnabled,否则 CDP 物理点击不会生效。
输出
每次录制落两份文件:
evidence/
hidden.mp4
hidden.evidence.json # 版本、视口、DPR、帧数、时长、SHA256、步骤、自检结果
hidden.overlay-frame.png # 覆盖层校验帧(--plain 时不生成)
自检全过才返回 0:窗口无 Node、帧到达、时长误差 ≤0.5s、30fps、1920x1080、 黑帧占比 <50%、覆盖层真的进了视频。
结构
src/main/
index.ts # 启动、退出码、全局失败兜底
cli.ts # 参数
app.ts # app 模式装配:Scene 服务、Director、IPC
app-controller.ts # 状态快照、场景加载、运行、录制
ipc.ts # Director 通道与控制命令
control-dispatch.ts # Director IPC 与本地 API 共用的命令分发
control/ # 本地 HTTP 控制 API(回环 + token + SSE)
stage-layout.ts # 场景尺寸/安全策略/访问顺序的纯计算
demo-protocol.ts # demo:// 场景协议(按 session 注册)
electron-page-driver.ts # PageDriver 的 Electron 实现(CDP 物理事件)
electron-overlay.ts # OverlayPort:光标/高亮/聚光/标注/遮罩/黑屏
scenes/ # Zod 校验、Scene 加载、变量插值、脚本 hash
runner/ # 状态机、18 个动作、目标解析
capture/ # screencast 抓帧、30fps 时间轴、ffmpeg、evidence
security/ # 白名单、路径安全、权限与下载策略
windows/ # Stage(BaseWindow+WebContentsView)、Director
phase0.ts # Phase 0 冒烟脚本(保留)
src/renderer/director # 讲解人控制台(React + lucide)
src/preload/ # Director 桥接(沙箱可用)
src/fixture/ # React fixture:计数按钮、搜索、风险行、动画
fixtures/scenes/ # 可直接加载的场景(report-review)
scripts/
run-phase0.sh # 主证据复现
baseline_chrome.py # 无头 Chrome 对照(复用 recorder-pipe Recorder)
compare.cjs # SSIM 对比
场景(Phase 1)
fixtures/scenes/report-review/scene.yaml 是当前可用场景:7 步,包含
wait_for → highlight → click → fill → assert → spotlight → annotate。
本地页面通过 demo://scene/report-review/index.html 加载,只允许读写场景目录内的文件。
fixtures/scenes/multi-a → multi-b 验证多场景切换:1280×720 切到 1600×900,
同一路 screencast 不断流,evidence 里记 run.scene_ids。
Scene 层校验一次报全所有错误;未知字段、越界时间、缺目标都会在加载前失败。
录制产物落在 <out>/recordings/<时间戳>_<场景>/take-01.mp4 和同名 .evidence.json。
字段契约见 schemas/scene.schema.json。
目标解析(步骤怎么找到元素)
带目标的动作按固定顺序解析,先命中先用:
| 优先级 | 写法 | 解析为 |
|---|---|---|
| 1 | data_demo_id: increment |
[data-demo-id="increment"] |
| 2 | selector: ".btn-primary" |
原样 CSS 选择器 |
| 3 | text: 金额异常 |
文本匹配(规则见下) |
| 4 | x: 640 + y: 360 |
视口坐标点(无选择器,兜底用) |
文本匹配在页面内找可见(非隐藏且有尺寸)且文本包含目标串的最小元素,
命中后打上运行期属性供该步后续命令复用;找不到报 E_SELECTOR_NOT_FOUND,
四层都没写则报「步骤没有可解析的点击或视觉目标」。
两个特例里 text 不当目标用:
fill的text是要输入的内容,目标另写selector/data_demo_id/ 坐标, 或用target_text指定要点的元素文本;annotate的text是标注文案,目标必须是selector/data_demo_id/ 坐标。
另外 wait_for 的 text 是等待条件(等文本出现),不是点击目标。
界面与主进程通过 director:get-snapshot / director:send / director:event 通信,
缺少桥接时 Director 会显示 MOCK 标记,不会把假数据当成真实会话。
失败行为
- 找不到 ffmpeg、CDP 附加失败、选择器找不到、断言失败、没有帧、编码失败都非零退出。
- 主进程未捕获异常直接打印并退出,不弹系统错误框(无头场景会静默阻塞)。
- 录制失败保留失败 evidence;
--keep-frames额外保留原始帧。
退出码稳定,CI 可按码分支处理:
| 退出码 | 含义 |
|---|---|
| 0 | 成功:自检全过(autorun 成功时 stdout 另输出单行 JSON 快照) |
| 2 | 参数错误,或通用失败(页面加载失败、E_SELECTOR_NOT_FOUND、断言失败、CDP 附加失败、场景校验失败、端口被占用等) |
| 3 | ffmpeg/ffprobe 缺失或执行失败(E_FFMPEG_MISSING / E_FFMPEG_FAILED,先安装 ffmpeg) |
| 4 | 录制器失败(E_RECORDER_NO_FRAMES 等采帧/时间轴错误) |
详细结论见 docs/phase0-report.md、docs/phase1.md、 docs/phase1b.md。
No comments yet. Be the first to write one.