@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.
- package/README.md +156 -106
- package/bin/nodesign.js +0 -0
- package/package.json +1 -1
- package/server/api/README.md +45 -45
- package/server/api/_guard.js +75 -75
- package/server/api/admin.js +187 -187
- package/server/api/assets/docx-page.js +103 -103
- package/server/api/assets/helpers.js +42 -22
- package/server/api/assets/notes.js +194 -144
- package/server/api/assets.js +891 -896
- package/server/api/board.js +134 -57
- package/server/api/browse.js +138 -138
- package/server/api/canvas.js +444 -444
- package/server/api/chatai.js +324 -324
- package/server/api/exports/build-standalone.js +980 -980
- package/server/api/exports/cards.js +118 -118
- package/server/api/exports/docx-pdf.js +46 -46
- package/server/api/exports/export-page.js +142 -142
- package/server/api/exports/handoff.js +147 -147
- package/server/api/exports.js +690 -690
- package/server/api/instruction.js +75 -75
- package/server/api/me.js +116 -116
- package/server/api/memory.js +185 -185
- package/server/api/pending-changes.js +366 -366
- package/server/api/plugins.js +164 -164
- package/server/api/projects.js +220 -220
- package/server/api/publish.js +64 -64
- package/server/api/recent.js +73 -73
- package/server/api/sessions.js +589 -589
- package/server/api/skills.js +77 -77
- package/server/api/standalone-fit.js +168 -168
- package/server/api/turn-compose.js +180 -180
- package/server/api/turn-inflight.js +44 -44
- package/server/auth/README.md +17 -17
- package/server/auth/middleware.js +138 -138
- package/server/auth/origin-guard.js +71 -71
- package/server/auth/session.js +108 -108
- package/server/auth/tier.js +113 -113
- package/server/auth/users-store.js +348 -348
- package/server/edit/README.md +29 -29
- package/server/engine/README.md +113 -113
- package/server/engine/agent/agent-shared.js +550 -550
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
- package/server/engine/agent/auto-mode-rules.js +78 -78
- package/server/engine/agent/context.js +418 -418
- package/server/engine/agent/events.js +465 -464
- package/server/engine/agent/hooks/canvas-validate.js +199 -199
- package/server/engine/agent/hooks/failure.js +120 -120
- package/server/engine/agent/hooks/file-events.js +75 -75
- package/server/engine/agent/hooks/lifecycle.js +233 -233
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
- package/server/engine/agent/hooks/post-guidance.js +206 -206
- package/server/engine/agent/hooks/post-subagent-report.js +104 -104
- package/server/engine/agent/hooks/post-trim.js +58 -58
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
- package/server/engine/agent/hooks/pre-defaults.js +84 -84
- package/server/engine/agent/hooks/pre-injectors.js +277 -277
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
- package/server/engine/agent/hooks/pre-starter-files.js +61 -61
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
- package/server/engine/agent/hooks/site-validate.js +94 -94
- package/server/engine/agent/hooks/tool-prompts.js +31 -31
- package/server/engine/agent/hooks/turn-state-memory.js +56 -56
- package/server/engine/agent/hooks/user-prompt-submit.js +258 -215
- package/server/engine/agent/hooks.js +354 -354
- package/server/engine/agent/init-contract.js +69 -69
- package/server/engine/agent/isolation.js +159 -159
- package/server/engine/agent/model-table.js +292 -292
- package/server/engine/agent/plugin-loader.js +266 -266
- package/server/engine/agent/prompts/nodesign-prelude.md +388 -336
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
- package/server/engine/agent/prompts/tools/site-reference.md +106 -106
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
- package/server/engine/agent/session-loop.js +1035 -1024
- package/server/engine/agent/session-model.js +164 -164
- package/server/engine/agent/skill.js +204 -204
- package/server/engine/agent/task-events.js +78 -78
- package/server/engine/agents/ds-extractor.md +132 -132
- package/server/engine/agents/explorer.md +190 -190
- package/server/engine/agents/index.js +230 -230
- package/server/engine/agents/schemas/design-system.json +156 -156
- package/server/engine/agents/schemas/tweak-schema.json +113 -113
- package/server/engine/agents/tweak-proposer.md +127 -127
- package/server/engine/agents/vision-checker.md +254 -254
- package/server/engine/browse/capture.js +422 -422
- package/server/engine/browse/card.js +137 -137
- package/server/engine/browse/handover.js +59 -59
- package/server/engine/browse/page-digest.js +119 -119
- package/server/engine/browse/refs.js +196 -196
- package/server/engine/browse/registry.js +307 -307
- package/server/engine/browse/screencast.js +170 -170
- package/server/engine/browse/state.js +121 -121
- package/server/engine/chatai/chat-log.js +79 -79
- package/server/engine/chatai/index.js +193 -193
- package/server/engine/chatai/openai-compat.js +152 -152
- package/server/engine/chatai/orchestrate.js +309 -309
- package/server/engine/chatai/perform.js +40 -40
- package/server/engine/chatai/summarize.js +79 -79
- package/server/engine/mcp/capability-gate.js +52 -52
- package/server/engine/mcp/index.js +351 -339
- package/server/engine/mcp/param-sanitizer.js +142 -142
- package/server/engine/mcp/tools/arrange-on-board.js +124 -124
- package/server/engine/mcp/tools/artifact-session.js +278 -278
- package/server/engine/mcp/tools/browse-computer.js +421 -421
- package/server/engine/mcp/tools/browse-find-batch.js +183 -183
- package/server/engine/mcp/tools/browse-screenshot.js +157 -157
- package/server/engine/mcp/tools/browse.js +480 -480
- package/server/engine/mcp/tools/build-docx.js +101 -101
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
- package/server/engine/mcp/tools/create-on-board.js +152 -155
- package/server/engine/mcp/tools/crystallize-skill.js +168 -168
- package/server/engine/mcp/tools/edit-sketch.js +167 -0
- package/server/engine/mcp/tools/explain-style.js +237 -237
- package/server/engine/mcp/tools/export-handoff.js +133 -133
- package/server/engine/mcp/tools/expose-tweaks.js +149 -149
- package/server/engine/mcp/tools/generate-image.js +842 -842
- package/server/engine/mcp/tools/get-computed-styles.js +164 -164
- package/server/engine/mcp/tools/get-pending-changes.js +205 -205
- package/server/engine/mcp/tools/h3box-ssh.js +84 -84
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
- package/server/engine/mcp/tools/highlight.js +68 -68
- package/server/engine/mcp/tools/list-pages.js +222 -222
- package/server/engine/mcp/tools/look-at-board.js +124 -0
- package/server/engine/mcp/tools/lookup-tags.js +59 -59
- package/server/engine/mcp/tools/navigate-to-page.js +60 -60
- package/server/engine/mcp/tools/organize-board.js +64 -64
- package/server/engine/mcp/tools/paint-still.js +434 -434
- package/server/engine/mcp/tools/pin-to-board.js +130 -130
- package/server/engine/mcp/tools/preview-deck.js +130 -130
- package/server/engine/mcp/tools/profile-scroll.js +305 -305
- package/server/engine/mcp/tools/publish-site.js +102 -102
- package/server/engine/mcp/tools/query-elements.js +195 -195
- package/server/engine/mcp/tools/read-board.js +190 -103
- package/server/engine/mcp/tools/read-document.js +93 -93
- package/server/engine/mcp/tools/read-page.js +264 -264
- package/server/engine/mcp/tools/read-tavern-json.js +143 -143
- package/server/engine/mcp/tools/read-user-view.js +59 -0
- package/server/engine/mcp/tools/record-decision.js +140 -140
- package/server/engine/mcp/tools/relate-on-board.js +112 -110
- package/server/engine/mcp/tools/report-issue.js +115 -115
- package/server/engine/mcp/tools/roll-film.js +266 -266
- package/server/engine/mcp/tools/screenshot-docx.js +138 -138
- package/server/engine/mcp/tools/screenshot-url.js +195 -195
- package/server/engine/mcp/tools/screenshot.js +583 -583
- package/server/engine/mcp/tools/sketch-on-board.js +337 -0
- package/server/engine/mcp/tools/tier-gate.js +96 -96
- package/server/engine/mcp/tools/trace-motion.js +215 -215
- package/server/engine/mcp/tools/web-search.js +548 -548
- package/server/engine/mcp/tools/write-on-board.js +130 -0
- package/server/engine/motion/inventory.js +349 -349
- package/server/engine/perception/session.js +235 -235
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +199 -192
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +169 -162
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +235 -228
- 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
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +330 -323
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
- package/server/engine/runs/active-runs.js +698 -698
- package/server/engine/runs/board-tasklist.js +130 -0
- package/server/engine/runs/live-turn.js +252 -252
- package/server/engine/runs/store.js +364 -364
- package/server/engine/runs/turn-relay.js +217 -217
- package/server/engine/runtime/workspace.js +164 -164
- package/server/index.js +3 -0
- package/server/lib/artifact-file-path.js +80 -80
- package/server/lib/artifact-target.js +284 -284
- package/server/lib/asset-refs.js +239 -239
- package/server/lib/async-queue.js +104 -104
- package/server/lib/auto-relations.js +130 -130
- package/server/lib/binding-types.js +72 -60
- package/server/lib/board-graph-export.js +191 -0
- package/server/lib/board-groups.js +167 -0
- package/server/lib/board-hero.js +76 -0
- package/server/lib/board-kind-sizes.js +85 -62
- package/server/lib/board-relations.js +133 -133
- package/server/lib/browse-proxy.js +180 -180
- package/server/lib/canvas-id.js +36 -36
- package/server/lib/chalk.js +121 -0
- package/server/lib/cover.js +227 -227
- package/server/lib/danbooru-tags.js +291 -291
- package/server/lib/doc-extract.js +156 -156
- package/server/lib/docx/build-from-source.js +260 -260
- package/server/lib/docx/build.js +490 -490
- package/server/lib/docx/dump-styles.js +287 -287
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
- package/server/lib/docx/merge-runs.js +129 -129
- package/server/lib/docx/numbering.js +218 -218
- package/server/lib/docx/order.js +152 -152
- package/server/lib/docx/rawzip.js +172 -172
- package/server/lib/docx/render.js +92 -92
- package/server/lib/docx/text-lint.js +225 -225
- package/server/lib/docx/tokens.js +335 -335
- package/server/lib/docx/units.js +29 -29
- package/server/lib/docx/xml.js +291 -291
- package/server/lib/docx-pages.js +187 -187
- package/server/lib/export-collect.js +289 -289
- package/server/lib/export-package.js +210 -210
- package/server/lib/html-srcset.js +145 -145
- package/server/lib/image-variant.js +460 -460
- package/server/lib/ingress/forward-openai-chat.js +305 -305
- package/server/lib/ingress/openai-chat.js +491 -491
- package/server/lib/ingress/session-notice.js +66 -66
- package/server/lib/ingress/session-routes.js +68 -68
- package/server/lib/ingress/slot-probe.js +130 -130
- package/server/lib/ingress/upstream-billing.js +52 -52
- package/server/lib/ingress/upstream-fail-streak.js +72 -72
- package/server/lib/ingress/upstream-truncation.js +48 -48
- package/server/lib/issues-store.js +182 -182
- package/server/lib/kinds/deck.js +72 -72
- package/server/lib/kinds/docx.js +283 -283
- package/server/lib/kinds/file-kinds.js +113 -113
- package/server/lib/kinds/index.js +202 -202
- package/server/lib/kinds/site.js +161 -161
- package/server/lib/model-ingress.js +543 -543
- package/server/lib/moderation.js +255 -255
- package/server/lib/notice-store.js +103 -103
- package/server/lib/plugin-install.js +104 -104
- package/server/lib/plugin-validator.js +721 -721
- package/server/lib/publish-store.js +115 -115
- package/server/lib/quick-summary.js +52 -52
- package/server/lib/quota.js +270 -270
- package/server/lib/rate-window.js +29 -29
- package/server/lib/reference-assets.js +92 -92
- package/server/lib/region-shot.js +135 -135
- package/server/lib/safe-path.js +73 -73
- package/server/lib/sdk-session.js +33 -33
- package/server/lib/showcase-store.js +92 -92
- package/server/lib/site-publish.js +465 -465
- package/server/lib/sketch-layout.js +277 -0
- package/server/lib/ssrf-guard.js +432 -432
- package/server/lib/task-scan.js +158 -158
- package/server/lib/tavern-json.js +154 -154
- package/server/lib/video-variant.js +237 -237
- package/server/lib/workspace-path.js +33 -33
- package/server/ops/fix-sdk-musl.mjs +40 -40
- package/server/ops/install-macos-fonts.sh +114 -114
- package/server/ops/macos-fonts.conf +336 -336
- package/server/ops/sandbox-shim/bwrap +57 -57
- package/server/projects/assets-summary.js +119 -119
- package/server/projects/auto-name.js +53 -53
- package/server/projects/board-limits.js +12 -0
- package/server/projects/board-sanitize.js +252 -0
- package/server/projects/board-store.js +507 -640
- package/server/projects/move-entry.js +103 -103
- package/server/projects/store.js +270 -270
- package/server/projects/ui-config.js +60 -54
- package/server/projects/viewpoint-store.js +78 -0
- package/server/projects/workspace-templates.js +53 -53
- package/server/projects/workspace.js +1025 -1025
- package/server/runtime/local-config.js +177 -177
- package/server/runtime/profile.js +72 -72
- package/server/services/rembg-service.py +334 -334
- package/server/shared/README.md +25 -25
- package/server/shared/deck.js +54 -54
- package/server/shared/time.js +49 -49
- package/server/style-pipeline/README.md +53 -53
- package/server/ws/broker.js +39 -36
- package/server/ws/browse-channel.js +177 -177
- package/server/ws/index.js +386 -386
- package/web/README.md +89 -89
- package/web/dist/assets/BrowserWindow-P_vIdh-G.js +11 -0
- package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-BpLv5082.js} +3 -3
- package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-DSAU1-tD.js} +4 -4
- package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
- package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
- package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
- package/web/dist/assets/arc-D_khH_xN.js +1 -0
- package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
- package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
- package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
- package/web/dist/assets/channel-DGkccMO1.js +1 -0
- package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
- package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
- package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
- package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
- package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
- package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
- package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
- package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
- package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
- package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
- package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
- package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
- package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
- package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
- package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
- package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
- package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
- package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
- package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
- package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
- package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
- package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
- package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
- package/web/dist/assets/index-GAamh1p4.js +2152 -0
- package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
- package/web/dist/assets/init-Gi6I4Gst.js +1 -0
- package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
- package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
- package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
- package/web/dist/assets/layout-CnXiF_NB.js +1 -0
- package/web/dist/assets/linear-BMW0u6G6.js +1 -0
- package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
- package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
- package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
- package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
- package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
- package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
- package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
- package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-KAfChbGV.js} +1 -1
- package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
- package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
- package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
- package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
- package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
- package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
- package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
- package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
- package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
- package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
- package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
- package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
- package/web/dist/index.html +17 -17
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
- package/web/dist/assets/SiteWindow-D6eS-2gM.js +0 -18
- package/web/dist/assets/index-Cbl3ATvp.js +0 -2125
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* server/lib/reference-assets.js — 参考素材清单(2026-08-18)
|
|
3
|
-
*
|
|
4
|
-
* `assets/references/**` 里的东西:web-search 下载的参考图、`browser_capture` 从
|
|
5
|
-
* 参照站带回来的截图与调色板/字体/结构 json。
|
|
6
|
-
*
|
|
7
|
-
* ## 它们此前完全看不见,原因不是"kinds 不认图片"
|
|
8
|
-
*
|
|
9
|
-
* 图片有自己的 file-kind(`kinds/file-kinds.js`,IMAGE_EXTS 六种齐全)。真正的断点
|
|
10
|
-
* 是**扫描根**:`api/assets.js` 的 `/assets` 端点只 readdir 顶层文件,而产物清单那遍
|
|
11
|
-
* `scanDir` 用的是默认 `depth = 0`(`depth <= 0` 就不进子目录)——
|
|
12
|
-
* `assets/generated` 和 `assets/notes` 能上墙只因为它们被**单独列了一行**,
|
|
13
|
-
* `assets/references/` 没被列。所以修法是加一个扫描口,不是改形态注册表。
|
|
14
|
-
*
|
|
15
|
-
* ## 走抽屉不走画布(用户 2026-08-18 拍板)
|
|
16
|
-
*
|
|
17
|
-
* 每逛一站甩十几张卡到画布上是噪音,而 web-search 下载的参考图还会成倍冒出来
|
|
18
|
-
* (它按 `count*2` 下载但只上报 `count` 张,磁盘上本来就有 agent 不知道的孤儿)。
|
|
19
|
-
* 收进「参考素材」抽屉杂波小得多。
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import path from 'node:path';
|
|
23
|
-
import fs from 'node:fs/promises';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 最多下探几层:`references/`(0) → `web/`(1) → `<站名>/`(2) → 文件。
|
|
27
|
-
*
|
|
28
|
-
* ⭐ 2026-08-18 下午 +1:`browser_capture` 改成**一站一文件夹**(按站点产物那条
|
|
29
|
-
* 范式:目录 = 单位)。深度不够的话整棵采集树在清单里凭空消失 —— 而"扫描根/深度
|
|
30
|
-
* 不够"正是当初参考素材完全看不见的真因(见文件头),同一个坑别踩第二次。
|
|
31
|
-
*/
|
|
32
|
-
const MAX_DEPTH = 3;
|
|
33
|
-
|
|
34
|
-
/** 这份素材属于哪个参照站(`assets/references/web/<站名>/…` 的那一段) */
|
|
35
|
-
function siteOf(rel) {
|
|
36
|
-
const m = /^assets\/references\/web\/([^/]+)\//.exec(rel);
|
|
37
|
-
return m ? m[1] : null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {string} assetsDir `<工作区>/assets` 的绝对路径
|
|
42
|
-
* @returns {Promise<Array<{rel,name,size,mtime,source?,lookingFor?,capturedAt?}>>}
|
|
43
|
-
* 按 mtime 倒序。读不到就返回空数组 —— 素材列不出来不该让整份 /assets 响应失败。
|
|
44
|
-
*/
|
|
45
|
-
export async function listReferences(assetsDir) {
|
|
46
|
-
const out = [];
|
|
47
|
-
|
|
48
|
-
const walk = async (dir, relPrefix, depth) => {
|
|
49
|
-
let ents;
|
|
50
|
-
try { ents = await fs.readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
51
|
-
for (const e of ents) {
|
|
52
|
-
if (e.name === '.meta') continue; // sidecar 目录本身不列
|
|
53
|
-
if (e.isDirectory()) {
|
|
54
|
-
if (depth > 0) await walk(path.join(dir, e.name), `${relPrefix}/${e.name}`, depth - 1);
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
if (!e.isFile() || e.name.startsWith('.')) continue;
|
|
58
|
-
let st;
|
|
59
|
-
try { st = await fs.stat(path.join(dir, e.name)); } catch { continue; }
|
|
60
|
-
|
|
61
|
-
// 出处:照 generate-image 的 `.meta/<主干>.json` 约定读(不另发明一套)。
|
|
62
|
-
// 没有出处很正常 —— web-search 下载的那些本来就没写 sidecar。
|
|
63
|
-
let meta = null;
|
|
64
|
-
try {
|
|
65
|
-
const stem = e.name.replace(/\.[^.]+$/, '');
|
|
66
|
-
meta = JSON.parse(await fs.readFile(path.join(dir, '.meta', `${stem}.json`), 'utf8'));
|
|
67
|
-
} catch { /* 无出处 */ }
|
|
68
|
-
|
|
69
|
-
const rel = `${relPrefix}/${e.name}`;
|
|
70
|
-
out.push({
|
|
71
|
-
rel,
|
|
72
|
-
name: e.name,
|
|
73
|
-
site: siteOf(rel),
|
|
74
|
-
// 类别写在文件名里(`.screenshot.webp` / `.palette.json` / `.css` …),
|
|
75
|
-
// sidecar 里也记着一份 —— 文件名优先,它是用户和 agent 直接看到的那个
|
|
76
|
-
category: /\.(screenshot)\.\w+$/.test(e.name) ? 'screenshot'
|
|
77
|
-
: (/\.(palette|fonts|skeleton)\.json$/.exec(e.name)?.[1] || (e.name.endsWith('.css') ? 'css' : null)),
|
|
78
|
-
size: st.size,
|
|
79
|
-
mtime: st.mtime.toISOString(),
|
|
80
|
-
...(meta ? {
|
|
81
|
-
source: meta.sourceUrl || null,
|
|
82
|
-
lookingFor: meta.lookingFor || null,
|
|
83
|
-
capturedAt: meta.capturedAt || null,
|
|
84
|
-
} : {}),
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
await walk(path.join(assetsDir, 'references'), 'assets/references', MAX_DEPTH);
|
|
90
|
-
out.sort((a, b) => (a.mtime < b.mtime ? 1 : -1));
|
|
91
|
-
return out;
|
|
92
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/reference-assets.js — 参考素材清单(2026-08-18)
|
|
3
|
+
*
|
|
4
|
+
* `assets/references/**` 里的东西:web-search 下载的参考图、`browser_capture` 从
|
|
5
|
+
* 参照站带回来的截图与调色板/字体/结构 json。
|
|
6
|
+
*
|
|
7
|
+
* ## 它们此前完全看不见,原因不是"kinds 不认图片"
|
|
8
|
+
*
|
|
9
|
+
* 图片有自己的 file-kind(`kinds/file-kinds.js`,IMAGE_EXTS 六种齐全)。真正的断点
|
|
10
|
+
* 是**扫描根**:`api/assets.js` 的 `/assets` 端点只 readdir 顶层文件,而产物清单那遍
|
|
11
|
+
* `scanDir` 用的是默认 `depth = 0`(`depth <= 0` 就不进子目录)——
|
|
12
|
+
* `assets/generated` 和 `assets/notes` 能上墙只因为它们被**单独列了一行**,
|
|
13
|
+
* `assets/references/` 没被列。所以修法是加一个扫描口,不是改形态注册表。
|
|
14
|
+
*
|
|
15
|
+
* ## 走抽屉不走画布(用户 2026-08-18 拍板)
|
|
16
|
+
*
|
|
17
|
+
* 每逛一站甩十几张卡到画布上是噪音,而 web-search 下载的参考图还会成倍冒出来
|
|
18
|
+
* (它按 `count*2` 下载但只上报 `count` 张,磁盘上本来就有 agent 不知道的孤儿)。
|
|
19
|
+
* 收进「参考素材」抽屉杂波小得多。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import fs from 'node:fs/promises';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 最多下探几层:`references/`(0) → `web/`(1) → `<站名>/`(2) → 文件。
|
|
27
|
+
*
|
|
28
|
+
* ⭐ 2026-08-18 下午 +1:`browser_capture` 改成**一站一文件夹**(按站点产物那条
|
|
29
|
+
* 范式:目录 = 单位)。深度不够的话整棵采集树在清单里凭空消失 —— 而"扫描根/深度
|
|
30
|
+
* 不够"正是当初参考素材完全看不见的真因(见文件头),同一个坑别踩第二次。
|
|
31
|
+
*/
|
|
32
|
+
const MAX_DEPTH = 3;
|
|
33
|
+
|
|
34
|
+
/** 这份素材属于哪个参照站(`assets/references/web/<站名>/…` 的那一段) */
|
|
35
|
+
function siteOf(rel) {
|
|
36
|
+
const m = /^assets\/references\/web\/([^/]+)\//.exec(rel);
|
|
37
|
+
return m ? m[1] : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} assetsDir `<工作区>/assets` 的绝对路径
|
|
42
|
+
* @returns {Promise<Array<{rel,name,size,mtime,source?,lookingFor?,capturedAt?}>>}
|
|
43
|
+
* 按 mtime 倒序。读不到就返回空数组 —— 素材列不出来不该让整份 /assets 响应失败。
|
|
44
|
+
*/
|
|
45
|
+
export async function listReferences(assetsDir) {
|
|
46
|
+
const out = [];
|
|
47
|
+
|
|
48
|
+
const walk = async (dir, relPrefix, depth) => {
|
|
49
|
+
let ents;
|
|
50
|
+
try { ents = await fs.readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
51
|
+
for (const e of ents) {
|
|
52
|
+
if (e.name === '.meta') continue; // sidecar 目录本身不列
|
|
53
|
+
if (e.isDirectory()) {
|
|
54
|
+
if (depth > 0) await walk(path.join(dir, e.name), `${relPrefix}/${e.name}`, depth - 1);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!e.isFile() || e.name.startsWith('.')) continue;
|
|
58
|
+
let st;
|
|
59
|
+
try { st = await fs.stat(path.join(dir, e.name)); } catch { continue; }
|
|
60
|
+
|
|
61
|
+
// 出处:照 generate-image 的 `.meta/<主干>.json` 约定读(不另发明一套)。
|
|
62
|
+
// 没有出处很正常 —— web-search 下载的那些本来就没写 sidecar。
|
|
63
|
+
let meta = null;
|
|
64
|
+
try {
|
|
65
|
+
const stem = e.name.replace(/\.[^.]+$/, '');
|
|
66
|
+
meta = JSON.parse(await fs.readFile(path.join(dir, '.meta', `${stem}.json`), 'utf8'));
|
|
67
|
+
} catch { /* 无出处 */ }
|
|
68
|
+
|
|
69
|
+
const rel = `${relPrefix}/${e.name}`;
|
|
70
|
+
out.push({
|
|
71
|
+
rel,
|
|
72
|
+
name: e.name,
|
|
73
|
+
site: siteOf(rel),
|
|
74
|
+
// 类别写在文件名里(`.screenshot.webp` / `.palette.json` / `.css` …),
|
|
75
|
+
// sidecar 里也记着一份 —— 文件名优先,它是用户和 agent 直接看到的那个
|
|
76
|
+
category: /\.(screenshot)\.\w+$/.test(e.name) ? 'screenshot'
|
|
77
|
+
: (/\.(palette|fonts|skeleton)\.json$/.exec(e.name)?.[1] || (e.name.endsWith('.css') ? 'css' : null)),
|
|
78
|
+
size: st.size,
|
|
79
|
+
mtime: st.mtime.toISOString(),
|
|
80
|
+
...(meta ? {
|
|
81
|
+
source: meta.sourceUrl || null,
|
|
82
|
+
lookingFor: meta.lookingFor || null,
|
|
83
|
+
capturedAt: meta.capturedAt || null,
|
|
84
|
+
} : {}),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await walk(path.join(assetsDir, 'references'), 'assets/references', MAX_DEPTH);
|
|
90
|
+
out.sort((a, b) => (a.mtime < b.mtime ? 1 : -1));
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/region-shot.js —— 把用户在预览上圈的那一块截下来。
|
|
3
|
-
*
|
|
4
|
-
* 为什么在服务端截而不是前端:产物是 iframe 里的**真实网页**,前端要把它变成
|
|
5
|
-
* 位图只能靠 html2canvas 那类重写渲染器的库 —— 那玩意儿在有 3D、滤镜、
|
|
6
|
-
* 自定义字体、伪元素的页面上出来的东西跟屏幕上不是一回事,而这张图恰恰是要
|
|
7
|
-
* 给 agent 当"眼见为实"的证据用的。chromium 真跑一遍才是它自己的样子,
|
|
8
|
-
* 而且这台机器上封面和 screenshot_canvas 早就是这么干的(lib/cover.js)。
|
|
9
|
-
*
|
|
10
|
-
* 取景:以圈为中心带一圈余量。只截圈内的话 agent 拿到一块没有上下文的碎片,
|
|
11
|
-
* 分不清"这是页面哪儿";余量给到它周围一点参照。
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import path from 'node:path';
|
|
15
|
-
import { openArtifactPage, launchPerceptionBrowser, degradedNote } from '../engine/mcp/tools/helpers/perception-page.js';
|
|
16
|
-
import fs from 'node:fs/promises';
|
|
17
|
-
|
|
18
|
-
/** 圈外多留这么多像素当上下文 */
|
|
19
|
-
export const PADDING = 32;
|
|
20
|
-
|
|
21
|
-
/** 出图长边上限,跟 screenshot 工具同口径(API 侧本来就会缩到这个规格) */
|
|
22
|
-
export const OUT_LONG_EDGE = 1200;
|
|
23
|
-
|
|
24
|
-
/** 截图串行:一台 1 核机器上同时开两个 chromium 会双双卡住 */
|
|
25
|
-
let chain = Promise.resolve();
|
|
26
|
-
function serialize(fn) {
|
|
27
|
-
const next = chain.then(fn, fn);
|
|
28
|
-
chain = next.catch(() => {});
|
|
29
|
-
return next;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @param {object} p
|
|
34
|
-
* @param {string} p.absPath 产物页面的绝对路径
|
|
35
|
-
* @param {{x,y,w,h}} p.region 页面坐标(CSS px,含滚动量)
|
|
36
|
-
* @param {{width,height}} p.viewport 用户当时的取景宽高
|
|
37
|
-
* @param {string} [p.projectId] 有它就走 http(与用户预览同源)
|
|
38
|
-
* @param {string} [p.workspaceRoot] 同上,两个都给才走 http
|
|
39
|
-
* @returns {Promise<{ buffer: Buffer, clip: {x,y,width,height}, degraded: string|null }>}
|
|
40
|
-
*/
|
|
41
|
-
export async function renderRegionShot({ absPath, region, viewport, projectId, workspaceRoot }) {
|
|
42
|
-
return serialize(async () => {
|
|
43
|
-
let browser;
|
|
44
|
-
try {
|
|
45
|
-
browser = await launchPerceptionBrowser();
|
|
46
|
-
// 圈选截的是**用户圈的那一块**,所以更要跟用户看到的同源:file:// 下
|
|
47
|
-
// fetch 回来的内容不会出现在图里,agent 会以为用户圈了一块空白。
|
|
48
|
-
const opened = await openArtifactPage(browser, {
|
|
49
|
-
projectId, workspaceRoot, absPath, viewport, deviceScaleFactor: 1, timeout: 20_000,
|
|
50
|
-
});
|
|
51
|
-
const ctx = opened.context;
|
|
52
|
-
const page = opened.page;
|
|
53
|
-
await opened.goto();
|
|
54
|
-
// 字体加载完再截(CJK 子集是 lazy 的,不显式 load 会截到 fallback 字形)
|
|
55
|
-
await page.evaluate(async () => {
|
|
56
|
-
document.body.offsetHeight;
|
|
57
|
-
await Promise.all(Array.from(document.fonts).map(f => f.load().catch(() => {})));
|
|
58
|
-
await document.fonts.ready;
|
|
59
|
-
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
60
|
-
}).catch(() => { /* 页面自己抛错也照截 */ });
|
|
61
|
-
// deck 的 standalone-fit 会在真实 viewport 下再缩一次,抹掉(同 cover.js)
|
|
62
|
-
await page.addStyleTag({ content: `
|
|
63
|
-
body { margin: 0 !important; padding: 0 !important; }
|
|
64
|
-
body.__nd-fit-active > .__nd-deck-wrap > .__nd-page-frame {
|
|
65
|
-
width: ${viewport.width}px !important; height: ${viewport.height}px !important;
|
|
66
|
-
display: block !important; overflow: visible !important;
|
|
67
|
-
}
|
|
68
|
-
body.__nd-fit-active section[data-page] { transform: none !important; }
|
|
69
|
-
` }).catch(() => {});
|
|
70
|
-
await page.waitForTimeout(500);
|
|
71
|
-
|
|
72
|
-
const want = {
|
|
73
|
-
x: Math.max(0, region.x - PADDING),
|
|
74
|
-
y: Math.max(0, region.y - PADDING),
|
|
75
|
-
w: region.w + PADDING * 2,
|
|
76
|
-
h: region.h + PADDING * 2,
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// 滚到目标位置再按视口坐标裁。**滚动会被文档末尾夹住**,所以要把真实
|
|
80
|
-
// 落点读回来算裁剪框,不然页尾附近截出来的是错位的一块。
|
|
81
|
-
const scroll = await page.evaluate(([x, y]) => {
|
|
82
|
-
window.scrollTo(x, y);
|
|
83
|
-
const e = document.scrollingElement || document.documentElement;
|
|
84
|
-
return [e.scrollLeft, e.scrollTop];
|
|
85
|
-
}, [want.x, want.y]);
|
|
86
|
-
|
|
87
|
-
const clip = {
|
|
88
|
-
x: Math.max(0, want.x - scroll[0]),
|
|
89
|
-
y: Math.max(0, want.y - scroll[1]),
|
|
90
|
-
width: Math.min(want.w, viewport.width),
|
|
91
|
-
height: Math.min(want.h, viewport.height),
|
|
92
|
-
};
|
|
93
|
-
clip.width = Math.max(1, Math.min(clip.width, viewport.width - clip.x));
|
|
94
|
-
clip.height = Math.max(1, Math.min(clip.height, viewport.height - clip.y));
|
|
95
|
-
|
|
96
|
-
// ⚠️ 滚完要**等一拍**再截。上面那个 500ms 花在滚动**之前**,滚完立刻截 →
|
|
97
|
-
// 视口里新进来的懒加载图(`loading=lazy` / IntersectionObserver)还没解码,
|
|
98
|
-
// 图里是空的。而 `get_pending_changes` 把这张图当作"用户所见的 ground truth"
|
|
99
|
-
// 交给 agent —— 空白处会被当成"用户圈了一块空白"。
|
|
100
|
-
await page.evaluate(async () => {
|
|
101
|
-
// 给 IO 回调一次机会,再等两帧确保解码后的图上了屏
|
|
102
|
-
await new Promise(r => setTimeout(r, 250));
|
|
103
|
-
await Promise.all([...document.images].filter(i => !i.complete)
|
|
104
|
-
.map(i => new Promise((res) => { i.addEventListener('load', res, { once: true });
|
|
105
|
-
i.addEventListener('error', res, { once: true }); setTimeout(res, 1200); })));
|
|
106
|
-
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
107
|
-
}).catch(() => { /* 页面自己抛错也照截 */ });
|
|
108
|
-
|
|
109
|
-
const png = await page.screenshot({ type: 'png', clip });
|
|
110
|
-
await ctx.close();
|
|
111
|
-
|
|
112
|
-
const { default: sharp } = await import('sharp');
|
|
113
|
-
const buffer = await sharp(png)
|
|
114
|
-
.resize({ width: OUT_LONG_EDGE, height: OUT_LONG_EDGE, fit: 'inside', withoutEnlargement: true })
|
|
115
|
-
.webp({ quality: 82 })
|
|
116
|
-
.toBuffer();
|
|
117
|
-
// note 非空 = 没走成 http。圈选截图尤其在意这个:file:// 下 fetch 回来的
|
|
118
|
-
// 内容不会出现在图里,而这张图是被当"ground truth"用的
|
|
119
|
-
return { buffer, clip, degraded: degradedNote(opened) };
|
|
120
|
-
} finally {
|
|
121
|
-
await browser?.close().catch(() => {});
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** 圈选截图在会话工作区里的落点(agent 按这个相对路径找得到) */
|
|
127
|
-
export const SHOT_DIR = 'region-shots';
|
|
128
|
-
|
|
129
|
-
export async function saveRegionShot(sessionRoot, id, buffer) {
|
|
130
|
-
const dir = path.join(sessionRoot, SHOT_DIR);
|
|
131
|
-
await fs.mkdir(dir, { recursive: true });
|
|
132
|
-
const rel = `${SHOT_DIR}/${id}.webp`;
|
|
133
|
-
await fs.writeFile(path.join(sessionRoot, rel), buffer);
|
|
134
|
-
return rel;
|
|
135
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* lib/region-shot.js —— 把用户在预览上圈的那一块截下来。
|
|
3
|
+
*
|
|
4
|
+
* 为什么在服务端截而不是前端:产物是 iframe 里的**真实网页**,前端要把它变成
|
|
5
|
+
* 位图只能靠 html2canvas 那类重写渲染器的库 —— 那玩意儿在有 3D、滤镜、
|
|
6
|
+
* 自定义字体、伪元素的页面上出来的东西跟屏幕上不是一回事,而这张图恰恰是要
|
|
7
|
+
* 给 agent 当"眼见为实"的证据用的。chromium 真跑一遍才是它自己的样子,
|
|
8
|
+
* 而且这台机器上封面和 screenshot_canvas 早就是这么干的(lib/cover.js)。
|
|
9
|
+
*
|
|
10
|
+
* 取景:以圈为中心带一圈余量。只截圈内的话 agent 拿到一块没有上下文的碎片,
|
|
11
|
+
* 分不清"这是页面哪儿";余量给到它周围一点参照。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { openArtifactPage, launchPerceptionBrowser, degradedNote } from '../engine/mcp/tools/helpers/perception-page.js';
|
|
16
|
+
import fs from 'node:fs/promises';
|
|
17
|
+
|
|
18
|
+
/** 圈外多留这么多像素当上下文 */
|
|
19
|
+
export const PADDING = 32;
|
|
20
|
+
|
|
21
|
+
/** 出图长边上限,跟 screenshot 工具同口径(API 侧本来就会缩到这个规格) */
|
|
22
|
+
export const OUT_LONG_EDGE = 1200;
|
|
23
|
+
|
|
24
|
+
/** 截图串行:一台 1 核机器上同时开两个 chromium 会双双卡住 */
|
|
25
|
+
let chain = Promise.resolve();
|
|
26
|
+
function serialize(fn) {
|
|
27
|
+
const next = chain.then(fn, fn);
|
|
28
|
+
chain = next.catch(() => {});
|
|
29
|
+
return next;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {object} p
|
|
34
|
+
* @param {string} p.absPath 产物页面的绝对路径
|
|
35
|
+
* @param {{x,y,w,h}} p.region 页面坐标(CSS px,含滚动量)
|
|
36
|
+
* @param {{width,height}} p.viewport 用户当时的取景宽高
|
|
37
|
+
* @param {string} [p.projectId] 有它就走 http(与用户预览同源)
|
|
38
|
+
* @param {string} [p.workspaceRoot] 同上,两个都给才走 http
|
|
39
|
+
* @returns {Promise<{ buffer: Buffer, clip: {x,y,width,height}, degraded: string|null }>}
|
|
40
|
+
*/
|
|
41
|
+
export async function renderRegionShot({ absPath, region, viewport, projectId, workspaceRoot }) {
|
|
42
|
+
return serialize(async () => {
|
|
43
|
+
let browser;
|
|
44
|
+
try {
|
|
45
|
+
browser = await launchPerceptionBrowser();
|
|
46
|
+
// 圈选截的是**用户圈的那一块**,所以更要跟用户看到的同源:file:// 下
|
|
47
|
+
// fetch 回来的内容不会出现在图里,agent 会以为用户圈了一块空白。
|
|
48
|
+
const opened = await openArtifactPage(browser, {
|
|
49
|
+
projectId, workspaceRoot, absPath, viewport, deviceScaleFactor: 1, timeout: 20_000,
|
|
50
|
+
});
|
|
51
|
+
const ctx = opened.context;
|
|
52
|
+
const page = opened.page;
|
|
53
|
+
await opened.goto();
|
|
54
|
+
// 字体加载完再截(CJK 子集是 lazy 的,不显式 load 会截到 fallback 字形)
|
|
55
|
+
await page.evaluate(async () => {
|
|
56
|
+
document.body.offsetHeight;
|
|
57
|
+
await Promise.all(Array.from(document.fonts).map(f => f.load().catch(() => {})));
|
|
58
|
+
await document.fonts.ready;
|
|
59
|
+
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
60
|
+
}).catch(() => { /* 页面自己抛错也照截 */ });
|
|
61
|
+
// deck 的 standalone-fit 会在真实 viewport 下再缩一次,抹掉(同 cover.js)
|
|
62
|
+
await page.addStyleTag({ content: `
|
|
63
|
+
body { margin: 0 !important; padding: 0 !important; }
|
|
64
|
+
body.__nd-fit-active > .__nd-deck-wrap > .__nd-page-frame {
|
|
65
|
+
width: ${viewport.width}px !important; height: ${viewport.height}px !important;
|
|
66
|
+
display: block !important; overflow: visible !important;
|
|
67
|
+
}
|
|
68
|
+
body.__nd-fit-active section[data-page] { transform: none !important; }
|
|
69
|
+
` }).catch(() => {});
|
|
70
|
+
await page.waitForTimeout(500);
|
|
71
|
+
|
|
72
|
+
const want = {
|
|
73
|
+
x: Math.max(0, region.x - PADDING),
|
|
74
|
+
y: Math.max(0, region.y - PADDING),
|
|
75
|
+
w: region.w + PADDING * 2,
|
|
76
|
+
h: region.h + PADDING * 2,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// 滚到目标位置再按视口坐标裁。**滚动会被文档末尾夹住**,所以要把真实
|
|
80
|
+
// 落点读回来算裁剪框,不然页尾附近截出来的是错位的一块。
|
|
81
|
+
const scroll = await page.evaluate(([x, y]) => {
|
|
82
|
+
window.scrollTo(x, y);
|
|
83
|
+
const e = document.scrollingElement || document.documentElement;
|
|
84
|
+
return [e.scrollLeft, e.scrollTop];
|
|
85
|
+
}, [want.x, want.y]);
|
|
86
|
+
|
|
87
|
+
const clip = {
|
|
88
|
+
x: Math.max(0, want.x - scroll[0]),
|
|
89
|
+
y: Math.max(0, want.y - scroll[1]),
|
|
90
|
+
width: Math.min(want.w, viewport.width),
|
|
91
|
+
height: Math.min(want.h, viewport.height),
|
|
92
|
+
};
|
|
93
|
+
clip.width = Math.max(1, Math.min(clip.width, viewport.width - clip.x));
|
|
94
|
+
clip.height = Math.max(1, Math.min(clip.height, viewport.height - clip.y));
|
|
95
|
+
|
|
96
|
+
// ⚠️ 滚完要**等一拍**再截。上面那个 500ms 花在滚动**之前**,滚完立刻截 →
|
|
97
|
+
// 视口里新进来的懒加载图(`loading=lazy` / IntersectionObserver)还没解码,
|
|
98
|
+
// 图里是空的。而 `get_pending_changes` 把这张图当作"用户所见的 ground truth"
|
|
99
|
+
// 交给 agent —— 空白处会被当成"用户圈了一块空白"。
|
|
100
|
+
await page.evaluate(async () => {
|
|
101
|
+
// 给 IO 回调一次机会,再等两帧确保解码后的图上了屏
|
|
102
|
+
await new Promise(r => setTimeout(r, 250));
|
|
103
|
+
await Promise.all([...document.images].filter(i => !i.complete)
|
|
104
|
+
.map(i => new Promise((res) => { i.addEventListener('load', res, { once: true });
|
|
105
|
+
i.addEventListener('error', res, { once: true }); setTimeout(res, 1200); })));
|
|
106
|
+
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
107
|
+
}).catch(() => { /* 页面自己抛错也照截 */ });
|
|
108
|
+
|
|
109
|
+
const png = await page.screenshot({ type: 'png', clip });
|
|
110
|
+
await ctx.close();
|
|
111
|
+
|
|
112
|
+
const { default: sharp } = await import('sharp');
|
|
113
|
+
const buffer = await sharp(png)
|
|
114
|
+
.resize({ width: OUT_LONG_EDGE, height: OUT_LONG_EDGE, fit: 'inside', withoutEnlargement: true })
|
|
115
|
+
.webp({ quality: 82 })
|
|
116
|
+
.toBuffer();
|
|
117
|
+
// note 非空 = 没走成 http。圈选截图尤其在意这个:file:// 下 fetch 回来的
|
|
118
|
+
// 内容不会出现在图里,而这张图是被当"ground truth"用的
|
|
119
|
+
return { buffer, clip, degraded: degradedNote(opened) };
|
|
120
|
+
} finally {
|
|
121
|
+
await browser?.close().catch(() => {});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 圈选截图在会话工作区里的落点(agent 按这个相对路径找得到) */
|
|
127
|
+
export const SHOT_DIR = 'region-shots';
|
|
128
|
+
|
|
129
|
+
export async function saveRegionShot(sessionRoot, id, buffer) {
|
|
130
|
+
const dir = path.join(sessionRoot, SHOT_DIR);
|
|
131
|
+
await fs.mkdir(dir, { recursive: true });
|
|
132
|
+
const rel = `${SHOT_DIR}/${id}.webp`;
|
|
133
|
+
await fs.writeFile(path.join(sessionRoot, rel), buffer);
|
|
134
|
+
return rel;
|
|
135
|
+
}
|
package/server/lib/safe-path.js
CHANGED
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/safe-path.js — 工作区内路径的越界检查。
|
|
3
|
-
*
|
|
4
|
-
* ⚠️ **词法检查不够。** `path.resolve` + `path.relative` 挡得住 `../..`,
|
|
5
|
-
* 挡不住工作区里一个**指向外面的软链** —— 那条路径逐字看都在工作区内,
|
|
6
|
-
* 解引用之后在 `/etc` 或者 `.env` 上。
|
|
7
|
-
*
|
|
8
|
-
* 这个洞在这个仓库里已经出现过两次形状相同的实例(导出收集器、docx 页图路由),
|
|
9
|
-
* 第二次是照着第一次的**调用形状**抄的、没抄它的**正确性**。所以判据收成一份:
|
|
10
|
-
* 谁要在工作区里按用户给的相对路径开文件,就 import 这里,别再各写一遍。
|
|
11
|
-
*
|
|
12
|
-
* 读和写的判据不一样,分两个函数:
|
|
13
|
-
* - 读:目标必须存在,realpath 之后仍在工作区内
|
|
14
|
-
* - 写:目标可能还不存在(正要创建),所以查**父目录**的 realpath;
|
|
15
|
-
* 另外目标自己若已经是软链,一律拒 —— 顺着它写等于把别人的文件覆盖掉
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import path from 'node:path';
|
|
19
|
-
import { promises as fs } from 'node:fs';
|
|
20
|
-
|
|
21
|
-
/** 词法层:拼出绝对路径并确认字面上没跑出去。跑出去返回 null */
|
|
22
|
-
function lexical(rootAbs, rel) {
|
|
23
|
-
const abs = path.resolve(rootAbs, String(rel || ''));
|
|
24
|
-
const within = path.relative(rootAbs, abs);
|
|
25
|
-
if (within.startsWith('..') || path.isAbsolute(within)) return null;
|
|
26
|
-
return abs;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 读用:解析一个工作区内的相对路径。
|
|
31
|
-
* @returns {Promise<string|null>} 绝对路径;越界(含软链穿透)返回 null
|
|
32
|
-
* 目标不存在**不算越界** —— 让调用方自己去 stat 报 404,这里只管边界
|
|
33
|
-
*/
|
|
34
|
-
export async function safeResolveRead(workspaceRoot, rel) {
|
|
35
|
-
const abs = lexical(path.resolve(workspaceRoot), rel);
|
|
36
|
-
if (!abs) return null;
|
|
37
|
-
try {
|
|
38
|
-
const realRoot = await fs.realpath(workspaceRoot);
|
|
39
|
-
const real = await fs.realpath(abs);
|
|
40
|
-
const rw = path.relative(realRoot, real);
|
|
41
|
-
if (rw.startsWith('..') || path.isAbsolute(rw)) return null;
|
|
42
|
-
} catch (err) {
|
|
43
|
-
// ENOENT:文件还没写出来,边界上没问题,留给调用方 stat
|
|
44
|
-
if (err?.code !== 'ENOENT') return null;
|
|
45
|
-
}
|
|
46
|
-
return abs;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 写用:解析一个工作区内的写入目标。
|
|
51
|
-
* @returns {Promise<string|null>} 绝对路径;越界 / 目标是软链 返回 null
|
|
52
|
-
*/
|
|
53
|
-
export async function safeResolveWrite(workspaceRoot, rel) {
|
|
54
|
-
const abs = lexical(path.resolve(workspaceRoot), rel);
|
|
55
|
-
if (!abs) return null;
|
|
56
|
-
try {
|
|
57
|
-
const realRoot = await fs.realpath(workspaceRoot);
|
|
58
|
-
// 目标本身若已存在且是软链 → 拒。顺着它写会覆盖软链指向的那个文件,
|
|
59
|
-
// 而那个文件可以在工作区外(lstat 不解引用,这是判据的关键)
|
|
60
|
-
try {
|
|
61
|
-
if ((await fs.lstat(abs)).isSymbolicLink()) return null;
|
|
62
|
-
} catch (err) {
|
|
63
|
-
if (err?.code !== 'ENOENT') return null; // 目标还不存在是正常的
|
|
64
|
-
}
|
|
65
|
-
// 父目录必须真的在工作区内(父目录是软链的话,新建的文件会落到外面)
|
|
66
|
-
const realParent = await fs.realpath(path.dirname(abs));
|
|
67
|
-
const rw = path.relative(realRoot, realParent);
|
|
68
|
-
if (rw.startsWith('..') || path.isAbsolute(rw)) return null;
|
|
69
|
-
} catch {
|
|
70
|
-
return null; // 父目录都解析不了,不给写
|
|
71
|
-
}
|
|
72
|
-
return abs;
|
|
73
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* lib/safe-path.js — 工作区内路径的越界检查。
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ **词法检查不够。** `path.resolve` + `path.relative` 挡得住 `../..`,
|
|
5
|
+
* 挡不住工作区里一个**指向外面的软链** —— 那条路径逐字看都在工作区内,
|
|
6
|
+
* 解引用之后在 `/etc` 或者 `.env` 上。
|
|
7
|
+
*
|
|
8
|
+
* 这个洞在这个仓库里已经出现过两次形状相同的实例(导出收集器、docx 页图路由),
|
|
9
|
+
* 第二次是照着第一次的**调用形状**抄的、没抄它的**正确性**。所以判据收成一份:
|
|
10
|
+
* 谁要在工作区里按用户给的相对路径开文件,就 import 这里,别再各写一遍。
|
|
11
|
+
*
|
|
12
|
+
* 读和写的判据不一样,分两个函数:
|
|
13
|
+
* - 读:目标必须存在,realpath 之后仍在工作区内
|
|
14
|
+
* - 写:目标可能还不存在(正要创建),所以查**父目录**的 realpath;
|
|
15
|
+
* 另外目标自己若已经是软链,一律拒 —— 顺着它写等于把别人的文件覆盖掉
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { promises as fs } from 'node:fs';
|
|
20
|
+
|
|
21
|
+
/** 词法层:拼出绝对路径并确认字面上没跑出去。跑出去返回 null */
|
|
22
|
+
function lexical(rootAbs, rel) {
|
|
23
|
+
const abs = path.resolve(rootAbs, String(rel || ''));
|
|
24
|
+
const within = path.relative(rootAbs, abs);
|
|
25
|
+
if (within.startsWith('..') || path.isAbsolute(within)) return null;
|
|
26
|
+
return abs;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 读用:解析一个工作区内的相对路径。
|
|
31
|
+
* @returns {Promise<string|null>} 绝对路径;越界(含软链穿透)返回 null
|
|
32
|
+
* 目标不存在**不算越界** —— 让调用方自己去 stat 报 404,这里只管边界
|
|
33
|
+
*/
|
|
34
|
+
export async function safeResolveRead(workspaceRoot, rel) {
|
|
35
|
+
const abs = lexical(path.resolve(workspaceRoot), rel);
|
|
36
|
+
if (!abs) return null;
|
|
37
|
+
try {
|
|
38
|
+
const realRoot = await fs.realpath(workspaceRoot);
|
|
39
|
+
const real = await fs.realpath(abs);
|
|
40
|
+
const rw = path.relative(realRoot, real);
|
|
41
|
+
if (rw.startsWith('..') || path.isAbsolute(rw)) return null;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
// ENOENT:文件还没写出来,边界上没问题,留给调用方 stat
|
|
44
|
+
if (err?.code !== 'ENOENT') return null;
|
|
45
|
+
}
|
|
46
|
+
return abs;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 写用:解析一个工作区内的写入目标。
|
|
51
|
+
* @returns {Promise<string|null>} 绝对路径;越界 / 目标是软链 返回 null
|
|
52
|
+
*/
|
|
53
|
+
export async function safeResolveWrite(workspaceRoot, rel) {
|
|
54
|
+
const abs = lexical(path.resolve(workspaceRoot), rel);
|
|
55
|
+
if (!abs) return null;
|
|
56
|
+
try {
|
|
57
|
+
const realRoot = await fs.realpath(workspaceRoot);
|
|
58
|
+
// 目标本身若已存在且是软链 → 拒。顺着它写会覆盖软链指向的那个文件,
|
|
59
|
+
// 而那个文件可以在工作区外(lstat 不解引用,这是判据的关键)
|
|
60
|
+
try {
|
|
61
|
+
if ((await fs.lstat(abs)).isSymbolicLink()) return null;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (err?.code !== 'ENOENT') return null; // 目标还不存在是正常的
|
|
64
|
+
}
|
|
65
|
+
// 父目录必须真的在工作区内(父目录是软链的话,新建的文件会落到外面)
|
|
66
|
+
const realParent = await fs.realpath(path.dirname(abs));
|
|
67
|
+
const rw = path.relative(realRoot, realParent);
|
|
68
|
+
if (rw.startsWith('..') || path.isAbsolute(rw)) return null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null; // 父目录都解析不了,不给写
|
|
71
|
+
}
|
|
72
|
+
return abs;
|
|
73
|
+
}
|