@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.
- package/LICENSE +661 -0
- package/README.md +140 -0
- package/bin/nodesign.js +176 -0
- package/package.json +104 -0
- package/server/api/README.md +45 -0
- package/server/api/_guard.js +75 -0
- package/server/api/admin.js +187 -0
- package/server/api/assets/docx-page.js +103 -0
- package/server/api/assets/helpers.js +22 -0
- package/server/api/assets/notes.js +144 -0
- package/server/api/assets.js +896 -0
- package/server/api/board.js +57 -0
- package/server/api/browse.js +138 -0
- package/server/api/canvas.js +444 -0
- package/server/api/chatai.js +324 -0
- package/server/api/exports/build-standalone.js +980 -0
- package/server/api/exports/cards.js +118 -0
- package/server/api/exports/docx-pdf.js +46 -0
- package/server/api/exports/export-page.js +142 -0
- package/server/api/exports/handoff.js +147 -0
- package/server/api/exports.js +690 -0
- package/server/api/instruction.js +75 -0
- package/server/api/local.js +115 -0
- package/server/api/me.js +116 -0
- package/server/api/memory.js +185 -0
- package/server/api/pending-changes.js +366 -0
- package/server/api/plugins.js +164 -0
- package/server/api/projects.js +220 -0
- package/server/api/publish.js +64 -0
- package/server/api/recent.js +73 -0
- package/server/api/sessions.js +589 -0
- package/server/api/skills.js +77 -0
- package/server/api/standalone-fit.js +168 -0
- package/server/api/turn-compose.js +180 -0
- package/server/api/turn-inflight.js +44 -0
- package/server/api/turn.js +669 -0
- package/server/auth/README.md +17 -0
- package/server/auth/middleware.js +138 -0
- package/server/auth/origin-guard.js +71 -0
- package/server/auth/session.js +108 -0
- package/server/auth/tier.js +113 -0
- package/server/auth/users-store.js +348 -0
- package/server/edit/README.md +29 -0
- package/server/engine/README.md +113 -0
- package/server/engine/agent/agent-shared.js +550 -0
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -0
- package/server/engine/agent/auto-mode-rules.js +78 -0
- package/server/engine/agent/context.js +418 -0
- package/server/engine/agent/events.js +464 -0
- package/server/engine/agent/hooks/canvas-validate.js +199 -0
- package/server/engine/agent/hooks/failure.js +120 -0
- package/server/engine/agent/hooks/file-events.js +75 -0
- package/server/engine/agent/hooks/lifecycle.js +233 -0
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -0
- package/server/engine/agent/hooks/post-guidance.js +206 -0
- package/server/engine/agent/hooks/post-subagent-report.js +104 -0
- package/server/engine/agent/hooks/post-trim.js +58 -0
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -0
- package/server/engine/agent/hooks/pre-defaults.js +84 -0
- package/server/engine/agent/hooks/pre-injectors.js +277 -0
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -0
- package/server/engine/agent/hooks/pre-starter-files.js +61 -0
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -0
- package/server/engine/agent/hooks/site-validate.js +94 -0
- package/server/engine/agent/hooks/tool-prompts.js +31 -0
- package/server/engine/agent/hooks/turn-state-memory.js +56 -0
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -0
- package/server/engine/agent/hooks.js +354 -0
- package/server/engine/agent/init-contract.js +69 -0
- package/server/engine/agent/isolation.js +159 -0
- package/server/engine/agent/model-context.js +359 -0
- package/server/engine/agent/model-table.js +292 -0
- package/server/engine/agent/plugin-loader.js +266 -0
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -0
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -0
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -0
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -0
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -0
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -0
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -0
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -0
- package/server/engine/agent/prompts/tools/site-reference.md +106 -0
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -0
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -0
- package/server/engine/agent/session-loop.js +1024 -0
- package/server/engine/agent/session-model.js +164 -0
- package/server/engine/agent/skill.js +204 -0
- package/server/engine/agent/system-prompts.js +86 -0
- package/server/engine/agent/task-events.js +78 -0
- package/server/engine/agents/ds-extractor.md +132 -0
- package/server/engine/agents/explorer.md +190 -0
- package/server/engine/agents/index.js +230 -0
- package/server/engine/agents/schemas/design-system.json +156 -0
- package/server/engine/agents/schemas/tweak-schema.json +113 -0
- package/server/engine/agents/tweak-proposer.md +127 -0
- package/server/engine/agents/vision-checker.md +254 -0
- package/server/engine/browse/capture.js +422 -0
- package/server/engine/browse/card.js +137 -0
- package/server/engine/browse/handover.js +59 -0
- package/server/engine/browse/page-digest.js +119 -0
- package/server/engine/browse/refs.js +196 -0
- package/server/engine/browse/registry.js +307 -0
- package/server/engine/browse/screencast.js +170 -0
- package/server/engine/browse/state.js +121 -0
- package/server/engine/chatai/chat-log.js +79 -0
- package/server/engine/chatai/index.js +193 -0
- package/server/engine/chatai/openai-compat.js +152 -0
- package/server/engine/chatai/orchestrate.js +309 -0
- package/server/engine/chatai/perform.js +40 -0
- package/server/engine/chatai/summarize.js +79 -0
- package/server/engine/mcp/capability-gate.js +52 -0
- package/server/engine/mcp/index.js +339 -0
- package/server/engine/mcp/param-sanitizer.js +142 -0
- package/server/engine/mcp/tools/arrange-on-board.js +124 -0
- package/server/engine/mcp/tools/artifact-session.js +278 -0
- package/server/engine/mcp/tools/browse-computer.js +421 -0
- package/server/engine/mcp/tools/browse-find-batch.js +183 -0
- package/server/engine/mcp/tools/browse-screenshot.js +157 -0
- package/server/engine/mcp/tools/browse.js +480 -0
- package/server/engine/mcp/tools/build-docx.js +101 -0
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -0
- package/server/engine/mcp/tools/create-on-board.js +155 -0
- package/server/engine/mcp/tools/crystallize-skill.js +168 -0
- package/server/engine/mcp/tools/deliver-files.js +0 -0
- package/server/engine/mcp/tools/explain-style.js +237 -0
- package/server/engine/mcp/tools/export-handoff.js +133 -0
- package/server/engine/mcp/tools/expose-tweaks.js +149 -0
- package/server/engine/mcp/tools/generate-image.js +842 -0
- package/server/engine/mcp/tools/get-computed-styles.js +164 -0
- package/server/engine/mcp/tools/get-pending-changes.js +205 -0
- package/server/engine/mcp/tools/h3box-ssh.js +84 -0
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -0
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -0
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -0
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -0
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -0
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -0
- package/server/engine/mcp/tools/helpers/rembg.js +273 -0
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -0
- package/server/engine/mcp/tools/highlight.js +68 -0
- package/server/engine/mcp/tools/list-pages.js +222 -0
- package/server/engine/mcp/tools/lookup-tags.js +59 -0
- package/server/engine/mcp/tools/navigate-to-page.js +60 -0
- package/server/engine/mcp/tools/organize-board.js +64 -0
- package/server/engine/mcp/tools/paint-still.js +434 -0
- package/server/engine/mcp/tools/pin-to-board.js +130 -0
- package/server/engine/mcp/tools/preview-deck.js +130 -0
- package/server/engine/mcp/tools/profile-scroll.js +305 -0
- package/server/engine/mcp/tools/publish-site.js +102 -0
- package/server/engine/mcp/tools/query-elements.js +195 -0
- package/server/engine/mcp/tools/read-board.js +103 -0
- package/server/engine/mcp/tools/read-document.js +93 -0
- package/server/engine/mcp/tools/read-page.js +264 -0
- package/server/engine/mcp/tools/read-tavern-json.js +143 -0
- package/server/engine/mcp/tools/record-decision.js +140 -0
- package/server/engine/mcp/tools/relate-on-board.js +110 -0
- package/server/engine/mcp/tools/remove-background.js +391 -0
- package/server/engine/mcp/tools/report-issue.js +115 -0
- package/server/engine/mcp/tools/roll-film.js +266 -0
- package/server/engine/mcp/tools/screenshot-docx.js +138 -0
- package/server/engine/mcp/tools/screenshot-url.js +195 -0
- package/server/engine/mcp/tools/screenshot.js +583 -0
- package/server/engine/mcp/tools/tier-gate.js +96 -0
- package/server/engine/mcp/tools/trace-motion.js +215 -0
- package/server/engine/mcp/tools/web-search.js +548 -0
- package/server/engine/motion/inventory.js +349 -0
- package/server/engine/perception/session.js +235 -0
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -0
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -0
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -0
- 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
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -0
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -0
- package/server/engine/runs/active-runs.js +698 -0
- package/server/engine/runs/live-turn.js +252 -0
- package/server/engine/runs/store.js +364 -0
- package/server/engine/runs/turn-relay.js +217 -0
- package/server/engine/runtime/workspace.js +164 -0
- package/server/index.js +235 -0
- package/server/lib/artifact-file-path.js +80 -0
- package/server/lib/artifact-target.js +284 -0
- package/server/lib/asset-refs.js +239 -0
- package/server/lib/async-queue.js +104 -0
- package/server/lib/auto-relations.js +130 -0
- package/server/lib/binding-types.js +60 -0
- package/server/lib/board-kind-sizes.js +62 -0
- package/server/lib/board-relations.js +133 -0
- package/server/lib/browse-proxy.js +180 -0
- package/server/lib/canvas-id.js +36 -0
- package/server/lib/cover.js +227 -0
- package/server/lib/danbooru-tags.js +291 -0
- package/server/lib/doc-extract.js +156 -0
- package/server/lib/docx/build-from-source.js +260 -0
- package/server/lib/docx/build.js +490 -0
- package/server/lib/docx/dump-styles.js +287 -0
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -0
- package/server/lib/docx/merge-runs.js +129 -0
- package/server/lib/docx/numbering.js +218 -0
- package/server/lib/docx/order.js +152 -0
- package/server/lib/docx/rawzip.js +172 -0
- package/server/lib/docx/render.js +92 -0
- package/server/lib/docx/text-lint.js +225 -0
- package/server/lib/docx/tokens.js +335 -0
- package/server/lib/docx/units.js +29 -0
- package/server/lib/docx/xml.js +291 -0
- package/server/lib/docx-pages.js +187 -0
- package/server/lib/export-collect.js +289 -0
- package/server/lib/export-package.js +210 -0
- package/server/lib/html-srcset.js +145 -0
- package/server/lib/image-variant.js +460 -0
- package/server/lib/ingress/forward-openai-chat.js +305 -0
- package/server/lib/ingress/openai-chat.js +491 -0
- package/server/lib/ingress/session-notice.js +66 -0
- package/server/lib/ingress/session-routes.js +68 -0
- package/server/lib/ingress/slot-probe.js +130 -0
- package/server/lib/ingress/upstream-billing.js +52 -0
- package/server/lib/ingress/upstream-fail-streak.js +72 -0
- package/server/lib/ingress/upstream-truncation.js +48 -0
- package/server/lib/issues-store.js +182 -0
- package/server/lib/kinds/deck.js +72 -0
- package/server/lib/kinds/docx.js +283 -0
- package/server/lib/kinds/file-kinds.js +113 -0
- package/server/lib/kinds/index.js +202 -0
- package/server/lib/kinds/site.js +161 -0
- package/server/lib/model-ingress.js +543 -0
- package/server/lib/moderation.js +255 -0
- package/server/lib/notice-store.js +103 -0
- package/server/lib/plugin-install.js +104 -0
- package/server/lib/plugin-validator.js +721 -0
- package/server/lib/publish-store.js +115 -0
- package/server/lib/quick-summary.js +52 -0
- package/server/lib/quota.js +270 -0
- package/server/lib/rate-window.js +29 -0
- package/server/lib/reference-assets.js +92 -0
- package/server/lib/region-shot.js +135 -0
- package/server/lib/safe-path.js +73 -0
- package/server/lib/sdk-session.js +33 -0
- package/server/lib/showcase-store.js +92 -0
- package/server/lib/site-publish.js +465 -0
- package/server/lib/ssrf-guard.js +432 -0
- package/server/lib/task-scan.js +158 -0
- package/server/lib/tavern-json.js +154 -0
- package/server/lib/video-variant.js +237 -0
- package/server/lib/workspace-path.js +33 -0
- package/server/ops/fix-sdk-musl.mjs +40 -0
- package/server/ops/install-macos-fonts.sh +114 -0
- package/server/ops/macos-fonts.conf +336 -0
- package/server/ops/sandbox-shim/bwrap +57 -0
- package/server/projects/assets-summary.js +119 -0
- package/server/projects/auto-name.js +53 -0
- package/server/projects/board-store.js +640 -0
- package/server/projects/move-entry.js +103 -0
- package/server/projects/store.js +270 -0
- package/server/projects/ui-config.js +54 -0
- package/server/projects/workspace-templates.js +53 -0
- package/server/projects/workspace.js +1025 -0
- package/server/runtime/capabilities.js +153 -0
- package/server/runtime/local-config.js +177 -0
- package/server/runtime/local-env.js +88 -0
- package/server/runtime/platform.js +342 -0
- package/server/runtime/profile.js +72 -0
- package/server/services/rembg-launcher.js +313 -0
- package/server/services/rembg-service.py +334 -0
- package/server/shared/README.md +25 -0
- package/server/shared/deck.js +54 -0
- package/server/shared/time.js +49 -0
- package/server/style-pipeline/README.md +53 -0
- package/server/ws/broker.js +36 -0
- package/server/ws/browse-channel.js +177 -0
- package/server/ws/index.js +386 -0
- package/web/README.md +89 -0
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +11 -0
- package/web/dist/assets/DeckWindow-4G0xX7P3.js +16 -0
- package/web/dist/assets/DocxWindow-BXQsoPkh.js +16 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/SiteWindow-D6eS-2gM.js +18 -0
- package/web/dist/assets/again-D8tfgKKO.webp +0 -0
- package/web/dist/assets/caveat-nd-CP6HlsNg.woff2 +0 -0
- package/web/dist/assets/clock-xJhvfYpS.webp +0 -0
- package/web/dist/assets/film-cut-BBKMwH7q.webp +0 -0
- package/web/dist/assets/film-still-BZ_DL9Tt.webp +0 -0
- package/web/dist/assets/index-C0KUf6Wd.css +1 -0
- package/web/dist/assets/index-Cbl3ATvp.js +2125 -0
- package/web/dist/assets/ink-desk-BRbxeDsR.webp +0 -0
- package/web/dist/assets/ink-night-Bz4BT3XH.webp +0 -0
- package/web/dist/assets/ink-portrait-C0DM4V8Q.webp +0 -0
- package/web/dist/assets/longcang-regular-C0uvTuKa.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
- package/web/dist/assets/pending-edit-apply-DTjo2geW.js +31 -0
- package/web/dist/assets/question-8H4HSqxh.webp +0 -0
- package/web/dist/assets/reject-BaPJqElj.webp +0 -0
- package/web/dist/assets/rotate-cw-LMT3x1Zj.js +11 -0
- package/web/dist/assets/rp-portrait-CbrGpew2.webp +0 -0
- package/web/dist/assets/rp-street-C4jVKjxx.webp +0 -0
- package/web/dist/assets/square-dashed-mouse-pointer-CBoAkfhi.js +11 -0
- package/web/dist/assets/tangle-DIxHVNuT.webp +0 -0
- package/web/dist/assets/thisone-BylZKglV.webp +0 -0
- package/web/dist/assets/thumb-DO1wfK1I.webp +0 -0
- package/web/dist/index.html +18 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp/tools/screenshot.js — screenshot_canvas:产物的眼睛(站点页 / deck / docx 页图)
|
|
3
|
+
*
|
|
4
|
+
* 页面从 helpers/acquire-page.js 拿(一次性可复现;live:true 对着产物会话页);
|
|
5
|
+
* 静帧 / 裁元素 / 整页 / 设备宽 / waitFor / beforeShot / scrollTo 在这里;
|
|
6
|
+
* 胶片条(frames + trigger/click/scrollBy + 元素探针)的录制器是 helpers/motion-lab.js
|
|
7
|
+
* + helpers/motion-scroll.js,跟浏览通道的 browser_screenshot 共用一份。
|
|
8
|
+
* 归一化 / 诊断 / 保真探针在 helpers/shot-pipeline.js。docx 走 screenshot-docx.js。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import fs from 'node:fs/promises';
|
|
13
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { resolveDeckSize, extractDeckAspect } from '../../../shared/deck.js';
|
|
16
|
+
import { resolveCanvasTarget, CANVAS_PATH_DESC, KIND_SITE, requireBrowsable,
|
|
17
|
+
} from '../../../lib/artifact-target.js';
|
|
18
|
+
import { can } from '../../../lib/kinds/index.js';
|
|
19
|
+
import { screenshotDocx } from './screenshot-docx.js';
|
|
20
|
+
import { SITE_DEVICE_W } from './helpers/perception-page.js';
|
|
21
|
+
import { acquireArtifactPage, LIVE_PARAM_DESC } from './helpers/acquire-page.js';
|
|
22
|
+
import { normalizeShot, detectPaintTransform, attachPageDiagnostics, runWaitFor, runBeforeShot } from './helpers/shot-pipeline.js';
|
|
23
|
+
import { recordMotion, pickNearestFrames, composeSheet, encodeWebm, motionCaptionLines } from './helpers/motion-lab.js';
|
|
24
|
+
import { wheelScroll, elementMotionReport, elementMotionLines } from './helpers/motion-scroll.js';
|
|
25
|
+
|
|
26
|
+
// 截图光栅倍率:布局按 deck 逻辑尺寸,位图按这个倍率出(vision token 按像素计费)
|
|
27
|
+
const RASTER_SCALE = 0.6;
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {object} deps
|
|
32
|
+
* @param {string} deps.workspaceRoot
|
|
33
|
+
* @param {import('../../agent/context.js').AgentContext} [deps.ctx]
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export function makeScreenshotCanvasTool({ workspaceRoot, projectId, sessionId, ctx }) {
|
|
37
|
+
return tool(
|
|
38
|
+
'screenshot_canvas',
|
|
39
|
+
`Take a screenshot of the artifact you are working on — a site page (any
|
|
40
|
+
.html inside a site folder; the folder's artifact root is whatever holds its
|
|
41
|
+
index.html — hand-written pages or a build output dir), a deck (.html), or a
|
|
42
|
+
.docx (rendered to page images) — and return it as an image. Use this to
|
|
43
|
+
visually inspect what you wrote: spacing, contrast, hierarchy, layout, alignment.
|
|
44
|
+
|
|
45
|
+
The tool detects the artifact kind from the path. Each call is a FRESH load
|
|
46
|
+
(reproducible). To look at a page in its current interactive state (menus open,
|
|
47
|
+
game mid-play) use live:true against the artifact session (artifact_open).
|
|
48
|
+
|
|
49
|
+
DECK — default viewport = the deck-aspect declared on canvas wrap (16:9 → 1920×1080,
|
|
50
|
+
9:16 → 1080×1920, 16:10 → 1920×1200, 4:3 → 1440×1080). Target one page with pageIndex.
|
|
51
|
+
|
|
52
|
+
SITE — there is no fixed aspect. Default viewport is desktop 1440×900 and the whole
|
|
53
|
+
page is captured (fullPage). Use the device param to check a breakpoint: desktop=1440,
|
|
54
|
+
tablet=834, mobile=390. **Checking mobile means rendering AT 390px wide**, not shrinking
|
|
55
|
+
a desktop shot — that is the only way to see whether your media queries actually fire.
|
|
56
|
+
pageIndex does not apply to sites; pass path to screenshot a specific page file.
|
|
57
|
+
|
|
58
|
+
**Targeting (cheapest → most expensive)**:
|
|
59
|
+
- pageIndex: capture only section[data-page="N"] — **prefer this for per-page checks** (~30-50KB image)
|
|
60
|
+
- selector: capture only the first element matching this CSS selector
|
|
61
|
+
- (default, no targeting): capture viewport only (~30-50KB)
|
|
62
|
+
- fullPage=true: capture full scrollable page — **N× more expensive for N-page deck**
|
|
63
|
+
(~150-300KB for 9 pages). Only use for true deck-wide overview; otherwise prefer
|
|
64
|
+
pageIndex loop or dispatch the vision-checker subagent (subagent context is
|
|
65
|
+
isolated, your main context stays small).
|
|
66
|
+
|
|
67
|
+
Targeted captures (selector / pageIndex) override fullPage.
|
|
68
|
+
|
|
69
|
+
Returns: image content block (you see it directly via vision) plus a text caption
|
|
70
|
+
with size info AND page diagnostics: console errors/warnings and failed resource
|
|
71
|
+
loads (CDN scripts, fonts, images). "console clean, all requests OK" means your
|
|
72
|
+
CDN libs actually loaded — no more guessing whether GSAP/Lenis are alive.
|
|
73
|
+
|
|
74
|
+
beforeShot: the screenshot environment never scrolls, so scroll-linked animations
|
|
75
|
+
(ScrollTrigger, IntersectionObserver reveals) leave elements at opacity:0 and they
|
|
76
|
+
vanish from the shot. Pass beforeShot:"scrollToBottom" to scroll through the whole
|
|
77
|
+
page and back to top first — every scroll trigger fires, then the shot is taken.
|
|
78
|
+
Or pass a JS snippet (async/await OK) to click/hover/setup any state before capture.
|
|
79
|
+
Do NOT delete entrance animations just to make screenshots work — use beforeShot.
|
|
80
|
+
|
|
81
|
+
Slow-booting pages (3D scenes, heavy asset loads): pass waitFor:"window.__yourReadyFlag"
|
|
82
|
+
to poll until the app is ready (own 15s budget), keep beforeShot for the setup itself
|
|
83
|
+
(10s budget). The caption reports how long each phase took.
|
|
84
|
+
|
|
85
|
+
Console output: the caption carries warnings/errors by default; pass console:'all'
|
|
86
|
+
to also read your own console.log output from the page (grouped, capped).
|
|
87
|
+
|
|
88
|
+
FILMSTRIP — the eye for ANIMATION. A single still cannot show easing, overshoot,
|
|
89
|
+
hard cuts between tweens, or "the whole move played off-screen". Pass
|
|
90
|
+
frames (2-30 ms offsets, e.g. [0,120,240,400,700]) + trigger:"window.game.reload()" (JS that
|
|
91
|
+
starts the move) and you get ONE contact sheet: the same viewport at each of those
|
|
92
|
+
moments, timestamped. One image = one motion curve — judge attack, overshoot,
|
|
93
|
+
settle and cuts directly. click:"#start" performs a REAL trusted click instead
|
|
94
|
+
(needed for pointer lock / AudioContext / anything gated on a user gesture);
|
|
95
|
+
combine both if the move needs click-then-call. scrollBy:<px> drives REAL wheel
|
|
96
|
+
scrolling spread over the recording (the way to record scroll-driven motion: reveals,
|
|
97
|
+
parallax, snap, sticky), and an ELEMENT PROBE lists which elements moved / faded /
|
|
98
|
+
scaled during the recording, by how much and when (fixed layers, parallax and
|
|
99
|
+
entrances are told apart). The caption also reports frame
|
|
100
|
+
health (fps / p95 / worst frame — catches per-frame decay bugs and jank) and an
|
|
101
|
+
audio event log (every media.play() / bufferSource.start() with its timestamp —
|
|
102
|
+
you cannot hear, but you CAN see when sound was attempted). Pass saveVideo:true
|
|
103
|
+
to also encode the full recording as a .webm under exports/motion/ — you cannot
|
|
104
|
+
watch it, but deliver_files hands it to the user for final judgement.
|
|
105
|
+
For NUMERIC motion data (exact positions/rotations per frame, overshoot %, settle
|
|
106
|
+
time, hard-cut detection) use the trace_motion tool instead — ToolSearch it.
|
|
107
|
+
|
|
108
|
+
Use this tool when:
|
|
109
|
+
- You finished writing or editing a page / deck and want to verify it looks right
|
|
110
|
+
- The user asks "what does it look like" or "show me the result"
|
|
111
|
+
- You suspect a layout bug and want to see the rendered output
|
|
112
|
+
- You want a closeup of one specific page or element (use pageIndex / selector)
|
|
113
|
+
|
|
114
|
+
Do NOT use this tool when:
|
|
115
|
+
- the file doesn't exist yet (write it first)
|
|
116
|
+
- You haven't actually changed the design since the last screenshot`,
|
|
117
|
+
{
|
|
118
|
+
viewport: z
|
|
119
|
+
.object({
|
|
120
|
+
width: z.number().int().min(320).max(3840),
|
|
121
|
+
height: z.number().int().min(240).max(2160),
|
|
122
|
+
})
|
|
123
|
+
.optional()
|
|
124
|
+
.describe('Browser viewport size; defaults to the deck-aspect declared on canvas wrap (16:9=1920×1080, 9:16=1080×1920, 16:10=1920×1200, 4:3=1440×1080)'),
|
|
125
|
+
fullPage: z
|
|
126
|
+
.boolean()
|
|
127
|
+
.optional()
|
|
128
|
+
.describe('Capture full scrollable page instead of just viewport (default false — N× more expensive for N-page deck). Ignored if selector or pageIndex is given.'),
|
|
129
|
+
selector: z
|
|
130
|
+
.string()
|
|
131
|
+
.optional()
|
|
132
|
+
.describe('If given, capture only the first element matching this CSS selector (overrides fullPage). Works on continuously-animating elements too (WebGL canvas etc.) — the capture crops the element box, it does not wait for the element to stop repainting. Plain CSS only — no playwright syntax (:has-text, >>, nth=), ASCII quotes not HTML entities (" breaks the parse)'),
|
|
133
|
+
detail: z
|
|
134
|
+
.enum(['normal', 'high'])
|
|
135
|
+
.optional()
|
|
136
|
+
.describe("Raster detail. 'normal' (default) renders at 0.6x pixels — ~45% cheaper in context, enough for layout/spacing/palette checks. 'high' = full resolution, use only when you must read small text."),
|
|
137
|
+
pageIndex: z
|
|
138
|
+
.number()
|
|
139
|
+
.int()
|
|
140
|
+
.min(1)
|
|
141
|
+
.optional()
|
|
142
|
+
.describe('DECK ONLY. If given, capture only section[data-page="N"] (overrides fullPage)'),
|
|
143
|
+
device: z
|
|
144
|
+
.enum(['desktop', 'tablet', 'mobile'])
|
|
145
|
+
.optional()
|
|
146
|
+
.describe('SITE ONLY. Render at a real device width to check responsive behaviour: desktop=1440, tablet=834, mobile=390. Ignored for decks.'),
|
|
147
|
+
waitFor: z
|
|
148
|
+
.string()
|
|
149
|
+
.optional()
|
|
150
|
+
.describe("JS expression polled every 100ms until truthy BEFORE beforeShot runs (own 15s budget). Use for slow-booting pages: waitFor:\"window.__game\" waits for the app to be ready, then beforeShot only does the setup. Timeout doesn't block the shot — the caption tells you the condition never became truthy."),
|
|
151
|
+
beforeShot: z
|
|
152
|
+
.string()
|
|
153
|
+
.optional()
|
|
154
|
+
.describe("Run before capture: 'scrollToBottom' scrolls through the page and back (fires all scroll-linked animations — ScrollTrigger / IntersectionObserver reveals), or pass a JS snippet evaluated in page context (await supported, 10s timeout). Don't burn this budget waiting for boot — pair with waitFor. Errors don't block the shot, they're reported in the caption."),
|
|
155
|
+
scrollTo: z
|
|
156
|
+
.union([z.number(), z.string()])
|
|
157
|
+
.optional()
|
|
158
|
+
.describe("Scroll the REAL viewport here, then capture one viewport-sized frame. Accepts a pixel number, a percentage string ('50%', '100%'), or a CSS selector to scroll into view. Use this — not fullPage — to check anything scroll-driven: reveal animations, scroll-snap landing points, parallax offsets, sticky headers. fullPage cannot show these because it expands the viewport to the whole document instead of scrolling (innerHeight never changes, so scroll handlers never fire the way they do for the user). Implies fullPage=false."),
|
|
159
|
+
settleMs: z
|
|
160
|
+
.number()
|
|
161
|
+
.int()
|
|
162
|
+
.min(0)
|
|
163
|
+
.max(10000)
|
|
164
|
+
.optional()
|
|
165
|
+
.describe('Extra wait after scrolling before the shot (default 350ms) — long CSS transitions may need more.'),
|
|
166
|
+
console: z
|
|
167
|
+
.enum(['warn', 'all'])
|
|
168
|
+
.optional()
|
|
169
|
+
.describe("Console capture level for the caption. Default 'warn' returns only warnings/errors (the count of filtered log lines is reported). Pass 'all' to also get console.log/info/debug output — the only way to read your own debug logging from the page."),
|
|
170
|
+
frames: z
|
|
171
|
+
.array(z.number().min(0).max(15000))
|
|
172
|
+
.min(1)
|
|
173
|
+
.max(30)
|
|
174
|
+
.optional()
|
|
175
|
+
.describe('FILMSTRIP mode: capture the viewport at these millisecond offsets (t=0 is the moment click/trigger fires) and return ONE timestamped contact sheet. 2-30 offsets. The sheet has a FIXED pixel budget (~2.3MP, ≈3-3.7k tokens whatever the count), so more cells = smaller cells: 6-10 to read detail, 12-16 for a whole choreography, 20-30 only for long sequences where rhythm matters more than detail (crop with selector/pageIndex or zoom afterwards). Place them where the motion lives (dense during the move, one late frame to confirm settle). Include 0 to see the starting pose. With selector/pageIndex the cells are cropped to that element (it must be inside the viewport).'),
|
|
176
|
+
trigger: z
|
|
177
|
+
.string()
|
|
178
|
+
.optional()
|
|
179
|
+
.describe('FILMSTRIP: JS snippet that STARTS the motion, evaluated in page context at t=0 (await OK). E.g. "window.game.startReload()" or dispatching a keydown. Runs after waitFor/beforeShot. Omit to record whatever is already animating.'),
|
|
180
|
+
click: z
|
|
181
|
+
.string()
|
|
182
|
+
.optional()
|
|
183
|
+
.describe('FILMSTRIP: CSS selector to REAL-click at t=0 (trusted user gesture — required for pointer lock, AudioContext, autoplay). Fires before trigger if both are given.'),
|
|
184
|
+
saveVideo: z
|
|
185
|
+
.boolean()
|
|
186
|
+
.optional()
|
|
187
|
+
.describe('FILMSTRIP: also encode the full recording as .webm under exports/motion/ (real timing preserved, jank and all). You cannot watch it — use deliver_files to hand it to the user.'),
|
|
188
|
+
scrollBy: z.number().min(-8000).max(8000).optional()
|
|
189
|
+
.describe('FILMSTRIP: pixels of REAL wheel scrolling dispatched over the recording window (positive = down). Use this for scroll-driven motion (reveal / parallax / snap / sticky) — it is what a visitor does; trigger/click are for JS-started moves. Can combine with trigger/click.'),
|
|
190
|
+
elements: z.boolean().optional()
|
|
191
|
+
.describe('FILMSTRIP: run the element probe (default true): per-frame position/opacity/scale of the elements likely to move, reported as who moved, how much, when. Set false to save a little CPU.'),
|
|
192
|
+
pages: z
|
|
193
|
+
.string()
|
|
194
|
+
.optional()
|
|
195
|
+
.describe('WORD (.docx) ONLY. Which pages to render: "3", "2-5", or "all". Defaults to the first 2 pages; max 6 per call. Ignored for decks and sites.'),
|
|
196
|
+
path: z
|
|
197
|
+
.string()
|
|
198
|
+
.optional()
|
|
199
|
+
.describe(CANVAS_PATH_DESC),
|
|
200
|
+
live: z.boolean().optional().describe(LIVE_PARAM_DESC),
|
|
201
|
+
},
|
|
202
|
+
async ({ viewport, fullPage, selector, pageIndex, detail, device, waitFor, beforeShot, pages, scrollTo, settleMs, console: consoleLevel, frames, trigger, click, saveVideo, scrollBy, elements, path: relPath, live }) => {
|
|
203
|
+
// 任务模型(2026-07-28):deck 住 tasks/<任务>/canvas.html。寻址统一走
|
|
204
|
+
// canvas-target(显式 path → 本会话当前 deck → cwd/canvas.html → 唯一任务 deck)
|
|
205
|
+
const target = await resolveCanvasTarget(workspaceRoot, relPath, sessionId);
|
|
206
|
+
if (!target.ok) return { content: [{ type: 'text', text: target.message }], isError: true };
|
|
207
|
+
// 形态分流按**能力位**不按形态名:能渲染的(docx)走 LibreOffice 页图管线,
|
|
208
|
+
// 能浏览的(deck / site)继续往下走 playwright。加第四种形态时改注册表不改这里。
|
|
209
|
+
if (can(target.kind, 'renderable')) return screenshotDocx(target, { pages, detail });
|
|
210
|
+
const notBrowsable = requireBrowsable(target);
|
|
211
|
+
if (notBrowsable) return { content: [{ type: 'text', text: notBrowsable }], isError: true };
|
|
212
|
+
const canvasPath = target.absPath;
|
|
213
|
+
let html;
|
|
214
|
+
try {
|
|
215
|
+
html = await fs.readFile(canvasPath, 'utf8');
|
|
216
|
+
} catch {
|
|
217
|
+
return {
|
|
218
|
+
content: [{ type: 'text', text: `${target.relPath} not found. Write it first before screenshotting.` }],
|
|
219
|
+
isError: true,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const isSite = target.kind === KIND_SITE;
|
|
224
|
+
|
|
225
|
+
// 站点没有"比例"这回事:版面是被视口宽度算出来的,所以档位给的是真实设备
|
|
226
|
+
// 宽度,直接当 viewport 用、不缩放。拿 deck 那套 1920×1080 去截站点,会得到
|
|
227
|
+
// 一张"看起来还行"但跟任何真实设备都对不上的图 —— 断点有没有生效看不出来。
|
|
228
|
+
const vp = viewport
|
|
229
|
+
|| (isSite
|
|
230
|
+
? { width: SITE_DEVICE_W[device || 'desktop'], height: 900 }
|
|
231
|
+
: (() => { const d = resolveDeckSize(extractDeckAspect(html)); return { width: d.width, height: d.height }; })());
|
|
232
|
+
|
|
233
|
+
if (isSite && pageIndex) {
|
|
234
|
+
return {
|
|
235
|
+
content: [{
|
|
236
|
+
type: 'text',
|
|
237
|
+
text: `${target.relPath} 是站点页面,没有 <section data-page="N"> 分页。`
|
|
238
|
+
+ '站点的"页"是独立文件:用 path 指定要截哪个页面(先 list_pages 看清单),'
|
|
239
|
+
+ '用 device 切换 desktop / tablet / mobile 检查断点。',
|
|
240
|
+
}],
|
|
241
|
+
isError: true,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 默认 false:fullPage 截图体积是 viewport 的 N× (N=页数),且会留在 context
|
|
246
|
+
// 多 turn 直到 autoCompact。agent 不显式传就走 viewport 单屏(cheapest),
|
|
247
|
+
// 真要 deck-wide overview 显式 fullPage:true 或派 vision-checker。
|
|
248
|
+
// 站点相反:默认整页 —— 网页本来就是长的,只截首屏等于没看过下面那些。
|
|
249
|
+
// scrollTo 与 fullPage 互斥:前者的语义就是"真的滚,按视口抓"
|
|
250
|
+
const fp = scrollTo != null ? false : (fullPage !== undefined ? fullPage === true : isSite);
|
|
251
|
+
|
|
252
|
+
let acq;
|
|
253
|
+
try {
|
|
254
|
+
// 位图缩放(2026-07-28 上下文瘦身,08-21 按新视觉档重算):布局仍按 deck 逻辑
|
|
255
|
+
// 尺寸排(1920 宽),光栅按 RASTER_SCALE 出图。高分辨率档 token = ⌈w/28⌉×⌈h/28⌉:
|
|
256
|
+
// 1920×1080 全幅 2691 token,×0.6 = 1152×648 → 1008 token,排版检查完全够看。
|
|
257
|
+
// 要读小字(版权行 / 数据标签)显式传 detail:'high' 走 1.0(现在真的是 1920×1080,
|
|
258
|
+
// 旧档会先缩到 1568)。
|
|
259
|
+
const rasterScale = detail === 'high' ? 1 : RASTER_SCALE;
|
|
260
|
+
// ⭐ 页面从统一口拿(helpers/acquire-page.js):live:true = 产物会话里现在这一页
|
|
261
|
+
// (状态保留、用完只松锁);否则新开一只保真 chromium 走 http(跟用户预览同一条
|
|
262
|
+
// artifact-file 通道、同源),用完关 —— "截图必须可复现"的契约不破。
|
|
263
|
+
acq = await acquireArtifactPage({
|
|
264
|
+
projectId, workspaceRoot, target, live,
|
|
265
|
+
viewport: vp, deviceScaleFactor: rasterScale,
|
|
266
|
+
});
|
|
267
|
+
const page = acq.page;
|
|
268
|
+
const opened = acq; // degradedNote / viaHttp 的口径不变
|
|
269
|
+
// live 页的视口是会话的,不是本次参数的 —— 下面所有按 vp 算的东西都得按真视口
|
|
270
|
+
if (acq.live) { vp.width = acq.viewport.width; vp.height = acq.viewport.height; }
|
|
271
|
+
const diag = attachPageDiagnostics(page, { console: consoleLevel });
|
|
272
|
+
let gotoNote = [opened.note, acq.gotoNote, acq.liveNote].filter(Boolean).join(' | ') || null;
|
|
273
|
+
|
|
274
|
+
// 三段各自计时(waitFor / beforeShot / settle),caption 报用时 ——
|
|
275
|
+
// agent 之前分不清 5 秒预算被哪一段吃掉,只能碰运气重截
|
|
276
|
+
const timing = [];
|
|
277
|
+
let waitForNote = null;
|
|
278
|
+
if (waitFor) {
|
|
279
|
+
const t0 = Date.now();
|
|
280
|
+
waitForNote = await runWaitFor(page, waitFor);
|
|
281
|
+
timing.push(`waitFor ${((Date.now() - t0) / 1000).toFixed(1)}s`);
|
|
282
|
+
}
|
|
283
|
+
let beforeShotNote = null;
|
|
284
|
+
if (beforeShot) {
|
|
285
|
+
const t0 = Date.now();
|
|
286
|
+
beforeShotNote = await runBeforeShot(page, beforeShot);
|
|
287
|
+
timing.push(`beforeShot ${((Date.now() - t0) / 1000).toFixed(1)}s`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── scrollTo:真滚视口(2026-08-18)──
|
|
291
|
+
//
|
|
292
|
+
// fullPage 走的是 captureBeyondViewport:视口被撑成整页高,`innerHeight`
|
|
293
|
+
// 从头到尾不变。所以页面里一切"按滚动位置判断"的逻辑(IntersectionObserver
|
|
294
|
+
// reveal、scroll-snap、视差、sticky)在 fullPage 下的行为跟用户看到的不是
|
|
295
|
+
// 一回事 —— 有 agent 因此反复改页面代码去迁就截图环境,而那些代码在真实
|
|
296
|
+
// 浏览器里从第一版起就是对的。这条路是"真的滚,然后按视口抓一帧"。
|
|
297
|
+
let scrollNote = null;
|
|
298
|
+
if (scrollTo != null) {
|
|
299
|
+
try {
|
|
300
|
+
const landed = await page.evaluate(async (spec) => {
|
|
301
|
+
const doc = document.documentElement;
|
|
302
|
+
const maxY = Math.max(0, doc.scrollHeight - window.innerHeight);
|
|
303
|
+
let y = null;
|
|
304
|
+
if (typeof spec === 'number') y = spec;
|
|
305
|
+
else if (/^-?[\d.]+%$/.test(spec)) y = maxY * (parseFloat(spec) / 100);
|
|
306
|
+
else {
|
|
307
|
+
const el = document.querySelector(spec);
|
|
308
|
+
if (!el) return { error: `scrollTo selector matched nothing: ${spec}` };
|
|
309
|
+
y = el.getBoundingClientRect().top + window.scrollY;
|
|
310
|
+
}
|
|
311
|
+
window.scrollTo({ top: Math.max(0, Math.min(y, maxY)), behavior: 'instant' });
|
|
312
|
+
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
313
|
+
return { y: window.scrollY, maxY, innerHeight: window.innerHeight };
|
|
314
|
+
}, scrollTo);
|
|
315
|
+
if (landed?.error) scrollNote = landed.error;
|
|
316
|
+
else {
|
|
317
|
+
await page.waitForTimeout(settleMs ?? 350);
|
|
318
|
+
scrollNote = `scrolled to y=${Math.round(landed.y)} of ${Math.round(landed.maxY)}`
|
|
319
|
+
+ ` (viewport ${landed.innerHeight}px tall, real scroll — reveal/snap/parallax behave as they do for the user)`;
|
|
320
|
+
}
|
|
321
|
+
} catch (err) {
|
|
322
|
+
scrollNote = `scrollTo failed: ${err.message}`;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── 胶片条(2026-08-19,iss_mszv782a_toab)──
|
|
327
|
+
// 动画的好坏在时间轴上,静帧看不见缓动/过冲/硬切。CDP screencast 录一段
|
|
328
|
+
// (渲染进程每次重绘推一帧、帧带 epoch 时间戳 —— page.screenshot 连拍一张
|
|
329
|
+
// 100~300ms,压不进 120ms 帧距),按请求时刻取最近帧,拼一张 contact sheet。
|
|
330
|
+
if (frames && frames.length) {
|
|
331
|
+
const wanted = [...frames].sort((a, b) => a - b);
|
|
332
|
+
const durationMs = Math.max(300, Math.round(Math.max(...wanted)));
|
|
333
|
+
|
|
334
|
+
// selector / pageIndex → 每格裁到该元素(视口坐标;录制不滚动,元素得在视口里)
|
|
335
|
+
let crop = null;
|
|
336
|
+
let cropNote = null;
|
|
337
|
+
const cropSelector = selector || (pageIndex ? `section[data-page="${pageIndex}"]` : null);
|
|
338
|
+
if (cropSelector) {
|
|
339
|
+
const r = await page.evaluate((sel) => {
|
|
340
|
+
const el = document.querySelector(sel);
|
|
341
|
+
if (!el) return { error: 'none' };
|
|
342
|
+
const b = el.getBoundingClientRect();
|
|
343
|
+
return { x: b.left, y: b.top, w: b.width, h: b.height, vw: window.innerWidth, vh: window.innerHeight };
|
|
344
|
+
}, cropSelector);
|
|
345
|
+
if (r.error) {
|
|
346
|
+
return { content: [{ type: 'text', text: `Selector matched no elements: ${cropSelector}` }], isError: true };
|
|
347
|
+
}
|
|
348
|
+
const ix = Math.max(0, r.x); const iy = Math.max(0, r.y);
|
|
349
|
+
const iw = Math.min(r.x + r.w, r.vw) - ix; const ih = Math.min(r.y + r.h, r.vh) - iy;
|
|
350
|
+
if (iw > 8 && ih > 8) crop = { x: ix, y: iy, w: iw, h: ih };
|
|
351
|
+
else {
|
|
352
|
+
cropNote = `selector "${cropSelector}" lies outside the viewport — filmstrip records the viewport only, `
|
|
353
|
+
+ 'cells show the full viewport (use scrollTo to bring it in first)';
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 真滚轮驱动 + 元素探针(08-21,跟浏览通道的胶片条同一份 motion-scroll)
|
|
358
|
+
const y0 = await page.evaluate(() => window.scrollY).catch(() => 0);
|
|
359
|
+
const during = scrollBy ? (p) => wheelScroll(p, { px: scrollBy, durationMs: Math.max(200, durationMs - 100) }) : null;
|
|
360
|
+
const rec = await recordMotion(page, {
|
|
361
|
+
durationMs, trigger, click, during, probeElements: elements !== false,
|
|
362
|
+
shotMaxW: Math.max(320, Math.round(vp.width * rasterScale)),
|
|
363
|
+
shotMaxH: Math.max(240, Math.round(vp.height * rasterScale)),
|
|
364
|
+
});
|
|
365
|
+
const y1 = await page.evaluate(() => window.scrollY).catch(() => y0);
|
|
366
|
+
const probe = elements !== false ? elementMotionReport(rec.elems, { scrolledPx: y1 - y0 }) : null;
|
|
367
|
+
if (rec.shots.length === 0) {
|
|
368
|
+
return {
|
|
369
|
+
content: [{
|
|
370
|
+
type: 'text',
|
|
371
|
+
text: [
|
|
372
|
+
'Filmstrip failed: the screencast captured zero frames — the page never painted during the window.',
|
|
373
|
+
rec.clickNote, rec.triggerNote, diag.summary(),
|
|
374
|
+
].filter(Boolean).join('\n'),
|
|
375
|
+
}],
|
|
376
|
+
isError: true,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const picked = pickNearestFrames(rec.shots, wanted);
|
|
381
|
+
const sheet = await composeSheet(picked, { crop, cropRefW: vp.width });
|
|
382
|
+
|
|
383
|
+
let videoNote = null;
|
|
384
|
+
if (saveVideo) {
|
|
385
|
+
try {
|
|
386
|
+
const base = path.basename(canvasPath, path.extname(canvasPath));
|
|
387
|
+
const rel = `exports/motion/${base}-${new Date().toISOString().slice(11, 19).replace(/:/g, '')}.webm`;
|
|
388
|
+
const { bytes } = await encodeWebm(rec.shots, path.join(workspaceRoot, rel));
|
|
389
|
+
videoNote = `video saved: ${rel} (${(bytes / 1024).toFixed(0)}KB, real frame timing — jank preserved) — deliver_files to hand it to the user`;
|
|
390
|
+
} catch (err) {
|
|
391
|
+
videoNote = `video encode failed: ${err?.message || err}`;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
ctx?.emit?.({ type: 'run.screenshot_taken', sizeBytes: sheet.buf.length, viewport: vp, mode: `filmstrip x${wanted.length}` });
|
|
397
|
+
} catch { /* emit fail-safe */ }
|
|
398
|
+
|
|
399
|
+
const shot = await normalizeShot(sheet.buf);
|
|
400
|
+
const cells = picked
|
|
401
|
+
.map((p, i) => (p ? `#${i + 1} t=${Math.round(p.want)}ms→${Math.round(p.actual)}ms` : `#${i + 1} (no frame)`))
|
|
402
|
+
.join(' ');
|
|
403
|
+
const cap = [
|
|
404
|
+
`Filmstrip of ${target.relPath} — ${wanted.length} cells, ${sheet.layout.cols}x${sheet.layout.rows} grid, `
|
|
405
|
+
+ `viewport ${vp.width}x${vp.height} (t=0 = the moment click/trigger fired; labels show requested vs captured time)`,
|
|
406
|
+
cells,
|
|
407
|
+
...(cropNote ? [cropNote] : []),
|
|
408
|
+
...(probe ? elementMotionLines(probe) : []),
|
|
409
|
+
...motionCaptionLines(rec),
|
|
410
|
+
...(videoNote ? [videoNote] : []),
|
|
411
|
+
...(gotoNote ? [gotoNote] : []),
|
|
412
|
+
...(waitForNote ? [waitForNote] : []),
|
|
413
|
+
...(beforeShotNote ? [beforeShotNote] : []),
|
|
414
|
+
...(scrollNote ? [scrollNote] : []),
|
|
415
|
+
...(shot.note ? [shot.note] : []),
|
|
416
|
+
diag.summary(),
|
|
417
|
+
];
|
|
418
|
+
return {
|
|
419
|
+
content: [
|
|
420
|
+
{ type: 'text', text: cap.join('\n') },
|
|
421
|
+
{ type: 'image', data: shot.data, mimeType: shot.mimeType },
|
|
422
|
+
],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// selector / pageIndex 优先,命中则截元素 bbox(locator.screenshot),
|
|
427
|
+
// 都不给走 fullPage / viewport。新范式所有 section 默认平铺可见
|
|
428
|
+
// (系统 fit script 包 frame + scroll-snap),locator.screenshot 自动
|
|
429
|
+
// 拿目标 section 的 bbox,不需要再操作 DOM。
|
|
430
|
+
let buf;
|
|
431
|
+
let captureMode;
|
|
432
|
+
const targetSelector = selector
|
|
433
|
+
|| (pageIndex ? `section[data-page="${pageIndex}"]` : null);
|
|
434
|
+
|
|
435
|
+
if (targetSelector) {
|
|
436
|
+
// 不走 locator.screenshot:它的 "waiting for element to be stable" 对
|
|
437
|
+
// requestAnimationFrame 持续重绘的元素(WebGL canvas / 无限动画)永远
|
|
438
|
+
// 等不到,白烧 20 秒后必失败(iss_msz24x5h_er8l)。改成读元素的文档
|
|
439
|
+
// 坐标 → fullPage 截图 clip 裁剪:clip 在 fullPage 下是文档坐标、可截
|
|
440
|
+
// 视口外(本机 playwright 探针验证过),既不用滚动也没有 stability 等待。
|
|
441
|
+
const rect = await page.evaluate((sel) => {
|
|
442
|
+
const el = document.querySelector(sel);
|
|
443
|
+
if (!el) return { error: 'none' };
|
|
444
|
+
const r = el.getBoundingClientRect();
|
|
445
|
+
const doc = document.documentElement;
|
|
446
|
+
return {
|
|
447
|
+
x: r.left + window.scrollX, y: r.top + window.scrollY,
|
|
448
|
+
width: r.width, height: r.height,
|
|
449
|
+
docW: Math.max(doc.scrollWidth, doc.clientWidth),
|
|
450
|
+
docH: Math.max(doc.scrollHeight, doc.clientHeight),
|
|
451
|
+
};
|
|
452
|
+
}, targetSelector);
|
|
453
|
+
if (rect.error) {
|
|
454
|
+
return {
|
|
455
|
+
content: [{
|
|
456
|
+
type: 'text',
|
|
457
|
+
text: `Selector matched no elements: ${targetSelector}`,
|
|
458
|
+
}],
|
|
459
|
+
isError: true,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
if (rect.width < 1 || rect.height < 1) {
|
|
463
|
+
return {
|
|
464
|
+
content: [{
|
|
465
|
+
type: 'text',
|
|
466
|
+
text: `Selector matched an element with zero size (${rect.width}x${rect.height}): ${targetSelector}`
|
|
467
|
+
+ ' — it is display:none / collapsed, nothing to capture.',
|
|
468
|
+
}],
|
|
469
|
+
isError: true,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
// clip 超出文档边界 playwright 直接报错 —— 夹回文档内
|
|
473
|
+
const clip = {
|
|
474
|
+
x: Math.max(0, rect.x),
|
|
475
|
+
y: Math.max(0, rect.y),
|
|
476
|
+
};
|
|
477
|
+
clip.width = Math.max(1, Math.min(rect.width, rect.docW - clip.x));
|
|
478
|
+
clip.height = Math.max(1, Math.min(rect.height, rect.docH - clip.y));
|
|
479
|
+
buf = await page.screenshot({ fullPage: true, clip, type: 'png' });
|
|
480
|
+
captureMode = `selector="${targetSelector}"`;
|
|
481
|
+
} else {
|
|
482
|
+
buf = await page.screenshot({ fullPage: fp, type: 'png' });
|
|
483
|
+
captureMode = isSite
|
|
484
|
+
? `site ${device || 'desktop'} ${vp.width}px, fullPage=${fp}`
|
|
485
|
+
: `fullPage=${fp}`;
|
|
486
|
+
}
|
|
487
|
+
// live 页的 DPR 是会话的(1),detail:'normal' 的 0.6 光栅在这里事后缩
|
|
488
|
+
if (acq.live && rasterScale < 1) {
|
|
489
|
+
const { default: sharp } = await import('sharp');
|
|
490
|
+
const m = await sharp(buf).metadata();
|
|
491
|
+
buf = await sharp(buf).resize({ width: Math.max(1, Math.round((m.width || 1) * rasterScale)) }).png().toBuffer();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// emit 让前端可见 agent 在自检
|
|
495
|
+
try {
|
|
496
|
+
ctx?.emit?.({
|
|
497
|
+
type: 'run.screenshot_taken',
|
|
498
|
+
sizeBytes: buf.length,
|
|
499
|
+
viewport: vp,
|
|
500
|
+
mode: captureMode,
|
|
501
|
+
});
|
|
502
|
+
} catch { /* emit fail-safe */ }
|
|
503
|
+
|
|
504
|
+
// ── fullPage 的 fixed/sticky 诊断(2026-08-18)──
|
|
505
|
+
// fullPage 下视口被撑成整页高,于是 position:fixed 的元素被画在"展开视口"
|
|
506
|
+
// 的对应位置:一个静止时藏在视口下方的转场帘幕(translateY(100%))会出现
|
|
507
|
+
// 在页面中段,看上去就是一大块盖住内容的色块;sticky 页头会横穿版面。
|
|
508
|
+
// 两个 agent 都把它当成真的布局 bug 去查了 computed style。一行字的事。
|
|
509
|
+
let fixedNote = null;
|
|
510
|
+
if (fp) {
|
|
511
|
+
try {
|
|
512
|
+
const found = await page.evaluate(() => {
|
|
513
|
+
const out = [];
|
|
514
|
+
for (const el of document.querySelectorAll('*')) {
|
|
515
|
+
const pos = getComputedStyle(el).position;
|
|
516
|
+
if (pos !== 'fixed' && pos !== 'sticky') continue;
|
|
517
|
+
const r = el.getBoundingClientRect();
|
|
518
|
+
if (r.width < 4 || r.height < 4) continue;
|
|
519
|
+
const id = el.id ? `#${el.id}` : (el.className && typeof el.className === 'string'
|
|
520
|
+
? `.${el.className.trim().split(/\s+/)[0]}` : el.tagName.toLowerCase());
|
|
521
|
+
out.push(`${id}(${pos})`);
|
|
522
|
+
if (out.length >= 6) break;
|
|
523
|
+
}
|
|
524
|
+
return out;
|
|
525
|
+
});
|
|
526
|
+
if (found.length) {
|
|
527
|
+
fixedNote = `⚠ ${found.length} fixed/sticky element(s) on this page (${found.join(', ')}).`
|
|
528
|
+
+ ' In a fullPage shot they are painted at their position within the EXPANDED viewport,'
|
|
529
|
+
+ ' not where the user sees them — a full-screen overlay parked below the fold will appear'
|
|
530
|
+
+ ' mid-page and look like it covers the content. Do not debug layout from that;'
|
|
531
|
+
+ ' use scrollTo to see their real position.';
|
|
532
|
+
}
|
|
533
|
+
} catch { /* 诊断挂了不挡截图 */ }
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const paintNote = await detectPaintTransform(page);
|
|
537
|
+
const shot = await normalizeShot(buf);
|
|
538
|
+
|
|
539
|
+
const captionParts = [
|
|
540
|
+
`Screenshot of ${target.relPath} (layout ${vp.width}x${vp.height} @${rasterScale}x raster, ${captureMode})`,
|
|
541
|
+
];
|
|
542
|
+
// 加载通道写进 caption:agent 不用再靠"把 location.protocol 写进 DOM 再截一张"
|
|
543
|
+
// 去反推自己被什么方式打开了(问题库 iss_msxk2oci_0v0v 就是这么查了四轮)
|
|
544
|
+
if (opened.viaHttp) {
|
|
545
|
+
captionParts.push('Loaded over http, same origin as the user preview — fetch/XHR, localStorage and dynamic imports behave exactly as they do for the user.');
|
|
546
|
+
}
|
|
547
|
+
if (shot.note) captionParts.push(shot.note);
|
|
548
|
+
if (gotoNote) captionParts.push(gotoNote);
|
|
549
|
+
if (scrollNote) captionParts.push(scrollNote);
|
|
550
|
+
if (waitForNote) captionParts.push(waitForNote);
|
|
551
|
+
if (beforeShotNote) captionParts.push(beforeShotNote);
|
|
552
|
+
if (timing.length) captionParts.push(`timing: ${timing.join(' · ')}`);
|
|
553
|
+
if (fixedNote) captionParts.push(fixedNote);
|
|
554
|
+
if (paintNote) captionParts.push(paintNote);
|
|
555
|
+
captionParts.push(diag.summary());
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
content: [
|
|
559
|
+
{
|
|
560
|
+
type: 'text',
|
|
561
|
+
text: captionParts.join('\n'),
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
type: 'image',
|
|
565
|
+
data: shot.data,
|
|
566
|
+
mimeType: shot.mimeType,
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
};
|
|
570
|
+
} catch (err) {
|
|
571
|
+
return {
|
|
572
|
+
content: [{
|
|
573
|
+
type: 'text',
|
|
574
|
+
text: `Screenshot failed: ${err?.message || String(err)}`,
|
|
575
|
+
}],
|
|
576
|
+
isError: true,
|
|
577
|
+
};
|
|
578
|
+
} finally {
|
|
579
|
+
await acq?.release?.(); // 一次性:关浏览器;live:松会话锁
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
);
|
|
583
|
+
}
|