@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,102 +1,102 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* mcp/tools/publish-site.js — publish_site MCP tool(2026-08-02)
|
|
3
|
-
*
|
|
4
|
-
* agent 版的「一键上线」:把任务里的目录站点发到 Cloudflare Pages 公网。
|
|
5
|
-
* 与站点窗的上线按钮共用 lib/site-publish.js 的全部闸门 —— 额度和权限按
|
|
6
|
-
* **项目 owner** 算(basic 档不能发、pro 档每人 2 个;档位见 auth/tier.js),不是"谁触发算谁"。
|
|
7
|
-
*
|
|
8
|
-
* 发到公网是外发动作:工具描述里写死"用户明确要求才调"。
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
12
|
-
import { z } from 'zod';
|
|
13
|
-
import { getProject } from '../../../projects/store.js';
|
|
14
|
-
import { getUserById } from '../../../auth/users-store.js';
|
|
15
|
-
import { publishSite, unpublishSite, lookupPublished } from '../../../lib/site-publish.js';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* @param {object} deps
|
|
19
|
-
* @param {string} [deps.projectId]
|
|
20
|
-
*/
|
|
21
|
-
export function makePublishSiteTool({ projectId }) {
|
|
22
|
-
return tool(
|
|
23
|
-
'publish_site',
|
|
24
|
-
`Publish a site task to the public web (Cloudflare Pages), update an already
|
|
25
|
-
published site, or take it offline.
|
|
26
|
-
|
|
27
|
-
Call this ONLY when the user explicitly asks to put the site online / update the
|
|
28
|
-
live version / take it down. Never publish unprompted — going public is the
|
|
29
|
-
user's call, not yours.
|
|
30
|
-
|
|
31
|
-
action:
|
|
32
|
-
- "publish": deploy the current files. First publish returns the permanent URL
|
|
33
|
-
(a brand-new domain may take a minute or two to start resolving); republishing
|
|
34
|
-
keeps the same URL.
|
|
35
|
-
- "unpublish": delete the deployment; the public URL dies immediately.
|
|
36
|
-
- "status": just report whether this task is published and at which URL.
|
|
37
|
-
|
|
38
|
-
If the workspace contains more than one site, the tool refuses to guess and lists
|
|
39
|
-
the candidates — pass "root" to name the one to publish ("." = the workspace root).
|
|
40
|
-
Each site holds ONE public URL, keyed by its own root directory.
|
|
41
|
-
|
|
42
|
-
**The public domain**: by default it is derived from the site's root directory
|
|
43
|
-
name; Chinese names slug down to nothing, so those fall back to a short hash
|
|
44
|
-
("a1b2c3.share..."). Pass "slug" to choose a readable one — do that whenever the
|
|
45
|
-
site has a name worth showing, because the URL is what the user hands to other
|
|
46
|
-
people. A slug is claimed on first publish and reused on every republish.
|
|
47
|
-
|
|
48
|
-
**Verifying a fresh publish**: a brand-new domain needs a certificate issued
|
|
49
|
-
before https works; until then screenshot_url fails with an SSL error. This tool
|
|
50
|
-
now waits for it and tells you whether it is ready. Also note Cloudflare Pages
|
|
51
|
-
serves the site's own 404.html for unknown paths — an HTTP 200 does NOT prove a
|
|
52
|
-
path exists, check the content type and body.
|
|
53
|
-
|
|
54
|
-
The user's publish quota and permissions apply (basic-tier accounts cannot publish;
|
|
55
|
-
pro accounts have a per-user site limit). If the tool returns a quota or
|
|
56
|
-
permission error, relay it as-is — do not retry.`,
|
|
57
|
-
{
|
|
58
|
-
task: z.string().describe(
|
|
59
|
-
'站点标识:站点根目录的相对路径(工作区根上那个站传 ".")。'
|
|
60
|
-
+ '不确定就传 "." —— 只有一个站时工具会自己认出来'),
|
|
61
|
-
action: z.enum(['publish', 'unpublish', 'status']).default('publish'),
|
|
62
|
-
root: z.string().optional().describe(
|
|
63
|
-
'有多个平行站点时点名要发布哪个(相对工作区根,根上那个站传 ".")。单站点省略'),
|
|
64
|
-
slug: z.string().optional().describe(
|
|
65
|
-
'自选公网域名前缀,如 "chenxi" → chenxi.share.<域>。'
|
|
66
|
-
+ '小写字母开头、3-32 位小写字母/数字/连字符。被占用会明确报错让你换一个'),
|
|
67
|
-
},
|
|
68
|
-
async ({ task, action, root, slug }) => {
|
|
69
|
-
const asText = (text) => ({ content: [{ type: 'text', text }] });
|
|
70
|
-
try {
|
|
71
|
-
const project = getProject(projectId);
|
|
72
|
-
if (!project) return asText('错误:项目不存在');
|
|
73
|
-
const owner = project.ownerId ? getUserById(project.ownerId) : null;
|
|
74
|
-
if (!owner) return asText('错误:找不到项目归属用户,不能发布');
|
|
75
|
-
|
|
76
|
-
if (action === 'status') {
|
|
77
|
-
const site = await lookupPublished(projectId, task);
|
|
78
|
-
return asText(site
|
|
79
|
-
? `已发布:${site.url}(站点根 ${site.task},最近发布 ${site.lastPublishedAt})`
|
|
80
|
-
: '未发布');
|
|
81
|
-
}
|
|
82
|
-
if (action === 'unpublish') {
|
|
83
|
-
const removed = await unpublishSite({ projectId, task });
|
|
84
|
-
return asText(removed ? '已下线,公网地址即刻失效。' : '本来就没有发布。');
|
|
85
|
-
}
|
|
86
|
-
const r = await publishSite({ projectId, task, root, slug, user: owner });
|
|
87
|
-
return asText([
|
|
88
|
-
`已上线:${r.site.url}`,
|
|
89
|
-
`发布的是 ${r.root === '.' ? '工作区根' : `${r.root}/`},${r.files} 个文件,入口 ${r.entry || '(无 html)'}`,
|
|
90
|
-
r.certReady === true
|
|
91
|
-
? '证书已就绪,现在就能 screenshot_url 自检。'
|
|
92
|
-
: (r.certReady === false
|
|
93
|
-
? '⚠ 证书还没签发完(等了 150 秒,通常 2-3 分钟)—— 现在截图会拿到 SSL 错误,过一会儿再试。'
|
|
94
|
-
: '重新发布地址不变。'),
|
|
95
|
-
r.warning ? `注意:${r.warning}` : null,
|
|
96
|
-
].filter(Boolean).join('\n'));
|
|
97
|
-
} catch (err) {
|
|
98
|
-
return asText(`发布操作失败:${err.message}`);
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
);
|
|
102
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* mcp/tools/publish-site.js — publish_site MCP tool(2026-08-02)
|
|
3
|
+
*
|
|
4
|
+
* agent 版的「一键上线」:把任务里的目录站点发到 Cloudflare Pages 公网。
|
|
5
|
+
* 与站点窗的上线按钮共用 lib/site-publish.js 的全部闸门 —— 额度和权限按
|
|
6
|
+
* **项目 owner** 算(basic 档不能发、pro 档每人 2 个;档位见 auth/tier.js),不是"谁触发算谁"。
|
|
7
|
+
*
|
|
8
|
+
* 发到公网是外发动作:工具描述里写死"用户明确要求才调"。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { getProject } from '../../../projects/store.js';
|
|
14
|
+
import { getUserById } from '../../../auth/users-store.js';
|
|
15
|
+
import { publishSite, unpublishSite, lookupPublished } from '../../../lib/site-publish.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} deps
|
|
19
|
+
* @param {string} [deps.projectId]
|
|
20
|
+
*/
|
|
21
|
+
export function makePublishSiteTool({ projectId }) {
|
|
22
|
+
return tool(
|
|
23
|
+
'publish_site',
|
|
24
|
+
`Publish a site task to the public web (Cloudflare Pages), update an already
|
|
25
|
+
published site, or take it offline.
|
|
26
|
+
|
|
27
|
+
Call this ONLY when the user explicitly asks to put the site online / update the
|
|
28
|
+
live version / take it down. Never publish unprompted — going public is the
|
|
29
|
+
user's call, not yours.
|
|
30
|
+
|
|
31
|
+
action:
|
|
32
|
+
- "publish": deploy the current files. First publish returns the permanent URL
|
|
33
|
+
(a brand-new domain may take a minute or two to start resolving); republishing
|
|
34
|
+
keeps the same URL.
|
|
35
|
+
- "unpublish": delete the deployment; the public URL dies immediately.
|
|
36
|
+
- "status": just report whether this task is published and at which URL.
|
|
37
|
+
|
|
38
|
+
If the workspace contains more than one site, the tool refuses to guess and lists
|
|
39
|
+
the candidates — pass "root" to name the one to publish ("." = the workspace root).
|
|
40
|
+
Each site holds ONE public URL, keyed by its own root directory.
|
|
41
|
+
|
|
42
|
+
**The public domain**: by default it is derived from the site's root directory
|
|
43
|
+
name; Chinese names slug down to nothing, so those fall back to a short hash
|
|
44
|
+
("a1b2c3.share..."). Pass "slug" to choose a readable one — do that whenever the
|
|
45
|
+
site has a name worth showing, because the URL is what the user hands to other
|
|
46
|
+
people. A slug is claimed on first publish and reused on every republish.
|
|
47
|
+
|
|
48
|
+
**Verifying a fresh publish**: a brand-new domain needs a certificate issued
|
|
49
|
+
before https works; until then screenshot_url fails with an SSL error. This tool
|
|
50
|
+
now waits for it and tells you whether it is ready. Also note Cloudflare Pages
|
|
51
|
+
serves the site's own 404.html for unknown paths — an HTTP 200 does NOT prove a
|
|
52
|
+
path exists, check the content type and body.
|
|
53
|
+
|
|
54
|
+
The user's publish quota and permissions apply (basic-tier accounts cannot publish;
|
|
55
|
+
pro accounts have a per-user site limit). If the tool returns a quota or
|
|
56
|
+
permission error, relay it as-is — do not retry.`,
|
|
57
|
+
{
|
|
58
|
+
task: z.string().describe(
|
|
59
|
+
'站点标识:站点根目录的相对路径(工作区根上那个站传 ".")。'
|
|
60
|
+
+ '不确定就传 "." —— 只有一个站时工具会自己认出来'),
|
|
61
|
+
action: z.enum(['publish', 'unpublish', 'status']).default('publish'),
|
|
62
|
+
root: z.string().optional().describe(
|
|
63
|
+
'有多个平行站点时点名要发布哪个(相对工作区根,根上那个站传 ".")。单站点省略'),
|
|
64
|
+
slug: z.string().optional().describe(
|
|
65
|
+
'自选公网域名前缀,如 "chenxi" → chenxi.share.<域>。'
|
|
66
|
+
+ '小写字母开头、3-32 位小写字母/数字/连字符。被占用会明确报错让你换一个'),
|
|
67
|
+
},
|
|
68
|
+
async ({ task, action, root, slug }) => {
|
|
69
|
+
const asText = (text) => ({ content: [{ type: 'text', text }] });
|
|
70
|
+
try {
|
|
71
|
+
const project = getProject(projectId);
|
|
72
|
+
if (!project) return asText('错误:项目不存在');
|
|
73
|
+
const owner = project.ownerId ? getUserById(project.ownerId) : null;
|
|
74
|
+
if (!owner) return asText('错误:找不到项目归属用户,不能发布');
|
|
75
|
+
|
|
76
|
+
if (action === 'status') {
|
|
77
|
+
const site = await lookupPublished(projectId, task);
|
|
78
|
+
return asText(site
|
|
79
|
+
? `已发布:${site.url}(站点根 ${site.task},最近发布 ${site.lastPublishedAt})`
|
|
80
|
+
: '未发布');
|
|
81
|
+
}
|
|
82
|
+
if (action === 'unpublish') {
|
|
83
|
+
const removed = await unpublishSite({ projectId, task });
|
|
84
|
+
return asText(removed ? '已下线,公网地址即刻失效。' : '本来就没有发布。');
|
|
85
|
+
}
|
|
86
|
+
const r = await publishSite({ projectId, task, root, slug, user: owner });
|
|
87
|
+
return asText([
|
|
88
|
+
`已上线:${r.site.url}`,
|
|
89
|
+
`发布的是 ${r.root === '.' ? '工作区根' : `${r.root}/`},${r.files} 个文件,入口 ${r.entry || '(无 html)'}`,
|
|
90
|
+
r.certReady === true
|
|
91
|
+
? '证书已就绪,现在就能 screenshot_url 自检。'
|
|
92
|
+
: (r.certReady === false
|
|
93
|
+
? '⚠ 证书还没签发完(等了 150 秒,通常 2-3 分钟)—— 现在截图会拿到 SSL 错误,过一会儿再试。'
|
|
94
|
+
: '重新发布地址不变。'),
|
|
95
|
+
r.warning ? `注意:${r.warning}` : null,
|
|
96
|
+
].filter(Boolean).join('\n'));
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return asText(`发布操作失败:${err.message}`);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -1,195 +1,195 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* mcp/tools/query-elements.js — query_elements MCP tool
|
|
3
|
-
*
|
|
4
|
-
* 用 CSS selector 在 canvas.html 里 querySelectorAll,返回每个元素的
|
|
5
|
-
* { anchor, tag, text, bbox, layoutBox, dataAttrs } —— anchor schema 跟前端
|
|
6
|
-
* lib/html-utils.js 的 serializeAnchor 一致,agent 可以直接拿这个 anchor
|
|
7
|
-
* 喂回前端做精确选中(未来)或继续 query。
|
|
8
|
-
*
|
|
9
|
-
* 典型场景:
|
|
10
|
-
* - "把所有 H1 字号统一成 56" → query_elements({selector:'h1'}) 拿全清单
|
|
11
|
-
* - "找到 cover 上那个 cta 按钮" → query_elements({selector:'[data-anchor="cover-cta"]'})
|
|
12
|
-
*
|
|
13
|
-
* 限制:返回 max 50 条,溢出截断 + 文本提示。
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import path from 'node:path';
|
|
17
|
-
import fs from 'node:fs/promises';
|
|
18
|
-
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
19
|
-
import { z } from 'zod';
|
|
20
|
-
import { degradedNote } from './helpers/perception-page.js';
|
|
21
|
-
import { acquireArtifactPage, LIVE_PARAM_DESC } from './helpers/acquire-page.js';
|
|
22
|
-
import { resolveDeckSize, extractDeckAspect } from '../../../shared/deck.js';
|
|
23
|
-
import { resolveCanvasTarget, CANVAS_PATH_DESC, KIND_SITE, requireBrowsable,
|
|
24
|
-
} from '../../../lib/artifact-target.js';
|
|
25
|
-
|
|
26
|
-
const MAX_RESULTS = 50;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {object} deps
|
|
30
|
-
* @param {string} deps.workspaceRoot
|
|
31
|
-
* @param {import('../../agent/context.js').AgentContext} [deps.ctx]
|
|
32
|
-
*/
|
|
33
|
-
export function makeQueryElementsTool({ workspaceRoot, projectId, sessionId, ctx: _ctx }) {
|
|
34
|
-
return tool(
|
|
35
|
-
'query_elements',
|
|
36
|
-
`Query elements in your artifact page (site page / deck) via CSS selector. Returns a list of
|
|
37
|
-
matched elements with { anchor, tag, text, bbox, dataAttrs }.
|
|
38
|
-
|
|
39
|
-
The anchor object (dataId / path / textHint / bbox) is the same schema used
|
|
40
|
-
by the frontend selection system — you can reference it later to highlight
|
|
41
|
-
or pin comments.
|
|
42
|
-
|
|
43
|
-
Use when:
|
|
44
|
-
- You need to inspect/operate on multiple elements matching a pattern
|
|
45
|
-
(all H1, all .cta, all data-anchor="page-N-title", etc.)
|
|
46
|
-
- Before bulk-editing, get the current state list first
|
|
47
|
-
- Verifying that a class / data-attr is applied where you expect
|
|
48
|
-
|
|
49
|
-
Returns up to 50 matches; further results truncated with a hint.
|
|
50
|
-
|
|
51
|
-
**bbox vs layoutBox** — bbox comes from getBoundingClientRect and INCLUDES CSS
|
|
52
|
-
transforms; layoutBox (offsetTop/Left/Width/Height) is the layout box without
|
|
53
|
-
them. Measure spacing with layoutBox, measure where-it-visually-sits with bbox.
|
|
54
|
-
An element carrying an entry animation reports "transformed: true" — on those,
|
|
55
|
-
bbox is offset by the animation and will give you impossible numbers.
|
|
56
|
-
⚠️ **Plain CSS selectors only.** Playwright syntax (":has-text(...)", ">>",
|
|
57
|
-
"nth=0") is NOT supported here and fails with a parser error. Use ASCII quotes,
|
|
58
|
-
never HTML entities (""" reaches the selector literally and breaks it).`,
|
|
59
|
-
{
|
|
60
|
-
selector: z
|
|
61
|
-
.string()
|
|
62
|
-
.min(1)
|
|
63
|
-
.describe('CSS selector (e.g., "h1", ".accent", \'[data-anchor="cover-title"]\', \'section[data-page="2"] p\')'),
|
|
64
|
-
page: z
|
|
65
|
-
.number()
|
|
66
|
-
.int()
|
|
67
|
-
.min(1)
|
|
68
|
-
.optional()
|
|
69
|
-
.describe('Optional: scope query to a specific page (prepends section[data-page="N"] to selector)'),
|
|
70
|
-
path: z.string().optional().describe(CANVAS_PATH_DESC),
|
|
71
|
-
live: z.boolean().optional().describe(LIVE_PARAM_DESC),
|
|
72
|
-
},
|
|
73
|
-
async ({ selector, page: pageIndex, path: relPath, live }) => {
|
|
74
|
-
if (!workspaceRoot) {
|
|
75
|
-
return {
|
|
76
|
-
content: [{ type: 'text', text: 'No workspace bound; cannot query.' }],
|
|
77
|
-
isError: true,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
const target = await resolveCanvasTarget(workspaceRoot, relPath, sessionId);
|
|
81
|
-
if (!target.ok) return { content: [{ type: 'text', text: target.message }], isError: true };
|
|
82
|
-
const notBrowsable = requireBrowsable(target);
|
|
83
|
-
if (notBrowsable) return { content: [{ type: 'text', text: notBrowsable }], isError: true };
|
|
84
|
-
const canvasPath = target.absPath;
|
|
85
|
-
let html;
|
|
86
|
-
try {
|
|
87
|
-
html = await fs.readFile(canvasPath, 'utf8');
|
|
88
|
-
} catch {
|
|
89
|
-
return { content: [{ type: 'text', text: `${target.relPath} not found in workspace.` }], isError: true };
|
|
90
|
-
}
|
|
91
|
-
// 站点没有 deck 比例:extractDeckAspect 找不到 __nd-deck-wrap 会静默回落到
|
|
92
|
-
// 16:9,于是按 1920x1080 量出来的盒子被当成真实布局报给 agent。站点按桌面档
|
|
93
|
-
// 1440 量,跟 screenshot_canvas 的 desktop 档一致。
|
|
94
|
-
const dims = target.kind === KIND_SITE
|
|
95
|
-
? { width: 1440, height: 900 }
|
|
96
|
-
: resolveDeckSize(extractDeckAspect(html));
|
|
97
|
-
|
|
98
|
-
const finalSelector = pageIndex
|
|
99
|
-
? `section[data-page="${pageIndex}"] ${selector}`
|
|
100
|
-
: selector;
|
|
101
|
-
|
|
102
|
-
let acq;
|
|
103
|
-
try {
|
|
104
|
-
// 页面从统一口拿(helpers/acquire-page.js):live:true = 会话里现在这一页;否则新开走 http
|
|
105
|
-
acq = await acquireArtifactPage({ projectId, workspaceRoot, target, live, viewport: { width: dims.width, height: dims.height } });
|
|
106
|
-
const page = acq.page;
|
|
107
|
-
const opened = acq;
|
|
108
|
-
|
|
109
|
-
const result = await page.evaluate(({ sel, max }) => {
|
|
110
|
-
const els = Array.from(document.querySelectorAll(sel));
|
|
111
|
-
const total = els.length;
|
|
112
|
-
const slice = els.slice(0, max);
|
|
113
|
-
|
|
114
|
-
const computePath = (el) => {
|
|
115
|
-
const segments = [];
|
|
116
|
-
let cur = el;
|
|
117
|
-
while (cur && cur !== document.body && cur.nodeType === 1) {
|
|
118
|
-
const tag = cur.tagName.toLowerCase();
|
|
119
|
-
const parent = cur.parentNode;
|
|
120
|
-
if (!parent || parent.nodeType !== 1) break;
|
|
121
|
-
const sameType = Array.from(parent.children).filter(c => c.tagName === cur.tagName);
|
|
122
|
-
const idx = sameType.indexOf(cur) + 1;
|
|
123
|
-
segments.unshift(sameType.length > 1 ? `${tag}:nth-of-type(${idx})` : tag);
|
|
124
|
-
cur = parent;
|
|
125
|
-
}
|
|
126
|
-
return segments.join(' > ');
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const items = slice.map((el) => {
|
|
130
|
-
const r = el.getBoundingClientRect();
|
|
131
|
-
// anchor 单写:dataId 字段值改读 data-anchor(跟前端 serializeAnchor 一致)
|
|
132
|
-
const dataId = el.getAttribute('data-anchor') || null;
|
|
133
|
-
const text = (el.textContent || '').trim();
|
|
134
|
-
const dataAttrs = {};
|
|
135
|
-
for (const a of Array.from(el.attributes)) {
|
|
136
|
-
if (a.name.startsWith('data-')) dataAttrs[a.name] = a.value;
|
|
137
|
-
}
|
|
138
|
-
// layoutBox / transformed(2026-08-18):bbox 走 getBoundingClientRect,
|
|
139
|
-
// **CSS transform 算在里面**。带入场动画的页面(这平台上绝大多数页面)
|
|
140
|
-
// 元素静止态常挂着 translateY(26px),量间距会得出物理上不可能的结果 ——
|
|
141
|
-
// 有个 agent 因此差点断言"最后一节和页脚重叠了",靠 25≈26 这个巧合才
|
|
142
|
-
// 反应过来。offset* 是布局盒,不含 transform,两个都给就不会误判。
|
|
143
|
-
const cs = getComputedStyle(el);
|
|
144
|
-
const transformed = cs.transform !== 'none' && cs.transform !== '';
|
|
145
|
-
return {
|
|
146
|
-
anchor: {
|
|
147
|
-
dataId,
|
|
148
|
-
path: computePath(el),
|
|
149
|
-
textHint: text.slice(0, 50),
|
|
150
|
-
bbox: { x: r.x, y: r.y, w: r.width, h: r.height },
|
|
151
|
-
},
|
|
152
|
-
tag: el.tagName.toLowerCase(),
|
|
153
|
-
text: text.slice(0, 200),
|
|
154
|
-
bbox: { x: r.x, y: r.y, w: r.width, h: r.height },
|
|
155
|
-
layoutBox: { x: el.offsetLeft, y: el.offsetTop, w: el.offsetWidth, h: el.offsetHeight },
|
|
156
|
-
...(transformed ? { transformed: true } : {}),
|
|
157
|
-
dataAttrs,
|
|
158
|
-
};
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
return { items, total };
|
|
162
|
-
}, { sel: finalSelector, max: MAX_RESULTS });
|
|
163
|
-
|
|
164
|
-
if (result.total === 0) {
|
|
165
|
-
return {
|
|
166
|
-
content: [{
|
|
167
|
-
type: 'text',
|
|
168
|
-
text: `No elements match selector: ${finalSelector}`,
|
|
169
|
-
}],
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const truncatedNote = result.total > MAX_RESULTS
|
|
174
|
-
? ` (showing first ${MAX_RESULTS} of ${result.total} matches)`
|
|
175
|
-
: '';
|
|
176
|
-
|
|
177
|
-
const degraded = degradedNote(opened); // 契约:note 非空必须写进返回文本
|
|
178
|
-
return {
|
|
179
|
-
content: [{
|
|
180
|
-
type: 'text',
|
|
181
|
-
text: `${degraded ? `${degraded}\n\n` : ''}${acq.liveNote ? `${acq.liveNote}\n\n` : ''}Matched ${result.total} element(s)${truncatedNote}:`
|
|
182
|
-
+ `\n\n${JSON.stringify(result.items, null, 2)}`,
|
|
183
|
-
}],
|
|
184
|
-
};
|
|
185
|
-
} catch (err) {
|
|
186
|
-
return {
|
|
187
|
-
content: [{ type: 'text', text: `query_elements failed: ${err?.message || String(err)}` }],
|
|
188
|
-
isError: true,
|
|
189
|
-
};
|
|
190
|
-
} finally {
|
|
191
|
-
await acq?.release?.(); // 一次性:关浏览器;live:松会话锁
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
);
|
|
195
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* mcp/tools/query-elements.js — query_elements MCP tool
|
|
3
|
+
*
|
|
4
|
+
* 用 CSS selector 在 canvas.html 里 querySelectorAll,返回每个元素的
|
|
5
|
+
* { anchor, tag, text, bbox, layoutBox, dataAttrs } —— anchor schema 跟前端
|
|
6
|
+
* lib/html-utils.js 的 serializeAnchor 一致,agent 可以直接拿这个 anchor
|
|
7
|
+
* 喂回前端做精确选中(未来)或继续 query。
|
|
8
|
+
*
|
|
9
|
+
* 典型场景:
|
|
10
|
+
* - "把所有 H1 字号统一成 56" → query_elements({selector:'h1'}) 拿全清单
|
|
11
|
+
* - "找到 cover 上那个 cta 按钮" → query_elements({selector:'[data-anchor="cover-cta"]'})
|
|
12
|
+
*
|
|
13
|
+
* 限制:返回 max 50 条,溢出截断 + 文本提示。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import fs from 'node:fs/promises';
|
|
18
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
import { degradedNote } from './helpers/perception-page.js';
|
|
21
|
+
import { acquireArtifactPage, LIVE_PARAM_DESC } from './helpers/acquire-page.js';
|
|
22
|
+
import { resolveDeckSize, extractDeckAspect } from '../../../shared/deck.js';
|
|
23
|
+
import { resolveCanvasTarget, CANVAS_PATH_DESC, KIND_SITE, requireBrowsable,
|
|
24
|
+
} from '../../../lib/artifact-target.js';
|
|
25
|
+
|
|
26
|
+
const MAX_RESULTS = 50;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {object} deps
|
|
30
|
+
* @param {string} deps.workspaceRoot
|
|
31
|
+
* @param {import('../../agent/context.js').AgentContext} [deps.ctx]
|
|
32
|
+
*/
|
|
33
|
+
export function makeQueryElementsTool({ workspaceRoot, projectId, sessionId, ctx: _ctx }) {
|
|
34
|
+
return tool(
|
|
35
|
+
'query_elements',
|
|
36
|
+
`Query elements in your artifact page (site page / deck) via CSS selector. Returns a list of
|
|
37
|
+
matched elements with { anchor, tag, text, bbox, dataAttrs }.
|
|
38
|
+
|
|
39
|
+
The anchor object (dataId / path / textHint / bbox) is the same schema used
|
|
40
|
+
by the frontend selection system — you can reference it later to highlight
|
|
41
|
+
or pin comments.
|
|
42
|
+
|
|
43
|
+
Use when:
|
|
44
|
+
- You need to inspect/operate on multiple elements matching a pattern
|
|
45
|
+
(all H1, all .cta, all data-anchor="page-N-title", etc.)
|
|
46
|
+
- Before bulk-editing, get the current state list first
|
|
47
|
+
- Verifying that a class / data-attr is applied where you expect
|
|
48
|
+
|
|
49
|
+
Returns up to 50 matches; further results truncated with a hint.
|
|
50
|
+
|
|
51
|
+
**bbox vs layoutBox** — bbox comes from getBoundingClientRect and INCLUDES CSS
|
|
52
|
+
transforms; layoutBox (offsetTop/Left/Width/Height) is the layout box without
|
|
53
|
+
them. Measure spacing with layoutBox, measure where-it-visually-sits with bbox.
|
|
54
|
+
An element carrying an entry animation reports "transformed: true" — on those,
|
|
55
|
+
bbox is offset by the animation and will give you impossible numbers.
|
|
56
|
+
⚠️ **Plain CSS selectors only.** Playwright syntax (":has-text(...)", ">>",
|
|
57
|
+
"nth=0") is NOT supported here and fails with a parser error. Use ASCII quotes,
|
|
58
|
+
never HTML entities (""" reaches the selector literally and breaks it).`,
|
|
59
|
+
{
|
|
60
|
+
selector: z
|
|
61
|
+
.string()
|
|
62
|
+
.min(1)
|
|
63
|
+
.describe('CSS selector (e.g., "h1", ".accent", \'[data-anchor="cover-title"]\', \'section[data-page="2"] p\')'),
|
|
64
|
+
page: z
|
|
65
|
+
.number()
|
|
66
|
+
.int()
|
|
67
|
+
.min(1)
|
|
68
|
+
.optional()
|
|
69
|
+
.describe('Optional: scope query to a specific page (prepends section[data-page="N"] to selector)'),
|
|
70
|
+
path: z.string().optional().describe(CANVAS_PATH_DESC),
|
|
71
|
+
live: z.boolean().optional().describe(LIVE_PARAM_DESC),
|
|
72
|
+
},
|
|
73
|
+
async ({ selector, page: pageIndex, path: relPath, live }) => {
|
|
74
|
+
if (!workspaceRoot) {
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: 'text', text: 'No workspace bound; cannot query.' }],
|
|
77
|
+
isError: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const target = await resolveCanvasTarget(workspaceRoot, relPath, sessionId);
|
|
81
|
+
if (!target.ok) return { content: [{ type: 'text', text: target.message }], isError: true };
|
|
82
|
+
const notBrowsable = requireBrowsable(target);
|
|
83
|
+
if (notBrowsable) return { content: [{ type: 'text', text: notBrowsable }], isError: true };
|
|
84
|
+
const canvasPath = target.absPath;
|
|
85
|
+
let html;
|
|
86
|
+
try {
|
|
87
|
+
html = await fs.readFile(canvasPath, 'utf8');
|
|
88
|
+
} catch {
|
|
89
|
+
return { content: [{ type: 'text', text: `${target.relPath} not found in workspace.` }], isError: true };
|
|
90
|
+
}
|
|
91
|
+
// 站点没有 deck 比例:extractDeckAspect 找不到 __nd-deck-wrap 会静默回落到
|
|
92
|
+
// 16:9,于是按 1920x1080 量出来的盒子被当成真实布局报给 agent。站点按桌面档
|
|
93
|
+
// 1440 量,跟 screenshot_canvas 的 desktop 档一致。
|
|
94
|
+
const dims = target.kind === KIND_SITE
|
|
95
|
+
? { width: 1440, height: 900 }
|
|
96
|
+
: resolveDeckSize(extractDeckAspect(html));
|
|
97
|
+
|
|
98
|
+
const finalSelector = pageIndex
|
|
99
|
+
? `section[data-page="${pageIndex}"] ${selector}`
|
|
100
|
+
: selector;
|
|
101
|
+
|
|
102
|
+
let acq;
|
|
103
|
+
try {
|
|
104
|
+
// 页面从统一口拿(helpers/acquire-page.js):live:true = 会话里现在这一页;否则新开走 http
|
|
105
|
+
acq = await acquireArtifactPage({ projectId, workspaceRoot, target, live, viewport: { width: dims.width, height: dims.height } });
|
|
106
|
+
const page = acq.page;
|
|
107
|
+
const opened = acq;
|
|
108
|
+
|
|
109
|
+
const result = await page.evaluate(({ sel, max }) => {
|
|
110
|
+
const els = Array.from(document.querySelectorAll(sel));
|
|
111
|
+
const total = els.length;
|
|
112
|
+
const slice = els.slice(0, max);
|
|
113
|
+
|
|
114
|
+
const computePath = (el) => {
|
|
115
|
+
const segments = [];
|
|
116
|
+
let cur = el;
|
|
117
|
+
while (cur && cur !== document.body && cur.nodeType === 1) {
|
|
118
|
+
const tag = cur.tagName.toLowerCase();
|
|
119
|
+
const parent = cur.parentNode;
|
|
120
|
+
if (!parent || parent.nodeType !== 1) break;
|
|
121
|
+
const sameType = Array.from(parent.children).filter(c => c.tagName === cur.tagName);
|
|
122
|
+
const idx = sameType.indexOf(cur) + 1;
|
|
123
|
+
segments.unshift(sameType.length > 1 ? `${tag}:nth-of-type(${idx})` : tag);
|
|
124
|
+
cur = parent;
|
|
125
|
+
}
|
|
126
|
+
return segments.join(' > ');
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const items = slice.map((el) => {
|
|
130
|
+
const r = el.getBoundingClientRect();
|
|
131
|
+
// anchor 单写:dataId 字段值改读 data-anchor(跟前端 serializeAnchor 一致)
|
|
132
|
+
const dataId = el.getAttribute('data-anchor') || null;
|
|
133
|
+
const text = (el.textContent || '').trim();
|
|
134
|
+
const dataAttrs = {};
|
|
135
|
+
for (const a of Array.from(el.attributes)) {
|
|
136
|
+
if (a.name.startsWith('data-')) dataAttrs[a.name] = a.value;
|
|
137
|
+
}
|
|
138
|
+
// layoutBox / transformed(2026-08-18):bbox 走 getBoundingClientRect,
|
|
139
|
+
// **CSS transform 算在里面**。带入场动画的页面(这平台上绝大多数页面)
|
|
140
|
+
// 元素静止态常挂着 translateY(26px),量间距会得出物理上不可能的结果 ——
|
|
141
|
+
// 有个 agent 因此差点断言"最后一节和页脚重叠了",靠 25≈26 这个巧合才
|
|
142
|
+
// 反应过来。offset* 是布局盒,不含 transform,两个都给就不会误判。
|
|
143
|
+
const cs = getComputedStyle(el);
|
|
144
|
+
const transformed = cs.transform !== 'none' && cs.transform !== '';
|
|
145
|
+
return {
|
|
146
|
+
anchor: {
|
|
147
|
+
dataId,
|
|
148
|
+
path: computePath(el),
|
|
149
|
+
textHint: text.slice(0, 50),
|
|
150
|
+
bbox: { x: r.x, y: r.y, w: r.width, h: r.height },
|
|
151
|
+
},
|
|
152
|
+
tag: el.tagName.toLowerCase(),
|
|
153
|
+
text: text.slice(0, 200),
|
|
154
|
+
bbox: { x: r.x, y: r.y, w: r.width, h: r.height },
|
|
155
|
+
layoutBox: { x: el.offsetLeft, y: el.offsetTop, w: el.offsetWidth, h: el.offsetHeight },
|
|
156
|
+
...(transformed ? { transformed: true } : {}),
|
|
157
|
+
dataAttrs,
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return { items, total };
|
|
162
|
+
}, { sel: finalSelector, max: MAX_RESULTS });
|
|
163
|
+
|
|
164
|
+
if (result.total === 0) {
|
|
165
|
+
return {
|
|
166
|
+
content: [{
|
|
167
|
+
type: 'text',
|
|
168
|
+
text: `No elements match selector: ${finalSelector}`,
|
|
169
|
+
}],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const truncatedNote = result.total > MAX_RESULTS
|
|
174
|
+
? ` (showing first ${MAX_RESULTS} of ${result.total} matches)`
|
|
175
|
+
: '';
|
|
176
|
+
|
|
177
|
+
const degraded = degradedNote(opened); // 契约:note 非空必须写进返回文本
|
|
178
|
+
return {
|
|
179
|
+
content: [{
|
|
180
|
+
type: 'text',
|
|
181
|
+
text: `${degraded ? `${degraded}\n\n` : ''}${acq.liveNote ? `${acq.liveNote}\n\n` : ''}Matched ${result.total} element(s)${truncatedNote}:`
|
|
182
|
+
+ `\n\n${JSON.stringify(result.items, null, 2)}`,
|
|
183
|
+
}],
|
|
184
|
+
};
|
|
185
|
+
} catch (err) {
|
|
186
|
+
return {
|
|
187
|
+
content: [{ type: 'text', text: `query_elements failed: ${err?.message || String(err)}` }],
|
|
188
|
+
isError: true,
|
|
189
|
+
};
|
|
190
|
+
} finally {
|
|
191
|
+
await acq?.release?.(); // 一次性:关浏览器;live:松会话锁
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
}
|