@xiaobuyu/nodesign 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +140 -0
- package/bin/nodesign.js +176 -0
- package/package.json +104 -0
- package/server/api/README.md +45 -0
- package/server/api/_guard.js +75 -0
- package/server/api/admin.js +187 -0
- package/server/api/assets/docx-page.js +103 -0
- package/server/api/assets/helpers.js +22 -0
- package/server/api/assets/notes.js +144 -0
- package/server/api/assets.js +896 -0
- package/server/api/board.js +57 -0
- package/server/api/browse.js +138 -0
- package/server/api/canvas.js +444 -0
- package/server/api/chatai.js +324 -0
- package/server/api/exports/build-standalone.js +980 -0
- package/server/api/exports/cards.js +118 -0
- package/server/api/exports/docx-pdf.js +46 -0
- package/server/api/exports/export-page.js +142 -0
- package/server/api/exports/handoff.js +147 -0
- package/server/api/exports.js +690 -0
- package/server/api/instruction.js +75 -0
- package/server/api/local.js +115 -0
- package/server/api/me.js +116 -0
- package/server/api/memory.js +185 -0
- package/server/api/pending-changes.js +366 -0
- package/server/api/plugins.js +164 -0
- package/server/api/projects.js +220 -0
- package/server/api/publish.js +64 -0
- package/server/api/recent.js +73 -0
- package/server/api/sessions.js +589 -0
- package/server/api/skills.js +77 -0
- package/server/api/standalone-fit.js +168 -0
- package/server/api/turn-compose.js +180 -0
- package/server/api/turn-inflight.js +44 -0
- package/server/api/turn.js +669 -0
- package/server/auth/README.md +17 -0
- package/server/auth/middleware.js +138 -0
- package/server/auth/origin-guard.js +71 -0
- package/server/auth/session.js +108 -0
- package/server/auth/tier.js +113 -0
- package/server/auth/users-store.js +348 -0
- package/server/edit/README.md +29 -0
- package/server/engine/README.md +113 -0
- package/server/engine/agent/agent-shared.js +550 -0
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -0
- package/server/engine/agent/auto-mode-rules.js +78 -0
- package/server/engine/agent/context.js +418 -0
- package/server/engine/agent/events.js +464 -0
- package/server/engine/agent/hooks/canvas-validate.js +199 -0
- package/server/engine/agent/hooks/failure.js +120 -0
- package/server/engine/agent/hooks/file-events.js +75 -0
- package/server/engine/agent/hooks/lifecycle.js +233 -0
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -0
- package/server/engine/agent/hooks/post-guidance.js +206 -0
- package/server/engine/agent/hooks/post-subagent-report.js +104 -0
- package/server/engine/agent/hooks/post-trim.js +58 -0
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -0
- package/server/engine/agent/hooks/pre-defaults.js +84 -0
- package/server/engine/agent/hooks/pre-injectors.js +277 -0
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -0
- package/server/engine/agent/hooks/pre-starter-files.js +61 -0
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -0
- package/server/engine/agent/hooks/site-validate.js +94 -0
- package/server/engine/agent/hooks/tool-prompts.js +31 -0
- package/server/engine/agent/hooks/turn-state-memory.js +56 -0
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -0
- package/server/engine/agent/hooks.js +354 -0
- package/server/engine/agent/init-contract.js +69 -0
- package/server/engine/agent/isolation.js +159 -0
- package/server/engine/agent/model-context.js +359 -0
- package/server/engine/agent/model-table.js +292 -0
- package/server/engine/agent/plugin-loader.js +266 -0
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -0
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -0
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -0
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -0
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -0
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -0
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -0
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -0
- package/server/engine/agent/prompts/tools/site-reference.md +106 -0
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -0
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -0
- package/server/engine/agent/session-loop.js +1024 -0
- package/server/engine/agent/session-model.js +164 -0
- package/server/engine/agent/skill.js +204 -0
- package/server/engine/agent/system-prompts.js +86 -0
- package/server/engine/agent/task-events.js +78 -0
- package/server/engine/agents/ds-extractor.md +132 -0
- package/server/engine/agents/explorer.md +190 -0
- package/server/engine/agents/index.js +230 -0
- package/server/engine/agents/schemas/design-system.json +156 -0
- package/server/engine/agents/schemas/tweak-schema.json +113 -0
- package/server/engine/agents/tweak-proposer.md +127 -0
- package/server/engine/agents/vision-checker.md +254 -0
- package/server/engine/browse/capture.js +422 -0
- package/server/engine/browse/card.js +137 -0
- package/server/engine/browse/handover.js +59 -0
- package/server/engine/browse/page-digest.js +119 -0
- package/server/engine/browse/refs.js +196 -0
- package/server/engine/browse/registry.js +307 -0
- package/server/engine/browse/screencast.js +170 -0
- package/server/engine/browse/state.js +121 -0
- package/server/engine/chatai/chat-log.js +79 -0
- package/server/engine/chatai/index.js +193 -0
- package/server/engine/chatai/openai-compat.js +152 -0
- package/server/engine/chatai/orchestrate.js +309 -0
- package/server/engine/chatai/perform.js +40 -0
- package/server/engine/chatai/summarize.js +79 -0
- package/server/engine/mcp/capability-gate.js +52 -0
- package/server/engine/mcp/index.js +339 -0
- package/server/engine/mcp/param-sanitizer.js +142 -0
- package/server/engine/mcp/tools/arrange-on-board.js +124 -0
- package/server/engine/mcp/tools/artifact-session.js +278 -0
- package/server/engine/mcp/tools/browse-computer.js +421 -0
- package/server/engine/mcp/tools/browse-find-batch.js +183 -0
- package/server/engine/mcp/tools/browse-screenshot.js +157 -0
- package/server/engine/mcp/tools/browse.js +480 -0
- package/server/engine/mcp/tools/build-docx.js +101 -0
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -0
- package/server/engine/mcp/tools/create-on-board.js +155 -0
- package/server/engine/mcp/tools/crystallize-skill.js +168 -0
- package/server/engine/mcp/tools/deliver-files.js +0 -0
- package/server/engine/mcp/tools/explain-style.js +237 -0
- package/server/engine/mcp/tools/export-handoff.js +133 -0
- package/server/engine/mcp/tools/expose-tweaks.js +149 -0
- package/server/engine/mcp/tools/generate-image.js +842 -0
- package/server/engine/mcp/tools/get-computed-styles.js +164 -0
- package/server/engine/mcp/tools/get-pending-changes.js +205 -0
- package/server/engine/mcp/tools/h3box-ssh.js +84 -0
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -0
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -0
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -0
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -0
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -0
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -0
- package/server/engine/mcp/tools/helpers/rembg.js +273 -0
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -0
- package/server/engine/mcp/tools/highlight.js +68 -0
- package/server/engine/mcp/tools/list-pages.js +222 -0
- package/server/engine/mcp/tools/lookup-tags.js +59 -0
- package/server/engine/mcp/tools/navigate-to-page.js +60 -0
- package/server/engine/mcp/tools/organize-board.js +64 -0
- package/server/engine/mcp/tools/paint-still.js +434 -0
- package/server/engine/mcp/tools/pin-to-board.js +130 -0
- package/server/engine/mcp/tools/preview-deck.js +130 -0
- package/server/engine/mcp/tools/profile-scroll.js +305 -0
- package/server/engine/mcp/tools/publish-site.js +102 -0
- package/server/engine/mcp/tools/query-elements.js +195 -0
- package/server/engine/mcp/tools/read-board.js +103 -0
- package/server/engine/mcp/tools/read-document.js +93 -0
- package/server/engine/mcp/tools/read-page.js +264 -0
- package/server/engine/mcp/tools/read-tavern-json.js +143 -0
- package/server/engine/mcp/tools/record-decision.js +140 -0
- package/server/engine/mcp/tools/relate-on-board.js +110 -0
- package/server/engine/mcp/tools/remove-background.js +391 -0
- package/server/engine/mcp/tools/report-issue.js +115 -0
- package/server/engine/mcp/tools/roll-film.js +266 -0
- package/server/engine/mcp/tools/screenshot-docx.js +138 -0
- package/server/engine/mcp/tools/screenshot-url.js +195 -0
- package/server/engine/mcp/tools/screenshot.js +583 -0
- package/server/engine/mcp/tools/tier-gate.js +96 -0
- package/server/engine/mcp/tools/trace-motion.js +215 -0
- package/server/engine/mcp/tools/web-search.js +548 -0
- package/server/engine/motion/inventory.js +349 -0
- package/server/engine/perception/session.js +235 -0
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -0
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -0
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -0
- package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -0
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -0
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -0
- package/server/engine/runs/active-runs.js +698 -0
- package/server/engine/runs/live-turn.js +252 -0
- package/server/engine/runs/store.js +364 -0
- package/server/engine/runs/turn-relay.js +217 -0
- package/server/engine/runtime/workspace.js +164 -0
- package/server/index.js +235 -0
- package/server/lib/artifact-file-path.js +80 -0
- package/server/lib/artifact-target.js +284 -0
- package/server/lib/asset-refs.js +239 -0
- package/server/lib/async-queue.js +104 -0
- package/server/lib/auto-relations.js +130 -0
- package/server/lib/binding-types.js +60 -0
- package/server/lib/board-kind-sizes.js +62 -0
- package/server/lib/board-relations.js +133 -0
- package/server/lib/browse-proxy.js +180 -0
- package/server/lib/canvas-id.js +36 -0
- package/server/lib/cover.js +227 -0
- package/server/lib/danbooru-tags.js +291 -0
- package/server/lib/doc-extract.js +156 -0
- package/server/lib/docx/build-from-source.js +260 -0
- package/server/lib/docx/build.js +490 -0
- package/server/lib/docx/dump-styles.js +287 -0
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -0
- package/server/lib/docx/merge-runs.js +129 -0
- package/server/lib/docx/numbering.js +218 -0
- package/server/lib/docx/order.js +152 -0
- package/server/lib/docx/rawzip.js +172 -0
- package/server/lib/docx/render.js +92 -0
- package/server/lib/docx/text-lint.js +225 -0
- package/server/lib/docx/tokens.js +335 -0
- package/server/lib/docx/units.js +29 -0
- package/server/lib/docx/xml.js +291 -0
- package/server/lib/docx-pages.js +187 -0
- package/server/lib/export-collect.js +289 -0
- package/server/lib/export-package.js +210 -0
- package/server/lib/html-srcset.js +145 -0
- package/server/lib/image-variant.js +460 -0
- package/server/lib/ingress/forward-openai-chat.js +305 -0
- package/server/lib/ingress/openai-chat.js +491 -0
- package/server/lib/ingress/session-notice.js +66 -0
- package/server/lib/ingress/session-routes.js +68 -0
- package/server/lib/ingress/slot-probe.js +130 -0
- package/server/lib/ingress/upstream-billing.js +52 -0
- package/server/lib/ingress/upstream-fail-streak.js +72 -0
- package/server/lib/ingress/upstream-truncation.js +48 -0
- package/server/lib/issues-store.js +182 -0
- package/server/lib/kinds/deck.js +72 -0
- package/server/lib/kinds/docx.js +283 -0
- package/server/lib/kinds/file-kinds.js +113 -0
- package/server/lib/kinds/index.js +202 -0
- package/server/lib/kinds/site.js +161 -0
- package/server/lib/model-ingress.js +543 -0
- package/server/lib/moderation.js +255 -0
- package/server/lib/notice-store.js +103 -0
- package/server/lib/plugin-install.js +104 -0
- package/server/lib/plugin-validator.js +721 -0
- package/server/lib/publish-store.js +115 -0
- package/server/lib/quick-summary.js +52 -0
- package/server/lib/quota.js +270 -0
- package/server/lib/rate-window.js +29 -0
- package/server/lib/reference-assets.js +92 -0
- package/server/lib/region-shot.js +135 -0
- package/server/lib/safe-path.js +73 -0
- package/server/lib/sdk-session.js +33 -0
- package/server/lib/showcase-store.js +92 -0
- package/server/lib/site-publish.js +465 -0
- package/server/lib/ssrf-guard.js +432 -0
- package/server/lib/task-scan.js +158 -0
- package/server/lib/tavern-json.js +154 -0
- package/server/lib/video-variant.js +237 -0
- package/server/lib/workspace-path.js +33 -0
- package/server/ops/fix-sdk-musl.mjs +40 -0
- package/server/ops/install-macos-fonts.sh +114 -0
- package/server/ops/macos-fonts.conf +336 -0
- package/server/ops/sandbox-shim/bwrap +57 -0
- package/server/projects/assets-summary.js +119 -0
- package/server/projects/auto-name.js +53 -0
- package/server/projects/board-store.js +640 -0
- package/server/projects/move-entry.js +103 -0
- package/server/projects/store.js +270 -0
- package/server/projects/ui-config.js +54 -0
- package/server/projects/workspace-templates.js +53 -0
- package/server/projects/workspace.js +1025 -0
- package/server/runtime/capabilities.js +153 -0
- package/server/runtime/local-config.js +177 -0
- package/server/runtime/local-env.js +88 -0
- package/server/runtime/platform.js +342 -0
- package/server/runtime/profile.js +72 -0
- package/server/services/rembg-launcher.js +313 -0
- package/server/services/rembg-service.py +334 -0
- package/server/shared/README.md +25 -0
- package/server/shared/deck.js +54 -0
- package/server/shared/time.js +49 -0
- package/server/style-pipeline/README.md +53 -0
- package/server/ws/broker.js +36 -0
- package/server/ws/browse-channel.js +177 -0
- package/server/ws/index.js +386 -0
- package/web/README.md +89 -0
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +11 -0
- package/web/dist/assets/DeckWindow-4G0xX7P3.js +16 -0
- package/web/dist/assets/DocxWindow-BXQsoPkh.js +16 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/SiteWindow-D6eS-2gM.js +18 -0
- package/web/dist/assets/again-D8tfgKKO.webp +0 -0
- package/web/dist/assets/caveat-nd-CP6HlsNg.woff2 +0 -0
- package/web/dist/assets/clock-xJhvfYpS.webp +0 -0
- package/web/dist/assets/film-cut-BBKMwH7q.webp +0 -0
- package/web/dist/assets/film-still-BZ_DL9Tt.webp +0 -0
- package/web/dist/assets/index-C0KUf6Wd.css +1 -0
- package/web/dist/assets/index-Cbl3ATvp.js +2125 -0
- package/web/dist/assets/ink-desk-BRbxeDsR.webp +0 -0
- package/web/dist/assets/ink-night-Bz4BT3XH.webp +0 -0
- package/web/dist/assets/ink-portrait-C0DM4V8Q.webp +0 -0
- package/web/dist/assets/longcang-regular-C0uvTuKa.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
- package/web/dist/assets/pending-edit-apply-DTjo2geW.js +31 -0
- package/web/dist/assets/question-8H4HSqxh.webp +0 -0
- package/web/dist/assets/reject-BaPJqElj.webp +0 -0
- package/web/dist/assets/rotate-cw-LMT3x1Zj.js +11 -0
- package/web/dist/assets/rp-portrait-CbrGpew2.webp +0 -0
- package/web/dist/assets/rp-street-C4jVKjxx.webp +0 -0
- package/web/dist/assets/square-dashed-mouse-pointer-CBoAkfhi.js +11 -0
- package/web/dist/assets/tangle-DIxHVNuT.webp +0 -0
- package/web/dist/assets/thisone-BylZKglV.webp +0 -0
- package/web/dist/assets/thumb-DO1wfK1I.webp +0 -0
- package/web/dist/index.html +18 -0
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp/tools/read-board.js —— read_board(2026-08-14,agent 摆位批·读侧)
|
|
3
|
+
*
|
|
4
|
+
* 让 agent **看得见版面**。在这之前它对画布的了解只有关系线摘要 —— 每件东西
|
|
5
|
+
* 坐哪、挨着谁、谁是主角,全是盲区,"摆放"无从谈起。这个工具把 board.json
|
|
6
|
+
* 翻译成一张按层分组、按行排读的座次表。
|
|
7
|
+
*
|
|
8
|
+
* 口径说明(都写进输出,agent 不用猜):
|
|
9
|
+
* - 只列**摆过的**:board.json 是稀疏表,刚产出还没排座的产物没有条目
|
|
10
|
+
* (前端首排后几百毫秒内落盘,通常都在)
|
|
11
|
+
* - 层归属是服务端近似(zone 字段优先,其次沿路径找已知文件夹)
|
|
12
|
+
* - 尺寸是形态估算(文字/涂鸦用存档实测值)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { readBoard } from '../../../projects/board-store.js';
|
|
18
|
+
import { estimateSize } from '../../../lib/board-kind-sizes.js';
|
|
19
|
+
import { layerOf } from '../../../lib/canvas-id.js';
|
|
20
|
+
import { relationsDigest } from '../../../lib/board-relations.js';
|
|
21
|
+
|
|
22
|
+
/** 同一"行"的 y 容差:入座算法一行内顶对齐,40 世界像素内视作同行 */
|
|
23
|
+
const ROW_TOLERANCE = 40;
|
|
24
|
+
|
|
25
|
+
function describeEntry(id, entry) {
|
|
26
|
+
const sz = estimateSize(id, entry);
|
|
27
|
+
const at = `@(${Math.round(entry.x)},${Math.round(entry.y)}) ${Math.round(sz.w)}x${Math.round(sz.h)}`;
|
|
28
|
+
if (entry.kind === 'text') {
|
|
29
|
+
const t = String(entry.data?.t || '').replace(/\s+/g, ' ').slice(0, 24);
|
|
30
|
+
return `- [手写] 「${t}」 ${at} (id: ${id})${entry.by === 'agent' ? ' ·你写的' : ''}`;
|
|
31
|
+
}
|
|
32
|
+
if (entry.kind === 'scribble') return `- [涂鸦] ${at} (id: ${id})`;
|
|
33
|
+
return `- ${id} ${at}${entry.by === 'agent' ? ' ·你摆的' : ''}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function makeReadBoardTool({ projectId }) {
|
|
37
|
+
return tool(
|
|
38
|
+
'read_board',
|
|
39
|
+
`Read the workbench canvas seating chart: what sits where, row by row, per folder layer.
|
|
40
|
+
|
|
41
|
+
Use this BEFORE arranging anything (arrange_on_board) or writing notes near things
|
|
42
|
+
(create_on_board) — placement without looking is guessing. Coordinates are world
|
|
43
|
+
pixels; items listed top-to-bottom, left-to-right per row. Only items that have
|
|
44
|
+
been seated appear (freshly produced artifacts get their seat within a second).`,
|
|
45
|
+
{
|
|
46
|
+
layer: z.string().max(300).optional()
|
|
47
|
+
.describe("Folder path to read ('' or omitted = the root desktop plus a folder list)"),
|
|
48
|
+
},
|
|
49
|
+
async ({ layer }) => {
|
|
50
|
+
if (!projectId) {
|
|
51
|
+
return { content: [{ type: 'text', text: 'No project bound.' }], isError: true };
|
|
52
|
+
}
|
|
53
|
+
const board = await readBoard(projectId);
|
|
54
|
+
const known = new Set(Object.keys(board.zones || {}));
|
|
55
|
+
const want = typeof layer === 'string' ? layer : '';
|
|
56
|
+
|
|
57
|
+
// 分层
|
|
58
|
+
const byLayer = new Map();
|
|
59
|
+
for (const [id, entry] of Object.entries(board.objects || {})) {
|
|
60
|
+
if (!Number.isFinite(entry?.x) || !Number.isFinite(entry?.y)) continue;
|
|
61
|
+
const l = layerOf(id, entry, known);
|
|
62
|
+
if (!byLayer.has(l)) byLayer.set(l, []);
|
|
63
|
+
byLayer.get(l).push({ id, entry });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const lines = [];
|
|
67
|
+
const renderLayer = (l) => {
|
|
68
|
+
const items = (byLayer.get(l) || []).sort((a, b) =>
|
|
69
|
+
(a.entry.y - b.entry.y) || (a.entry.x - b.entry.x));
|
|
70
|
+
if (!items.length) { lines.push('(这一层还没有摆过的东西)'); return; }
|
|
71
|
+
let rowY = null;
|
|
72
|
+
for (const { id, entry } of items) {
|
|
73
|
+
if (rowY === null || Math.abs(entry.y - rowY) > ROW_TOLERANCE) {
|
|
74
|
+
rowY = entry.y;
|
|
75
|
+
lines.push(`— 行 y≈${Math.round(rowY)} —`);
|
|
76
|
+
}
|
|
77
|
+
lines.push(describeEntry(id, entry));
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
lines.push(want ? `文件夹「${want}」的座次:` : '桌面(根层)的座次:');
|
|
82
|
+
renderLayer(want);
|
|
83
|
+
if (!want) {
|
|
84
|
+
const folders = Object.keys(board.zones || {}).sort();
|
|
85
|
+
if (folders.length) {
|
|
86
|
+
lines.push('', `文件夹卡:${folders.map(f => {
|
|
87
|
+
const zz = board.zones[f];
|
|
88
|
+
return `${f}@(${Math.round(zz.x)},${Math.round(zz.y)})`;
|
|
89
|
+
}).join('、')}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (board.hero) lines.push('', `★ 显式主角:${board.hero}(arrange_on_board 的 feature/unfeature 管它)`);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const digest = await relationsDigest(projectId, { limit: 16 });
|
|
96
|
+
if (digest) lines.push('', '关系线:', digest);
|
|
97
|
+
} catch { /* 关系读不到不挡座次 */ }
|
|
98
|
+
|
|
99
|
+
lines.push('', '(口径:稀疏表只列摆过的;层归属为服务端近似;尺寸为形态估算)');
|
|
100
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp/tools/read-document.js — read_document MCP tool(2026-08-07)
|
|
3
|
+
*
|
|
4
|
+
* Word / Excel / PowerPoint 三种格式的读取口。
|
|
5
|
+
*
|
|
6
|
+
* 为什么需要它:用户往上下文托盘里拖一份 .docx("照这份需求做一版海报"),
|
|
7
|
+
* agent 拿 Read 去读得到的是二进制乱码 —— **而且不报错**,它会当成一份读不懂
|
|
8
|
+
* 的文件继续往下干,交出一个跟需求无关的东西。这是最坏的一种失败:安静的。
|
|
9
|
+
*
|
|
10
|
+
* PDF 不走这里:SDK 的 Read 原生支持,2026-08-07 真跑验过。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import fs from 'node:fs/promises';
|
|
15
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { extractDocument, isExtractable, DOC_KINDS, MAX_CHARS } from '../../../lib/doc-extract.js';
|
|
18
|
+
|
|
19
|
+
const EXT_LIST = Object.keys(DOC_KINDS).join(' / ');
|
|
20
|
+
|
|
21
|
+
export function makeReadDocumentTool({ workspaceRoot, sharedRoot }) {
|
|
22
|
+
return tool(
|
|
23
|
+
'read_document',
|
|
24
|
+
`Read the text out of a Word / Excel / PowerPoint file (${EXT_LIST}).
|
|
25
|
+
|
|
26
|
+
The plain Read tool CANNOT read these — they are zip archives, so Read returns
|
|
27
|
+
binary garbage without failing, and you end up working from nothing. Whenever a
|
|
28
|
+
user attaches or points at one of these files, use this tool instead.
|
|
29
|
+
|
|
30
|
+
(PDF is different: the normal Read tool handles PDFs natively — use Read for those.)
|
|
31
|
+
|
|
32
|
+
What you get back is text with structure markers, not layout:
|
|
33
|
+
- .docx — the document text, headings/lists/tables flattened in reading order
|
|
34
|
+
- .xlsx — one section per worksheet, rows as tab-separated cells
|
|
35
|
+
- .pptx — one section per slide, in slide order
|
|
36
|
+
|
|
37
|
+
Layout is deliberately not preserved. You are being asked what the document
|
|
38
|
+
*says*; how it should look is your job to decide.
|
|
39
|
+
|
|
40
|
+
Long files are cut at ${MAX_CHARS} characters and the result says so.`,
|
|
41
|
+
{
|
|
42
|
+
path: z.string().describe(
|
|
43
|
+
'Path to the file. Relative paths resolve against the session workspace, '
|
|
44
|
+
+ 'then against the shared project dir (e.g. "assets/需求.docx").',
|
|
45
|
+
),
|
|
46
|
+
},
|
|
47
|
+
async ({ path: rel }) => {
|
|
48
|
+
const fail = (text) => ({ content: [{ type: 'text', text }], isError: true });
|
|
49
|
+
try {
|
|
50
|
+
const raw = String(rel || '').trim();
|
|
51
|
+
if (!raw) return fail('read_document needs a path.');
|
|
52
|
+
|
|
53
|
+
// 绝对路径直接用;相对路径先会话工作区、再项目共享目录
|
|
54
|
+
const candidates = path.isAbsolute(raw)
|
|
55
|
+
? [raw]
|
|
56
|
+
: [workspaceRoot && path.resolve(workspaceRoot, raw),
|
|
57
|
+
sharedRoot && path.resolve(sharedRoot, raw)].filter(Boolean);
|
|
58
|
+
|
|
59
|
+
let absPath = null;
|
|
60
|
+
for (const c of candidates) {
|
|
61
|
+
try { await fs.access(c); absPath = c; break; } catch { /* 下一个 */ }
|
|
62
|
+
}
|
|
63
|
+
if (!absPath) {
|
|
64
|
+
return fail(`File not found: ${raw}\nLooked in:\n${candidates.map(c => ` ${c}`).join('\n')}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!isExtractable(absPath)) {
|
|
68
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
69
|
+
return fail(
|
|
70
|
+
ext === '.pdf'
|
|
71
|
+
? 'PDFs are handled by the normal Read tool — call Read on this path instead.'
|
|
72
|
+
: `read_document only handles ${EXT_LIST}. For "${ext}" try the normal Read tool.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { kind, text, chars, truncated, note } = await extractDocument(absPath);
|
|
77
|
+
if (!text) {
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: 'text', text: `${kind} ${path.basename(absPath)} 里没有可读的文字${note ? `(${note})` : ''}。` }],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const head = [
|
|
83
|
+
`${kind}:${path.basename(absPath)}`,
|
|
84
|
+
`${chars} 字${truncated ? `,只给前 ${MAX_CHARS} 字` : ''}`,
|
|
85
|
+
note,
|
|
86
|
+
].filter(Boolean).join(' · ');
|
|
87
|
+
return { content: [{ type: 'text', text: `${head}\n\n${text}` }] };
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return fail(`read_document failed: ${err?.message || String(err)}`);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp/tools/read-page.js — read_page MCP tool
|
|
3
|
+
*
|
|
4
|
+
* 让 agent 精确读 canvas.html 的某一页(`<section data-page="N">` 一段),
|
|
5
|
+
* 不必每次 Read 整个 canvas.html 然后自己切片。
|
|
6
|
+
*
|
|
7
|
+
* 行为:
|
|
8
|
+
* - input: { page: number }(1-based,跟 data-page="N" 一致)
|
|
9
|
+
* - 找 canvas.html 里 `<section[^>]*data-page="N"[^>]*>...</section>` 一段
|
|
10
|
+
* - 返该段 outerHTML(含 attributes + 完整子树)
|
|
11
|
+
* - **Hybrid 范式扩展(2026-05-06)**:检测 `<script type="text/babel">` 存在
|
|
12
|
+
* 时,section 里若有 `data-react-mount="xxx"` 或 React mount 用的 `id="xxx"`,
|
|
13
|
+
* 额外 grep babel script 里 `getElementById('xxx')` 上下文 (±20 行) 一并返
|
|
14
|
+
* 回——agent 想改 React 部分时不必再 Read 整个 babel script
|
|
15
|
+
* - 找不到该页 → isError + 列出当前 canvas.html 实际有哪些 page
|
|
16
|
+
* - canvas.html 不存在 → isError + 提示 agent 需要先 Write 创建
|
|
17
|
+
*
|
|
18
|
+
* 用 regex 而非 DOM parser:纯字符串匹配,避免 jsdom 等大依赖;
|
|
19
|
+
* canvas.html 是 agent 自己写的,section 嵌套不会出现(约定每页是 sibling section)。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import fs from 'node:fs/promises';
|
|
24
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
25
|
+
import { z } from 'zod';
|
|
26
|
+
import { resolveCanvasTarget, CANVAS_PATH_DESC, KIND_SITE, requireBrowsable,
|
|
27
|
+
} from '../../../lib/artifact-target.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Thumbnail hint:检测 sectionHtml 是否含 `assets/generated/` 图 + thumbnails 目录存在
|
|
31
|
+
* 返 prepend hint 字符串(无需 hint 时返空)。
|
|
32
|
+
*
|
|
33
|
+
* 让 agent 知道:preview iframe 加载的 <img src> 是 thumbnail 快照(GET /canvas
|
|
34
|
+
* 路径透明改写),文件系统 / 这里返的 outerHTML 中 src 是真实 src,重生图后
|
|
35
|
+
* thumbnail 自动更新。
|
|
36
|
+
*/
|
|
37
|
+
async function detectThumbnailHint(workspaceRoot, sectionHtml) {
|
|
38
|
+
if (!workspaceRoot || !sectionHtml) return '';
|
|
39
|
+
if (!/assets\/generated\//.test(sectionHtml)) return '';
|
|
40
|
+
// thumbnail 与真实 src 的差别 08-21 起只在 generate-image cookbook 里说一次(每次 read_page 都带是 N 倍重复)
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Hybrid 范式:从 section html 抽 mount id(data-react-mount + id 双源),
|
|
46
|
+
* 然后到 raw canvas.html 的 babel script 段里 grep `getElementById('<id>')` 上下文。
|
|
47
|
+
*
|
|
48
|
+
* 为什么取 ±20 行:覆盖 createRoot 调用 + 上方 component 函数定义大概率在这窗口内。
|
|
49
|
+
* agent 拿到这段后能直接看到 "<MyComponent .../>" 的调用 + 定义,不需要再 Read。
|
|
50
|
+
*
|
|
51
|
+
* @param {string} raw 整个 canvas.html
|
|
52
|
+
* @param {string} sectionHtml 当前 section outerHTML
|
|
53
|
+
* @returns {Array<{ mountId: string, snippet: string, startLine: number }>}
|
|
54
|
+
*/
|
|
55
|
+
function extractReactMountSources(raw, sectionHtml) {
|
|
56
|
+
// 1. 检测 hybrid(必有 babel script)
|
|
57
|
+
if (!/<script[^>]*type=["']text\/babel["']/i.test(raw)) return [];
|
|
58
|
+
|
|
59
|
+
// 2. 抽 section 里的 mount id —— data-react-mount 优先,id 兜底
|
|
60
|
+
const mountIds = new Set();
|
|
61
|
+
for (const m of sectionHtml.matchAll(/\bdata-react-mount\s*=\s*["']([^"']+)["']/gi)) mountIds.add(m[1]);
|
|
62
|
+
for (const m of sectionHtml.matchAll(/\bid\s*=\s*["']([^"']+)["']/gi)) mountIds.add(m[1]);
|
|
63
|
+
if (mountIds.size === 0) return [];
|
|
64
|
+
|
|
65
|
+
// 3. 抽出所有 <script type="text/babel">...</script> 段
|
|
66
|
+
const babelBlocks = [...raw.matchAll(/<script[^>]*type=["']text\/babel["'][^>]*>([\s\S]*?)<\/script>/gi)];
|
|
67
|
+
if (babelBlocks.length === 0) return [];
|
|
68
|
+
|
|
69
|
+
// 用整个 raw 行号做参考,方便 agent 知道 babel 段大概在文件哪里
|
|
70
|
+
const rawLines = raw.split('\n');
|
|
71
|
+
const segments = [];
|
|
72
|
+
|
|
73
|
+
for (const id of mountIds) {
|
|
74
|
+
// 在每个 babel 段里找 getElementById('<id>') 调用
|
|
75
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
76
|
+
const re = new RegExp(`getElementById\\s*\\(\\s*['"]${escaped}['"]\\s*\\)`, 'g');
|
|
77
|
+
|
|
78
|
+
for (const block of babelBlocks) {
|
|
79
|
+
const blockText = block[1];
|
|
80
|
+
if (!re.test(blockText)) { re.lastIndex = 0; continue; }
|
|
81
|
+
re.lastIndex = 0;
|
|
82
|
+
|
|
83
|
+
// 找 block 在 raw 中的起始行
|
|
84
|
+
const blockStartIdx = raw.indexOf(block[0]);
|
|
85
|
+
const blockStartLine = blockStartIdx >= 0 ? raw.slice(0, blockStartIdx).split('\n').length : 1;
|
|
86
|
+
|
|
87
|
+
// grep 行号(相对 block)
|
|
88
|
+
const blockLines = blockText.split('\n');
|
|
89
|
+
blockLines.forEach((line, i) => {
|
|
90
|
+
if (line.includes(`getElementById('${id}')`) || line.includes(`getElementById("${id}")`)) {
|
|
91
|
+
const start = Math.max(0, i - 20);
|
|
92
|
+
const end = Math.min(blockLines.length, i + 21);
|
|
93
|
+
const snippet = blockLines.slice(start, end).join('\n');
|
|
94
|
+
segments.push({
|
|
95
|
+
mountId: id,
|
|
96
|
+
snippet,
|
|
97
|
+
startLine: blockStartLine + start + 1, // raw 文件行号
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
break; // 一个 mount id 在第一个匹配的 block 找到就停(避免重复)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return segments;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {object} deps
|
|
110
|
+
* @param {string} deps.workspaceRoot
|
|
111
|
+
* @param {import('../../agent/context.js').AgentContext} [deps.ctx]
|
|
112
|
+
*/
|
|
113
|
+
export function makeReadPageTool({ workspaceRoot, sessionId, ctx: _ctx }) {
|
|
114
|
+
return tool(
|
|
115
|
+
'read_page',
|
|
116
|
+
`DECK ONLY. Read a specific page (a single \`<section data-page="N">\`) from the deck .html.
|
|
117
|
+
|
|
118
|
+
Use this instead of Read+Grep+offset/limit when you want to inspect or
|
|
119
|
+
reason about one specific page in detail. Returns the outerHTML of that
|
|
120
|
+
section (including attributes and full subtree).
|
|
121
|
+
|
|
122
|
+
**Hybrid mode**: when the deck has \`<script type="text/babel">\` blocks
|
|
123
|
+
(the Hybrid format from the deck starter canvas.template.html), this tool also
|
|
124
|
+
returns the corresponding React mount source code for any
|
|
125
|
+
\`data-react-mount\` / \`id\` attribute it finds in the section — so you
|
|
126
|
+
don't need to Read the whole babel script just to find the component
|
|
127
|
+
that renders to a mount point on this page.
|
|
128
|
+
|
|
129
|
+
When to use:
|
|
130
|
+
- "Show me what page 3 looks like in code" — read_page(3)
|
|
131
|
+
- Before editing page N — read_page(N) to see exact current markup
|
|
132
|
+
- Debugging why a specific page renders wrong
|
|
133
|
+
|
|
134
|
+
When NOT to use:
|
|
135
|
+
- Reading the whole deck structure → use Read on the deck .html with limit:50
|
|
136
|
+
to see all section openings
|
|
137
|
+
- Finding a specific element across pages → use Grep
|
|
138
|
+
- the deck file doesn't exist yet (creating from scratch) → use Write directly
|
|
139
|
+
- sites: pages are separate files — Read them, or list_pages for the site map`,
|
|
140
|
+
{
|
|
141
|
+
page: z
|
|
142
|
+
.number()
|
|
143
|
+
.int()
|
|
144
|
+
.min(1)
|
|
145
|
+
.describe('Page number (1-based, matches data-page="N" attribute)'),
|
|
146
|
+
path: z.string().optional().describe(CANVAS_PATH_DESC),
|
|
147
|
+
},
|
|
148
|
+
async ({ page, path: relPath }) => {
|
|
149
|
+
try {
|
|
150
|
+
if (!workspaceRoot) {
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: 'text', text: 'No workspace bound; cannot read page.' }],
|
|
153
|
+
isError: true,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const target = await resolveCanvasTarget(workspaceRoot, relPath, sessionId);
|
|
158
|
+
if (!target.ok) return { content: [{ type: 'text', text: target.message }], isError: true };
|
|
159
|
+
const notBrowsable = requireBrowsable(target);
|
|
160
|
+
if (notBrowsable) return { content: [{ type: 'text', text: notBrowsable }], isError: true };
|
|
161
|
+
const canvasPath = target.absPath;
|
|
162
|
+
let raw;
|
|
163
|
+
try {
|
|
164
|
+
raw = await fs.readFile(canvasPath, 'utf8');
|
|
165
|
+
} catch (err) {
|
|
166
|
+
if (err.code === 'ENOENT') {
|
|
167
|
+
return {
|
|
168
|
+
content: [{
|
|
169
|
+
type: 'text',
|
|
170
|
+
text: 'That deck file does not exist yet. Use Write to create it first '
|
|
171
|
+
+ '(see SKILL.md for the section data-page="N" structure).',
|
|
172
|
+
}],
|
|
173
|
+
isError: true,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 匹配 `<section ... data-page="<page>" ...>...</section>`
|
|
180
|
+
// 双引号 / 单引号 / 不引号都接受。section 内不嵌套 section(约定)。
|
|
181
|
+
const pageStr = String(page);
|
|
182
|
+
const escaped = pageStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
183
|
+
// 三种 attribute quoting 尝试
|
|
184
|
+
const patterns = [
|
|
185
|
+
new RegExp(`<section\\b[^>]*\\bdata-page\\s*=\\s*"${escaped}"[^>]*>[\\s\\S]*?</section>`, 'i'),
|
|
186
|
+
new RegExp(`<section\\b[^>]*\\bdata-page\\s*=\\s*'${escaped}'[^>]*>[\\s\\S]*?</section>`, 'i'),
|
|
187
|
+
new RegExp(`<section\\b[^>]*\\bdata-page\\s*=\\s*${escaped}\\b[^>]*>[\\s\\S]*?</section>`, 'i'),
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
let match = null;
|
|
191
|
+
for (const re of patterns) {
|
|
192
|
+
const m = raw.match(re);
|
|
193
|
+
if (m) { match = m; break; }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!match) {
|
|
197
|
+
// 列出 canvas.html 里实际有哪些 page,给 agent 反馈
|
|
198
|
+
const allPages = [...raw.matchAll(/<section\b[^>]*\bdata-page\s*=\s*['"]?(\d+)/gi)]
|
|
199
|
+
.map(m => m[1])
|
|
200
|
+
.filter((v, i, arr) => arr.indexOf(v) === i);
|
|
201
|
+
// 站点:分页读根本不适用,直说该怎么做,别丢一句"没有分页结构"让 agent
|
|
202
|
+
// 自己猜(它多半会改用 Read 全文件,把整份站点灌进上下文)
|
|
203
|
+
if (target.kind === KIND_SITE) {
|
|
204
|
+
return {
|
|
205
|
+
content: [{
|
|
206
|
+
type: 'text',
|
|
207
|
+
text: `${target.relPath} 是站点页面,没有 <section data-page="N"> 分页,read_page 的页码语义不适用。\n`
|
|
208
|
+
+ '站点这样读:先 list_pages 看站点结构(每页的标题 / 小标题 / 站内链接 / 断链),'
|
|
209
|
+
+ '要看某一页的实际内容用 query_elements 按 selector 取,或者直接 Read 那个文件(站点页面通常不大)。',
|
|
210
|
+
}],
|
|
211
|
+
isError: true,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const pagesList = allPages.length > 0
|
|
215
|
+
? `Available pages: ${allPages.join(', ')}`
|
|
216
|
+
: `${target.relPath} has no <section data-page="N"> structure.`;
|
|
217
|
+
return {
|
|
218
|
+
content: [{
|
|
219
|
+
type: 'text',
|
|
220
|
+
text: `Page ${page} not found in ${target.relPath}. ${pagesList}`,
|
|
221
|
+
}],
|
|
222
|
+
isError: true,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const sectionHtml = match[0];
|
|
227
|
+
|
|
228
|
+
// Hybrid 范式扩展:检测 babel script + section 里的 mount id
|
|
229
|
+
// 找到 mount id 后从 babel script 抓 ±20 行上下文返给 agent
|
|
230
|
+
const hybridSegments = extractReactMountSources(raw, sectionHtml);
|
|
231
|
+
|
|
232
|
+
// Thumbnail hint:section 含 assets/generated/ 图 + thumbnails 目录存在 → prepend hint
|
|
233
|
+
const thumbnailHint = await detectThumbnailHint(workspaceRoot, sectionHtml);
|
|
234
|
+
|
|
235
|
+
const parts = [];
|
|
236
|
+
if (thumbnailHint) parts.push(thumbnailHint + '\n\n');
|
|
237
|
+
parts.push(`Page ${page} (${sectionHtml.length} chars):\n\n${sectionHtml}`);
|
|
238
|
+
if (hybridSegments.length > 0) {
|
|
239
|
+
parts.push(
|
|
240
|
+
`\n\n--- React mount sources for this page (${hybridSegments.length} mount${hybridSegments.length > 1 ? 's' : ''}) ---\n`
|
|
241
|
+
+ hybridSegments.map(({ mountId, snippet, startLine }) =>
|
|
242
|
+
`\n## mount: ${mountId} (babel script line ~${startLine})\n\n\`\`\`tsx\n${snippet}\n\`\`\``
|
|
243
|
+
).join('\n')
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
content: [{
|
|
249
|
+
type: 'text',
|
|
250
|
+
text: parts.join(''),
|
|
251
|
+
}],
|
|
252
|
+
};
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return {
|
|
255
|
+
content: [{
|
|
256
|
+
type: 'text',
|
|
257
|
+
text: `read_page failed: ${err?.message || String(err)}`,
|
|
258
|
+
}],
|
|
259
|
+
isError: true,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
);
|
|
264
|
+
}
|