@1game/cli-1gameplay
v1.21.0
Published
1Game gameplay recording CLI
Readme
@1game/cli-1gameplay(1gameplay)
@1game/cli-1gameplay 是 1Game 的无头录制与回放工具包,命令名为 1gameplay。
日常入口是
1gameplay命令。本包同时导出给@1game/test使用的 JS API(addFrameComments/listFrameComments/bundleReplayHtml等);不承诺其它内部模块路径稳定。
本 README 面向使用者,说明如何构建、如何执行常用命令、以及常见错误处理方式。
快速开始
在仓库根目录安装依赖:
pnpm install
构建 CLI 包:
pnpm run build(工作目录:packages-public/cli-1gameplay)
查看帮助:
node ./bin/1gameplay.mjs --help(工作目录:packages-public/cli-1gameplay)
说明:命令入口位于
bin/1gameplay.mjs,会调用dist/cli.mjs导出的runCli。
归档格式说明(v2)
- 唯一形态:单个 SQLite 文件,扩展名为
.1gamerecord(例如out/demo.1gamerecord)。 - 存储:符合 SQLite Archive (sqlar) 的
sqlar表存放bundle/game.worker.js与bundle/worker-bootstrap.js;结构化表record_meta、frames(DAG)、record_branches(refs)、events、store_rows、kv_store/kv_store_journal、runtime_logs(可选,step --capture-console采集的 console 调试元数据)、runtime_perf(可选,step --perf summary写一行1gameplay.perf.summary/1;frames|trace写逐帧1gameplay.perf.frame/1)等存放录制与元数据(不再使用目录 + ndjson 主存储)。Schema 版本见GAMEPLAY_SCHEMA_VERSION。 - 破坏性变更:v2 不再支持旧「目录归档」;传入目录会得到
CLI_BAD_ARGS(见错误提示)。不提供自动迁移命令。 frame query|diff|batch使用 frame protocol:stdout 固定为 JSON envelope(成功1gameplay.frame,失败1gameplay.error),并按错误类型映射退出码(见下文)。frame screenshot成功为1gameplay.screenshot.v1;frame paint成功为1gameplay.paint.snapshot/1。
与 1game init 默认约定的配合:1game init 生成的项目将构建输出目录设为 out/,并在 .gitignore 中忽略 out/、*.1gamerecord 与 *.1gamerecord.swp。下文示例将录制文件与回放 HTML 也放在 out/ 下,便于与浏览器构建产物一起被忽略、避免误提交。
--dry-run / .swp 试跑会话
用途:在不改主档的前提下做完整 step 试跑(可多帧追加);其它命令加 --dry-run 时读写 sidecar <archive>.swp。
规则:
- 无
--dry-run→ 只碰主档;主档读取不受.swp影响 - 有
--dry-run→ 只碰.swp - 仅
step --dry-run会在.swp缺失时从主档复制创建;其它命令要求.swp已存在 - 存在
.swp时,无--dry-run的主档写入硬失败(含create --out同一路径;create本身不支持--dry-run) - 不提供 commit/abort 子命令:采纳
mv <archive>.swp <archive>,丢弃rm <archive>.swp
step --dry-run 会把真实 step 提交进 sidecar(可 --from-frame 截断暂存时间线),不是进程内假 tick。观察用 frame query --dry-run / frame paint --dry-run。
示例:
1gameplay step out/demo.1gamerecord --dry-run --ms 16 --event '...'
1gameplay frame query out/demo.1gamerecord --dry-run --at last --select store:state
1gameplay step out/demo.1gamerecord --dry-run --ms 16 --repeat 2
1gameplay step out/demo.1gamerecord --ms 16 # 有 .swp 时失败
mv out/demo.1gamerecord.swp out/demo.1gamerecord命令总览
1gameplay create --entry <path> --out <file.1gamerecord>
1gameplay branch create <file.1gamerecord> [--dry-run] --name <语义化分支名> [--at <seq|last>] [--from-branch <id>]
1gameplay step <file.1gamerecord> [--dry-run] [--ms <n>=100] [--repeat <n>] [--until <pred>] [--until-not] [--until-ms <n>] [--event <json>] [--event-file <path>] [--from-frame <seq>] [--branch <id>] [--perf off|summary|frames|trace] [--perf-max-frames <n>] [--surface record|display] [--flush] [--capture-console off|error|warn|info|log|debug|all] [--console-max-per-step <n>] [--console-max-bytes <n>]
1gameplay obs report <file.1gamerecord> [--profile lab.worker|lab.display|lab.paint|device.budget] [--at last|spike|<seq>] [--out report.json]
1gameplay comments add <file.1gamerecord> [--dry-run] --at <seq|last> --body <text> [--author-id <id>] [--anchor-json <json>] [--comment-id <id>]
1gameplay frames list <file.1gamerecord> [--dry-run] [--from <n>=0] [--to <n>=0]
1gameplay frame query <file.1gamerecord> [--dry-run] --at <seq|last> --select <expr>... [--payload summary|full]
1gameplay frame diff <file.1gamerecord> [--dry-run] --from <seq> --to <seq|last> --select <expr>... [--payload summary|full]
1gameplay frame batch <file.1gamerecord> [--dry-run] --plan-file <path> [--payload summary|full]
1gameplay frame screenshot <file.1gamerecord> [--dry-run] --at <seq|last> --out <file.png|jpg> [--scene-stable-uid <stableUid>] [--width <n>] [--height <n>] [--dpr <n>] [--format png|jpeg] [--quality <1-100>]
1gameplay frame paint <file.1gamerecord> [--dry-run] --at <seq|last> [--scene-stable-uid <stableUid>] [--width <n>] [--height <n>] [--dpr <n>] [--top <0-20>]
1gameplay bundle-player-html <file.1gamerecord> [--dry-run] --out <replay.html> [--title <s>] [--single-html|--multi-file]
1gameplay frames delete <file.1gamerecord> [--dry-run] (--ranges <spec> | --last <n>) [--compact]
1gameplay record extract <file.1gamerecord> [--dry-run] --out <file.1gamerecord> (--from-seq <n> --to-seq <n> | --from-ms <n> --to-ms <n>) [--merge-diff [n]] [--branch-meta]bundle-player-html 默认单文件会把归档 gzip 后再 base64 写入 recordBase64(并带 recordEncoding: "gzip")。
使用说明
1) 创建归档:create
用途:通过 1game build --target worker --with-record 将入口构建成 game.worker.js(仅用户代码 + virtual solid re-export shim;@1game/engine-bundle/runtime/* 通过 externalization 在运行时按 ./engine/*.mjs 或 CDN URL 加载,详见 docs/engine-cdn-resolution.md),并初始化 .1gamerecord 文件(record_meta.bundleFormat 恒为 buildjs-worker,bundle 写入 sqlar)。本地模式下引擎运行时文件被同步捕获到 bundle/engine/*;CDN 模式(ONEGAME_ENGINE_CDN_BASE)下仅 game.worker.js 入档(engineExternal.files 仍记录引用的 runtime basename),Node 无头运行时优先用版本严格匹配的本地 @1game/engine-bundle,否则再从 CDN 拉取。
录制运行时:归档内的
game.worker.js必须包含@1game/engine-bundle/runtime/record(sql.js / 回放 RPC)导入。create会自动传入--with-record。后续step/frame/bundle-player-html/@1game/test openGame都依赖该 worker;手工1game build的纯游玩包默认不含 record,不能直接拿去当归档 worker。
示例:
1gameplay create --entry src/game.tsx --out out/demo.1gamerecord
create 成功输出会附带 sceneStableUids(当前所有 scene 的 stableUid)。单 scene 时后续 --click / hit:point=x,y / --event 可省略 uid;多个 scene 正在渲染时省略会失败,须复制 uid 或传 --scene-stable-uid。
关键参数:
--entry:游戏入口文件路径(必填)--out:输出.1gamerecord文件路径(必填)。若同路径已有<archive>.swp,创建会失败(先mv/rm暂存)。
成功输出:stdout 一行 JSON,包含 ok、archivePath、frameCount,以及 bundle.sha256。
创建分支:branch create
在指定帧 fork 一条新 ref(不复制帧)。必须 --name 语义化分支名(不可与已有 label 重复,也不能写成生成 branchId)。省略 --at 时在父分支 HEAD fork;--from-branch 默认档案 activeBranchId。成功后该新 ref 成为 active,后续 step 在其上 append。
1gameplay branch create out/demo.1gamerecord --name 探活岔路1gameplay branch create out/demo.1gamerecord --name 探活岔路 --at 12 --from-branch main
2) 继续步进:step
用途:在已有 .1gamerecord 上继续执行逻辑帧,可注入事件。
示例:
1gameplay step out/demo.1gamerecord --ms 16.6661gameplay step out/demo.1gamerecord(不传--ms时默认100)1gameplay step out/demo.1gamerecord --ms 16 --repeat 101gameplay step out/demo.1gamerecord --ms 16 --until store:state:anim.phase=idle --until-ms 8001gameplay step out/demo.1gamerecord --click 100,1001gameplay step out/demo.1gamerecord --ms 16 --event '{"type":"hover","sceneStableUid":"<sceneStableUid>","data":{"id":1,"x":1,"y":2,"time":0,"domClientX":1,"domClientY":2}}'1gameplay step out/demo.1gamerecord --event '{"type":"click","sceneStableUid":"<sceneStableUid>","data":{"x":100,"y":100,"ms":200}}'1gameplay step out/demo.1gamerecord --event '{"type":"swipe","sceneStableUid":"<sceneStableUid>","data":{"from":{"x":144,"y":220},"to":{"x":144,"y":170},"ms":300}}'1gameplay step out/demo.1gamerecord --event '{"type":"keypress","sceneStableUid":"<sceneStableUid>","data":{"code":"ArrowLeft","ms":180}}'1gameplay step out/demo.1gamerecord --ms 16 --event '{"type":"pointer.down","sceneStableUid":"<sceneStableUid>","data":{"id":1,"x":100,"y":100}}'1gameplay step out/demo.1gamerecord --ms 16 --event '{"type":"pointer.move","sceneStableUid":"<sceneStableUid>","data":{"id":1,"x":116,"y":100}}'1gameplay step out/demo.1gamerecord --ms 16 --event '{"type":"pointer.up","sceneStableUid":"<sceneStableUid>","data":{"id":1,"x":116,"y":100}}'1gameplay step out/demo.1gamerecord --ms 16 --event-file ./events.json1gameplay step out/demo.1gamerecord --ms 16 --from-frame 2401gameplay step out/demo.1gamerecord --ms 16 --repeat 60 --perf summary1gameplay step out/demo.1gamerecord --ms 16 --repeat 10 --perf frames1gameplay step out/demo.1gamerecord --dry-run --ms 16 --repeat 2
关键参数:
--dry-run:可选,写入<archive>.swp(不存在则从主档复制创建);有.swp时无旗标写主档会失败(见上文「--dry-run/.swp」)--ms:每步毫秒数,需>= 0(可选,默认100);注入click/swipe/keypress动作宏时该参数不生效(见下文)。--until模式必须显式传入且> 0--repeat:开环步进次数,默认1;不能与--until同时使用--until:谓词停机(store:state:<dotted-path>=<scalar>或完整hit:point=/hit:sceneStableUid=:uid=)。每空 tick 后求值,真则停。--until-not取反。与--repeat互斥。--until-ms:until 空 tick 的仿真时间上限(累计--ms,不含动作宏微帧)。until 模式必填且> 0。hit until 需要--surface display。--click:x,y点击动作宏糖(data.ms默认 200;不能与--event/--event-file同时出现)。负坐标用--click -10,20或--click=-10,20。--scene-stable-uid:只给省略了sceneStableUid的注入事件填 uid(须为当前帧真实 scene uid);已写在 JSON 上的 uid 不改写;未省略时不校验该旗标。--event:单个事件 JSON 字符串--event-file:事件文件路径(单个对象或数组)- 省略
sceneStableUid时(--click与--event都适用)仅当当前帧恰好一个 scene 才填充。多 scene 必须显式 uid 或--scene-stable-uid。stdoutmeta.invokeSummary.eventSceneStableUids/defaultSceneStableUid带回实际 stamp。 - 事件类型支持 runtime 事件
touch|hover|keyboard|wheel|visibleChange;另支持瞬时宏事件keydown|keyup|pointer.down|pointer.move|pointer.up(CLI 会按统一 InputSnapshot 差分展开;键盘与触摸均支持跨step/from-frame恢复),以及跨多帧的动作宏click|swipe|keypress(见下文) --from-frame:从指定帧裁剪后续再继续执行(只删当前分支独占的后缀;其它 ref 占用该后缀则失败)--branch:在该record_branchesref 上 append(不 fork;缺省activeBranchId或main)
动作宏 click / swipe / keypress(跨多帧)
click、swipe、keypress 描述的是一个完整的多帧动作:data.ms 是这个动作从开始到结束经过的真实引擎时间,CLI 会自动拆成多帧分别注入并各自 tick,而不是把按下/抬起压缩进同一帧。单次 step 调用里,click/swipe/keypress 不能与其它事件混用(会拆分成多帧、与该次调用里的其它事件产生歧义),也不受该次调用的 --ms/--repeat(--repeat 仍生效,含义是"重复整个动作 N 次",但 --ms 被动作自身的 data.ms 取代):
click:data = { x, y, ms?=16, pressMs?=ms/2, id?, time?=0, domClientX?, domClientY? }。展开为 2 帧:第一帧注入按下并 tickpressMs;第二帧注入抬起并 tickms - pressMs(两帧 tick 之和精确等于ms)。swipe:data = { from:{x,y}, to:{x,y}, ms?=300, frames?, id?, time?=0 }。展开为frames帧(默认按ms/16估算并夹在[3, 60]之间,可用frames覆盖,允许范围[2, 60]):第一帧pointer.down@from,中间帧按位置线性插值pointer.move,最后一帧pointer.up@to,每帧deltaMs = ms/frames(余数计入最后一帧)。注意:frames=2(无中间pointer.move)不会触发引擎的hasGestureMoved/DragStart判定,只有位置直接跳变,一般不建议使用。keypress:data = { code, key?, shift?, meta?, alt?, ctrl?, ms?=16, pressMs?=ms/2, time?=0 }。展开为 2 帧,与click结构完全对称:第一帧注入keydown并 tickpressMs;第二帧注入keyup并 tickms - pressMs(两帧 tick 之和精确等于ms)。按住期间(两帧之间)InputSnapshot.keyboard中该code保持存在,可跨真实引擎帧被useFrame等逻辑观察到。
示例:
# 点击(单 scene 可省略 uid;--click 默认按住 200ms)
1gameplay step out/demo.1gamerecord --click 180,320
1gameplay step out/demo.1gamerecord --event '{"type":"click","data":{"x":180,"y":320,"ms":200}}'
# 滑动(300ms 内从 (144,220) 滑到 (144,170),默认约按 60fps 拆成多帧)
1gameplay step out/demo.1gamerecord --event '{"type":"swipe","sceneStableUid":"<sceneStableUid>","data":{"from":{"x":144,"y":220},"to":{"x":144,"y":170},"ms":300}}'
# 按键(等价于按住 ArrowLeft 180ms 后松开,产生 2 帧)
1gameplay step out/demo.1gamerecord --event '{"type":"keypress","sceneStableUid":"<sceneStableUid>","data":{"code":"ArrowLeft","ms":180}}'低层 pointer.down/pointer.move/pointer.up/touch/keydown/keyup 宏仍然保留,供需要精细控制(自定义 domClientX/Y、多指、跨命令分帧按住/移动/释放)的场景使用;click/swipe/keypress 只是构建在其上的高层封装。frame batch 不支持 click/swipe/keypress(会返回 INPUT_ACTION_MACRO_UNSUPPORTED),请改用底层宏。试跑不写主档请用 step --dry-run。
--capture-console:采集 Worker 内console到 SQLiteruntime_logs(off|error|warn|info|log|debug|all,默认warn)。采集为旁路元数据,不影响回放状态;原console仍会照常输出到进程 stderr。--console-max-per-step:每步最多保留的日志条数(默认50),超出部分丢弃并计入invokeSummary.consoleDroppedByLimit。--console-max-bytes:单条messageJSON 序列化后的最大字节数(默认4096),超出截断并标记truncated。--perf:Host Lab 性能采集(off|summary|frames|trace,默认off)。日常解读用obs report --profile(只读,不采样)。summary写入 stdoutmeta.perf.summary并落一行1gameplay.perf.summary/1(挂该次 lastSeq);frames/trace写逐帧1gameplay.perf.frame/1,不写 summary 行。旧--with-metrics已移除。含meta.captureSession的真机档禁止再--perf(硬拒发生在.swp之前;无源码请obs report --profile device.budget)。--perf-max-frames:截断 stdoutmeta.perf.frames条数(>= 0;不影响已写入runtime_perf的行)。--surface:Host Lab 面(record|display,默认record)。record关 publish;display开 publish。--flush把flushFrame等待记入host.flushWaitMs(不是浏览器 paint)。invokeSummary.surface回显record|display。
pointer.* 跨命令恢复的 stableUid 契约:
跨包冻结语义与 strict 默认行为以
../../docs/stable-uid-identity-contract.md为准。构建期为每个 DSL 节点注入内部
__sid,运行时派生全局唯一的stableUid(路径拼接)。一致性边界:
stableUid仅保证同一构建产物在多次运行中的节点一致;跨构建或跨源码改动后不保证保持同一 stableUid。输入快照中持久化
targetStableUid/downStableUid,跨step或--from-frame时用于回查节点。不支持跨源码位点(如
Show主分支与fallback)视为同一实体;scene vnode replacement 切换分支后,onClick/onDrag*语义不再保证连续。生成期冲突(生成重复 stableUid)属于硬错误:运行时直接抛
E_STABLE_UID_COLLISION(dev/prod 一致)。消费期 duplicate 去重策略默认值:非
production为strict,production为first-wins,可通过ONEGAME_DUPLICATE_STABLE_UID_STRATEGY覆盖(非法值回退默认)。ONEGAME_STEP_STORE_DUMP_FULL=1:step每帧强制全量debug.dumpStore(关闭 dumpRange watermark;用于排障 / A/B)。debug.dumpStore(RPC v9 硬切):返回值必须含meta(mode/fromIndex/length/pruneEpoch);includeCurrentState默认false(frame query --select store:state会显式传true)。无meta的旧 Worker 不再被接受。
成功输出:stdout 为 JSON envelope,schema 为 1gameplay.step,ok: true。meta.statePointer 含与 manifest.runtime 对齐的 lastSeq、frameCount、lastTickedTimeMs(便于后续 frame query --at、frames list 无需猜 --at)。meta.invokeSummary 含本次实际生效的 msEffective、repeatEffective(until 模式为 0)、until(开环为 null;until 成功含 met/ticks/simMs/lastValue)、是否 --from-frame 截断、注入的 --event / --event-file 数量、clickInjected、eventSceneStableUids、defaultSceneStableUid,以及 captureConsole、consoleCaptured、consoleDroppedByLimit、surface。启用 --perf 时额外含 meta.perf(summary 恒有,含 phases/counters/host/derived;frames/trace 另附 frames 数组)。
Host 解读:hostOverheadMs = frameWallMs − workerTotalMs(冻结)。storeDumpMs 高而 workerTotalMs 极低时,墙钟主导是 journal/host,不是仿真。
失败:step 使用与 frame 一致的 1gameplay.error 信封,并默认写入 stdout。退出码:2 参数(含 STEP_UNTIL_PATH_MISSING / STEP_UNTIL_PATH_INVALID / CAPTURE_SESSION_HOST_LAB_REFUSED);3 缺失路径等;4 记录/bundle 不兼容;5 回放运行时错误;6 STEP_UNTIL_NOT_MET(仿真预算用尽,谓词仍假;已步进的帧会提交)。
2b) 只读性能报告:obs report
用途:按剖面读取 Worker / 真机 Main rAF / paint-lab 三只时钟桶。不采样。信封 1gameplay.obs.report/1 只出现在 stdout / --out。
1gameplay obs report userplay.1gamerecord --profile device.budget --at spike
1gameplay obs report out/lab.1gamerecord --profile lab.worker
1gameplay obs report out/lab.1gamerecord --profile lab.paint --at last- Agent 必须显式
--profile(lab.worker|lab.display|lab.paint|device.budget)。CLI 默认:有captureSession→device.budget,否则lab.worker。 device.budget需要真机信封;无信封实验室档用lab.worker(须已有1gameplay.perf.*行)。- 有信封时 stdout
meta.captureSession与frame query同键(1game.capture.session/1);Main rAF 读buckets.main。缺档是NOT_FOUND,不要当成CAPTURE_SESSION_REQUIRED。 - 不要跨桶比较毫秒。可见超预算
rafSpikes须与 Worker seq / 点击互证。
3) 添加帧评论:comments add
用途:向已有 runtime 帧附加一条 frame_comments 记录(与 schemaVersion 仍为 3 的归档兼容)。
示例:
1gameplay comments add out/demo.1gamerecord --at last --body "checkpoint ok"(省略--author-id→authorId: null)1gameplay comments add out/demo.1gamerecord --at 12 --body "review" --author-id ai
关键参数:
--at:last或具体seq(必须是档案中存在的帧)--body:评论正文(必填)--author-id/--anchor-json/--comment-id:可选。Agent 钉锚点请传--author-id ai;回放页人手新建/改过的评论为user
成功输出:stdout 为 JSON envelope,schema 为 1gameplay.comment.v1,result.comment 为写入后的记录。
4) 列出帧:frames list
用途:以 JSON envelope 列出帧摘要。
示例:
1gameplay frames list out/demo.1gamerecord1gameplay frames list out/demo.1gamerecord --from 100 --to 160
参数约束:
--from/--to必须是>= 0的整数,且from <= to
成功输出:stdout 为 JSON envelope,schema 为 1gameplay.frames.list.v1。result.columns 固定为 seq|deltaMs|tickedTimeMs|events|storeCursor,result.rows 为范围内帧摘要数组。
5) 帧调试:frame query | diff | batch
用途:以稳定 JSON envelope 查询/对比/批处理;无隐式 fallback(缺失 event、缺失 store、缺失 scene 均报错)。
成功 1gameplay.frame 的 result 除 select 外,还包含 commentsBySeq:以帧 seq 字符串为键、该帧关联的 frame_comments 行数组(query 为单键;diff 为 --from 与 --to 两端;batch 每条成功任务的 result 各自携带,避免任务间混淆)。
--select DSL(可重复):
- 所有
sceneStableUid入参都必须传 render 输出里的scenes[].stableUid(或sceneStableUids[]);不再接受 scenename的隐式匹配。 - 只要本次
frame query的--select触发了 worker 回放(例如render、store:frame/store:state、hit:*),响应的meta.sceneStableUids就会自动带出当前所有 scene 的stableUid,省去单独再查一次render。仅用runtime/events/store:dump等无需回放的 lite 路径查询时不会附带(避免为此多启动一次 worker)。 runtime:该 seq 的 runtime 行events:该 seq 的事件列表logs:该 seq 的runtime_logs旁路行perf:该 seq 的runtime_perf旁路行(需先前step --perf frames|trace写入)store:dump:从记录文件的 store 表读行(不回放 worker)store:frame/store:state:store-only 架构约定下同样从 journal 纯计算得出(不回放 worker);仅当同一次查询还选择了render/hit/hitNode时才会随之启动 workerrender:全量 render 摘要(--payload full时返回完整对象)render:sceneStableUid=<sceneStableUid>:仅该 scene;scene 不存在则失败(sceneStableUid为 CLI 字段名兼容保留,值语义为 scene stableUid)hit:point=<x>,<y>[:sceneStableUid=<sceneStableUid>][:mode=clickable|any][:pick=top|all]:坐标命中测试。省略sceneStableUid时当前帧必须恰好一个 scene。返回{ stableUid, stableChain, sceneStableUid }(未命中时stableUid: null)hit:sceneStableUid=<sceneStableUid>:uid=<stableUid>[:shape=polygon|aabb][:space=scene|local][:include=...]:节点区域/可点击性快照;uid=*表示 scene 下首个非 scene 节点。stableUid片段格式为纯sid(静态唯一位点)或sid@k_<encodeURIComponent(String(key))>(列表 key);列表必须提供稳定 key。旧node=不再支持。
示例:
1gameplay frame query out/demo.1gamerecord --at last --select runtime --select store:dump1gameplay frame query out/demo.1gamerecord --at 1 --select perf1gameplay frame query out/demo.1gamerecord --at last --select 'hit:point=20,20:sceneStableUid=<sceneStableUid>'1gameplay frame query out/demo.1gamerecord --at last --select 'hit:sceneStableUid=<sceneStableUid>:uid=*:shape=aabb:include=meta,path,aabb'1gameplay frame diff out/demo.1gamerecord --from 0 --to last --select store:dump- 试跑不改主档:
1gameplay step out/demo.1gamerecord --dry-run --ms 16,再frame query out/demo.1gamerecord --dry-run --at last --select store:state 1gameplay frame batch out/demo.1gamerecord --plan-file ./plan.json
计划文件:schema 必须为 1gameplay.frame.plan,tasks[] 含 taskId、type(query | diff)及对应字段。
frame 子命令退出码:0 成功;2 参数/越界 seq;3 对象不存在;4 归档/bundle 兼容性;5 回放运行时错误。
6) 帧截图:frame screenshot
用途:在 Node 主线程用 @napi-rs/canvas 回放指定帧并写出 PNG/JPEG。不依赖 Chromium / Playwright。
示例:
1gameplay frame screenshot out/demo.1gamerecord --at 0 --out out/frame0.png1gameplay frame screenshot out/demo.1gamerecord --at last --out out/last.jpg --format jpeg1gameplay frame screenshot out/demo.1gamerecord --at last --out out/scene.png --scene-stable-uid <sceneStableUid>
关键参数:
--at:last或具体seq(必填)--out:输出图片路径(必填;扩展名可暗示格式)--scene-stable-uid:可选,仅截取该 scene--width/--height/--dpr:视口与设备像素比(默认800×600,dpr=1)--format:png|jpeg(默认由--out扩展名推断,否则png)--quality:JPEG 质量1–100(默认90;PNG 忽略)
成功输出:stdout 为 JSON envelope,schema 为 1gameplay.screenshot.v1,含 resolvedSeq、image.{format,width,height,dpr,bytes}、stats(可选 resourceWaitTimedOut)。
6b) 绘制快照:frame paint
用途:同一 display 宿主(Node @napi-rs/canvas)对指定 seq 做一次 Canvas 绘制采集。stdout JSON(1gameplay.paint.snapshot/1),不写 PNG。量 scene / byType / 截断 topNodes,不是 Host Lab step --perf,也不是浏览器 FPS。
示例:
1gameplay frame paint out/demo.1gamerecord --at last1gameplay frame paint out/demo.1gamerecord --at last --top 01gameplay frame paint out/demo.1gamerecord --at last --scene-stable-uid <sceneStableUid>
关键参数:--at 必填;禁止 --out;--top 默认 5(0–20,0 省略 topNodes);视口默认与 screenshot 相同(800×600,dpr=1)。
成功信封含 backend: "napi-canvas"、notFps: true、scenes[](overlay scene 的 name 以 overlay: 开头)、topNodes、stats.paintMs(各 scene 强制 getRenderedFrame 之和,不含 compositeMs / PNG encode)。stats.forcedRedraw 只表示 scene canvas 被强制重绘,不会打掉 render.drawCache;cache 子树计在 nodesCacheElided,不要当成 nodesCulled。stats.compositeMs 是 harvest 之后把已画好的 scene 帧 drawImage 到主画布,不是第二次 engine.doRender。不要和 meta.perf / captureSession.main.raf* 对比。禁止 --out / --select / --format / --quality / --payload。极简场景 selfMs 可能为 0。
默认中文字体:依赖 @fontpkg/source-han-sans-hw-sc(思源黑体 HW SC Regular + Bold),在 Node 主线程与 Worker 启动时注册;场景未配置 textMeasureFontFamily 时主线程默认使用 Source Han Sans HW SC。
已知差异(相对浏览器回放):
- 像素与浏览器 Canvas 不完全一致(字体、抗锯齿、合成路径不同)
- 远程 URL 图片在无头 Node 路径下不会绘制
- 依赖 GPU/WebGL 上下文的滤镜在 headless 主线程会被跳过/降级(无头路径是 Canvas 2D /
@napi-rs/canvas,不是仓内实验 WebGL 后端)
7) 删除帧:frames delete
用途:在原 .1gamerecord 上按区间删除 runtime frames,可选 compact;需要保留原文件时请先复制或使用 record extract 生成新文件。
示例:
1gameplay frames delete out/demo.1gamerecord --ranges 100-300,450-5001gameplay frames delete out/demo.1gamerecord --ranges 100-300 --compact1gameplay frames delete out/demo.1gamerecord --last 31gameplay frames delete out/demo.1gamerecord --last 3 --compact
关键参数(--ranges 与 --last 二选一):
--ranges:格式如100-300,450-500--last:删除末尾n个已存在的 runtime 帧(n >= 1整数,且不得超过当前帧数);按 seq 升序取尾部并合并为deletedRanges(连续档案通常为单段{ from: maxSeq-n+1, to: maxSeq };有缺口时可为多段)--dry-run:可选,改写<archive>.swp(须已由step --dry-run创建);有.swp时无旗标写主档会失败--compact:可选,重写并压缩 store journals
成功输出:stdout JSON,包含 deletedRanges、runtimeFrameCount、eventCount;使用 --last 时额外含 last;--dry-run 时额外含 dryRun / effectivePath。
8) 抽取帧区间:record extract
用途:从已有 .1gamerecord 中抽取连续 runtime 帧区间,写成新的独立 .1gamerecord 文件;输入文件不会被覆盖。若 --out 指向已有文件,会按当前写入策略覆盖该目标文件。
示例:
1gameplay record extract out/demo.1gamerecord --out out/clip.1gamerecord --from-seq 100 --to-seq 1601gameplay record extract out/demo.1gamerecord --out out/clip.1gamerecord --from-seq 100 --to-seq 160 --merge-diff1gameplay record extract out/demo.1gamerecord --out out/clip.1gamerecord --from-seq 100 --to-seq 160 --merge-diff 31gameplay record extract out/demo.1gamerecord --out out/clip.1gamerecord --from-seq 100 --to-seq 160 --branch-meta1gameplay record extract out/demo.1gamerecord --out out/clip.1gamerecord --from-ms 1000 --to-ms 2600
关键参数:
--out:新.1gamerecord输出路径,不能与输入路径相同;目标文件已存在时会被覆盖--from-seq/--to-seq:按 runtime seq 抽取闭区间--from-ms/--to-ms:按tickedTimeMs解析到不超过该时间的 runtime seq(at-or-before)后抽取闭区间;早于首帧时会解析到首帧--merge-diff [n]:可选,合并未被硬锚定的连续 store diff,并删除因此失去 store 终点的 runtime 帧(这些时刻不可再 seek)。默认n=2(相邻 2 条可合并 diff → 1 条);可传n>=2。某一连续可合并段长度不足n时不合并该段尾巴,继续向后扫描。硬锚点:full、窗口首/末 runtime 的storeCursor、带评论帧的storeCursor--branch-meta:可选,在输出档案写入 provenance:输入文件字节 SHA-256 与原始fromSeq;默认不继承输入档案的分支元数据
与 frames delete 的区别:frames delete 会就地修改输入档案;record extract 会写出一个新的独立档案,并保留输入档案不变。
成功输出:stdout JSON envelope,schema 为 1gameplay.record.extract.v1,包含解析后的 fromSeq、toSeq、mergeDiff、mergeDiffCount、branchMeta、frameCount、commentCount、store / runtime 行数变化(--merge-diff 时 frameCount 可能小于抽取窗口);传入 --branch-meta 时还包含 originRecordSha 与 branchedFromSeq。
9) 导出回放页:bundle-player-html
用途:默认在 --out 位置生成单文件 replay.html(内联薄 ESM player.bundle.js,并对 .1gamerecord 先 gzip 再 base64 写入 recordBase64,同时带 recordEncoding: "gzip",不写 sidecar 文件)。单文件下引擎解析由 ONEGAME_ENGINE_CDN_BASE 切换(与 1game build --target single-file 同规则):已设 → HTML import map 指向 CDN runtime-main.mjs;未设 → 内嵌本地 dist/engine/runtime-main.mjs,经典 boot 生成 blob: 再写 import map。若传 --multi-file,则在 --out 所在目录生成 replay.html、player.bundle.js、engine/runtime-main.mjs 以及 .1gamerecord 的副本(多文件模式不做外层 gzip,可离线静态托管)。
sql.js WASM 地址在导出时写入 gr-config.wasm:
{ONEGAME_ENGINE_CDN_BASE|https://cdn.jsdelivr.net/npm}/sql.js@{installedVersion}/dist/sql-wasm.wasm复用 ONEGAME_ENGINE_CDN_BASE(未设则 sql.js 仍默许 jsDelivr,与引擎 getEngineCdnBase()===null 语义分离);版本对齐导出进程安装的 sql.js。单文件导出时原样内联已发布的薄 ESM 壳,不改写壳字节、不跑 esbuild。引擎 local ./engine/ / CDN / inline-blob 规则见 docs/engine-cdn-resolution.md。
导出页会在 gr-config 里默认写入两个可替换模板值:
shareButtonText: "__1GR_SHARE_BUTTON_TEXT__"(分享按钮文案)shareActionMessage: "__1GR_SHARE_ACTION_MESSAGE__"(iframe 分享消息类型)
你可在产出的 replay.html 上替换这两个固定字符串来自定义。若 shareActionMessage 有值且页面被 iframe 加载,评论区「分享」(须有评论)或 userplay 暂停后更多菜单的采集分享(须已游玩)会向父窗口发送 postMessage({ type, fileBuffer, commentsJson, filename, kind }),kind 为 comments 或 capture;否则走默认下载。采集分享不要求评论。
示例:
1gameplay bundle-player-html out/demo.1gamerecord --out out/replay.html(未设 CDN → inline-blob)ONEGAME_ENGINE_CDN_BASE=https://cdn.jsdelivr.net/npm 1gameplay bundle-player-html out/demo.1gamerecord --out out/replay.html1gameplay bundle-player-html out/demo.1gamerecord --out out/replay.html --multi-file
打开 replay.html?mode=autoplay 进页自动播放(可暂停);?mode=userplay 打开玩家游戏采集面板(空回放时只留不可关闭的进入游戏浮层,隐藏进度条);?userplay=0 隐藏「进入玩家游戏模式」菜单(忽略 mode=userplay)。
关键参数:
--title:可选,浏览器页标题--single-html:可选,显式指定单文件模式(默认即单文件;CDN 或 inline-blob)--multi-file:可选,离线多文件模式(replay.html + player.bundle.js + engine/runtime-main.mjs + 归档副本)--dry-run:可选,从已有<archive>.swp读归档导出(不改主档)--shell:已移除。传任意值(含play-only/full)会失败;自动播用replay.html?mode=autoplay
构建前提:需先在仓库根构建播放器壳包:pnpm --filter @1game/1gamerecord-player-html build(否则 CLI 会报缺少 player.html / player.bundle.js;--multi-file 与 inline-blob 另需 engine/runtime-main.mjs)。
输出与错误约定
- 所有命令默认输出 JSON(成功为对应 schema,失败为
1gameplay.error),并统一写入 stdout。 frame query|diff|batch:成功与失败均向 stdout 输出 JSON(1gameplay.frame/1gameplay.error),退出码见上文。frame screenshot:成功1gameplay.screenshot.v1;frame paint:成功1gameplay.paint.snapshot/1。失败均为1gameplay.error。frames list:成功 schema 为1gameplay.frames.list.v1。
常见问题
1) 报错:bundle-player-html 需要 --out <replay.html>
请补齐 --out 参数。
2) 报错:engineVersion 不兼容
这表示归档版本与当前运行版本不兼容。请使用与归档匹配的 CLI / 引擎版本,或重新录制。
3) 报错:--ms 必须是 >= 0 的数字 或 --repeat 必须是 >= 1 的整数
请检查 step 参数取值。--until 必须搭配 --until-ms 与显式 --ms,且不能同时传 --repeat。
3b) 报错:STEP_UNTIL_NOT_MET
谓词在 --until-ms 内仍为假。查 details.lastValue / frame query --select store:state;加大 --until-ms 或修正 path。这不是 worker 超时(那是退出码 5)。
4) 报错:better-sqlite3.node is missing / Cannot find module 'better-sqlite3'
1gameplay 在 Node 环境下会使用 better-sqlite3 读写 .1gamerecord。如果你的 pnpm 环境启用了构建脚本限制(例如忽略未授权构建),可能导致 better-sqlite3 未完成原生构建,从而在运行时抛出该错误。
可选修复方式:
一次性审批当前项目的构建脚本(推荐):
pnpm approve-builds
在项目
package.json中显式允许构建better-sqlite3(适合团队统一配置):- 在根
package.json增加:
- 在根
{
"pnpm": {
"onlyBuiltDependencies": ["better-sqlite3"]
}
}完成后重新安装依赖:
pnpm install
