@xiaobuyu/nodesign 0.0.1 → 0.0.3

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 (358) hide show
  1. package/README.md +156 -106
  2. package/bin/nodesign.js +0 -0
  3. package/package.json +1 -1
  4. package/server/api/README.md +45 -45
  5. package/server/api/_guard.js +75 -75
  6. package/server/api/admin.js +187 -187
  7. package/server/api/assets/docx-page.js +103 -103
  8. package/server/api/assets/helpers.js +42 -22
  9. package/server/api/assets/notes.js +194 -144
  10. package/server/api/assets.js +891 -896
  11. package/server/api/board.js +134 -57
  12. package/server/api/browse.js +138 -138
  13. package/server/api/canvas.js +444 -444
  14. package/server/api/chatai.js +324 -324
  15. package/server/api/exports/build-standalone.js +980 -980
  16. package/server/api/exports/cards.js +118 -118
  17. package/server/api/exports/docx-pdf.js +46 -46
  18. package/server/api/exports/export-page.js +142 -142
  19. package/server/api/exports/handoff.js +147 -147
  20. package/server/api/exports.js +690 -690
  21. package/server/api/instruction.js +75 -75
  22. package/server/api/me.js +116 -116
  23. package/server/api/memory.js +185 -185
  24. package/server/api/pending-changes.js +366 -366
  25. package/server/api/plugins.js +164 -164
  26. package/server/api/projects.js +220 -220
  27. package/server/api/publish.js +64 -64
  28. package/server/api/recent.js +73 -73
  29. package/server/api/sessions.js +589 -589
  30. package/server/api/skills.js +77 -77
  31. package/server/api/standalone-fit.js +168 -168
  32. package/server/api/turn-compose.js +180 -180
  33. package/server/api/turn-inflight.js +44 -44
  34. package/server/auth/README.md +17 -17
  35. package/server/auth/middleware.js +138 -138
  36. package/server/auth/origin-guard.js +71 -71
  37. package/server/auth/session.js +108 -108
  38. package/server/auth/tier.js +113 -113
  39. package/server/auth/users-store.js +348 -348
  40. package/server/edit/README.md +29 -29
  41. package/server/engine/README.md +113 -113
  42. package/server/engine/agent/agent-shared.js +550 -550
  43. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
  44. package/server/engine/agent/auto-mode-rules.js +78 -78
  45. package/server/engine/agent/context.js +418 -418
  46. package/server/engine/agent/events.js +465 -464
  47. package/server/engine/agent/hooks/canvas-validate.js +199 -199
  48. package/server/engine/agent/hooks/failure.js +120 -120
  49. package/server/engine/agent/hooks/file-events.js +75 -75
  50. package/server/engine/agent/hooks/lifecycle.js +233 -233
  51. package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
  52. package/server/engine/agent/hooks/post-guidance.js +206 -206
  53. package/server/engine/agent/hooks/post-subagent-report.js +104 -104
  54. package/server/engine/agent/hooks/post-trim.js +58 -58
  55. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
  56. package/server/engine/agent/hooks/pre-defaults.js +84 -84
  57. package/server/engine/agent/hooks/pre-injectors.js +277 -277
  58. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
  59. package/server/engine/agent/hooks/pre-starter-files.js +61 -61
  60. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
  61. package/server/engine/agent/hooks/site-validate.js +94 -94
  62. package/server/engine/agent/hooks/tool-prompts.js +31 -31
  63. package/server/engine/agent/hooks/turn-state-memory.js +56 -56
  64. package/server/engine/agent/hooks/user-prompt-submit.js +258 -215
  65. package/server/engine/agent/hooks.js +354 -354
  66. package/server/engine/agent/init-contract.js +69 -69
  67. package/server/engine/agent/isolation.js +159 -159
  68. package/server/engine/agent/model-table.js +292 -292
  69. package/server/engine/agent/plugin-loader.js +266 -266
  70. package/server/engine/agent/prompts/nodesign-prelude.md +388 -336
  71. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
  72. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
  73. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
  74. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
  75. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
  76. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
  77. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
  78. package/server/engine/agent/prompts/tools/site-reference.md +106 -106
  79. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
  80. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
  81. package/server/engine/agent/session-loop.js +1035 -1024
  82. package/server/engine/agent/session-model.js +164 -164
  83. package/server/engine/agent/skill.js +204 -204
  84. package/server/engine/agent/task-events.js +78 -78
  85. package/server/engine/agents/ds-extractor.md +132 -132
  86. package/server/engine/agents/explorer.md +190 -190
  87. package/server/engine/agents/index.js +230 -230
  88. package/server/engine/agents/schemas/design-system.json +156 -156
  89. package/server/engine/agents/schemas/tweak-schema.json +113 -113
  90. package/server/engine/agents/tweak-proposer.md +127 -127
  91. package/server/engine/agents/vision-checker.md +254 -254
  92. package/server/engine/browse/capture.js +422 -422
  93. package/server/engine/browse/card.js +137 -137
  94. package/server/engine/browse/handover.js +59 -59
  95. package/server/engine/browse/page-digest.js +119 -119
  96. package/server/engine/browse/refs.js +196 -196
  97. package/server/engine/browse/registry.js +307 -307
  98. package/server/engine/browse/screencast.js +170 -170
  99. package/server/engine/browse/state.js +121 -121
  100. package/server/engine/chatai/chat-log.js +79 -79
  101. package/server/engine/chatai/index.js +193 -193
  102. package/server/engine/chatai/openai-compat.js +152 -152
  103. package/server/engine/chatai/orchestrate.js +309 -309
  104. package/server/engine/chatai/perform.js +40 -40
  105. package/server/engine/chatai/summarize.js +79 -79
  106. package/server/engine/mcp/capability-gate.js +52 -52
  107. package/server/engine/mcp/index.js +351 -339
  108. package/server/engine/mcp/param-sanitizer.js +142 -142
  109. package/server/engine/mcp/tools/arrange-on-board.js +124 -124
  110. package/server/engine/mcp/tools/artifact-session.js +278 -278
  111. package/server/engine/mcp/tools/browse-computer.js +421 -421
  112. package/server/engine/mcp/tools/browse-find-batch.js +183 -183
  113. package/server/engine/mcp/tools/browse-screenshot.js +157 -157
  114. package/server/engine/mcp/tools/browse.js +480 -480
  115. package/server/engine/mcp/tools/build-docx.js +101 -101
  116. package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
  117. package/server/engine/mcp/tools/create-on-board.js +152 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/edit-sketch.js +167 -0
  120. package/server/engine/mcp/tools/explain-style.js +237 -237
  121. package/server/engine/mcp/tools/export-handoff.js +133 -133
  122. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  123. package/server/engine/mcp/tools/generate-image.js +842 -842
  124. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  125. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  126. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  127. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  128. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  129. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  130. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  131. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  132. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  133. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  134. package/server/engine/mcp/tools/highlight.js +68 -68
  135. package/server/engine/mcp/tools/list-pages.js +222 -222
  136. package/server/engine/mcp/tools/look-at-board.js +124 -0
  137. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  138. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  139. package/server/engine/mcp/tools/organize-board.js +64 -64
  140. package/server/engine/mcp/tools/paint-still.js +434 -434
  141. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  142. package/server/engine/mcp/tools/preview-deck.js +130 -130
  143. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  144. package/server/engine/mcp/tools/publish-site.js +102 -102
  145. package/server/engine/mcp/tools/query-elements.js +195 -195
  146. package/server/engine/mcp/tools/read-board.js +190 -103
  147. package/server/engine/mcp/tools/read-document.js +93 -93
  148. package/server/engine/mcp/tools/read-page.js +264 -264
  149. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  150. package/server/engine/mcp/tools/read-user-view.js +59 -0
  151. package/server/engine/mcp/tools/record-decision.js +140 -140
  152. package/server/engine/mcp/tools/relate-on-board.js +112 -110
  153. package/server/engine/mcp/tools/report-issue.js +115 -115
  154. package/server/engine/mcp/tools/roll-film.js +266 -266
  155. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  156. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  157. package/server/engine/mcp/tools/screenshot.js +583 -583
  158. package/server/engine/mcp/tools/sketch-on-board.js +337 -0
  159. package/server/engine/mcp/tools/tier-gate.js +96 -96
  160. package/server/engine/mcp/tools/trace-motion.js +215 -215
  161. package/server/engine/mcp/tools/web-search.js +548 -548
  162. package/server/engine/mcp/tools/write-on-board.js +130 -0
  163. package/server/engine/motion/inventory.js +349 -349
  164. package/server/engine/perception/session.js +235 -235
  165. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +199 -192
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  174. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +169 -162
  175. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  176. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  177. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +235 -228
  178. 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 -78
  179. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  180. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +330 -323
  181. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  182. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  183. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  185. package/server/engine/runs/active-runs.js +698 -698
  186. package/server/engine/runs/board-tasklist.js +130 -0
  187. package/server/engine/runs/live-turn.js +252 -252
  188. package/server/engine/runs/store.js +364 -364
  189. package/server/engine/runs/turn-relay.js +217 -217
  190. package/server/engine/runtime/workspace.js +164 -164
  191. package/server/index.js +3 -0
  192. package/server/lib/artifact-file-path.js +80 -80
  193. package/server/lib/artifact-target.js +284 -284
  194. package/server/lib/asset-refs.js +239 -239
  195. package/server/lib/async-queue.js +104 -104
  196. package/server/lib/auto-relations.js +130 -130
  197. package/server/lib/binding-types.js +72 -60
  198. package/server/lib/board-graph-export.js +191 -0
  199. package/server/lib/board-groups.js +167 -0
  200. package/server/lib/board-hero.js +76 -0
  201. package/server/lib/board-kind-sizes.js +85 -62
  202. package/server/lib/board-relations.js +133 -133
  203. package/server/lib/browse-proxy.js +180 -180
  204. package/server/lib/canvas-id.js +36 -36
  205. package/server/lib/chalk.js +121 -0
  206. package/server/lib/cover.js +227 -227
  207. package/server/lib/danbooru-tags.js +291 -291
  208. package/server/lib/doc-extract.js +156 -156
  209. package/server/lib/docx/build-from-source.js +260 -260
  210. package/server/lib/docx/build.js +490 -490
  211. package/server/lib/docx/dump-styles.js +287 -287
  212. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  213. package/server/lib/docx/merge-runs.js +129 -129
  214. package/server/lib/docx/numbering.js +218 -218
  215. package/server/lib/docx/order.js +152 -152
  216. package/server/lib/docx/rawzip.js +172 -172
  217. package/server/lib/docx/render.js +92 -92
  218. package/server/lib/docx/text-lint.js +225 -225
  219. package/server/lib/docx/tokens.js +335 -335
  220. package/server/lib/docx/units.js +29 -29
  221. package/server/lib/docx/xml.js +291 -291
  222. package/server/lib/docx-pages.js +187 -187
  223. package/server/lib/export-collect.js +289 -289
  224. package/server/lib/export-package.js +210 -210
  225. package/server/lib/html-srcset.js +145 -145
  226. package/server/lib/image-variant.js +460 -460
  227. package/server/lib/ingress/forward-openai-chat.js +305 -305
  228. package/server/lib/ingress/openai-chat.js +491 -491
  229. package/server/lib/ingress/session-notice.js +66 -66
  230. package/server/lib/ingress/session-routes.js +68 -68
  231. package/server/lib/ingress/slot-probe.js +130 -130
  232. package/server/lib/ingress/upstream-billing.js +52 -52
  233. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  234. package/server/lib/ingress/upstream-truncation.js +48 -48
  235. package/server/lib/issues-store.js +182 -182
  236. package/server/lib/kinds/deck.js +72 -72
  237. package/server/lib/kinds/docx.js +283 -283
  238. package/server/lib/kinds/file-kinds.js +113 -113
  239. package/server/lib/kinds/index.js +202 -202
  240. package/server/lib/kinds/site.js +161 -161
  241. package/server/lib/model-ingress.js +543 -543
  242. package/server/lib/moderation.js +255 -255
  243. package/server/lib/notice-store.js +103 -103
  244. package/server/lib/plugin-install.js +104 -104
  245. package/server/lib/plugin-validator.js +721 -721
  246. package/server/lib/publish-store.js +115 -115
  247. package/server/lib/quick-summary.js +52 -52
  248. package/server/lib/quota.js +270 -270
  249. package/server/lib/rate-window.js +29 -29
  250. package/server/lib/reference-assets.js +92 -92
  251. package/server/lib/region-shot.js +135 -135
  252. package/server/lib/safe-path.js +73 -73
  253. package/server/lib/sdk-session.js +33 -33
  254. package/server/lib/showcase-store.js +92 -92
  255. package/server/lib/site-publish.js +465 -465
  256. package/server/lib/sketch-layout.js +277 -0
  257. package/server/lib/ssrf-guard.js +432 -432
  258. package/server/lib/task-scan.js +158 -158
  259. package/server/lib/tavern-json.js +154 -154
  260. package/server/lib/video-variant.js +237 -237
  261. package/server/lib/workspace-path.js +33 -33
  262. package/server/ops/fix-sdk-musl.mjs +40 -40
  263. package/server/ops/install-macos-fonts.sh +114 -114
  264. package/server/ops/macos-fonts.conf +336 -336
  265. package/server/ops/sandbox-shim/bwrap +57 -57
  266. package/server/projects/assets-summary.js +119 -119
  267. package/server/projects/auto-name.js +53 -53
  268. package/server/projects/board-limits.js +12 -0
  269. package/server/projects/board-sanitize.js +252 -0
  270. package/server/projects/board-store.js +507 -640
  271. package/server/projects/move-entry.js +103 -103
  272. package/server/projects/store.js +270 -270
  273. package/server/projects/ui-config.js +60 -54
  274. package/server/projects/viewpoint-store.js +78 -0
  275. package/server/projects/workspace-templates.js +53 -53
  276. package/server/projects/workspace.js +1025 -1025
  277. package/server/runtime/local-config.js +177 -177
  278. package/server/runtime/profile.js +72 -72
  279. package/server/services/rembg-service.py +334 -334
  280. package/server/shared/README.md +25 -25
  281. package/server/shared/deck.js +54 -54
  282. package/server/shared/time.js +49 -49
  283. package/server/style-pipeline/README.md +53 -53
  284. package/server/ws/broker.js +39 -36
  285. package/server/ws/browse-channel.js +177 -177
  286. package/server/ws/index.js +386 -386
  287. package/web/README.md +89 -89
  288. package/web/dist/assets/BrowserWindow-P_vIdh-G.js +11 -0
  289. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-BpLv5082.js} +3 -3
  290. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-DSAU1-tD.js} +4 -4
  291. package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
  292. package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
  293. package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
  294. package/web/dist/assets/arc-D_khH_xN.js +1 -0
  295. package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
  296. package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
  297. package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
  298. package/web/dist/assets/channel-DGkccMO1.js +1 -0
  299. package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
  300. package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
  301. package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
  302. package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
  303. package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
  304. package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
  305. package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
  306. package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
  307. package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
  308. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
  309. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
  310. package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
  311. package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
  312. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
  313. package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
  314. package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
  315. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  316. package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
  317. package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
  318. package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
  319. package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
  320. package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
  321. package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
  322. package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
  323. package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
  324. package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
  325. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
  326. package/web/dist/assets/index-GAamh1p4.js +2152 -0
  327. package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
  328. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  329. package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
  330. package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
  331. package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
  332. package/web/dist/assets/layout-CnXiF_NB.js +1 -0
  333. package/web/dist/assets/linear-BMW0u6G6.js +1 -0
  334. package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
  335. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  336. package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
  337. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
  338. package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
  339. package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
  340. package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
  341. package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
  342. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-KAfChbGV.js} +1 -1
  343. package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
  344. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
  345. package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
  346. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
  347. package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
  348. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
  349. package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
  350. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
  351. package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
  352. package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
  353. package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
  354. package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
  355. package/web/dist/index.html +17 -17
  356. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
  357. package/web/dist/assets/SiteWindow-D6eS-2gM.js +0 -18
  358. package/web/dist/assets/index-Cbl3ATvp.js +0 -2125
@@ -1,305 +1,305 @@
1
- /**
2
- * mcp/tools/profile-scroll.js — profile_scroll(2026-08-18)
3
- *
4
- * ## 为什么有这个工具
5
- *
6
- * 用户说"往下滑动一顿一顿的",这在设计工具里是最常见的反馈之一。而在这之前
7
- * 平台给 agent 的全部观测能力是「截一张静态图」—— 帧时、掉帧、图片体积、
8
- * 长任务一个都看不到。一个 agent 的实际绕路是:起 `python3 -m http.server`
9
- * → 从仓库 node_modules 直接 import playwright → 手写 rAF 采样器 → 逐项关掉
10
- * 嫌疑元素做 A/B。它量出来的结论一个都猜不出来:基线 63% 掉帧,单独关视差
11
- * 64%(没用),单独关全屏胶片颗粒层 40%,两个都关 5% —— 是**叠加效应**,
12
- * 真凶是「33.7MB 的 PNG + 一个带 mix-blend-mode 的全屏 fixed 层」这对组合。
13
- * 没有测量,它大概率只会把用户新抱怨的那个功能拆掉交差,剩下 40% 的掉帧
14
- * 原封不动留在站里。
15
- *
16
- * ## 数字怎么读
17
- *
18
- * ⚠️ 这台机器 1 vCPU,而且截图 worker 跟 server 抢同一颗核。**绝对值不代表
19
- * 用户设备上的体验**,它的用处是 A/B:改一处、再量一次、看方向。返回文本里
20
- * 会重复这句话,别让 agent 拿绝对数字去跟用户汇报。
21
- */
22
-
23
- import { tool } from '@anthropic-ai/claude-agent-sdk';
24
- import { z } from 'zod';
25
- import { resolveCanvasTarget, CANVAS_PATH_DESC, requireBrowsable } from '../../../lib/artifact-target.js';
26
- import { openArtifactPage, launchPerceptionBrowser, degradedNote } from './helpers/perception-page.js';
27
-
28
- const DEVICE_W = { desktop: 1440, tablet: 834, mobile: 390 };
29
- const DEVICE_H = { desktop: 900, tablet: 1112, mobile: 844 };
30
-
31
- /**
32
- * 页面里跑的采样器:**只测量,不自己滚**。
33
- *
34
- * ⛔ 前两版都是页面自己 `window.scrollBy` 推进的,两次都测错,原因不同:
35
- * v1「大跨步 + 每步 sleep」——合成器在步与步之间有空闲,坏页面照样报 3% 掉帧。
36
- * v2「rAF 里逐帧小步推」——压力对了,但**滚动方式还是假的**:程序化滚动
37
- * 不派发 wheel 事件,于是一整类最常见的"一顿一顿"完全隐形:
38
- * wheel handler 里做视差、滚动劫持(scrolljacking)、平滑滚库
39
- * (Locomotive/Lenis 那种自己接管滚动的)。用户的手指走的是 wheel 那条路。
40
- * 而且 `scroll-behavior: smooth` 会把程序化滚动变成动画,实测让量测范围
41
- * 悄悄缩到 3.5% —— 看着"滚完了",其实几乎没动。
42
- * 现在:滚动由外面用 CDP 派发**真 wheel 事件**(≈60Hz),这里只逐帧记 rAF 间隔。
43
- *
44
- * ⭐ 顺带白拿一个诊断:派发了多少像素 vs 页面实际走了多少像素。差得远
45
- * = 滚动被劫持了,这本身就是答案。
46
- */
47
- /* eslint-disable no-undef */
48
- function installSampler() {
49
- const st = {
50
- frames: [], longTasks: [], po: null, raf: 0, running: true,
51
- startedAt: performance.now(), last: performance.now(),
52
- container: null, startTop: 0,
53
- };
54
- window.__ndProf = st;
55
- try {
56
- st.po = new PerformanceObserver((l) => { for (const e of l.getEntries()) st.longTasks.push(e.duration); });
57
- st.po.observe({ entryTypes: ['longtask'] });
58
- } catch { /* 不支持就没有长任务数据 */ }
59
-
60
- // 滚的可能不是 window:应用式版面常常是某个内层容器在滚。wheel 事件落在
61
- // 指针下面那个元素上,所以内层容器**会**滚 —— 但 window.scrollY 不动,
62
- // 按它判断就会误报"这页滚不动"。先认出真正在滚的那个。
63
- const pickContainer = () => {
64
- const cx = Math.floor(window.innerWidth / 2);
65
- const cy = Math.floor(window.innerHeight / 2);
66
- let el = document.elementFromPoint(cx, cy);
67
- while (el && el !== document.body && el !== document.documentElement) {
68
- const cs = getComputedStyle(el);
69
- const scrollable = /(auto|scroll|overlay)/.test(`${cs.overflowY}`) && el.scrollHeight > el.clientHeight + 4;
70
- if (scrollable) return el;
71
- el = el.parentElement;
72
- }
73
- return document.scrollingElement || document.documentElement;
74
- };
75
- st.container = pickContainer();
76
- st.startTop = st.container.scrollTop;
77
-
78
- const tick = () => {
79
- if (!st.running) return;
80
- const now = performance.now();
81
- st.frames.push(now - st.last);
82
- st.last = now;
83
- st.raf = requestAnimationFrame(tick);
84
- };
85
- st.raf = requestAnimationFrame(tick);
86
- return {
87
- scrollHeight: st.container.scrollHeight,
88
- clientHeight: st.container.clientHeight,
89
- isWindow: st.container === (document.scrollingElement || document.documentElement),
90
- };
91
- }
92
-
93
- function collectSampler() {
94
- const st = window.__ndProf;
95
- if (!st) return null;
96
- st.running = false;
97
- cancelAnimationFrame(st.raf);
98
- try { st.po?.disconnect(); } catch { /* */ }
99
-
100
- const imgs = performance.getEntriesByType('resource')
101
- .filter(e => e.initiatorType === 'img' || /\.(png|jpe?g|webp|avif|gif|svg)(\?|$)/i.test(e.name))
102
- .map(e => ({
103
- name: decodeURIComponent(e.name.split('?')[0].split('/').pop() || '').slice(0, 60),
104
- // encodedBodySize 是**这次传输**的字节;transferSize 命中缓存时是 0。
105
- // 感知通道现在带 X-ND-Raw,拿的是磁盘原图,所以这个数字就是交付站点的数字。
106
- bytes: e.encodedBodySize || e.transferSize || 0,
107
- }));
108
- imgs.sort((a, b) => b.bytes - a.bytes);
109
-
110
- return {
111
- frames: st.frames.slice(2), // 头两帧含启动抖动
112
- longTasks: st.longTasks,
113
- elapsedMs: performance.now() - st.startedAt,
114
- scrollHeight: st.container.scrollHeight,
115
- clientHeight: st.container.clientHeight,
116
- movedPx: st.container.scrollTop - st.startTop,
117
- imgCount: imgs.length,
118
- imgBytes: imgs.reduce((a, b) => a + b.bytes, 0),
119
- topImgs: imgs.slice(0, 5),
120
- layers: (() => {
121
- // 「合成层嫌疑」:全屏定位 + 带混合模式/滤镜/半透明的那些,
122
- // 就是那个真实案例里的另一半凶手
123
- const out = [];
124
- for (const el of document.querySelectorAll('*')) {
125
- const cs = getComputedStyle(el);
126
- if (cs.position !== 'fixed' && cs.position !== 'sticky') continue;
127
- const r = el.getBoundingClientRect();
128
- const big = r.width * r.height > window.innerWidth * window.innerHeight * 0.5;
129
- const heavy = cs.mixBlendMode !== 'normal' || cs.filter !== 'none'
130
- || cs.backdropFilter !== 'none' || (cs.opacity !== '1' && cs.opacity !== '');
131
- if (big && heavy) {
132
- const id = el.id ? `#${el.id}` : (typeof el.className === 'string' && el.className.trim()
133
- ? `.${el.className.trim().split(/\s+/)[0]}` : el.tagName.toLowerCase());
134
- out.push(`${id} [${cs.position}, blend=${cs.mixBlendMode}, filter=${cs.filter === 'none' ? '-' : 'yes'}, backdrop=${cs.backdropFilter === 'none' ? '-' : 'yes'}, opacity=${cs.opacity}]`);
135
- }
136
- if (out.length >= 5) break;
137
- }
138
- return out;
139
- })(),
140
- };
141
- }
142
- /* eslint-enable no-undef */
143
-
144
- const pct = (arr, p) => {
145
- if (!arr.length) return 0;
146
- const s = [...arr].sort((a, b) => a - b);
147
- return s[Math.min(s.length - 1, Math.floor((p / 100) * s.length))];
148
- };
149
- const kb = (b) => (b >= 1024 * 1024 ? `${(b / 1024 / 1024).toFixed(1)}MB` : `${Math.round(b / 1024)}KB`);
150
-
151
- export function makeProfileScrollTool({ workspaceRoot, projectId, sessionId }) {
152
- return tool(
153
- 'profile_scroll',
154
- `Measure how a page actually performs while scrolling. Loads it over http
155
- (same origin as the user preview), scrolls programmatically, and returns frame
156
- timing, dropped-frame ratio, long tasks, total image bytes and the heaviest
157
- images, plus any full-screen fixed/sticky layers with blend modes or filters.
158
-
159
- Scrolling is driven by REAL wheel events, so this also catches the other common
160
- cause of "one frame at a time": the page eating the wheel and scrolling itself
161
- (scroll hijacking, Lenis/Locomotive-style smooth scroll). The report says how
162
- many pixels were dispatched vs how many the page actually moved — if those
163
- disagree, that is your answer and frame times are beside the point.
164
-
165
- Use this the moment a user says scrolling feels heavy, janky, laggy or "one
166
- frame at a time" — do NOT start deleting features to guess which one it is.
167
- The one real case on record: baseline 63% dropped frames; killing the parallax
168
- alone changed nothing (64%); killing the full-screen grain layer alone got it to
169
- 40%; killing both got 5%; hiding images got 3%. It was an additive effect
170
- between 33.7MB of PNGs and one blended full-screen layer. Guessing would have
171
- removed the wrong thing and left most of the jank in place.
172
-
173
- Also worth running before you hand over any image-heavy page: total image bytes
174
- is the single most common cause, and generate_image now writes a .webp sibling
175
- next to every PNG for exactly this reason.
176
-
177
- ⚠️ This machine has 1 vCPU and the browser competes with the server for it. The
178
- absolute numbers are NOT what the user's device will see. Use them for A/B:
179
- measure, change one thing, measure again, compare direction. Never report the
180
- raw percentage to the user as if it were their experience.`,
181
- {
182
- path: z.string().optional().describe(CANVAS_PATH_DESC),
183
- device: z.enum(['desktop', 'tablet', 'mobile']).optional()
184
- .describe('Viewport preset (default desktop 1440×900). Mobile is where jank shows up worst.'),
185
- frames: z.number().int().min(30).max(300).optional()
186
- .describe('How many wheel steps to scroll across (default 120 ≈ a 2-second scroll at 60Hz). Real wheel events are dispatched at ~60Hz, the same rate a trackpad fires — so wheel handlers, scroll hijacking and smooth-scroll libraries are all in the measurement.'),
187
- },
188
- async ({ path: relPath, device, frames: framesArg }) => {
189
- const asText = (text) => ({ content: [{ type: 'text', text }] });
190
- const target = await resolveCanvasTarget(workspaceRoot, relPath, sessionId);
191
- if (!target) return { content: [{ type: 'text', text: 'No page found to profile.' }], isError: true };
192
- const guard = requireBrowsable(target);
193
- if (guard) return { content: [{ type: 'text', text: guard }], isError: true };
194
-
195
- const dev = device || 'desktop';
196
- const viewport = { width: DEVICE_W[dev], height: DEVICE_H[dev] };
197
- const nFrames = framesArg ?? 120;
198
-
199
- let browser;
200
- try {
201
- browser = await launchPerceptionBrowser();
202
- const opened = await openArtifactPage(browser, {
203
- projectId, workspaceRoot, absPath: target.absPath, viewport,
204
- });
205
- await opened.goto();
206
- const page = opened.page;
207
-
208
- // 采样器先装上(它顺手认出真正在滚的那个容器),再从外面派发真 wheel
209
- const geom = await page.evaluate(installSampler);
210
- const scrollable = Math.max(0, geom.scrollHeight - geom.clientHeight);
211
- // 每帧推进量:把可滚范围在 nFrames 帧里走完。下限 6px —— 太小的话短页面
212
- // 一直停在原地,样本全是静止帧(那种"满分"没有意义)。
213
- const perFrame = Math.max(6, Math.round(scrollable / nFrames));
214
-
215
- // ⭐ 真 wheel,配速 ≈60Hz。配速是必要的:不配速的话 CDP 一秒能塞几百个
216
- // wheel,任何页面都会显得卡(假警报),而真人的滚轮/触摸板就是 60-120Hz。
217
- await page.mouse.move(Math.floor(viewport.width / 2), Math.floor(viewport.height / 2));
218
- const t0 = Date.now();
219
- for (let i = 0; i < nFrames; i += 1) {
220
- await page.mouse.wheel(0, perFrame);
221
- const wait = (t0 + (i + 1) * 16) - Date.now();
222
- if (wait > 1) await page.waitForTimeout(wait);
223
- }
224
- await page.waitForTimeout(120); // 让最后几帧和长任务落账
225
- const r = await page.evaluate(collectSampler);
226
-
227
- // ⛔ **不再过滤长帧**。原来是 `frames.filter(f => f > 0 && f < 2000)`,
228
- // 于是一个 3017ms 的冻结**被当成脏数据剔掉**,剩下的帧算出 `dropped 0%`
229
- // 且不作任何提示 —— 越卡越可能被删。A/B 甚至会反过来(把页面改得更卡
230
- // 反而报得更好)。长帧正是要找的东西。
231
- const frames = r.frames.filter(f => f > 0);
232
- const dropped = frames.filter(f => f > 16.7 * 1.5).length;
233
- const dropRatio = frames.length ? (dropped / frames.length) * 100 : 0;
234
- const worst = frames.length ? Math.max(...frames) : 0;
235
- const freezes = frames.filter(f => f > 250).length;
236
- // 派发了多少 vs 真走了多少。差很多 = 滚动被劫持/被平滑滚库接管,
237
- // 这本身就是"一顿一顿"的答案,不必再往下猜。
238
- const askedPx = perFrame * nFrames;
239
- const movedPx = Math.round(r.movedPx);
240
- const followRatio = askedPx ? (movedPx / askedPx) * 100 : 100;
241
- // 样本不足就说不足。原来这种情况报的是 `p50 0.0ms / dropped 0%` ——
242
- // 一句"满分",而真相是**什么都没量到**(溢出面板、短页、滚不动的页)。
243
- const tooFew = frames.length < 20;
244
- // 判据是**跟随率**不是绝对位移:实测那个劫持页走了 960px(不算"没动"),
245
- // 但只跟上了派发量的 13% —— 用户手上的感觉就是"滚了半天没动"。
246
- // 60% 是留给平滑滚库正常惯性的余量。
247
- const hijacked = askedPx > 400 && followRatio < 60;
248
-
249
- // 契约:note 非空必须写进返回文本。这个工具尤其不能漏 —— 它的描述铁口
250
- // 直断"走 http 同源",退回 file:// 后它按 Resource Timing 报的
251
- // 「images 0 files 0KB」正好是它主诊断的反面。
252
- const degraded = degradedNote(opened);
253
- const lines = [
254
- ...(degraded ? [degraded, ''] : []),
255
- `profile_scroll — ${target.relPath} @ ${dev} ${viewport.width}×${viewport.height}`,
256
- `scroll container ${geom.isWindow ? 'window' : 'an inner element (overflow:auto)'}`
257
- + `, scrollable ${scrollable}px, real wheel events ${perFrame}px × ${nFrames} @60Hz`,
258
- `moved ${movedPx}px of ${askedPx}px dispatched (${followRatio.toFixed(0)}% followed)`,
259
- '',
260
- ...(tooFew ? [
261
- `⚠ ONLY ${frames.length} frames sampled — that is not enough to say anything.`,
262
- ' Nothing below is a verdict. Usual causes: the page is barely taller than the',
263
- ' viewport, or the thing that scrolls is not under the centre of the screen.',
264
- '',
265
- ] : []),
266
- ...(hijacked ? [
267
- `⚠ ${nFrames} real wheel events asked for ${askedPx}px; the page moved ${movedPx}px (${followRatio.toFixed(0)}%).`,
268
- ' Something is EATING the scroll — a wheel handler doing custom scrolling,',
269
- ' scroll hijacking, or a smooth-scroll library (Lenis/Locomotive). That IS the',
270
- ' jank the user feels: their wheel and the page no longer agree. Look at wheel',
271
- ' listeners before you look at images or layers.',
272
- '',
273
- ] : []),
274
- `frame time p50 ${pct(frames, 50).toFixed(1)}ms p90 ${pct(frames, 90).toFixed(1)}ms p99 ${pct(frames, 99).toFixed(1)}ms (16.7ms = 60fps)`,
275
- `dropped ${dropRatio.toFixed(0)}% of ${frames.length} frames took >25ms`,
276
- `worst frame ${worst.toFixed(0)}ms${freezes ? ` · ${freezes} frame(s) over 250ms = a visible freeze` : ''}`,
277
- `long tasks ${r.longTasks.length}${r.longTasks.length ? ` (worst ${Math.max(...r.longTasks).toFixed(0)}ms)` : ''}`,
278
- '',
279
- `images ${r.imgCount} files, ${kb(r.imgBytes)} total (as delivered: originals, not preview variants)`,
280
- ...(r.topImgs.length
281
- ? [`heaviest ${r.topImgs.map(i => `${i.name} ${kb(i.bytes)}`).join(' · ')}`]
282
- : []),
283
- ...(r.layers.length
284
- ? ['', 'full-screen composited layers (a classic other half of the problem):',
285
- ...r.layers.map(l => ` ${l}`)]
286
- : []),
287
- '',
288
- r.imgBytes > 5 * 1024 * 1024
289
- ? `⚠ ${kb(r.imgBytes)} of images is the first thing to fix — reference the .webp siblings instead of the PNG masters.`
290
- : null,
291
- dropRatio > 25 && r.layers.length
292
- ? '⚠ Both heavy layers and dropped frames present. These are usually ADDITIVE — test removing them one at a time AND together before concluding.'
293
- : null,
294
- '',
295
- 'Numbers are for A/B comparison on this 1-vCPU box, not the user\'s device.',
296
- ].filter(l => l !== null);
297
- return asText(lines.join('\n'));
298
- } catch (err) {
299
- return { content: [{ type: 'text', text: `profile_scroll failed: ${err?.message || String(err)}` }], isError: true };
300
- } finally {
301
- await browser?.close().catch(() => {});
302
- }
303
- },
304
- );
305
- }
1
+ /**
2
+ * mcp/tools/profile-scroll.js — profile_scroll(2026-08-18)
3
+ *
4
+ * ## 为什么有这个工具
5
+ *
6
+ * 用户说"往下滑动一顿一顿的",这在设计工具里是最常见的反馈之一。而在这之前
7
+ * 平台给 agent 的全部观测能力是「截一张静态图」—— 帧时、掉帧、图片体积、
8
+ * 长任务一个都看不到。一个 agent 的实际绕路是:起 `python3 -m http.server`
9
+ * → 从仓库 node_modules 直接 import playwright → 手写 rAF 采样器 → 逐项关掉
10
+ * 嫌疑元素做 A/B。它量出来的结论一个都猜不出来:基线 63% 掉帧,单独关视差
11
+ * 64%(没用),单独关全屏胶片颗粒层 40%,两个都关 5% —— 是**叠加效应**,
12
+ * 真凶是「33.7MB 的 PNG + 一个带 mix-blend-mode 的全屏 fixed 层」这对组合。
13
+ * 没有测量,它大概率只会把用户新抱怨的那个功能拆掉交差,剩下 40% 的掉帧
14
+ * 原封不动留在站里。
15
+ *
16
+ * ## 数字怎么读
17
+ *
18
+ * ⚠️ 这台机器 1 vCPU,而且截图 worker 跟 server 抢同一颗核。**绝对值不代表
19
+ * 用户设备上的体验**,它的用处是 A/B:改一处、再量一次、看方向。返回文本里
20
+ * 会重复这句话,别让 agent 拿绝对数字去跟用户汇报。
21
+ */
22
+
23
+ import { tool } from '@anthropic-ai/claude-agent-sdk';
24
+ import { z } from 'zod';
25
+ import { resolveCanvasTarget, CANVAS_PATH_DESC, requireBrowsable } from '../../../lib/artifact-target.js';
26
+ import { openArtifactPage, launchPerceptionBrowser, degradedNote } from './helpers/perception-page.js';
27
+
28
+ const DEVICE_W = { desktop: 1440, tablet: 834, mobile: 390 };
29
+ const DEVICE_H = { desktop: 900, tablet: 1112, mobile: 844 };
30
+
31
+ /**
32
+ * 页面里跑的采样器:**只测量,不自己滚**。
33
+ *
34
+ * ⛔ 前两版都是页面自己 `window.scrollBy` 推进的,两次都测错,原因不同:
35
+ * v1「大跨步 + 每步 sleep」——合成器在步与步之间有空闲,坏页面照样报 3% 掉帧。
36
+ * v2「rAF 里逐帧小步推」——压力对了,但**滚动方式还是假的**:程序化滚动
37
+ * 不派发 wheel 事件,于是一整类最常见的"一顿一顿"完全隐形:
38
+ * wheel handler 里做视差、滚动劫持(scrolljacking)、平滑滚库
39
+ * (Locomotive/Lenis 那种自己接管滚动的)。用户的手指走的是 wheel 那条路。
40
+ * 而且 `scroll-behavior: smooth` 会把程序化滚动变成动画,实测让量测范围
41
+ * 悄悄缩到 3.5% —— 看着"滚完了",其实几乎没动。
42
+ * 现在:滚动由外面用 CDP 派发**真 wheel 事件**(≈60Hz),这里只逐帧记 rAF 间隔。
43
+ *
44
+ * ⭐ 顺带白拿一个诊断:派发了多少像素 vs 页面实际走了多少像素。差得远
45
+ * = 滚动被劫持了,这本身就是答案。
46
+ */
47
+ /* eslint-disable no-undef */
48
+ function installSampler() {
49
+ const st = {
50
+ frames: [], longTasks: [], po: null, raf: 0, running: true,
51
+ startedAt: performance.now(), last: performance.now(),
52
+ container: null, startTop: 0,
53
+ };
54
+ window.__ndProf = st;
55
+ try {
56
+ st.po = new PerformanceObserver((l) => { for (const e of l.getEntries()) st.longTasks.push(e.duration); });
57
+ st.po.observe({ entryTypes: ['longtask'] });
58
+ } catch { /* 不支持就没有长任务数据 */ }
59
+
60
+ // 滚的可能不是 window:应用式版面常常是某个内层容器在滚。wheel 事件落在
61
+ // 指针下面那个元素上,所以内层容器**会**滚 —— 但 window.scrollY 不动,
62
+ // 按它判断就会误报"这页滚不动"。先认出真正在滚的那个。
63
+ const pickContainer = () => {
64
+ const cx = Math.floor(window.innerWidth / 2);
65
+ const cy = Math.floor(window.innerHeight / 2);
66
+ let el = document.elementFromPoint(cx, cy);
67
+ while (el && el !== document.body && el !== document.documentElement) {
68
+ const cs = getComputedStyle(el);
69
+ const scrollable = /(auto|scroll|overlay)/.test(`${cs.overflowY}`) && el.scrollHeight > el.clientHeight + 4;
70
+ if (scrollable) return el;
71
+ el = el.parentElement;
72
+ }
73
+ return document.scrollingElement || document.documentElement;
74
+ };
75
+ st.container = pickContainer();
76
+ st.startTop = st.container.scrollTop;
77
+
78
+ const tick = () => {
79
+ if (!st.running) return;
80
+ const now = performance.now();
81
+ st.frames.push(now - st.last);
82
+ st.last = now;
83
+ st.raf = requestAnimationFrame(tick);
84
+ };
85
+ st.raf = requestAnimationFrame(tick);
86
+ return {
87
+ scrollHeight: st.container.scrollHeight,
88
+ clientHeight: st.container.clientHeight,
89
+ isWindow: st.container === (document.scrollingElement || document.documentElement),
90
+ };
91
+ }
92
+
93
+ function collectSampler() {
94
+ const st = window.__ndProf;
95
+ if (!st) return null;
96
+ st.running = false;
97
+ cancelAnimationFrame(st.raf);
98
+ try { st.po?.disconnect(); } catch { /* */ }
99
+
100
+ const imgs = performance.getEntriesByType('resource')
101
+ .filter(e => e.initiatorType === 'img' || /\.(png|jpe?g|webp|avif|gif|svg)(\?|$)/i.test(e.name))
102
+ .map(e => ({
103
+ name: decodeURIComponent(e.name.split('?')[0].split('/').pop() || '').slice(0, 60),
104
+ // encodedBodySize 是**这次传输**的字节;transferSize 命中缓存时是 0。
105
+ // 感知通道现在带 X-ND-Raw,拿的是磁盘原图,所以这个数字就是交付站点的数字。
106
+ bytes: e.encodedBodySize || e.transferSize || 0,
107
+ }));
108
+ imgs.sort((a, b) => b.bytes - a.bytes);
109
+
110
+ return {
111
+ frames: st.frames.slice(2), // 头两帧含启动抖动
112
+ longTasks: st.longTasks,
113
+ elapsedMs: performance.now() - st.startedAt,
114
+ scrollHeight: st.container.scrollHeight,
115
+ clientHeight: st.container.clientHeight,
116
+ movedPx: st.container.scrollTop - st.startTop,
117
+ imgCount: imgs.length,
118
+ imgBytes: imgs.reduce((a, b) => a + b.bytes, 0),
119
+ topImgs: imgs.slice(0, 5),
120
+ layers: (() => {
121
+ // 「合成层嫌疑」:全屏定位 + 带混合模式/滤镜/半透明的那些,
122
+ // 就是那个真实案例里的另一半凶手
123
+ const out = [];
124
+ for (const el of document.querySelectorAll('*')) {
125
+ const cs = getComputedStyle(el);
126
+ if (cs.position !== 'fixed' && cs.position !== 'sticky') continue;
127
+ const r = el.getBoundingClientRect();
128
+ const big = r.width * r.height > window.innerWidth * window.innerHeight * 0.5;
129
+ const heavy = cs.mixBlendMode !== 'normal' || cs.filter !== 'none'
130
+ || cs.backdropFilter !== 'none' || (cs.opacity !== '1' && cs.opacity !== '');
131
+ if (big && heavy) {
132
+ const id = el.id ? `#${el.id}` : (typeof el.className === 'string' && el.className.trim()
133
+ ? `.${el.className.trim().split(/\s+/)[0]}` : el.tagName.toLowerCase());
134
+ out.push(`${id} [${cs.position}, blend=${cs.mixBlendMode}, filter=${cs.filter === 'none' ? '-' : 'yes'}, backdrop=${cs.backdropFilter === 'none' ? '-' : 'yes'}, opacity=${cs.opacity}]`);
135
+ }
136
+ if (out.length >= 5) break;
137
+ }
138
+ return out;
139
+ })(),
140
+ };
141
+ }
142
+ /* eslint-enable no-undef */
143
+
144
+ const pct = (arr, p) => {
145
+ if (!arr.length) return 0;
146
+ const s = [...arr].sort((a, b) => a - b);
147
+ return s[Math.min(s.length - 1, Math.floor((p / 100) * s.length))];
148
+ };
149
+ const kb = (b) => (b >= 1024 * 1024 ? `${(b / 1024 / 1024).toFixed(1)}MB` : `${Math.round(b / 1024)}KB`);
150
+
151
+ export function makeProfileScrollTool({ workspaceRoot, projectId, sessionId }) {
152
+ return tool(
153
+ 'profile_scroll',
154
+ `Measure how a page actually performs while scrolling. Loads it over http
155
+ (same origin as the user preview), scrolls programmatically, and returns frame
156
+ timing, dropped-frame ratio, long tasks, total image bytes and the heaviest
157
+ images, plus any full-screen fixed/sticky layers with blend modes or filters.
158
+
159
+ Scrolling is driven by REAL wheel events, so this also catches the other common
160
+ cause of "one frame at a time": the page eating the wheel and scrolling itself
161
+ (scroll hijacking, Lenis/Locomotive-style smooth scroll). The report says how
162
+ many pixels were dispatched vs how many the page actually moved — if those
163
+ disagree, that is your answer and frame times are beside the point.
164
+
165
+ Use this the moment a user says scrolling feels heavy, janky, laggy or "one
166
+ frame at a time" — do NOT start deleting features to guess which one it is.
167
+ The one real case on record: baseline 63% dropped frames; killing the parallax
168
+ alone changed nothing (64%); killing the full-screen grain layer alone got it to
169
+ 40%; killing both got 5%; hiding images got 3%. It was an additive effect
170
+ between 33.7MB of PNGs and one blended full-screen layer. Guessing would have
171
+ removed the wrong thing and left most of the jank in place.
172
+
173
+ Also worth running before you hand over any image-heavy page: total image bytes
174
+ is the single most common cause, and generate_image now writes a .webp sibling
175
+ next to every PNG for exactly this reason.
176
+
177
+ ⚠️ This machine has 1 vCPU and the browser competes with the server for it. The
178
+ absolute numbers are NOT what the user's device will see. Use them for A/B:
179
+ measure, change one thing, measure again, compare direction. Never report the
180
+ raw percentage to the user as if it were their experience.`,
181
+ {
182
+ path: z.string().optional().describe(CANVAS_PATH_DESC),
183
+ device: z.enum(['desktop', 'tablet', 'mobile']).optional()
184
+ .describe('Viewport preset (default desktop 1440×900). Mobile is where jank shows up worst.'),
185
+ frames: z.number().int().min(30).max(300).optional()
186
+ .describe('How many wheel steps to scroll across (default 120 ≈ a 2-second scroll at 60Hz). Real wheel events are dispatched at ~60Hz, the same rate a trackpad fires — so wheel handlers, scroll hijacking and smooth-scroll libraries are all in the measurement.'),
187
+ },
188
+ async ({ path: relPath, device, frames: framesArg }) => {
189
+ const asText = (text) => ({ content: [{ type: 'text', text }] });
190
+ const target = await resolveCanvasTarget(workspaceRoot, relPath, sessionId);
191
+ if (!target) return { content: [{ type: 'text', text: 'No page found to profile.' }], isError: true };
192
+ const guard = requireBrowsable(target);
193
+ if (guard) return { content: [{ type: 'text', text: guard }], isError: true };
194
+
195
+ const dev = device || 'desktop';
196
+ const viewport = { width: DEVICE_W[dev], height: DEVICE_H[dev] };
197
+ const nFrames = framesArg ?? 120;
198
+
199
+ let browser;
200
+ try {
201
+ browser = await launchPerceptionBrowser();
202
+ const opened = await openArtifactPage(browser, {
203
+ projectId, workspaceRoot, absPath: target.absPath, viewport,
204
+ });
205
+ await opened.goto();
206
+ const page = opened.page;
207
+
208
+ // 采样器先装上(它顺手认出真正在滚的那个容器),再从外面派发真 wheel
209
+ const geom = await page.evaluate(installSampler);
210
+ const scrollable = Math.max(0, geom.scrollHeight - geom.clientHeight);
211
+ // 每帧推进量:把可滚范围在 nFrames 帧里走完。下限 6px —— 太小的话短页面
212
+ // 一直停在原地,样本全是静止帧(那种"满分"没有意义)。
213
+ const perFrame = Math.max(6, Math.round(scrollable / nFrames));
214
+
215
+ // ⭐ 真 wheel,配速 ≈60Hz。配速是必要的:不配速的话 CDP 一秒能塞几百个
216
+ // wheel,任何页面都会显得卡(假警报),而真人的滚轮/触摸板就是 60-120Hz。
217
+ await page.mouse.move(Math.floor(viewport.width / 2), Math.floor(viewport.height / 2));
218
+ const t0 = Date.now();
219
+ for (let i = 0; i < nFrames; i += 1) {
220
+ await page.mouse.wheel(0, perFrame);
221
+ const wait = (t0 + (i + 1) * 16) - Date.now();
222
+ if (wait > 1) await page.waitForTimeout(wait);
223
+ }
224
+ await page.waitForTimeout(120); // 让最后几帧和长任务落账
225
+ const r = await page.evaluate(collectSampler);
226
+
227
+ // ⛔ **不再过滤长帧**。原来是 `frames.filter(f => f > 0 && f < 2000)`,
228
+ // 于是一个 3017ms 的冻结**被当成脏数据剔掉**,剩下的帧算出 `dropped 0%`
229
+ // 且不作任何提示 —— 越卡越可能被删。A/B 甚至会反过来(把页面改得更卡
230
+ // 反而报得更好)。长帧正是要找的东西。
231
+ const frames = r.frames.filter(f => f > 0);
232
+ const dropped = frames.filter(f => f > 16.7 * 1.5).length;
233
+ const dropRatio = frames.length ? (dropped / frames.length) * 100 : 0;
234
+ const worst = frames.length ? Math.max(...frames) : 0;
235
+ const freezes = frames.filter(f => f > 250).length;
236
+ // 派发了多少 vs 真走了多少。差很多 = 滚动被劫持/被平滑滚库接管,
237
+ // 这本身就是"一顿一顿"的答案,不必再往下猜。
238
+ const askedPx = perFrame * nFrames;
239
+ const movedPx = Math.round(r.movedPx);
240
+ const followRatio = askedPx ? (movedPx / askedPx) * 100 : 100;
241
+ // 样本不足就说不足。原来这种情况报的是 `p50 0.0ms / dropped 0%` ——
242
+ // 一句"满分",而真相是**什么都没量到**(溢出面板、短页、滚不动的页)。
243
+ const tooFew = frames.length < 20;
244
+ // 判据是**跟随率**不是绝对位移:实测那个劫持页走了 960px(不算"没动"),
245
+ // 但只跟上了派发量的 13% —— 用户手上的感觉就是"滚了半天没动"。
246
+ // 60% 是留给平滑滚库正常惯性的余量。
247
+ const hijacked = askedPx > 400 && followRatio < 60;
248
+
249
+ // 契约:note 非空必须写进返回文本。这个工具尤其不能漏 —— 它的描述铁口
250
+ // 直断"走 http 同源",退回 file:// 后它按 Resource Timing 报的
251
+ // 「images 0 files 0KB」正好是它主诊断的反面。
252
+ const degraded = degradedNote(opened);
253
+ const lines = [
254
+ ...(degraded ? [degraded, ''] : []),
255
+ `profile_scroll — ${target.relPath} @ ${dev} ${viewport.width}×${viewport.height}`,
256
+ `scroll container ${geom.isWindow ? 'window' : 'an inner element (overflow:auto)'}`
257
+ + `, scrollable ${scrollable}px, real wheel events ${perFrame}px × ${nFrames} @60Hz`,
258
+ `moved ${movedPx}px of ${askedPx}px dispatched (${followRatio.toFixed(0)}% followed)`,
259
+ '',
260
+ ...(tooFew ? [
261
+ `⚠ ONLY ${frames.length} frames sampled — that is not enough to say anything.`,
262
+ ' Nothing below is a verdict. Usual causes: the page is barely taller than the',
263
+ ' viewport, or the thing that scrolls is not under the centre of the screen.',
264
+ '',
265
+ ] : []),
266
+ ...(hijacked ? [
267
+ `⚠ ${nFrames} real wheel events asked for ${askedPx}px; the page moved ${movedPx}px (${followRatio.toFixed(0)}%).`,
268
+ ' Something is EATING the scroll — a wheel handler doing custom scrolling,',
269
+ ' scroll hijacking, or a smooth-scroll library (Lenis/Locomotive). That IS the',
270
+ ' jank the user feels: their wheel and the page no longer agree. Look at wheel',
271
+ ' listeners before you look at images or layers.',
272
+ '',
273
+ ] : []),
274
+ `frame time p50 ${pct(frames, 50).toFixed(1)}ms p90 ${pct(frames, 90).toFixed(1)}ms p99 ${pct(frames, 99).toFixed(1)}ms (16.7ms = 60fps)`,
275
+ `dropped ${dropRatio.toFixed(0)}% of ${frames.length} frames took >25ms`,
276
+ `worst frame ${worst.toFixed(0)}ms${freezes ? ` · ${freezes} frame(s) over 250ms = a visible freeze` : ''}`,
277
+ `long tasks ${r.longTasks.length}${r.longTasks.length ? ` (worst ${Math.max(...r.longTasks).toFixed(0)}ms)` : ''}`,
278
+ '',
279
+ `images ${r.imgCount} files, ${kb(r.imgBytes)} total (as delivered: originals, not preview variants)`,
280
+ ...(r.topImgs.length
281
+ ? [`heaviest ${r.topImgs.map(i => `${i.name} ${kb(i.bytes)}`).join(' · ')}`]
282
+ : []),
283
+ ...(r.layers.length
284
+ ? ['', 'full-screen composited layers (a classic other half of the problem):',
285
+ ...r.layers.map(l => ` ${l}`)]
286
+ : []),
287
+ '',
288
+ r.imgBytes > 5 * 1024 * 1024
289
+ ? `⚠ ${kb(r.imgBytes)} of images is the first thing to fix — reference the .webp siblings instead of the PNG masters.`
290
+ : null,
291
+ dropRatio > 25 && r.layers.length
292
+ ? '⚠ Both heavy layers and dropped frames present. These are usually ADDITIVE — test removing them one at a time AND together before concluding.'
293
+ : null,
294
+ '',
295
+ 'Numbers are for A/B comparison on this 1-vCPU box, not the user\'s device.',
296
+ ].filter(l => l !== null);
297
+ return asText(lines.join('\n'));
298
+ } catch (err) {
299
+ return { content: [{ type: 'text', text: `profile_scroll failed: ${err?.message || String(err)}` }], isError: true };
300
+ } finally {
301
+ await browser?.close().catch(() => {});
302
+ }
303
+ },
304
+ );
305
+ }