@xiaobuyu/nodesign 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (372) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +140 -0
  3. package/bin/nodesign.js +176 -0
  4. package/package.json +104 -0
  5. package/server/api/README.md +45 -0
  6. package/server/api/_guard.js +75 -0
  7. package/server/api/admin.js +187 -0
  8. package/server/api/assets/docx-page.js +103 -0
  9. package/server/api/assets/helpers.js +22 -0
  10. package/server/api/assets/notes.js +144 -0
  11. package/server/api/assets.js +896 -0
  12. package/server/api/board.js +57 -0
  13. package/server/api/browse.js +138 -0
  14. package/server/api/canvas.js +444 -0
  15. package/server/api/chatai.js +324 -0
  16. package/server/api/exports/build-standalone.js +980 -0
  17. package/server/api/exports/cards.js +118 -0
  18. package/server/api/exports/docx-pdf.js +46 -0
  19. package/server/api/exports/export-page.js +142 -0
  20. package/server/api/exports/handoff.js +147 -0
  21. package/server/api/exports.js +690 -0
  22. package/server/api/instruction.js +75 -0
  23. package/server/api/local.js +115 -0
  24. package/server/api/me.js +116 -0
  25. package/server/api/memory.js +185 -0
  26. package/server/api/pending-changes.js +366 -0
  27. package/server/api/plugins.js +164 -0
  28. package/server/api/projects.js +220 -0
  29. package/server/api/publish.js +64 -0
  30. package/server/api/recent.js +73 -0
  31. package/server/api/sessions.js +589 -0
  32. package/server/api/skills.js +77 -0
  33. package/server/api/standalone-fit.js +168 -0
  34. package/server/api/turn-compose.js +180 -0
  35. package/server/api/turn-inflight.js +44 -0
  36. package/server/api/turn.js +669 -0
  37. package/server/auth/README.md +17 -0
  38. package/server/auth/middleware.js +138 -0
  39. package/server/auth/origin-guard.js +71 -0
  40. package/server/auth/session.js +108 -0
  41. package/server/auth/tier.js +113 -0
  42. package/server/auth/users-store.js +348 -0
  43. package/server/edit/README.md +29 -0
  44. package/server/engine/README.md +113 -0
  45. package/server/engine/agent/agent-shared.js +550 -0
  46. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -0
  47. package/server/engine/agent/auto-mode-rules.js +78 -0
  48. package/server/engine/agent/context.js +418 -0
  49. package/server/engine/agent/events.js +464 -0
  50. package/server/engine/agent/hooks/canvas-validate.js +199 -0
  51. package/server/engine/agent/hooks/failure.js +120 -0
  52. package/server/engine/agent/hooks/file-events.js +75 -0
  53. package/server/engine/agent/hooks/lifecycle.js +233 -0
  54. package/server/engine/agent/hooks/post-canvas-focus.js +80 -0
  55. package/server/engine/agent/hooks/post-guidance.js +206 -0
  56. package/server/engine/agent/hooks/post-subagent-report.js +104 -0
  57. package/server/engine/agent/hooks/post-trim.js +58 -0
  58. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -0
  59. package/server/engine/agent/hooks/pre-defaults.js +84 -0
  60. package/server/engine/agent/hooks/pre-injectors.js +277 -0
  61. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -0
  62. package/server/engine/agent/hooks/pre-starter-files.js +61 -0
  63. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -0
  64. package/server/engine/agent/hooks/site-validate.js +94 -0
  65. package/server/engine/agent/hooks/tool-prompts.js +31 -0
  66. package/server/engine/agent/hooks/turn-state-memory.js +56 -0
  67. package/server/engine/agent/hooks/user-prompt-submit.js +215 -0
  68. package/server/engine/agent/hooks.js +354 -0
  69. package/server/engine/agent/init-contract.js +69 -0
  70. package/server/engine/agent/isolation.js +159 -0
  71. package/server/engine/agent/model-context.js +359 -0
  72. package/server/engine/agent/model-table.js +292 -0
  73. package/server/engine/agent/plugin-loader.js +266 -0
  74. package/server/engine/agent/prompts/nodesign-prelude.md +336 -0
  75. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -0
  76. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -0
  77. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -0
  78. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -0
  79. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -0
  80. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -0
  81. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -0
  82. package/server/engine/agent/prompts/tools/site-reference.md +106 -0
  83. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -0
  84. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -0
  85. package/server/engine/agent/session-loop.js +1024 -0
  86. package/server/engine/agent/session-model.js +164 -0
  87. package/server/engine/agent/skill.js +204 -0
  88. package/server/engine/agent/system-prompts.js +86 -0
  89. package/server/engine/agent/task-events.js +78 -0
  90. package/server/engine/agents/ds-extractor.md +132 -0
  91. package/server/engine/agents/explorer.md +190 -0
  92. package/server/engine/agents/index.js +230 -0
  93. package/server/engine/agents/schemas/design-system.json +156 -0
  94. package/server/engine/agents/schemas/tweak-schema.json +113 -0
  95. package/server/engine/agents/tweak-proposer.md +127 -0
  96. package/server/engine/agents/vision-checker.md +254 -0
  97. package/server/engine/browse/capture.js +422 -0
  98. package/server/engine/browse/card.js +137 -0
  99. package/server/engine/browse/handover.js +59 -0
  100. package/server/engine/browse/page-digest.js +119 -0
  101. package/server/engine/browse/refs.js +196 -0
  102. package/server/engine/browse/registry.js +307 -0
  103. package/server/engine/browse/screencast.js +170 -0
  104. package/server/engine/browse/state.js +121 -0
  105. package/server/engine/chatai/chat-log.js +79 -0
  106. package/server/engine/chatai/index.js +193 -0
  107. package/server/engine/chatai/openai-compat.js +152 -0
  108. package/server/engine/chatai/orchestrate.js +309 -0
  109. package/server/engine/chatai/perform.js +40 -0
  110. package/server/engine/chatai/summarize.js +79 -0
  111. package/server/engine/mcp/capability-gate.js +52 -0
  112. package/server/engine/mcp/index.js +339 -0
  113. package/server/engine/mcp/param-sanitizer.js +142 -0
  114. package/server/engine/mcp/tools/arrange-on-board.js +124 -0
  115. package/server/engine/mcp/tools/artifact-session.js +278 -0
  116. package/server/engine/mcp/tools/browse-computer.js +421 -0
  117. package/server/engine/mcp/tools/browse-find-batch.js +183 -0
  118. package/server/engine/mcp/tools/browse-screenshot.js +157 -0
  119. package/server/engine/mcp/tools/browse.js +480 -0
  120. package/server/engine/mcp/tools/build-docx.js +101 -0
  121. package/server/engine/mcp/tools/clear-pending-changes.js +99 -0
  122. package/server/engine/mcp/tools/create-on-board.js +155 -0
  123. package/server/engine/mcp/tools/crystallize-skill.js +168 -0
  124. package/server/engine/mcp/tools/deliver-files.js +0 -0
  125. package/server/engine/mcp/tools/explain-style.js +237 -0
  126. package/server/engine/mcp/tools/export-handoff.js +133 -0
  127. package/server/engine/mcp/tools/expose-tweaks.js +149 -0
  128. package/server/engine/mcp/tools/generate-image.js +842 -0
  129. package/server/engine/mcp/tools/get-computed-styles.js +164 -0
  130. package/server/engine/mcp/tools/get-pending-changes.js +205 -0
  131. package/server/engine/mcp/tools/h3box-ssh.js +84 -0
  132. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -0
  133. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -0
  134. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -0
  135. package/server/engine/mcp/tools/helpers/perception-page.js +215 -0
  136. package/server/engine/mcp/tools/helpers/reference-download.js +103 -0
  137. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -0
  138. package/server/engine/mcp/tools/helpers/rembg.js +273 -0
  139. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -0
  140. package/server/engine/mcp/tools/highlight.js +68 -0
  141. package/server/engine/mcp/tools/list-pages.js +222 -0
  142. package/server/engine/mcp/tools/lookup-tags.js +59 -0
  143. package/server/engine/mcp/tools/navigate-to-page.js +60 -0
  144. package/server/engine/mcp/tools/organize-board.js +64 -0
  145. package/server/engine/mcp/tools/paint-still.js +434 -0
  146. package/server/engine/mcp/tools/pin-to-board.js +130 -0
  147. package/server/engine/mcp/tools/preview-deck.js +130 -0
  148. package/server/engine/mcp/tools/profile-scroll.js +305 -0
  149. package/server/engine/mcp/tools/publish-site.js +102 -0
  150. package/server/engine/mcp/tools/query-elements.js +195 -0
  151. package/server/engine/mcp/tools/read-board.js +103 -0
  152. package/server/engine/mcp/tools/read-document.js +93 -0
  153. package/server/engine/mcp/tools/read-page.js +264 -0
  154. package/server/engine/mcp/tools/read-tavern-json.js +143 -0
  155. package/server/engine/mcp/tools/record-decision.js +140 -0
  156. package/server/engine/mcp/tools/relate-on-board.js +110 -0
  157. package/server/engine/mcp/tools/remove-background.js +391 -0
  158. package/server/engine/mcp/tools/report-issue.js +115 -0
  159. package/server/engine/mcp/tools/roll-film.js +266 -0
  160. package/server/engine/mcp/tools/screenshot-docx.js +138 -0
  161. package/server/engine/mcp/tools/screenshot-url.js +195 -0
  162. package/server/engine/mcp/tools/screenshot.js +583 -0
  163. package/server/engine/mcp/tools/tier-gate.js +96 -0
  164. package/server/engine/mcp/tools/trace-motion.js +215 -0
  165. package/server/engine/mcp/tools/web-search.js +548 -0
  166. package/server/engine/motion/inventory.js +349 -0
  167. package/server/engine/perception/session.js +235 -0
  168. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -0
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -0
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -0
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -0
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -0
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -0
  174. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -0
  175. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -0
  176. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -0
  177. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -0
  178. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -0
  179. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -0
  180. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -0
  181. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -0
  182. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -0
  183. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -0
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -0
  185. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -0
  186. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -0
  187. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -0
  188. package/server/engine/runs/active-runs.js +698 -0
  189. package/server/engine/runs/live-turn.js +252 -0
  190. package/server/engine/runs/store.js +364 -0
  191. package/server/engine/runs/turn-relay.js +217 -0
  192. package/server/engine/runtime/workspace.js +164 -0
  193. package/server/index.js +235 -0
  194. package/server/lib/artifact-file-path.js +80 -0
  195. package/server/lib/artifact-target.js +284 -0
  196. package/server/lib/asset-refs.js +239 -0
  197. package/server/lib/async-queue.js +104 -0
  198. package/server/lib/auto-relations.js +130 -0
  199. package/server/lib/binding-types.js +60 -0
  200. package/server/lib/board-kind-sizes.js +62 -0
  201. package/server/lib/board-relations.js +133 -0
  202. package/server/lib/browse-proxy.js +180 -0
  203. package/server/lib/canvas-id.js +36 -0
  204. package/server/lib/cover.js +227 -0
  205. package/server/lib/danbooru-tags.js +291 -0
  206. package/server/lib/doc-extract.js +156 -0
  207. package/server/lib/docx/build-from-source.js +260 -0
  208. package/server/lib/docx/build.js +490 -0
  209. package/server/lib/docx/dump-styles.js +287 -0
  210. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -0
  211. package/server/lib/docx/merge-runs.js +129 -0
  212. package/server/lib/docx/numbering.js +218 -0
  213. package/server/lib/docx/order.js +152 -0
  214. package/server/lib/docx/rawzip.js +172 -0
  215. package/server/lib/docx/render.js +92 -0
  216. package/server/lib/docx/text-lint.js +225 -0
  217. package/server/lib/docx/tokens.js +335 -0
  218. package/server/lib/docx/units.js +29 -0
  219. package/server/lib/docx/xml.js +291 -0
  220. package/server/lib/docx-pages.js +187 -0
  221. package/server/lib/export-collect.js +289 -0
  222. package/server/lib/export-package.js +210 -0
  223. package/server/lib/html-srcset.js +145 -0
  224. package/server/lib/image-variant.js +460 -0
  225. package/server/lib/ingress/forward-openai-chat.js +305 -0
  226. package/server/lib/ingress/openai-chat.js +491 -0
  227. package/server/lib/ingress/session-notice.js +66 -0
  228. package/server/lib/ingress/session-routes.js +68 -0
  229. package/server/lib/ingress/slot-probe.js +130 -0
  230. package/server/lib/ingress/upstream-billing.js +52 -0
  231. package/server/lib/ingress/upstream-fail-streak.js +72 -0
  232. package/server/lib/ingress/upstream-truncation.js +48 -0
  233. package/server/lib/issues-store.js +182 -0
  234. package/server/lib/kinds/deck.js +72 -0
  235. package/server/lib/kinds/docx.js +283 -0
  236. package/server/lib/kinds/file-kinds.js +113 -0
  237. package/server/lib/kinds/index.js +202 -0
  238. package/server/lib/kinds/site.js +161 -0
  239. package/server/lib/model-ingress.js +543 -0
  240. package/server/lib/moderation.js +255 -0
  241. package/server/lib/notice-store.js +103 -0
  242. package/server/lib/plugin-install.js +104 -0
  243. package/server/lib/plugin-validator.js +721 -0
  244. package/server/lib/publish-store.js +115 -0
  245. package/server/lib/quick-summary.js +52 -0
  246. package/server/lib/quota.js +270 -0
  247. package/server/lib/rate-window.js +29 -0
  248. package/server/lib/reference-assets.js +92 -0
  249. package/server/lib/region-shot.js +135 -0
  250. package/server/lib/safe-path.js +73 -0
  251. package/server/lib/sdk-session.js +33 -0
  252. package/server/lib/showcase-store.js +92 -0
  253. package/server/lib/site-publish.js +465 -0
  254. package/server/lib/ssrf-guard.js +432 -0
  255. package/server/lib/task-scan.js +158 -0
  256. package/server/lib/tavern-json.js +154 -0
  257. package/server/lib/video-variant.js +237 -0
  258. package/server/lib/workspace-path.js +33 -0
  259. package/server/ops/fix-sdk-musl.mjs +40 -0
  260. package/server/ops/install-macos-fonts.sh +114 -0
  261. package/server/ops/macos-fonts.conf +336 -0
  262. package/server/ops/sandbox-shim/bwrap +57 -0
  263. package/server/projects/assets-summary.js +119 -0
  264. package/server/projects/auto-name.js +53 -0
  265. package/server/projects/board-store.js +640 -0
  266. package/server/projects/move-entry.js +103 -0
  267. package/server/projects/store.js +270 -0
  268. package/server/projects/ui-config.js +54 -0
  269. package/server/projects/workspace-templates.js +53 -0
  270. package/server/projects/workspace.js +1025 -0
  271. package/server/runtime/capabilities.js +153 -0
  272. package/server/runtime/local-config.js +177 -0
  273. package/server/runtime/local-env.js +88 -0
  274. package/server/runtime/platform.js +342 -0
  275. package/server/runtime/profile.js +72 -0
  276. package/server/services/rembg-launcher.js +313 -0
  277. package/server/services/rembg-service.py +334 -0
  278. package/server/shared/README.md +25 -0
  279. package/server/shared/deck.js +54 -0
  280. package/server/shared/time.js +49 -0
  281. package/server/style-pipeline/README.md +53 -0
  282. package/server/ws/broker.js +36 -0
  283. package/server/ws/browse-channel.js +177 -0
  284. package/server/ws/index.js +386 -0
  285. package/web/README.md +89 -0
  286. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +11 -0
  287. package/web/dist/assets/DeckWindow-4G0xX7P3.js +16 -0
  288. package/web/dist/assets/DocxWindow-BXQsoPkh.js +16 -0
  289. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  290. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  291. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  292. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  293. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  294. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  295. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  296. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  297. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  298. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  299. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  300. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  301. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  302. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  303. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  304. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  305. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  306. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  307. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  308. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  309. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  310. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  311. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  312. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  313. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  314. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  315. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  316. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  317. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  318. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  319. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  320. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  321. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  322. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  323. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  324. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  325. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  326. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  327. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  328. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  329. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  330. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  331. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  332. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  333. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  334. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  335. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  336. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  337. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  338. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  339. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  340. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  341. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  342. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  343. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  344. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  345. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  346. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  347. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  348. package/web/dist/assets/SiteWindow-D6eS-2gM.js +18 -0
  349. package/web/dist/assets/again-D8tfgKKO.webp +0 -0
  350. package/web/dist/assets/caveat-nd-CP6HlsNg.woff2 +0 -0
  351. package/web/dist/assets/clock-xJhvfYpS.webp +0 -0
  352. package/web/dist/assets/film-cut-BBKMwH7q.webp +0 -0
  353. package/web/dist/assets/film-still-BZ_DL9Tt.webp +0 -0
  354. package/web/dist/assets/index-C0KUf6Wd.css +1 -0
  355. package/web/dist/assets/index-Cbl3ATvp.js +2125 -0
  356. package/web/dist/assets/ink-desk-BRbxeDsR.webp +0 -0
  357. package/web/dist/assets/ink-night-Bz4BT3XH.webp +0 -0
  358. package/web/dist/assets/ink-portrait-C0DM4V8Q.webp +0 -0
  359. package/web/dist/assets/longcang-regular-C0uvTuKa.woff2 +0 -0
  360. package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
  361. package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
  362. package/web/dist/assets/pending-edit-apply-DTjo2geW.js +31 -0
  363. package/web/dist/assets/question-8H4HSqxh.webp +0 -0
  364. package/web/dist/assets/reject-BaPJqElj.webp +0 -0
  365. package/web/dist/assets/rotate-cw-LMT3x1Zj.js +11 -0
  366. package/web/dist/assets/rp-portrait-CbrGpew2.webp +0 -0
  367. package/web/dist/assets/rp-street-C4jVKjxx.webp +0 -0
  368. package/web/dist/assets/square-dashed-mouse-pointer-CBoAkfhi.js +11 -0
  369. package/web/dist/assets/tangle-DIxHVNuT.webp +0 -0
  370. package/web/dist/assets/thisone-BylZKglV.webp +0 -0
  371. package/web/dist/assets/thumb-DO1wfK1I.webp +0 -0
  372. package/web/dist/index.html +18 -0
@@ -0,0 +1,554 @@
1
+ /**
2
+ * helpers/motion-lab.js — 时间维度感知(2026-08-19,问题库 iss_mszv782a_toab)
3
+ *
4
+ * ## 为什么有这一层
5
+ *
6
+ * agent 的眼睛一直只有静帧,而动画/演出类工作的好坏**全在时间轴上**:缓动像不像
7
+ * 机械臂、过冲回稳够不够、两个 tween 之间有没有硬切、低帧率下衰减是不是按帧写的。
8
+ * 上报里的四条真实代价:装弹位姿写到镜头背后、倒地绕错轴转进桌底、枪口白闪按帧
9
+ * 衰减 —— 前三条是**碰巧**在静帧上暴露的,第四类纯时间问题一条都抓不到。
10
+ *
11
+ * 这层给三条通道,共用一次录制:
12
+ * - 胶片条:CDP screencast 录一段,按请求时刻取最近帧拼 contact sheet(眼睛)
13
+ * - 数值示波器:rAF 逐帧采样任意 JS 表达式,报过冲/稳定时间/硬切(仪表)
14
+ * - 帧健康 + 音频事件:rAF 间隔统计、play()/start() 时刻表(顺带白拿)
15
+ *
16
+ * ## 录制方式为什么是 screencast 不是连拍 page.screenshot
17
+ *
18
+ * page.screenshot 一张 100~300ms,帧与帧的间距根本压不进 120ms —— 拿它连拍
19
+ * 得到的是"每张都晚点、晚多少不知道"的时间轴。Page.startScreencast 是渲染进程
20
+ * **每次重绘推一帧**,帧上带 epoch 时间戳,事后挑最近帧,时刻是真的。
21
+ *
22
+ * ⚠️ 观察者效应:1 vCPU 上 screencast 的 jpeg 编码自己就吃帧。帧健康数字只用于
23
+ * A/B 与"有没有 100ms+ 冻结"这种量级判断,别当用户设备的绝对值报。
24
+ */
25
+
26
+ import path from 'node:path';
27
+ import os from 'node:os';
28
+ import { promises as fs } from 'node:fs';
29
+ import { execFile } from 'node:child_process';
30
+ import { promisify } from 'node:util';
31
+
32
+ const execFileP = promisify(execFile);
33
+
34
+ // ── 纯函数(可单测)─────────────────────────────────────────────
35
+
36
+ /**
37
+ * 请求时刻 → 该时刻**屏幕上真正显示的帧**。frames: [{tMs, ...}],wanted: number[]。
38
+ *
39
+ * 语义是 last-≤-want 不是 nearest:screencast 只在重绘时推帧,屏幕在时刻 t
40
+ * 显示的内容严格等于"最后一次 ≤t 的合成帧"。nearest 有两个真 bug(真跑校验
41
+ * 逮到的):t=0 会取到触发后 13ms 的帧(起手式已经开动),动画停稳后的请求点
42
+ * 会报"误差 289ms"—— 其实那 289ms 里屏幕纹丝没动,最后一帧就是正确画面。
43
+ * +8ms 容差吃掉合成与时间戳之间的抖动。同一帧可被多个时刻共用。
44
+ */
45
+ export function pickNearestFrames(frames, wanted) {
46
+ const sorted = [...frames].sort((a, b) => a.tMs - b.tMs);
47
+ return wanted.map((want) => {
48
+ let best = null;
49
+ for (const f of sorted) {
50
+ if (f.tMs <= want + 8) best = f;
51
+ else break;
52
+ }
53
+ if (!best) best = sorted[0] || null; // 请求点比第一帧还早:给第一帧
54
+ return best ? { want, actual: best.tMs, frame: best } : null;
55
+ });
56
+ }
57
+
58
+ /**
59
+ * 拼图布局(08-21 按新视觉档重算):预算 2.3MP(实测一张 ≈3.0~3.7k token,旧档 1.05MP 是 07-29 按
60
+ * 1.15MP 定的)+ **长边 ≤2000**(否则 normalizeShot 又整体缩回去,白给)。列数不再按
61
+ * 阶梯死规定,而是在两条约束下**挑格子面积最大**的那种排法 —— 格数从 2 到 30 都能拼,
62
+ * 格数越多每格越小,这是 agent 自己权衡的事(描述里说清)。
63
+ */
64
+ export function sheetLayout(n, frameW, frameH, { maxPixels = 2_300_000, maxEdge = 2000, gap = 6 } = {}) {
65
+ let best = null;
66
+ for (let cols = 1; cols <= n; cols += 1) {
67
+ const rows = Math.ceil(n / cols);
68
+ // 三条上限取最严:总像素、横向长边、纵向长边
69
+ const sPix = Math.sqrt(maxPixels / (n * frameW * frameH));
70
+ const sW = (maxEdge - (cols + 1) * gap) / (cols * frameW);
71
+ const sH = (maxEdge - (rows + 1) * gap) / (rows * frameH);
72
+ const scale = Math.min(1, sPix, sW, sH);
73
+ const cellW = Math.max(80, Math.round(frameW * scale));
74
+ const cellH = Math.max(45, Math.round(frameH * scale));
75
+ const area = cellW * cellH;
76
+ // 面积相同(都被总像素卡住)时取更方正的排法:阅读顺序更像时间轴
77
+ const squareness = -Math.abs((cols * cellW) - (rows * cellH));
78
+ if (!best || area > best.area + 1 || (Math.abs(area - best.area) <= 1 && squareness > best.squareness)) {
79
+ best = { cols, rows, cellW, cellH, area, squareness };
80
+ }
81
+ }
82
+ const { cols, rows, cellW, cellH } = best;
83
+ return {
84
+ cols, rows, cellW, cellH, gap,
85
+ sheetW: cols * cellW + (cols + 1) * gap,
86
+ sheetH: rows * cellH + (rows + 1) * gap,
87
+ };
88
+ }
89
+
90
+ const pct = (arr, p) => {
91
+ if (!arr.length) return 0;
92
+ const s = [...arr].sort((a, b) => a - b);
93
+ return s[Math.min(s.length - 1, Math.floor((p / 100) * s.length))];
94
+ };
95
+
96
+ /** rAF 时间戳序列 → 帧健康报告。头两帧含启动抖动,剔掉。 */
97
+ export function frameHealth(rafTs) {
98
+ const iv = [];
99
+ for (let i = 3; i < rafTs.length; i += 1) iv.push(rafTs[i] - rafTs[i - 1]);
100
+ if (iv.length < 10) return { ok: false, n: iv.length };
101
+ const worst = Math.max(...iv);
102
+ return {
103
+ ok: true,
104
+ n: iv.length,
105
+ fps: 1000 / (iv.reduce((a, b) => a + b, 0) / iv.length),
106
+ p50: pct(iv, 50),
107
+ p95: pct(iv, 95),
108
+ worst,
109
+ droppedPct: (iv.filter((f) => f > 16.7 * 1.5).length / iv.length) * 100,
110
+ freezes: iv.filter((f) => f > 120).length,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * 一条采样曲线 → 数值报告。points: [{t, v}](t 毫秒、相对触发时刻,v 数值)。
116
+ *
117
+ * 判读逻辑(都是"给 agent 一个开工方向"级别,不是学术级):
118
+ * final = 末尾 10% 样本的中位数(动画应该已经停了)
119
+ * overshoot = 相对"起点→终点"行程,越过终点最远多少 %(缓动手感的核心数字)
120
+ * settleMs = 最后一次离 final 超过行程 3% 的时刻(之后就算稳住了)
121
+ * cuts = 单帧(≤40ms 间隔)内跳变超过全程 22% 的时刻 —— 两个 tween 之间
122
+ * 的硬切、瞬移、状态机跳变都长这样
123
+ */
124
+ export function seriesReport(points) {
125
+ const pts = points.filter((p) => Number.isFinite(p.v));
126
+ if (pts.length < 8) return { ok: false, n: pts.length };
127
+ const vs = pts.map((p) => p.v);
128
+ const min = Math.min(...vs);
129
+ const max = Math.max(...vs);
130
+ const range = max - min;
131
+ const tail = vs.slice(Math.max(0, Math.floor(vs.length * 0.9)));
132
+ const final = [...tail].sort((a, b) => a - b)[Math.floor(tail.length / 2)];
133
+ const first = vs[0];
134
+ const travel = Math.abs(final - first);
135
+
136
+ let overshootPct = 0;
137
+ if (travel > 1e-9) {
138
+ const beyond = final >= first ? max - final : final - min;
139
+ overshootPct = Math.max(0, (beyond / travel) * 100);
140
+ }
141
+
142
+ let settleMs = 0;
143
+ if (travel > 1e-9) {
144
+ const tol = travel * 0.03;
145
+ for (let i = pts.length - 1; i >= 0; i -= 1) {
146
+ if (Math.abs(pts[i].v - final) > tol) { settleMs = pts[i].t; break; }
147
+ }
148
+ }
149
+
150
+ const cuts = [];
151
+ if (range > 1e-9) {
152
+ for (let i = 1; i < pts.length; i += 1) {
153
+ const dt = pts[i].t - pts[i - 1].t;
154
+ const dv = Math.abs(pts[i].v - pts[i - 1].v);
155
+ if (dt > 0 && dt <= 40 && dv > range * 0.22) cuts.push(Math.round(pts[i].t));
156
+ }
157
+ }
158
+ // 相邻帧连续跳变是同一次瞬移,收拢成一条
159
+ const cutTimes = cuts.filter((t, i) => i === 0 || t - cuts[i - 1] > 60).slice(0, 4);
160
+
161
+ return {
162
+ ok: true, n: pts.length, first, final, min, max, range,
163
+ overshootPct, settleMs, cuts: cutTimes,
164
+ still: range < Math.max(1e-9, Math.abs(final) * 1e-6) || range === 0,
165
+ };
166
+ }
167
+
168
+ const CHART_COLORS = ['#e4572e', '#2e86ab', '#5c9e31', '#8a4fff', '#d81159', '#0f8b8d'];
169
+ const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
170
+
171
+ /**
172
+ * 多条曲线 → 一张 SVG 折线图(各自归一化到 0..1,图例标真实范围)。
173
+ * 硬切时刻画红色虚线。给 sharp 转位图用,字体走系统缺省。
174
+ */
175
+ export function chartSvg(seriesList, durationMs, { width = 1180, height = 460 } = {}) {
176
+ const padL = 56; const padR = 16; const padT = 14; const padB = 40 + seriesList.length * 20;
177
+ const plotW = width - padL - padR;
178
+ const plotH = height - padT - padB;
179
+ const x = (t) => padL + (t / Math.max(1, durationMs)) * plotW;
180
+
181
+ const parts = [
182
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`,
183
+ `<rect width="${width}" height="${height}" fill="#ffffff"/>`,
184
+ `<rect x="${padL}" y="${padT}" width="${plotW}" height="${plotH}" fill="#fafaf7" stroke="#ddd"/>`,
185
+ ];
186
+
187
+ // 时间刻度:挑个整步长,8~14 格
188
+ const step = [50, 100, 200, 250, 500, 1000, 2000, 5000].find((s) => durationMs / s <= 14) || 5000;
189
+ for (let t = 0; t <= durationMs; t += step) {
190
+ parts.push(`<line x1="${x(t)}" y1="${padT}" x2="${x(t)}" y2="${padT + plotH}" stroke="#e8e8e2" stroke-width="1"/>`);
191
+ parts.push(`<text x="${x(t)}" y="${padT + plotH + 16}" font-size="12" text-anchor="middle" fill="#666" font-family="sans-serif">${t}ms</text>`);
192
+ }
193
+
194
+ seriesList.forEach((s, i) => {
195
+ const color = CHART_COLORS[i % CHART_COLORS.length];
196
+ const r = s.report;
197
+ const lo = r.min; const span = (r.max - r.min) || 1;
198
+ const y = (v) => padT + plotH - ((v - lo) / span) * (plotH - 8) - 4;
199
+ // 降采样到 ≤300 点,SVG 别写成兆
200
+ const pts = s.points.filter((p) => Number.isFinite(p.v));
201
+ const stride = Math.max(1, Math.ceil(pts.length / 300));
202
+ const d = pts.filter((_, j) => j % stride === 0 || j === pts.length - 1)
203
+ .map((p, j) => `${j ? 'L' : 'M'}${x(p.t).toFixed(1)},${y(p.v).toFixed(1)}`).join('');
204
+ parts.push(`<path d="${d}" fill="none" stroke="${color}" stroke-width="2"/>`);
205
+ for (const ct of r.cuts || []) {
206
+ parts.push(`<line x1="${x(ct)}" y1="${padT}" x2="${x(ct)}" y2="${padT + plotH}" stroke="#d81a1a" stroke-width="1.5" stroke-dasharray="5 4"/>`);
207
+ }
208
+ const ly = padT + plotH + 34 + i * 20;
209
+ parts.push(`<rect x="${padL}" y="${ly - 10}" width="14" height="4" fill="${color}"/>`);
210
+ parts.push(`<text x="${padL + 20}" y="${ly - 4}" font-size="13" fill="#333" font-family="sans-serif">${esc(s.name)}: ${fmtNum(r.first)} → ${fmtNum(r.final)} (min ${fmtNum(r.min)}, max ${fmtNum(r.max)})</text>`);
211
+ });
212
+
213
+ parts.push('</svg>');
214
+ return parts.join('\n');
215
+ }
216
+
217
+ export function fmtNum(v) {
218
+ if (!Number.isFinite(v)) return '?';
219
+ const a = Math.abs(v);
220
+ if (a >= 1000) return v.toFixed(0);
221
+ if (a >= 10) return v.toFixed(1);
222
+ return v.toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
223
+ }
224
+
225
+ // ── 页面内采样器(page.evaluate 序列化过去跑)──────────────────────
226
+
227
+ /* eslint-disable no-undef */
228
+ function installMotionSampler(cfg) {
229
+ const st = {
230
+ running: true, raf: 0, rafTs: [], rows: [], errs: {}, audio: [],
231
+ timeOrigin: performance.timeOrigin,
232
+ };
233
+ window.__ndMotion = st;
234
+
235
+ const fns = {};
236
+ for (const name of Object.keys(cfg.exprs || {})) {
237
+ try {
238
+ fns[name] = new Function(`return (${cfg.exprs[name]});`);
239
+ } catch (e) { st.errs[name] = `compile: ${e.message}`; }
240
+ }
241
+
242
+ // 音频事件钩子:agent 听不见声音,但"什么时刻试图出声"是可记录的。
243
+ // play() 被 autoplay 政策拒掉也照记 —— "试图出声但被拒"本身就是发现。
244
+ try {
245
+ const note = (what) => { if (st.audio.length < 40) st.audio.push({ t: performance.now(), what }); };
246
+ const wrap = (proto, method, desc) => {
247
+ const orig = proto && proto[method];
248
+ if (!orig) return;
249
+ proto[method] = function (...a) { try { note(desc(this)); } catch { /* */ } return orig.apply(this, a); };
250
+ };
251
+ wrap(HTMLMediaElement.prototype, 'play', (el) => `media.play ${((el.currentSrc || el.src || '').split('/').pop() || el.tagName.toLowerCase()).slice(0, 48)}`);
252
+ wrap(AudioBufferSourceNode.prototype, 'start', () => 'audioBufferSource.start');
253
+ wrap(OscillatorNode.prototype, 'start', () => 'oscillator.start');
254
+ } catch { /* 没有 WebAudio 的环境 */ }
255
+
256
+ // 元素探针(08-21):不用 agent 写表达式,自动盯一批"可能在动"的元素,逐帧采
257
+ // 文档坐标 / 透明度 / 缩放。文档坐标 = 视口 rect + scroll,所以滚动本身不算"动",
258
+ // 视差 / 入场 / 固定元素随视口走的才算。参考站的全局 agent 不认识,这是它唯一能
259
+ // 拿到"谁在动、怎么动"的路。
260
+ st.elems = [];
261
+ if (cfg.probeElements) {
262
+ try {
263
+ const picked = [];
264
+ const seen = new Set();
265
+ const add = (el) => { if (picked.length >= 48 || seen.has(el)) return; const r = el.getBoundingClientRect(); if (r.width < 24 || r.height < 24) return; seen.add(el); picked.push(el); };
266
+ const vh = window.innerHeight;
267
+ // 先挑声明了会动的(will-change / transition / animation),再补视口附近的大块
268
+ for (const el of document.querySelectorAll('*')) {
269
+ if (picked.length >= 32) break;
270
+ const r = el.getBoundingClientRect();
271
+ if (r.bottom < -vh || r.top > vh * 2) continue;
272
+ const cs = getComputedStyle(el);
273
+ if (cs.willChange !== 'auto' || cs.animationName !== 'none' || (cs.transitionDuration !== '0s' && cs.transitionProperty !== 'none')) add(el);
274
+ }
275
+ for (const el of document.querySelectorAll('section, article, header, h1, h2, figure, img, video, canvas, [class*="hero"], [class*="card"], nav')) {
276
+ if (picked.length >= 48) break;
277
+ const r = el.getBoundingClientRect();
278
+ if (r.bottom < -vh || r.top > vh * 2) continue;
279
+ add(el);
280
+ }
281
+ const selOf = (el) => { const cls = typeof el.className === 'string' && el.className.trim() ? `.${el.className.trim().split(/\s+/).slice(0, 2).join('.')}` : ''; return `${el.tagName.toLowerCase()}${el.id ? `#${el.id}` : ''}${cls}`; };
282
+ // fixed/sticky 要沿祖先看:固定头部里的链接自己是 static,但整层随视口走
283
+ const fixedOf = (el) => { for (let n = el, i = 0; n && n.nodeType === 1 && i < 40; n = n.parentElement, i += 1) { const p = getComputedStyle(n).position; if (p === 'fixed' || p === 'sticky') return true; } return false; };
284
+ st.elems = picked.map((el) => ({ el, sel: selOf(el), fixed: fixedOf(el), samples: [] }));
285
+ } catch { st.elems = []; }
286
+ }
287
+ const sampleElems = (now) => {
288
+ for (const e of st.elems) {
289
+ if (e.samples.length >= 600) continue;
290
+ try {
291
+ const r = e.el.getBoundingClientRect();
292
+ const cs = getComputedStyle(e.el);
293
+ let s = 1;
294
+ const m = cs.transform && cs.transform !== 'none' ? cs.transform.match(/matrix\(([^)]+)\)/) : null;
295
+ if (m) { const p = m[1].split(',').map(Number); s = Math.hypot(p[0], p[1]); }
296
+ e.samples.push([Math.round(now), Math.round(r.left + window.scrollX), Math.round(r.top + window.scrollY), Number(cs.opacity), Number(s.toFixed(3))]);
297
+ } catch { /* 元素没了就不采 */ }
298
+ }
299
+ };
300
+
301
+ const hasExprs = Object.keys(fns).length > 0;
302
+ const tick = () => {
303
+ if (!st.running) return;
304
+ const now = performance.now();
305
+ if (st.rafTs.length < 4000) st.rafTs.push(now);
306
+ if (st.elems.length) sampleElems(now);
307
+ if (hasExprs && st.rows.length < 4000) {
308
+ const row = { t: now };
309
+ for (const k in fns) {
310
+ try {
311
+ const v = fns[k]();
312
+ const num = typeof v === 'number' ? v : Number(v);
313
+ if (Number.isFinite(num)) row[k] = num;
314
+ else { row[k] = null; if (!(k in st.errs)) st.errs[k] = `value not numeric: ${String(v).slice(0, 60)}`; }
315
+ } catch (e) {
316
+ row[k] = null;
317
+ if (!(k in st.errs)) st.errs[k] = String(e && e.message || e).slice(0, 120);
318
+ }
319
+ }
320
+ st.rows.push(row);
321
+ }
322
+ st.raf = requestAnimationFrame(tick);
323
+ };
324
+ st.raf = requestAnimationFrame(tick);
325
+ return true;
326
+ }
327
+
328
+ function collectMotionSampler() {
329
+ const st = window.__ndMotion;
330
+ if (!st) return null;
331
+ st.running = false;
332
+ cancelAnimationFrame(st.raf);
333
+ return {
334
+ timeOrigin: st.timeOrigin,
335
+ rafTs: st.rafTs,
336
+ rows: st.rows,
337
+ errs: st.errs,
338
+ audio: st.audio,
339
+ elems: (st.elems || []).map((e) => ({ sel: e.sel, fixed: e.fixed, samples: e.samples })),
340
+ };
341
+ }
342
+ /* eslint-enable no-undef */
343
+
344
+ // ── 录制主流程 ───────────────────────────────────────────────────
345
+
346
+ /**
347
+ * 在已就位的 page 上录一段:装采样器 →(可选 click / trigger)→ 等 durationMs →
348
+ * 收数。wantShots 为真才开 screencast(trace_motion 纯数值时省下 jpeg 编码那份 CPU)。
349
+ *
350
+ * 返回 {
351
+ * shots: [{tMs, buf}] tMs 相对触发时刻(可为负 = 触发前的画面)
352
+ * rafTs, rows, errs, audio 页面采样(rows/audio 的 t 已换算成相对触发时刻)
353
+ * clickNote, triggerNote 动作失败时的说明(不失败为 null)
354
+ * captureNote screencast 密度说明
355
+ * }
356
+ */
357
+ export async function recordMotion(page, {
358
+ durationMs, trigger, click, expressions, during = null, probeElements = false,
359
+ wantShots = true, shotMaxW = 1152, shotMaxH = 1152, shotQuality = 78,
360
+ }) {
361
+ // during(08-21):t=0 之后、录制期间并行跑的一段动作(典型:真滚轮滚一段,见
362
+ // motion-scroll.js 的 wheelScroll)。它跟 trigger 的区别:trigger 是页面里的 JS,
363
+ // during 是 Playwright 侧的输入事件 —— 滚动劫持那族只认后者。
364
+ await page.evaluate(installMotionSampler, { exprs: expressions || {}, probeElements });
365
+
366
+ const shots = [];
367
+ let cdp = null;
368
+ if (wantShots) {
369
+ cdp = await page.context().newCDPSession(page);
370
+ cdp.on('Page.screencastFrame', (ev) => {
371
+ if (shots.length < 900) {
372
+ shots.push({ epochMs: ev.metadata.timestamp * 1000, buf: Buffer.from(ev.data, 'base64') });
373
+ }
374
+ cdp.send('Page.screencastFrameAck', { sessionId: ev.sessionId }).catch(() => {});
375
+ });
376
+ await cdp.send('Page.startScreencast', {
377
+ format: 'jpeg', quality: shotQuality, everyNthFrame: 1,
378
+ maxWidth: shotMaxW, maxHeight: shotMaxH,
379
+ });
380
+ // 触发前先攒一两帧:t=0 的"起点画面"从这里来
381
+ await page.waitForTimeout(150);
382
+ }
383
+
384
+ let clickNote = null;
385
+ let triggerNote = null;
386
+ const t0 = Date.now(); // ← 时刻零:第一个动作派发的瞬间
387
+ if (click) {
388
+ try {
389
+ await page.click(click, { timeout: 2500, noWaitAfter: true });
390
+ } catch (e) { clickNote = `click "${click}" failed: ${String(e?.message || e).split('\n')[0]}`; }
391
+ }
392
+ let trigP = null;
393
+ if (trigger) {
394
+ trigP = page.evaluate(`(async () => { ${trigger} })()`)
395
+ .catch((e) => { triggerNote = `trigger error: ${e?.message || e}`; });
396
+ }
397
+ let duringNote = null;
398
+ let duringP = null;
399
+ if (typeof during === 'function') {
400
+ duringP = Promise.resolve().then(() => during(page))
401
+ .then((note) => { if (typeof note === 'string') duringNote = note; })
402
+ .catch((e) => { duringNote = `during error: ${e?.message || e}`; });
403
+ }
404
+
405
+ await page.waitForTimeout(durationMs + 100);
406
+ if (cdp) await cdp.send('Page.stopScreencast').catch(() => {});
407
+ if (trigP) await Promise.race([trigP, new Promise((r) => { setTimeout(r, 10); })]);
408
+ // during 是按时间预算自己收尾的(wheelScroll 按 elapsed 配额派发),多等一秒拿它的说明
409
+ if (duringP) await Promise.race([duringP, new Promise((r) => { setTimeout(r, 1000); })]);
410
+
411
+ const raw = await page.evaluate(collectMotionSampler);
412
+ if (!raw) throw new Error('motion sampler vanished from the page (a reload during recording?)');
413
+
414
+ const rel = (perfNow) => (raw.timeOrigin + perfNow) - t0;
415
+ return {
416
+ shots: shots.map((s) => ({ tMs: s.epochMs - t0, buf: s.buf }))
417
+ .sort((a, b) => a.tMs - b.tMs),
418
+ rafTs: raw.rafTs,
419
+ rows: raw.rows.map((r) => ({ ...r, t: rel(r.t) })),
420
+ errs: raw.errs,
421
+ audio: raw.audio.map((a) => ({ t: rel(a.t), what: a.what })),
422
+ elems: (raw.elems || []).map((e) => ({ ...e, samples: e.samples.map((s) => [Math.round(rel(s[0])), s[1], s[2], s[3], s[4]]) })),
423
+ clickNote,
424
+ triggerNote,
425
+ duringNote,
426
+ captureNote: wantShots
427
+ ? `screencast captured ${shots.length} frames over ${((durationMs + 250) / 1000).toFixed(1)}s`
428
+ + (shots.length < 2 ? ' — the page never repainted; is anything actually animating?' : '')
429
+ : null,
430
+ };
431
+ }
432
+
433
+ /**
434
+ * 挑好的帧 → 一张带时间戳标注的 contact sheet(png buffer)。
435
+ * crop(可选)= {x,y,w,h},**CSS 视口坐标系**;screencast 帧是视口按 maxW 缩过的,
436
+ * 这里用 帧宽/cropRefW 把裁剪框换算到帧坐标(调用方传 cropRefW = viewport 宽)。
437
+ */
438
+ export async function composeSheet(picked, { crop = null, cropRefW = null } = {}) {
439
+ const { default: sharp } = await import('sharp');
440
+ const valid = picked.filter(Boolean);
441
+ if (!valid.length) throw new Error('no frames captured to compose');
442
+
443
+ const meta = await sharp(valid[0].frame.buf).metadata();
444
+ let srcW = meta.width; let srcH = meta.height;
445
+ let extract = null;
446
+ if (crop && cropRefW) {
447
+ const k = meta.width / cropRefW;
448
+ const left = Math.max(0, Math.round(crop.x * k));
449
+ const top = Math.max(0, Math.round(crop.y * k));
450
+ extract = {
451
+ left,
452
+ top,
453
+ width: Math.max(8, Math.min(Math.round(crop.w * k), srcW - left)),
454
+ height: Math.max(8, Math.min(Math.round(crop.h * k), srcH - top)),
455
+ };
456
+ srcW = extract.width; srcH = extract.height;
457
+ }
458
+
459
+ const L = sheetLayout(valid.length, srcW, srcH);
460
+ const composites = [];
461
+ const labels = [];
462
+ for (let i = 0; i < valid.length; i += 1) {
463
+ const col = i % L.cols; const row = Math.floor(i / L.cols);
464
+ const left = L.gap + col * (L.cellW + L.gap);
465
+ const top = L.gap + row * (L.cellH + L.gap);
466
+ let img = sharp(valid[i].frame.buf);
467
+ if (extract) img = img.extract(extract);
468
+ composites.push({
469
+ input: await img.resize(L.cellW, L.cellH, { fit: 'fill' }).png().toBuffer(),
470
+ left, top,
471
+ });
472
+ const want = Math.round(valid[i].want);
473
+ const actual = Math.round(valid[i].actual);
474
+ // painted = 这帧实际合成的时刻;差得远 = 这段时间页面没重绘(画面就是没变)
475
+ const label = Math.abs(actual - want) <= 12 ? `t=${want}ms` : `t=${want}ms (painted ${actual})`;
476
+ labels.push({ left, top, label });
477
+ }
478
+
479
+ const fontH = Math.max(13, Math.round(L.cellH * 0.055));
480
+ const labelSvg = [
481
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${L.sheetW}" height="${L.sheetH}">`,
482
+ ...labels.map((l) => {
483
+ const w = l.label.length * fontH * 0.62 + 12;
484
+ return `<rect x="${l.left}" y="${l.top}" width="${w.toFixed(0)}" height="${fontH + 10}" fill="#000" fill-opacity="0.72"/>`
485
+ + `<text x="${l.left + 6}" y="${l.top + fontH + 2}" font-size="${fontH}" fill="#fff" font-family="monospace">${esc(l.label)}</text>`;
486
+ }),
487
+ '</svg>',
488
+ ].join('');
489
+
490
+ const buf = await sharp({
491
+ create: { width: L.sheetW, height: L.sheetH, channels: 3, background: '#15150f' },
492
+ })
493
+ .composite([...composites, { input: Buffer.from(labelSvg), left: 0, top: 0 }])
494
+ .png()
495
+ .toBuffer();
496
+ return { buf, layout: L };
497
+ }
498
+
499
+ /**
500
+ * 录到的全部帧 → webm(vp8,给**用户**看的真动图;模型视觉通道只吃静帧)。
501
+ * ffmpeg concat demuxer 按真实帧间距排时刻,不匀速化 —— 卡顿保真。
502
+ */
503
+ export async function encodeWebm(shots, outAbs) {
504
+ if (shots.length < 2) throw new Error('need at least 2 captured frames to encode video');
505
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'nd-motion-'));
506
+ try {
507
+ const lines = ['ffconcat version 1.0'];
508
+ for (let i = 0; i < shots.length; i += 1) {
509
+ const f = path.join(tmp, `f${String(i).padStart(4, '0')}.jpg`);
510
+ await fs.writeFile(f, shots[i].buf);
511
+ const dur = i + 1 < shots.length
512
+ ? Math.max(0.008, (shots[i + 1].tMs - shots[i].tMs) / 1000)
513
+ : 0.35;
514
+ lines.push(`file '${f}'`, `duration ${dur.toFixed(4)}`);
515
+ }
516
+ const list = path.join(tmp, 'list.txt');
517
+ await fs.writeFile(list, lines.join('\n'));
518
+ await fs.mkdir(path.dirname(outAbs), { recursive: true });
519
+ // ⛔ 不用 execFileSync:会冻死事件循环(docx 引擎那边真踩过)
520
+ await execFileP('ffmpeg', [
521
+ '-y', '-f', 'concat', '-safe', '0', '-i', list,
522
+ '-c:v', 'libvpx', '-b:v', '1M', '-deadline', 'realtime', '-cpu-used', '8',
523
+ '-pix_fmt', 'yuv420p', '-vf', 'pad=ceil(iw/2)*2:ceil(ih/2)*2',
524
+ outAbs,
525
+ ], { timeout: 60_000 });
526
+ const st = await fs.stat(outAbs);
527
+ return { bytes: st.size };
528
+ } finally {
529
+ await fs.rm(tmp, { recursive: true, force: true }).catch(() => {});
530
+ }
531
+ }
532
+
533
+ /** 帧健康 + 音频事件 → caption 文本行(两个工具共用同一份措辞)。 */
534
+ export function motionCaptionLines(rec) {
535
+ const lines = [];
536
+ const fh = frameHealth(rec.rafTs);
537
+ if (fh.ok) {
538
+ lines.push(
539
+ `frame health: ~${fh.fps.toFixed(0)}fps, p95 ${fh.p95.toFixed(1)}ms, worst ${fh.worst.toFixed(0)}ms`
540
+ + `, ${fh.droppedPct.toFixed(0)}% frames >25ms`
541
+ + (fh.freezes ? `, ${fh.freezes} freeze(s) >120ms` : '')
542
+ + ' (1-vCPU box + recording overhead — read as A/B and orders of magnitude, not as the user device)',
543
+ );
544
+ }
545
+ if (rec.audio.length) {
546
+ lines.push(`audio events: ${rec.audio.slice(0, 8).map((a) => `${Math.round(a.t)}ms ${a.what}`).join(' · ')}`
547
+ + (rec.audio.length > 8 ? ` (+${rec.audio.length - 8} more)` : ''));
548
+ }
549
+ if (rec.captureNote) lines.push(rec.captureNote);
550
+ if (rec.clickNote) lines.push(rec.clickNote);
551
+ if (rec.triggerNote) lines.push(rec.triggerNote);
552
+ if (rec.duringNote) lines.push(rec.duringNote);
553
+ return lines;
554
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * helpers/motion-scroll.js — 录制期的真滚轮驱动 + 元素探针报告(2026-08-21)
3
+ *
4
+ * 跟 motion-lab.js 配套、对页面无知:浏览通道(参考站)和感知通道(自己的产物)共用。
5
+ *
6
+ * - wheelScroll:在 recordMotion 的 `during` 里跑 —— 按 16ms 一 tick 派发真滚轮事件,
7
+ * 跟 profile_scroll 同一条输入路径(滚动劫持那族只认这个,window.scrollTo 对它们无效)。
8
+ * - elementMotionReport:motion-lab 元素探针的逐帧样本 → "谁在动、怎么动"的人话。
9
+ * 样本是文档坐标,所以滚动本身不算动;算的是入场(位移+透明度)、视差(位移跟
10
+ * 滚动量不成比例)、缩放、固定元素随视口走。
11
+ */
12
+
13
+ /**
14
+ * 真滚轮滚 px 像素,摊在 durationMs 里。返回一句给 caption 的话(派发了多少、页面真滚了多少)。
15
+ * @param {import('playwright').Page} page
16
+ */
17
+ export async function wheelScroll(page, { px, durationMs, at = null }) {
18
+ const vp = page.viewportSize() || { width: 1280, height: 800 };
19
+ const [cx, cy] = at || [Math.floor(vp.width / 2), Math.floor(vp.height / 2)];
20
+ await page.mouse.move(cx, cy);
21
+ const y0 = await page.evaluate(() => window.scrollY).catch(() => 0);
22
+ // 按**时间预算**派发,不按固定 tick 数:每次 mouse.wheel 在这台 1 vCPU 机器上是一次
23
+ // 往返(10~30ms 不等),固定 16ms × N 次会拖过录制窗口(真跑逮到:劫持说明没赶上
24
+ // caption)。改成"过了多少时间就该派到多少像素",到点必停。
25
+ const t0 = Date.now();
26
+ let dispatched = 0;
27
+ while (true) {
28
+ const elapsed = Date.now() - t0;
29
+ const due = Math.min(px, px * (elapsed / durationMs));
30
+ const delta = due - dispatched;
31
+ if (Math.abs(delta) >= 1) { await page.mouse.wheel(0, delta); dispatched += delta; }
32
+ if (elapsed >= durationMs) break;
33
+ await page.waitForTimeout(16);
34
+ }
35
+ if (Math.abs(px - dispatched) >= 1) { await page.mouse.wheel(0, px - dispatched); dispatched = px; }
36
+ await page.waitForTimeout(80);
37
+ const y1 = await page.evaluate(() => window.scrollY).catch(() => y0);
38
+ const moved = y1 - y0;
39
+ const note = `wheel-scrolled ${Math.round(px)}px over ${durationMs}ms → window moved ${Math.round(moved)}px`;
40
+ if (px > 200 && Math.abs(moved) < Math.abs(px) * 0.5) {
41
+ return `${note} — ⚠ the page moved far less than dispatched: scroll hijacking (Lenis/Locomotive-style smooth scroll) or a scroll container that is not the window`;
42
+ }
43
+ return note;
44
+ }
45
+
46
+ /** 一个元素的样本 → 统计 */
47
+ function summarize(e) {
48
+ const s = e.samples;
49
+ if (s.length < 3) return null;
50
+ let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -Infinity;
51
+ let minO = Infinity; let maxO = -Infinity; let minS = Infinity; let maxS = -Infinity;
52
+ for (const [, x, y, o, sc] of s) {
53
+ if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y;
54
+ if (o < minO) minO = o; if (o > maxO) maxO = o; if (sc < minS) minS = sc; if (sc > maxS) maxS = sc;
55
+ }
56
+ const dx = maxX - minX; const dy = maxY - minY;
57
+ const travel = Math.hypot(dx, dy);
58
+ const fade = maxO - minO;
59
+ const scale = maxS - minS;
60
+ // 首末变化时刻:第一个/最后一个和起点不同的样本
61
+ const [t00, x0, y0, o0, s0] = s[0];
62
+ const diff = (r) => Math.abs(r[1] - x0) > 1 || Math.abs(r[2] - y0) > 1 || Math.abs(r[3] - o0) > 0.02 || Math.abs(r[4] - s0) > 0.01;
63
+ let start = null; let end = null;
64
+ for (const r of s) { if (diff(r)) { if (start == null) start = r[0]; end = r[0]; } }
65
+ return { sel: e.sel, fixed: !!e.fixed, travel, dx, dy, fade, scale, from: { x: x0, y: y0, o: o0, s: s0 }, to: { x: s[s.length - 1][1], y: s[s.length - 1][2], o: s[s.length - 1][3], s: s[s.length - 1][4] }, startMs: start, endMs: end, t0: t00, n: s.length };
66
+ }
67
+
68
+ /**
69
+ * 元素探针样本 → 报告。`scrolledPx`(录制期 window 真滚了多少)用来把"随视口走的固定
70
+ * 元素"和"视差"从"真入场"里分开:文档坐标下固定元素的位移 ≈ 滚动量,视差 ≈ 滚动量的
71
+ * 一个比例,入场动画的位移跟滚动量无关。
72
+ */
73
+ export function elementMotionReport(elems, { scrolledPx = 0 } = {}) {
74
+ const rows = (elems || []).map(summarize).filter(Boolean);
75
+ const moving = rows.filter(r => r.travel > 3 || r.fade > 0.05 || r.scale > 0.02);
76
+ const classify = (r) => {
77
+ const tags = [];
78
+ // fixed/sticky 层(含其子元素):文档坐标天然随视口走,报成"固定层"而不是视差/位移
79
+ if (r.fixed && r.travel > 3) tags.push(`fixed/sticky 层(随视口走,dy ${r.dy}${scrolledPx ? `,滚动量 ${Math.round(scrolledPx)}` : ''})`);
80
+ // 视差:位移是滚动量的一个明显比例(≥15%)且只在竖向、不带淡入 —— 入场那种 40~120px
81
+ // 的平移在上千像素的滚动里占比很小,归"位移"不归视差
82
+ else if (scrolledPx && Math.abs(r.dy) >= Math.abs(scrolledPx) * 0.15 && Math.abs(r.dy) < Math.abs(scrolledPx) * 0.95 && Math.abs(r.dx) < 8 && r.fade < 0.05) tags.push(`视差(位移≈滚动量的 ${(Math.abs(r.dy) / Math.abs(scrolledPx) * 100).toFixed(0)}%)`);
83
+ else if (r.travel > 3) tags.push(`位移 ${Math.round(r.travel)}px(dx ${r.dx}, dy ${r.dy})`);
84
+ if (r.fade > 0.05) tags.push(`透明度 ${r.from.o}→${r.to.o}`);
85
+ if (r.scale > 0.02) tags.push(`缩放 ${r.from.s}→${r.to.s}`);
86
+ return tags;
87
+ };
88
+ const score = (r) => r.travel + r.fade * 200 + r.scale * 200;
89
+ // 固定层排最后:它们"动"是因为视口动了,真正有信息量的是前面那些
90
+ const items = moving.map(r => ({ ...r, tags: classify(r) }))
91
+ .sort((a, b) => (a.fixed === b.fixed ? score(b) - score(a) : (a.fixed ? 1 : -1)));
92
+ return { total: rows.length, moving: items.length, items };
93
+ }
94
+
95
+ /** 报告 → 给 agent 的行。固定层的子元素合并成一行(真跑时一个页头 15 个链接刷了 15 行)。 */
96
+ export function elementMotionLines(rep, { max = 10 } = {}) {
97
+ if (!rep || !rep.total) return ['element probe: no elements sampled'];
98
+ if (!rep.moving) return [`element probe: ${rep.total} elements watched, none moved/faded/scaled during the recording`];
99
+ const free = rep.items.filter(r => !r.fixed);
100
+ const fixed = rep.items.filter(r => r.fixed);
101
+ const lines = [`element probe: ${rep.moving} of ${rep.total} watched elements moved during the recording:`];
102
+ for (const r of free.slice(0, max)) {
103
+ const when = r.startMs != null ? `${Math.max(0, Math.round(r.startMs))}→${Math.round(r.endMs)}ms` : '';
104
+ lines.push(` ${r.sel} ${r.tags.join(' · ')}${when ? ` [${when}]` : ''}`);
105
+ }
106
+ if (free.length > max) lines.push(` (+${free.length - max} more)`);
107
+ if (fixed.length) {
108
+ const names = [...new Set(fixed.map(r => r.sel))].slice(0, 4).join(', ');
109
+ lines.push(` fixed/sticky 层 ×${fixed.length}(${names}${fixed.length > 4 ? ' …' : ''})随视口走 —— 页头/浮层那类,不是页面内容在动`);
110
+ }
111
+ return lines;
112
+ }