@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,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/artifact-file-path.js — artifact-file 的路径判据(2026-08-18 抽出)
|
|
3
|
+
*
|
|
4
|
+
* 抽出来的直接原因是行数棘轮,但抽对了:这几行是**安全判据**,而且它有一条已知的
|
|
5
|
+
* 错(见下面 `tasks/` 那段),judge 类的东西住在路由里没法单测。
|
|
6
|
+
*
|
|
7
|
+
* 可服务根 = 整个项目工作区(产物就住在这儿),但挡掉基础设施:`.claude/`(含
|
|
8
|
+
* settings.json 和整份 SDK 转录)、`.nd/`(会话私档)、`.git/`。扁平化之前这三样
|
|
9
|
+
* 都在可服务根之外、是目录结构在替我们把门;现在它们跟产物同级,必须显式拦 ——
|
|
10
|
+
* 否则 `artifact-file/.claude/settings.json` 是一个能公开读到配置的 URL。
|
|
11
|
+
*
|
|
12
|
+
* 点开头**一刀切拒是错的**:缩略图住在 `assets/generated/.thumbnails/`,一刀切下去
|
|
13
|
+
* 产物墙上所有缩略图全 403。所以按名字白名单(不是黑名单 —— 将来冒出个 `.env`
|
|
14
|
+
* 不该因为没人想到就漏出去)。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import fs from 'node:fs/promises';
|
|
19
|
+
import { safeResolveRead } from './safe-path.js';
|
|
20
|
+
|
|
21
|
+
const DOT_OK = new Set(['.thumbnails', '.meta']);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 这个绝对路径可不可以服出去:不越界、且路径里没有非白名单的点目录。
|
|
25
|
+
*
|
|
26
|
+
* 导出它是因为**改名转发**那条路要用同一个判据(`api/assets.js` 里原来自己又抄了
|
|
27
|
+
* 一遍 DOT_OK 和越界检查)。判据抄第二遍就是等着哪天改一处漏一处 ——
|
|
28
|
+
* 收成一份,两边都 import。
|
|
29
|
+
*/
|
|
30
|
+
export function isServablePath(sharedRoot, absPath) {
|
|
31
|
+
if (absPath !== sharedRoot && !absPath.startsWith(sharedRoot + path.sep)) return false;
|
|
32
|
+
const rel = path.relative(sharedRoot, absPath).split(path.sep);
|
|
33
|
+
return !rel.some(seg => seg.startsWith('.') && !DOT_OK.has(seg));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} sharedRootRaw 项目工作区根
|
|
38
|
+
* @param {string} rawSub 路由里的 *subPath(可能是数组段拼起来的)
|
|
39
|
+
* @returns {Promise<{ok:true, sharedRoot:string, absPath:string, subPath:string}
|
|
40
|
+
* | {ok:false, status:number, error:string}>}
|
|
41
|
+
*/
|
|
42
|
+
export async function resolveArtifactFile(sharedRootRaw, rawSub) {
|
|
43
|
+
let subPath = Array.isArray(rawSub) ? rawSub.join('/') : (rawSub || '');
|
|
44
|
+
if (!subPath) return { ok: false, status: 400, error: 'file path required' };
|
|
45
|
+
|
|
46
|
+
const sharedRoot = path.resolve(sharedRootRaw);
|
|
47
|
+
|
|
48
|
+
// 兼容旧形态:`tasks/<任务>/x` 是扁平化之前的路径,浏览器缓存里、旧 board.json 里、
|
|
49
|
+
// 用户收藏的链接里都还有。剥掉前两段就是现在的位置。
|
|
50
|
+
// ⚠️ **但不能无条件剥**(2026-08-18 审查抓到):扁平化之后 agent 完全可以在工作区里
|
|
51
|
+
// 真建一个叫 `tasks/` 的目录(它就是个普通名字),那时候剥掉前两段会把**磁盘上真实
|
|
52
|
+
// 存在的文件**变成 404。所以先看原路径在不在,在就别动它。
|
|
53
|
+
const asIs = path.resolve(sharedRoot, subPath);
|
|
54
|
+
const insideAsIs = asIs === sharedRoot || asIs.startsWith(sharedRoot + path.sep);
|
|
55
|
+
if (/^tasks\/[^/]+\//.test(subPath)) {
|
|
56
|
+
let existsAsIs = false;
|
|
57
|
+
if (insideAsIs) {
|
|
58
|
+
try { await fs.stat(asIs); existsAsIs = true; } catch { /* 不在 */ }
|
|
59
|
+
}
|
|
60
|
+
if (!existsAsIs) subPath = subPath.replace(/^tasks\/[^/]+\//, '');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ⛔ **纯词法的 `path.resolve` 挡不住软链**。真攻过(2026-08-18):在工作区里
|
|
64
|
+
// `ln -s /path/to/.env x.html`,`GET artifact-file/x.html` 回 **200 且字节数与
|
|
65
|
+
// .env 一字不差**。而这条路由今天更热了(感知通道走它、常驻浏览器的 cookie jar
|
|
66
|
+
// 也住在工作区里),并且**它跑在 server 主进程、沙盒管不着**。
|
|
67
|
+
//
|
|
68
|
+
// 仓库里早就有正确实现 `safe-path.js` 的 `safeResolveRead`(realpath 复核,
|
|
69
|
+
// ENOENT 不算越界)。照那条教训:**去掉第二份判据而不是添第三份**
|
|
70
|
+
// ([[feedback-copied-guard-shape]]:抄守卫要抄正确性不是形状 —— 08-17 同一天
|
|
71
|
+
// 栽两次,第二次就是抄了调用形状没抄 realpath 复核)。
|
|
72
|
+
const absPath = await safeResolveRead(sharedRoot, subPath);
|
|
73
|
+
if (!absPath) {
|
|
74
|
+
return { ok: false, status: 403, error: 'path escapes workspace (symlinks are resolved before the check)' };
|
|
75
|
+
}
|
|
76
|
+
if (!isServablePath(sharedRoot, absPath)) {
|
|
77
|
+
return { ok: false, status: 403, error: 'not a servable path' };
|
|
78
|
+
}
|
|
79
|
+
return { ok: true, sharedRoot, absPath, subPath };
|
|
80
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* artifact-target.js — 产物寻址(2026-07-28 由 canvas-target.js 泛化;
|
|
3
|
+
* 2026-07-29 形态判定与解析下沉到 kinds/ 注册表;2026-08-07 任务层退役)
|
|
4
|
+
*
|
|
5
|
+
* 定死三件事:
|
|
6
|
+
*
|
|
7
|
+
* **① 产物有形态(kind),文件就是真相。** 判定规则在 kinds/ 注册表:
|
|
8
|
+
* canvas.html → deck;index.html(工作区根 / 声明的产物根 / 约定构建目录)
|
|
9
|
+
* → site。
|
|
10
|
+
*
|
|
11
|
+
* **② 寻址永远带着 kind 一起返回。** 下游(截图 / 分页 / 导出 / fit 注入)必须
|
|
12
|
+
* 能看见"这是 deck 还是 site",才可能给出对的行为。kind 每次 resolve 按工作区
|
|
13
|
+
* 现状重算,不缓存。
|
|
14
|
+
*
|
|
15
|
+
* **③ 源和产物可以分开。** 构建型站点的源在工作区根、产物在 dist/ 之类的产物根。
|
|
16
|
+
* resolve 返回 workspaceRoot(源,agent 的地盘)和 artifactDir(被预览 / 导出 /
|
|
17
|
+
* 发布的根),deck 和手写站点两者相同。消费方要"看"产物就用 artifactDir,
|
|
18
|
+
* 别再自己 dirname(entryPath)。
|
|
19
|
+
*
|
|
20
|
+
* 寻址顺序(越靠前越明确):
|
|
21
|
+
* ① 调用方显式给的 path
|
|
22
|
+
* ② 本会话的"当前产物"——最近写过 / 截过 / 预览过哪份就是哪份
|
|
23
|
+
* ③ 工作区只有一份产物就是它;多份就报错列出来让调用方指定
|
|
24
|
+
*
|
|
25
|
+
* ## 2026-08-07:第 ③ 条曾经是第 ④ 条
|
|
26
|
+
*
|
|
27
|
+
* 中间夹着一条「认领绑在本会话名下那个任务」——`tasks/<任务>/.nd-task.json`
|
|
28
|
+
* 里记着 sessionId,寻址时按当前会话去反查它的任务。那条规则存在的前提是
|
|
29
|
+
* **一个项目里有多个任务、而每个任务属于一个会话**;线上数据说这个前提从来
|
|
30
|
+
* 没成立过(13 个有产物的项目,每个都恰好一个任务)。
|
|
31
|
+
*
|
|
32
|
+
* 任务层拆掉之后,"这个会话的产物"这个问题本身就没有意义了 —— 产物属于项目,
|
|
33
|
+
* 谁开的对话都看得见同一批。剩下的歧义(工作区里有多份产物、调用方没说哪份)
|
|
34
|
+
* 由 ② 的最近使用和 ③ 的显式报错解决,这两条本来就在,而且不依赖任何绑定。
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import path from 'node:path';
|
|
38
|
+
import fs from 'node:fs/promises';
|
|
39
|
+
import {
|
|
40
|
+
KINDS, kindDef, readTaskMarker, taskManifest, artifactOfPath, can,
|
|
41
|
+
} from './kinds/index.js';
|
|
42
|
+
|
|
43
|
+
export const KIND_DECK = 'deck';
|
|
44
|
+
export const KIND_SITE = 'site';
|
|
45
|
+
|
|
46
|
+
/** 每种形态的入口文件名(从注册表派生;改形态契约去 kinds/) */
|
|
47
|
+
export const ENTRY_FILE = Object.freeze(
|
|
48
|
+
Object.fromEntries(Object.values(KINDS).map(k => [k.id, k.entryFile])),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 产物入口**长什么样**(扩展名集合),同样从注册表派生。
|
|
53
|
+
* `.html` / `.htm` 是 deck+site 的形状,`.docx` 是 word 的 —— 加形态时这里
|
|
54
|
+
* 自动跟上,不用再回来补一条正则。
|
|
55
|
+
*/
|
|
56
|
+
const ENTRY_EXTS = new Set(
|
|
57
|
+
Object.values(KINDS)
|
|
58
|
+
.map(k => (k.entryFile.match(/\.[a-z0-9]+$/i) || [''])[0].toLowerCase())
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.concat('.htm'), // html 的老写法,注册表里只写了 .html
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// 形态判定与解析的权威在 kinds/,这里转发老名字(消费方 import 不用改两次)
|
|
64
|
+
export { readTaskMarker, taskManifest, kindDef, artifactOfPath };
|
|
65
|
+
export { formatAllowed, docxClaimedFiles, isDirArtifact } from './kinds/index.js';
|
|
66
|
+
export { can };
|
|
67
|
+
|
|
68
|
+
/** sessionId → { path, kind }。会话结束不清也无妨(几个短字符串) */
|
|
69
|
+
const activeArtifact = new Map();
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 记住这个会话正在做哪份产物(写文件 / 截图 / 预览时调)。
|
|
73
|
+
*
|
|
74
|
+
* 只认**产物入口的形状**(ENTRY_EXTS,从注册表派生)。以前还认"任务内的非
|
|
75
|
+
* html"(改 style.css 也算在这个任务上干活,寻址时走任务入口),任务层没了之后
|
|
76
|
+
* 那一支失去了落点:改 `style.css` 说明不了在做哪一份 deck,工作区根上可能并排
|
|
77
|
+
* 放着好几份。认不出来就不认,让 resolve 走"只有一份就是它 / 多份报错"那条 ——
|
|
78
|
+
* 比猜一个错的默认目标强。
|
|
79
|
+
*/
|
|
80
|
+
export function setActiveArtifact(sessionId, relPath, kind) {
|
|
81
|
+
if (!sessionId || typeof relPath !== 'string') return;
|
|
82
|
+
const p = normalizeRel(relPath);
|
|
83
|
+
if (/\.template\.(html?|css)$/i.test(p)) return; // 模板不是产物
|
|
84
|
+
const ext = (p.match(/\.[a-z0-9]+$/i) || [''])[0].toLowerCase();
|
|
85
|
+
if (!ENTRY_EXTS.has(ext)) return;
|
|
86
|
+
activeArtifact.set(sessionId, { path: p, kind: kind || null });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getActiveArtifact(sessionId) {
|
|
90
|
+
return (sessionId && activeArtifact.get(sessionId)) || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function clearActiveArtifact(sessionId) {
|
|
94
|
+
activeArtifact.delete(sessionId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeRel(p) {
|
|
98
|
+
return String(p).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function exists(p) {
|
|
102
|
+
try { await fs.access(p); return true; } catch { return false; }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** workspace 内才算数(resolve 不出根) */
|
|
106
|
+
function insideWorkspace(workspaceRoot, absPath) {
|
|
107
|
+
const root = path.resolve(workspaceRoot);
|
|
108
|
+
return absPath === root || absPath.startsWith(root + path.sep);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 站点的页面清单(相对产物根)。
|
|
113
|
+
*/
|
|
114
|
+
export async function listSitePages(workspaceRoot) {
|
|
115
|
+
const m = await KINDS[KIND_SITE].manifest(workspaceRoot, null);
|
|
116
|
+
return m.pages;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 推断某个 html 路径的产物形态。
|
|
121
|
+
*
|
|
122
|
+
* 多产物平权:canvas.html 跟一个根站并存时仍然是 deck、about.html 是站点子页 ——
|
|
123
|
+
* 同名文件形态可以不同,问 artifacts 清单而不是猜文件名。清单里找不到(文件
|
|
124
|
+
* 还没写出来)才退回文件名约定。
|
|
125
|
+
*/
|
|
126
|
+
export async function kindOfPath(workspaceRoot, relPath) {
|
|
127
|
+
const p = normalizeRel(relPath);
|
|
128
|
+
const base = path.posix.basename(p);
|
|
129
|
+
const m = await taskManifest(workspaceRoot);
|
|
130
|
+
if (m) {
|
|
131
|
+
const art = artifactOfPath(m, p);
|
|
132
|
+
if (art) return art.kind;
|
|
133
|
+
if (m.kind) return m.kind;
|
|
134
|
+
}
|
|
135
|
+
if (base === ENTRY_FILE[KIND_DECK]) return KIND_DECK;
|
|
136
|
+
if (base === ENTRY_FILE[KIND_SITE]) return KIND_SITE;
|
|
137
|
+
// 清单里没有(文件还没写出来)时按扩展名回退。.docx 只可能是 word 形态,
|
|
138
|
+
// 让它落进 deck 会让感知层拿 playwright 去开一个二进制包。
|
|
139
|
+
const ext = (p.match(/\.[a-z0-9]+$/i) || [''])[0].toLowerCase();
|
|
140
|
+
const byExt = Object.values(KINDS).find(k => k.entryFile.toLowerCase().endsWith(ext) && ext !== '.html');
|
|
141
|
+
if (byExt) return byExt.id;
|
|
142
|
+
return KIND_DECK; // 认不出的散装 .html 一律按 deck
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 这个工作区现有的产物入口(平权,没有主次) */
|
|
146
|
+
export async function listWorkspaceArtifacts(workspaceRoot) {
|
|
147
|
+
const m = await taskManifest(workspaceRoot);
|
|
148
|
+
return (m?.artifacts || []).map(a => ({ rel: a.entryRel, kind: a.kind }));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 兼容旧名:只要 deck 那些 */
|
|
152
|
+
export async function listWorkspaceDecks(workspaceRoot) {
|
|
153
|
+
return (await listWorkspaceArtifacts(workspaceRoot))
|
|
154
|
+
.filter(a => a.kind === KIND_DECK)
|
|
155
|
+
.map(a => a.rel);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 解析产物目标。
|
|
160
|
+
*
|
|
161
|
+
* @param {string} workspaceRoot
|
|
162
|
+
* @param {string|null} relPath 调用方显式给的路径(优先级最高)
|
|
163
|
+
* @param {string|null} sessionId
|
|
164
|
+
* @returns {Promise<{ ok: true, absPath: string, relPath: string, kind: 'deck'|'site',
|
|
165
|
+
* task: string|null, taskDir: string|null,
|
|
166
|
+
* artifactDir: string|null, artifactRel: string|null }
|
|
167
|
+
* | { ok: false, message: string }>}
|
|
168
|
+
* taskDir 源(任务根,agent 的地盘)
|
|
169
|
+
* artifactDir 产物根(被预览 / 导出 / 发布的目录);deck 和手写站点 = taskDir
|
|
170
|
+
*/
|
|
171
|
+
export async function resolveArtifactTarget(workspaceRoot, relPath, sessionId) {
|
|
172
|
+
const decorate = async (rel) => {
|
|
173
|
+
const p = normalizeRel(rel);
|
|
174
|
+
// 按所属产物定 kind 和产物根(多产物平权:canvas.html 是 deck、
|
|
175
|
+
// v2/index.html 是另一个站,root 各归各)
|
|
176
|
+
let kind = null;
|
|
177
|
+
let root = '';
|
|
178
|
+
let artifact = null;
|
|
179
|
+
const m = await taskManifest(workspaceRoot);
|
|
180
|
+
if (m) {
|
|
181
|
+
artifact = artifactOfPath(m, p);
|
|
182
|
+
if (artifact) { kind = artifact.kind; root = artifact.root || ''; }
|
|
183
|
+
else if (m.kind) { kind = m.kind; root = m.root || ''; }
|
|
184
|
+
}
|
|
185
|
+
if (!kind) kind = await kindOfPath(workspaceRoot, p);
|
|
186
|
+
return {
|
|
187
|
+
ok: true,
|
|
188
|
+
absPath: path.resolve(workspaceRoot, p),
|
|
189
|
+
relPath: p,
|
|
190
|
+
kind,
|
|
191
|
+
// task / taskDir 保留成兼容字段:任务层没了,"源目录"永远是工作区根。
|
|
192
|
+
// 下游有十几处读 taskDir 当"agent 的地盘",给它对的值比改十几处安全。
|
|
193
|
+
task: null,
|
|
194
|
+
taskDir: workspaceRoot,
|
|
195
|
+
artifact, // 所属产物条目(manifest.artifacts 里那条;散文件为 null)
|
|
196
|
+
artifactDir: root ? path.join(workspaceRoot, root) : workspaceRoot,
|
|
197
|
+
artifactRel: root || '.',
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const tryPath = async (rel) => {
|
|
202
|
+
if (!rel) return null;
|
|
203
|
+
const abs = path.resolve(workspaceRoot, rel);
|
|
204
|
+
if (!insideWorkspace(workspaceRoot, abs)) return null;
|
|
205
|
+
return (await exists(abs)) ? decorate(rel) : null;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (relPath) {
|
|
209
|
+
const hit = await tryPath(relPath);
|
|
210
|
+
if (hit) { setActiveArtifact(sessionId, hit.relPath, hit.kind); return hit; }
|
|
211
|
+
const abs = path.resolve(workspaceRoot, relPath);
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
message: insideWorkspace(workspaceRoot, abs)
|
|
215
|
+
? `${relPath} not found. Write it first, or check the path.`
|
|
216
|
+
: 'path escapes workspace',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const active = getActiveArtifact(sessionId);
|
|
221
|
+
if (active?.path) {
|
|
222
|
+
const activeHit = await tryPath(active.path);
|
|
223
|
+
if (activeHit) return activeHit;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const all = await listWorkspaceArtifacts(workspaceRoot);
|
|
227
|
+
if (all.length === 1) {
|
|
228
|
+
setActiveArtifact(sessionId, all[0].rel, all[0].kind);
|
|
229
|
+
return decorate(all[0].rel);
|
|
230
|
+
}
|
|
231
|
+
if (all.length > 1) {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
message: 'Multiple artifacts in this workspace — pass path explicitly:\n'
|
|
235
|
+
+ all.map(a => `- ${a.rel} (${a.kind})`).join('\n'),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
message: 'No artifact found. Deck = canvas.html, site = index.html — '
|
|
241
|
+
+ 'write one first, or pass path explicitly.',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 浏览器类工具的统一闸门:目标形态不能用浏览器打开就别往下走。
|
|
247
|
+
*
|
|
248
|
+
* 用法是 resolveCanvasTarget 之后立刻过一道:
|
|
249
|
+
* const target = await resolveCanvasTarget(...);
|
|
250
|
+
* if (!target.ok) return err(target.message);
|
|
251
|
+
* const gate = requireBrowsable(target);
|
|
252
|
+
* if (gate) return err(gate);
|
|
253
|
+
*
|
|
254
|
+
* 不把它塞进 resolveArtifactTarget 内部,是因为导出、寻址这些**正当**需要拿到
|
|
255
|
+
* 不可浏览目标的调用方也走那个函数,在源头拦会误伤。
|
|
256
|
+
*
|
|
257
|
+
* @returns {string|null} 不可浏览时返回给 agent 的说明,可浏览返回 null
|
|
258
|
+
*/
|
|
259
|
+
export function requireBrowsable(target) {
|
|
260
|
+
if (!target?.kind || can(target.kind, 'browsable')) return null;
|
|
261
|
+
const who = target.relPath || target.kind;
|
|
262
|
+
// 可渲染形态(docx)要给对路的替代方案。说"直接 Read 它的文件"是**错的建议**
|
|
263
|
+
// —— 那是个二进制 zip,Read 出来是乱码,而它其实是**看得见**的,只是要先渲染。
|
|
264
|
+
if (can(target.kind, 'renderable')) {
|
|
265
|
+
return `${who} 是 ${target.kind} 形态,没有 DOM,读页面 / 查元素 / 取计算样式这几个工具对它无效。`
|
|
266
|
+
+ '看长相用 screenshot(它会渲染成页图,可以带 pages 参数指定范围);'
|
|
267
|
+
+ '看结构读它的 token 源(同名 .json),别去 Read 那个 .docx 本身,它是二进制包。';
|
|
268
|
+
}
|
|
269
|
+
return `${who} 是 ${target.kind} 形态,没有可以用浏览器打开的入口,`
|
|
270
|
+
+ '截图 / 读页面 / 查元素这类工具对它没有意义。直接 Read 它的文件。';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** 给各工具复用的 path 参数描述(保持措辞一致) */
|
|
274
|
+
export const ARTIFACT_PATH_DESC =
|
|
275
|
+
'Relative path of the artifact entry (deck: "canvas.html", site: "index.html" '
|
|
276
|
+
+ 'or its built output like "dist/index.html", word: "文档.docx"). '
|
|
277
|
+
+ 'Omit to use the artifact you are currently working on.';
|
|
278
|
+
|
|
279
|
+
// ── 兼容层:老名字继续可用,内部全部走上面的实现 ────────────────────────────
|
|
280
|
+
export const CANVAS_PATH_DESC = ARTIFACT_PATH_DESC;
|
|
281
|
+
export const resolveCanvasTarget = resolveArtifactTarget;
|
|
282
|
+
export const setActiveDeck = (sessionId, relPath) => setActiveArtifact(sessionId, relPath);
|
|
283
|
+
export const getActiveDeck = (sessionId) => getActiveArtifact(sessionId)?.path || null;
|
|
284
|
+
export const clearActiveDeck = clearActiveArtifact;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/asset-refs.js — 产物真正引用到了哪些素材(2026-08-17)
|
|
3
|
+
*
|
|
4
|
+
* 为什么要这个:旧的工程交付包把**整个** `shared/assets` 递归塞进 zip。那是
|
|
5
|
+
* **项目级**共享目录,不是这份产物引用的东西 —— 生产上最大的一个项目 assets
|
|
6
|
+
* 有 280MB,导出一份 deck 会把项目里每一张图(包括别的任务的)一起交出去。
|
|
7
|
+
*
|
|
8
|
+
* 所以改成扫引用:产物的 html/css/js 里写了哪些素材,就带哪些。
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ **扫不到的一律不猜,但必须说出来。** 静态引用是确定的;算不出来的(动态拼
|
|
11
|
+
* 路径、脚本里的字面量、`<base href>` 改了解析基准)**不往包里多塞**,而是记进
|
|
12
|
+
* `unresolved` 由调用方写成清单放进包里。用户拍的板:宁可在纸面上说清缺什么,
|
|
13
|
+
* 也不靠多塞蒙混过去。**「既不进包也不进清单」是这条规矩里最坏的状态** —— 那是
|
|
14
|
+
* 假装没这回事,比多塞还糟。
|
|
15
|
+
*
|
|
16
|
+
* ── 2026-08-17 评审后重写,六条实测出来的病(每条都有单测钉着)──
|
|
17
|
+
* 1. `<script src="x.js">` 随整块脚本被摘掉 → 真引用静默丢失
|
|
18
|
+
* 2. HTML 注释里的旧引用照收 → 别的任务的图混进包
|
|
19
|
+
* 3. `src="it's.png"` 用混合字符类解析 → 幻影路径 `it`
|
|
20
|
+
* 4. `file:` 之类 scheme 漏过滤(原来是枚举表)→ 幻影 ref
|
|
21
|
+
* 5. 脚本里的字面量素材(`['a.png','b.png']`)既不进包也不进清单
|
|
22
|
+
* 6. 树外引用全域放行 → `<a href="../别的任务/x.html">` 把别人的产物收进来
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import fs from 'node:fs/promises';
|
|
27
|
+
|
|
28
|
+
/** 会被扫的文本类型(二进制不扫) */
|
|
29
|
+
const SCANNABLE = /\.(html?|css|jsx?|mjs|cjs)$/i;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 不是本地素材的引用。**用泛化判据不用枚举表** —— 枚举表漏一个 scheme(`file:`)
|
|
33
|
+
* 就会产出幻影 ref。代价是 `x:30.png` 这种带冒号的文件名会被当 scheme 滤掉,
|
|
34
|
+
* 但浏览器本来也按 scheme 解析它,滤掉才是对齐浏览器语义。
|
|
35
|
+
*/
|
|
36
|
+
const EXTERNAL = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 静态引用的抓取规则。
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ **引号必须分支写**(`"([^"]*)"|'([^']*)'`),不能用 `["']([^"']+)["']` 那种
|
|
42
|
+
* 混合字符类:后者遇到 `src="it's.png"` 会在撇号处截断,抓出幻影 `it`。
|
|
43
|
+
* ⚠️ 属性名前的 `(?<![.\w-])` 挡的是 JS 里的 `img.src = '...'` —— 没有它,属性
|
|
44
|
+
* 规则会把代码里的赋值当成 HTML 属性,同样抓出幻影。
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* **只对标记语言成立**的规则。`src=` / `href=` 是 HTML 属性 —— 拿它去扫 .js
|
|
48
|
+
* 会把 `const data = 'hello world'` 当成引用,捞出幻影路径 `站/hello world`
|
|
49
|
+
* 塞进 missing,交付物里印出假警报(实测踩过)。
|
|
50
|
+
*/
|
|
51
|
+
const MARKUP_PATTERNS = [
|
|
52
|
+
/(?<![.\w-])(?:src|href|poster|data-src|data)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi,
|
|
53
|
+
/\bsrcset\s*=\s*(?:"([^"]*)"|'([^']*)')/gi,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/** 在 html / css / js 里语义都成立的规则 */
|
|
57
|
+
const UNIVERSAL_PATTERNS = [
|
|
58
|
+
/url\(\s*(?:"([^"]*)"|'([^']*)'|([^'")][^)]*?))\s*\)/gi,
|
|
59
|
+
/@import\s+(?:"([^"]*)"|'([^']*)')/gi,
|
|
60
|
+
/\bfrom\s+["']([^"']+\.(?:css|png|jpe?g|webp|gif|svg))["']/gi,
|
|
61
|
+
// `new URL('x.png', import.meta.url)` 的第一个参数是**字面量**,静态可判,
|
|
62
|
+
// 不该只当成"算不出来"记进清单。
|
|
63
|
+
/new\s+URL\s*\(\s*(?:"([^"]*)"|'([^']*)')\s*,\s*import\.meta\.url/gi,
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/** 从一次匹配里取出那个真正被捕获的分支 */
|
|
67
|
+
function capturedOf(m) {
|
|
68
|
+
for (let i = 1; i < m.length; i++) if (m[i] != null) return m[i];
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 摘掉脚本体但**保留开标签** —— `<script src="lib/chart.js"></script>` 里的 src
|
|
74
|
+
* 是真引用,整块摘掉会静默丢失它(实测踩过)。
|
|
75
|
+
*/
|
|
76
|
+
const SCRIPT_BLOCK = /(<script\b[^>]*>)[\s\S]*?<\/script>/gi;
|
|
77
|
+
/** HTML 注释:里面的引用是历史残留,收进来就是把别的任务的旧图打进包 */
|
|
78
|
+
const HTML_COMMENT = /<!--[\s\S]*?-->/g;
|
|
79
|
+
/** CSS / JS 的块注释。**不摘 `//` 行注释** —— 会误杀 `https://` */
|
|
80
|
+
const BLOCK_COMMENT = /\/\*[\s\S]*?\*\//g;
|
|
81
|
+
|
|
82
|
+
/** 动态拼路径:抓到只报告不解析 */
|
|
83
|
+
const DYNAMIC_HINTS = [
|
|
84
|
+
{ re: /["'][^"']*assets\/[^"']*["']\s*\+/gi, why: '字符串拼接' },
|
|
85
|
+
{ re: /`[^`]*assets\/[^`]*\$\{[^`]*`/gi, why: '模板字符串插值' },
|
|
86
|
+
{ re: /\+\s*["'][^"']*\.(?:png|jpe?g|webp|gif|svg|mp4)["']/gi, why: '字符串拼接' },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/** 脚本里的素材字面量(画廊 / lightbox 的常见写法),只进清单不进包 */
|
|
90
|
+
const SCRIPT_LITERAL = /["'`]([^"'`\n]{1,200}\.(?:png|jpe?g|webp|gif|svg|mp4|webm|pdf|woff2?))["'`]/gi;
|
|
91
|
+
|
|
92
|
+
/** 去掉查询串和锚点(`x.webp?w=480` → `x.webp`)、反斜杠归一、URL 解码 */
|
|
93
|
+
function cleanRef(raw) {
|
|
94
|
+
const s = String(raw).trim().replace(/\\/g, '/').split('#')[0].split('?')[0];
|
|
95
|
+
try { return decodeURIComponent(s); } catch { return s; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** srcset 拆成一组候选:`a.png 1x, b.png 2x` → ['a.png','b.png'] */
|
|
99
|
+
function splitSrcset(value) {
|
|
100
|
+
return value.split(',').map(part => part.trim().split(/\s+/)[0]).filter(Boolean);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 扫一批产物文件,收出它们引用到的素材。
|
|
105
|
+
*
|
|
106
|
+
* @param {object} opts
|
|
107
|
+
* @param {Array<{abs:string, rel:string}>} opts.files 要扫的文件(rel 相对 baseRoot)
|
|
108
|
+
* @param {string} opts.baseRoot 解析基准(工作区根)。返回的路径都相对它
|
|
109
|
+
* @param {string[]} [opts.allowPrefixes] 允许收的前缀白名单(树外只认这些,通常是
|
|
110
|
+
* 产物自己的根 + `assets/`)。**留空 = 不设闸**(根层产物就是这种情况,它的树
|
|
111
|
+
* 本来就是整个工作区)。被挡下的引用不静默丢,进 unresolved。
|
|
112
|
+
* @returns {Promise<{refs:string[], unresolved:Array<{from,why,snippet}>}>}
|
|
113
|
+
*/
|
|
114
|
+
export async function collectAssetRefs({ files, baseRoot, allowPrefixes = [] }) {
|
|
115
|
+
const own = new Set(files.map(f => f.rel.replace(/\\/g, '/')));
|
|
116
|
+
const refs = new Set();
|
|
117
|
+
const unresolved = [];
|
|
118
|
+
const candidates = []; // 脚本里的字面量,等调用方按"磁盘上真有"再提升
|
|
119
|
+
const seenHint = new Set(); // 同一句话被多条规则命中时去重
|
|
120
|
+
|
|
121
|
+
const pushHint = (from, why, snippet) => {
|
|
122
|
+
const s = String(snippet).replace(/\s+/g, ' ').slice(0, 120);
|
|
123
|
+
const key = `${from}|${s}`;
|
|
124
|
+
if (seenHint.has(key)) return;
|
|
125
|
+
seenHint.add(key);
|
|
126
|
+
unresolved.push({ from, why, snippet: s });
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const allowed = (rel) => {
|
|
130
|
+
if (!allowPrefixes.length) return true;
|
|
131
|
+
return allowPrefixes.some(p => (p === '' ? true : rel === p || rel.startsWith(p)));
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
for (const f of files) {
|
|
135
|
+
if (!SCANNABLE.test(f.rel)) continue;
|
|
136
|
+
let text;
|
|
137
|
+
try { text = await fs.readFile(f.abs, 'utf8'); } catch { continue; }
|
|
138
|
+
|
|
139
|
+
const fileDirAbs = path.dirname(f.abs);
|
|
140
|
+
const isMarkup = /\.html?$/i.test(f.rel);
|
|
141
|
+
const isScript = /\.(m?js|cjs|jsx)$/i.test(f.rel);
|
|
142
|
+
// 静态引用只在「非注释、非脚本体」的部分找;动态线索照旧扫全文
|
|
143
|
+
const staticText = text
|
|
144
|
+
.replace(HTML_COMMENT, ' ')
|
|
145
|
+
.replace(BLOCK_COMMENT, ' ')
|
|
146
|
+
.replace(SCRIPT_BLOCK, '$1</script>');
|
|
147
|
+
|
|
148
|
+
for (const pattern of (isMarkup ? [...MARKUP_PATTERNS, ...UNIVERSAL_PATTERNS] : UNIVERSAL_PATTERNS)) {
|
|
149
|
+
pattern.lastIndex = 0;
|
|
150
|
+
let m;
|
|
151
|
+
while ((m = pattern.exec(staticText)) !== null) {
|
|
152
|
+
const captured = capturedOf(m);
|
|
153
|
+
if (captured == null) continue;
|
|
154
|
+
const isSrcset = /srcset/i.test(pattern.source);
|
|
155
|
+
for (const c of (isSrcset ? splitSrcset(captured) : [captured])) {
|
|
156
|
+
if (!c || EXTERNAL.test(c.trim())) continue;
|
|
157
|
+
const cleaned = cleanRef(c);
|
|
158
|
+
if (!cleaned || cleaned.startsWith('/')) continue; // 根路径引用平台本来就禁
|
|
159
|
+
const abs = path.resolve(fileDirAbs, cleaned);
|
|
160
|
+
const rel = path.relative(baseRoot, abs).replace(/\\/g, '/');
|
|
161
|
+
if (!rel || rel.startsWith('..')) continue; // 逃出工作区的不收
|
|
162
|
+
if (own.has(rel)) continue; // 产物自己的页面不算素材
|
|
163
|
+
if (!allowed(rel)) {
|
|
164
|
+
pushHint(f.rel, '引用落在产物树外(只有 assets/ 和产物自己的目录会进包)', cleaned);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
refs.add(rel);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// `<base href>` 会改写整页相对路径的解析基准,扫描器按文件目录解析就会整体偏移
|
|
173
|
+
if (/<base\b[^>]*\bhref/i.test(text)) {
|
|
174
|
+
pushHint(f.rel, '页面有 <base href>,本页所有相对引用的解析基准可能跟打包时算的不一样', '<base href=…>');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const { re, why } of DYNAMIC_HINTS) {
|
|
178
|
+
re.lastIndex = 0;
|
|
179
|
+
let m;
|
|
180
|
+
while ((m = re.exec(text)) !== null) pushHint(f.rel, why, m[0]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 脚本里的素材字面量(画廊 / lightbox 的常见写法)。
|
|
184
|
+
// **完整字面量是静态可判的**,所以不直接判死:能落进工作区、又过得了树外闸的
|
|
185
|
+
// 交给调用方按「磁盘上真有这个文件」提升成素材;剩下的才进清单。
|
|
186
|
+
// 这不违反「扫不到的不多塞」—— 它扫得到,只是来源是脚本而不是标签。
|
|
187
|
+
const scanLiterals = (body) => {
|
|
188
|
+
SCRIPT_LITERAL.lastIndex = 0;
|
|
189
|
+
let lit;
|
|
190
|
+
while ((lit = SCRIPT_LITERAL.exec(body)) !== null) {
|
|
191
|
+
const cleaned = cleanRef(lit[1]);
|
|
192
|
+
const snippet = lit[0].slice(0, 120);
|
|
193
|
+
if (!cleaned || EXTERNAL.test(cleaned) || cleaned.startsWith('/')) {
|
|
194
|
+
pushHint(f.rel, '脚本里的字符串字面量(打包时不解析)', snippet); continue;
|
|
195
|
+
}
|
|
196
|
+
const rel = path.relative(baseRoot, path.resolve(fileDirAbs, cleaned)).replace(/\\/g, '/');
|
|
197
|
+
if (!rel || rel.startsWith('..') || own.has(rel) || !allowed(rel)) {
|
|
198
|
+
pushHint(f.rel, '脚本里的字符串字面量(打包时不解析)', snippet); continue;
|
|
199
|
+
}
|
|
200
|
+
candidates.push({ rel, from: f.rel, snippet });
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
// ⚠️ 从**剥过注释**的文本里取脚本体:标签侧早就剥了注释,字面量侧漏剥的话,
|
|
204
|
+
// 注释里的旧图会被提升进包 —— 同一条规矩两个半边,只修一半等于没修。
|
|
205
|
+
const commentFree = text.replace(HTML_COMMENT, ' ');
|
|
206
|
+
for (const sm of commentFree.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)) scanLiterals(sm[1]);
|
|
207
|
+
// ⚠️ 独立的 .js 文件**整个就是脚本体**,没有 <script> 标签可匹配。
|
|
208
|
+
// 不加这条的话,同一份画廊代码写在 HTML 里能收、抽成 js 文件就双失踪。
|
|
209
|
+
if (isScript) scanLiterals(text.replace(BLOCK_COMMENT, ' '));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { refs: [...refs].sort(), candidates, unresolved };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 把 unresolved 渲染成放进包里的清单。没有动态引用时返回 null(不放空文件 ——
|
|
217
|
+
* 旧交付包里那个常年空着的 prompt.txt 就是这么来的)。
|
|
218
|
+
*/
|
|
219
|
+
export function renderUnresolvedReport(unresolved) {
|
|
220
|
+
if (!unresolved?.length) return null;
|
|
221
|
+
const lines = [
|
|
222
|
+
'# 没能解析的素材引用',
|
|
223
|
+
'',
|
|
224
|
+
'下面这些地方打包时算不出具体引用了哪个文件,**素材可能没进包**。',
|
|
225
|
+
'如果打开后有图裂了,照这个清单去原工程里找对应文件补进来。',
|
|
226
|
+
'',
|
|
227
|
+
];
|
|
228
|
+
const byFile = new Map();
|
|
229
|
+
for (const u of unresolved) {
|
|
230
|
+
if (!byFile.has(u.from)) byFile.set(u.from, []);
|
|
231
|
+
byFile.get(u.from).push(u);
|
|
232
|
+
}
|
|
233
|
+
for (const [file, items] of [...byFile.entries()].sort()) {
|
|
234
|
+
lines.push(`## ${file}`, '');
|
|
235
|
+
for (const it of items) lines.push(`- (${it.why})\`${it.snippet}\``);
|
|
236
|
+
lines.push('');
|
|
237
|
+
}
|
|
238
|
+
return lines.join('\n');
|
|
239
|
+
}
|