@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,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api/exports/cards.js — 按**产物卡**导出(2026-08-17 重做导出的入口)
|
|
3
|
+
*
|
|
4
|
+
* 从 exports.js 拆出来(行数棘轮:那份已经贴着上限)。放这儿也更对 ——
|
|
5
|
+
* 它跟老的四条烘焙路由(html/pdf/pptx/site)不是一档东西:
|
|
6
|
+
* 老路由 = **烘焙**,要跑 playwright / esbuild,产出是「给人看的文件」。
|
|
7
|
+
* 本路由 = **原样打包**,产出是「别人能接手继续开发的工程」。
|
|
8
|
+
*
|
|
9
|
+
* 旧导出以「会话当前产物」为单位,且把整个项目级 `shared/assets` 打进包
|
|
10
|
+
* (生产上最大的项目那目录 280MB)。新的以**画布上的那张卡**为单位:卡的类型
|
|
11
|
+
* 决定收什么,素材只收这份产物真正引用到的。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { collectCards } from '../../lib/export-collect.js';
|
|
15
|
+
import { packageBundles } from '../../lib/export-package.js';
|
|
16
|
+
|
|
17
|
+
const FORMATS = ['raw', 'zip', 'md', 'handoff'];
|
|
18
|
+
const MAX_CARDS = 200;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 并发闸。
|
|
22
|
+
*
|
|
23
|
+
* 打包是这台机器上最重的同步活:每个文件整读进内存,JSZip 的 DEFLATE 又在主线程
|
|
24
|
+
* 跑。生产机是 **1 vCPU / 8G / 无 swap**(尖峰直接进内核 OOM killer),压缩期间
|
|
25
|
+
* 唯一那颗核被占住,所有 SSE 与 agent 流一起饿着。没有闸的话,任何登录用户连点
|
|
26
|
+
* 几下导出就能把整台机器拖垮 —— 而且症状是「网站卡住」,没人会怀疑到导出头上。
|
|
27
|
+
*
|
|
28
|
+
* 全局 2 是按核数定的,不是按用户:护的是机器不是公平性。per-project 再压 1,
|
|
29
|
+
* 免得一个人连点把两个位置都占满。
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ 这只挡住**同时几个**,挡不住**单个太大**(200 张视频卡可以到 GB 级)。
|
|
32
|
+
* 体积闸是另一件事,还没做。
|
|
33
|
+
*/
|
|
34
|
+
const MAX_INFLIGHT = 2;
|
|
35
|
+
let inflight = 0;
|
|
36
|
+
const inflightByProject = new Map();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {object} deps
|
|
40
|
+
* @param {(req,res)=>object|null} deps.guard exports.js 的 guard(项目/会话双挂载)
|
|
41
|
+
* @param {(req)=>string} deps.rootOf 工作区根
|
|
42
|
+
*/
|
|
43
|
+
export function makeCardsExportHandler({ guard, rootOf }) {
|
|
44
|
+
return async function handleCardsExport(req, res, next) {
|
|
45
|
+
try {
|
|
46
|
+
const project = guard(req, res);
|
|
47
|
+
if (!project) return;
|
|
48
|
+
const { cardIds, format } = req.body || {};
|
|
49
|
+
if (!Array.isArray(cardIds) || !cardIds.length) {
|
|
50
|
+
return res.status(400).json({ error: '要导出哪几张卡(cardIds 不能为空)' });
|
|
51
|
+
}
|
|
52
|
+
if (cardIds.length > MAX_CARDS) {
|
|
53
|
+
return res.status(400).json({ error: `一次最多导出 ${MAX_CARDS} 张卡` });
|
|
54
|
+
}
|
|
55
|
+
if (!FORMATS.includes(format)) {
|
|
56
|
+
return res.status(400).json({ error: `不认识的导出格式:${format}` });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (inflight >= MAX_INFLIGHT) {
|
|
60
|
+
res.setHeader('Retry-After', '10');
|
|
61
|
+
return res.status(429).json({ error: '导出排队中(这台机器一次只打两个包),过几秒再点' });
|
|
62
|
+
}
|
|
63
|
+
if (inflightByProject.get(project.id)) {
|
|
64
|
+
res.setHeader('Retry-After', '10');
|
|
65
|
+
return res.status(429).json({ error: '这个项目已经有一个导出在跑了,等它完事' });
|
|
66
|
+
}
|
|
67
|
+
inflight += 1;
|
|
68
|
+
inflightByProject.set(project.id, true);
|
|
69
|
+
try {
|
|
70
|
+
const workspaceRoot = rootOf(req);
|
|
71
|
+
const { bundles, skipped } = await collectCards({ workspaceRoot, cardIds });
|
|
72
|
+
if (!bundles.length) {
|
|
73
|
+
return res.status(skipped[0]?.status === 404 ? 404 : 400)
|
|
74
|
+
.json({ error: '一张都没收到', skipped });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 格式合法性由 packageBundles 自己把关(raw 要单文件、md 要有 markdown),
|
|
78
|
+
// 不在这儿再抄一份判据。⚠️ 曾经拿 `bundle.exportFormats.includes(format)`
|
|
79
|
+
// 当闸,是错的:那张表说的是**产物形态**能出什么(site 卡是 site/html/handoff),
|
|
80
|
+
// 跟本路由的打包档(zip/raw/md/handoff)语义不同,混用会把正常请求判死。
|
|
81
|
+
const { filename, buffer, mime } = await packageBundles(bundles, {
|
|
82
|
+
format,
|
|
83
|
+
projectName: bundles.length === 1 ? bundles[0].title : project.name,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// 收不到的卡不拖累整批,但**必须让前端能说出少了哪几张**。
|
|
87
|
+
// 静默少东西是导出最贵的失败方式:用户解压之后才发现,那时已经不知道
|
|
88
|
+
// 是哪一步丢的。
|
|
89
|
+
if (skipped.length) {
|
|
90
|
+
// ⚠️ 头要封顶。中文经 UTF-8 + 百分号编码大约 9 倍膨胀,100 条 skipped
|
|
91
|
+
// 实测 32KB —— nginx 默认 proxy_buffer_size 4k/8k,超了就是
|
|
92
|
+
// "upstream sent too big header" → **502,打好的 zip 一个字节都到不了用户**。
|
|
93
|
+
// 卡 id 长度也不设限的话,一个恶意长 id 单发就能触发。
|
|
94
|
+
const items = skipped.slice(0, 20).map(x => ({
|
|
95
|
+
cardId: String(x.cardId).slice(0, 120),
|
|
96
|
+
reason: String(x.reason).slice(0, 120),
|
|
97
|
+
status: x.status,
|
|
98
|
+
}));
|
|
99
|
+
let v = encodeURIComponent(JSON.stringify({ total: skipped.length, items }));
|
|
100
|
+
if (v.length > 4000) v = encodeURIComponent(JSON.stringify({ total: skipped.length, items: [] }));
|
|
101
|
+
res.setHeader('X-Export-Skipped', v);
|
|
102
|
+
}
|
|
103
|
+
res.setHeader('Content-Type', mime);
|
|
104
|
+
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
|
|
105
|
+
res.setHeader('Content-Length', buffer.length);
|
|
106
|
+
res.end(buffer);
|
|
107
|
+
} finally {
|
|
108
|
+
// finally 不能漏:抛错时不减计数 = 闸门永久卡死,两次失败之后
|
|
109
|
+
// 这个功能就再也用不了了,而且重启前查不出原因
|
|
110
|
+
inflight -= 1;
|
|
111
|
+
inflightByProject.delete(project.id);
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (err.status) return res.status(err.status).json({ error: err.message });
|
|
115
|
+
next(err);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* exports/docx-pdf.js — docx → PDF 导出。
|
|
3
|
+
*
|
|
4
|
+
* 单开一个文件而不是往 exports.js 的 pdf 路由里塞 if:那条路由整段是
|
|
5
|
+
* playwright + `<section data-page>` 的 deck 逻辑,docx 跟它一行代码都不共享
|
|
6
|
+
* (LibreOffice 直接出 PDF,没有分页 section 这回事)。
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ 这里出的 PDF 跟用户在 Word 里「另存为 PDF」**不是同一个东西** —— 域
|
|
9
|
+
* (TOC)不更新、中文是替身字体。给用户下载时说清楚:要发出去的正式
|
|
10
|
+
* PDF,请在 Word 里导;这份是快速预览用的。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { renderDocx, cleanupRender } from '../../lib/docx/render.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {import('express').Response} res
|
|
17
|
+
* @param {{absPath:string, relPath:string}} target
|
|
18
|
+
*/
|
|
19
|
+
export async function docxToPdfResponse(res, target) {
|
|
20
|
+
let out;
|
|
21
|
+
try {
|
|
22
|
+
out = await renderDocx(target.absPath, {}); // 只要 pdf,不出 png
|
|
23
|
+
} catch (err) {
|
|
24
|
+
return res.status(500).json({
|
|
25
|
+
error: 'LibreOffice 转换失败',
|
|
26
|
+
details: String(err.message || err).slice(0, 500),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const { readFile } = await import('node:fs/promises');
|
|
31
|
+
const buf = await readFile(out.pdf);
|
|
32
|
+
const stem = target.relPath.replace(/\.docx$/i, '').split('/').pop() || '文档';
|
|
33
|
+
res.setHeader('Content-Type', 'application/pdf');
|
|
34
|
+
res.setHeader(
|
|
35
|
+
'Content-Disposition',
|
|
36
|
+
`attachment; filename*=UTF-8''${encodeURIComponent(`${stem}.pdf`)}`,
|
|
37
|
+
);
|
|
38
|
+
res.setHeader('Content-Length', buf.length);
|
|
39
|
+
// 让下载方知道这份 PDF 的成色,别拿它当正式稿发出去
|
|
40
|
+
res.setHeader('X-Docx-Pdf-Note', 'toc-field-not-updated; substituted-cjk-fonts');
|
|
41
|
+
res.end(buf);
|
|
42
|
+
} finally {
|
|
43
|
+
await cleanupRender(out);
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* exports/export-page.js — deck 导出(PDF / PPTX / 单页 HTML)的共用底座。
|
|
3
|
+
*
|
|
4
|
+
* 2026-08-17 从 exports.js 搬出来。这两个函数是**纯辅助**,被 pdf / pptx /
|
|
5
|
+
* html 三条路由共用,本来就不该住在路由文件里。
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ 上一次拆 exports.js(拆 handoff)时,它俩是被**误伤**带走的,`node --check`
|
|
8
|
+
* 和 vite build 都不报,只有 no-undef.lint 抓到了。这次是**故意**给它们安家,
|
|
9
|
+
* 顺带把那次事故的教训坐实成目录结构。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs/promises';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import { resolveDeckSize, extractDeckAspect } from '../../shared/deck.js';
|
|
16
|
+
import { fitInjectionBlock } from '../standalone-fit.js';
|
|
17
|
+
import { buildStandaloneHtml, isHybridHtml } from './build-standalone.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 共用导出准备:启 Playwright page、等字体/图片就绪、注入基线 reset、探测实际 section 尺寸。
|
|
21
|
+
* 返回 { page, ctx, pageSize: { w, h } }。
|
|
22
|
+
*
|
|
23
|
+
* 多比例支持:先读 canvas.html 抽 wrap data-deck-aspect → 设对应 viewport
|
|
24
|
+
* (16:9=1920×1080 / 9:16=1080×1920 / 4:3=1440×1080)。
|
|
25
|
+
*/
|
|
26
|
+
export async function prepareExportPage(browser, filePath, opts = {}) {
|
|
27
|
+
const dpr = opts.dpr ?? 2;
|
|
28
|
+
|
|
29
|
+
// 读文件抽 deck 比例 → 决定 viewport
|
|
30
|
+
const html = await fs.readFile(filePath, 'utf8').catch(() => '');
|
|
31
|
+
const aspect = extractDeckAspect(html);
|
|
32
|
+
const dims = resolveDeckSize(aspect);
|
|
33
|
+
|
|
34
|
+
// 跑跟 /exports/html 同款 standalone 管道——把 Google Fonts / 本地图片 / CDN
|
|
35
|
+
// 全 inline,写到 tmp 文件让 Playwright 从那加载。这样 PDF/PPT 用的字体跟
|
|
36
|
+
// HTML 下载产物 1:1,不再依赖 server 能否 reach fonts.googleapis.com(国内
|
|
37
|
+
// 网络 / firewall 直接封死的话原方案 PDF 字体会无声 fallback 到系统字体)。
|
|
38
|
+
// hybrid 检测失败或 build 失败 → fallback 到原 file://,至少导出能跑通。
|
|
39
|
+
let loadPath = filePath;
|
|
40
|
+
let cleanup = async () => {};
|
|
41
|
+
if (isHybridHtml(html)) {
|
|
42
|
+
try {
|
|
43
|
+
const baked = await buildStandaloneHtml(html, { sessionRoot: opts.sessionRoot, baseDir: path.dirname(filePath) });
|
|
44
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'nd-export-'));
|
|
45
|
+
const tmpFile = path.join(tmpDir, 'baked.html');
|
|
46
|
+
await fs.writeFile(tmpFile, baked, 'utf8');
|
|
47
|
+
loadPath = tmpFile;
|
|
48
|
+
cleanup = async () => { await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {}); };
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.warn('[prepareExportPage] standalone bake failed, loading raw canvas.html:', err.message);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ctx = await browser.newContext({
|
|
55
|
+
viewport: { width: dims.width, height: dims.height },
|
|
56
|
+
deviceScaleFactor: dpr,
|
|
57
|
+
});
|
|
58
|
+
const page = await ctx.newPage();
|
|
59
|
+
await page.goto('file://' + loadPath, { waitUntil: 'networkidle', timeout: 30_000 });
|
|
60
|
+
|
|
61
|
+
// 字体强等待——比 await document.fonts.ready 严格得多。
|
|
62
|
+
//
|
|
63
|
+
// 背景:Google Fonts 把 CJK 字体(Noto Sans SC)拆成 80+ 个 unicode-range
|
|
64
|
+
// 子集,每个子集 + 每个 weight 是独立 @font-face。HTML 用 font-display: swap
|
|
65
|
+
// 的话浏览器先用 fallback 字体绘制,字体异步加载完后 swap。
|
|
66
|
+
//
|
|
67
|
+
// document.fonts.ready 只等"已经 pending 的"face——CSS 引用了但还没被使用
|
|
68
|
+
// 过的 face 不算 pending(lazy load 机制)。截图时若某个 weight × range
|
|
69
|
+
// 子集还没被触发,PDF/PPT 就截到 swap 前的 fallback 字体(preview 走 macOS
|
|
70
|
+
// 系统字体看着对,PPT/PDF 走 Linux Chromium 默认字体看着错)。
|
|
71
|
+
//
|
|
72
|
+
// 4 步保 ready:
|
|
73
|
+
// 1. 强制 layout(让所有字体使用注册到 FontFaceSet)
|
|
74
|
+
// 2. 显式 .load() 所有声明的 face(含未被使用的 weight × range 子集)
|
|
75
|
+
// 3. await document.fonts.ready(兜底等剩余 pending)
|
|
76
|
+
// 4. 双 rAF 等 paint 真正应用 swap
|
|
77
|
+
await page.evaluate(async () => {
|
|
78
|
+
document.body.offsetHeight; // force layout
|
|
79
|
+
const faces = Array.from(document.fonts);
|
|
80
|
+
await Promise.all(faces.map((f) => f.load().catch(() => {})));
|
|
81
|
+
await document.fonts.ready;
|
|
82
|
+
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
83
|
+
});
|
|
84
|
+
await page.waitForTimeout(200); // 短兜底(rAF 后再给 paint 一帧时间)
|
|
85
|
+
|
|
86
|
+
// 抹掉 body margin + 砍掉 fit-active 视觉缩放(如果 file:// 加载的 canvas.html
|
|
87
|
+
// 有 standalone-fit script,frame 包装 + section transform: scale 会让 boundingBox
|
|
88
|
+
// 拿到的不是设计稿原坐标)。Playwright viewport 已经 = deck 比例,原生渲染。
|
|
89
|
+
await page.addStyleTag({ content: `
|
|
90
|
+
body { margin: 0 !important; padding: 0 !important; }
|
|
91
|
+
body.__nd-fit-active > .__nd-deck-wrap > .__nd-page-frame {
|
|
92
|
+
width: ${dims.width}px !important; height: ${dims.height}px !important;
|
|
93
|
+
display: block !important; overflow: visible !important;
|
|
94
|
+
}
|
|
95
|
+
body.__nd-fit-active section[data-page] { transform: none !important; }
|
|
96
|
+
` });
|
|
97
|
+
|
|
98
|
+
const fallback = { w: dims.width, h: dims.height };
|
|
99
|
+
const pageSize = await page.evaluate((fb) => {
|
|
100
|
+
const first = document.querySelector('section[data-page]');
|
|
101
|
+
if (!first) return fb;
|
|
102
|
+
const rect = first.getBoundingClientRect();
|
|
103
|
+
return { w: Math.round(rect.width), h: Math.round(rect.height) };
|
|
104
|
+
}, fallback);
|
|
105
|
+
|
|
106
|
+
return { page, ctx, pageSize, cleanup };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 导出 HTML 时注入 viewport 自适应脚本(服务端兜底)。
|
|
111
|
+
*
|
|
112
|
+
* 逻辑:scale(viewportWidth / DECK.width) 让任意视口宽都满铺 + 完整。
|
|
113
|
+
* 仅在独立打开时生效(iframe 内 window!==top 早退,前端 CanvasFrame 自算 scale)。
|
|
114
|
+
*
|
|
115
|
+
* 行为升级(2026-05-08):内部改调 server/api/standalone-fit.js 的 fitInjectionBlock(),
|
|
116
|
+
* 升级到 4 mode 感知(stack/ppt/carousel/custom)+ transform-origin: top left。
|
|
117
|
+
* 调用方零改动,老 deck(无 data-deck-mode attr)自动按 stack 兜底。
|
|
118
|
+
*/
|
|
119
|
+
export function injectViewportFit(html) {
|
|
120
|
+
// 1. 替换 agent 写的固定 viewport meta → 响应式 viewport(让 fit script 控)
|
|
121
|
+
if (/<meta\s+name=["']viewport["'][^>]*>/i.test(html)) {
|
|
122
|
+
html = html.replace(
|
|
123
|
+
/<meta\s+name=["']viewport["'][^>]*>/i,
|
|
124
|
+
'<meta name="viewport" content="width=device-width,initial-scale=1">',
|
|
125
|
+
);
|
|
126
|
+
} else if (html.includes('</head>')) {
|
|
127
|
+
html = html.replace(
|
|
128
|
+
'</head>',
|
|
129
|
+
'<meta name="viewport" content="width=device-width,initial-scale=1">\n</head>',
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 2. 调 standalone-fit 拼完整 fit injection block 注入
|
|
134
|
+
const block = fitInjectionBlock();
|
|
135
|
+
if (html.includes('</body>')) {
|
|
136
|
+
return html.replace('</body>', block + '\n</body>');
|
|
137
|
+
}
|
|
138
|
+
if (html.includes('</html>')) {
|
|
139
|
+
return html.replace('</html>', block + '\n</html>');
|
|
140
|
+
}
|
|
141
|
+
return html + block;
|
|
142
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* api/exports/handoff.js — 旧的「工程交付包」打包逻辑(2026-08-17 从 exports.js 拆出)
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ **这是旧口径,正在被 exports/cards.js 那条替换。** 它的已知毛病:
|
|
5
|
+
* - `design/assets/` 把整个项目级 `shared/assets` 递归打包(最大的项目 280MB,
|
|
6
|
+
* 会把别的任务的图一起交出去)
|
|
7
|
+
* - `prompt.txt` 恒空、`chat-history.json` 只有 runId 列表
|
|
8
|
+
* - `spec.json` 是 agent 私域档案却进包,而真决策(notes/决策.md)反而没进
|
|
9
|
+
* - README 是静态模板,不讲怎么跑 / 依赖 / 改哪里
|
|
10
|
+
*
|
|
11
|
+
* 拆出来是为了两件事:exports.js 贴着行数棘轮上限;以及把「待退役的那一坨」
|
|
12
|
+
* 圈在一个文件里,退役时整份删掉即可,不用在大文件里挑。
|
|
13
|
+
* MCP 工具 `export_handoff` 还在用它,退役要连那条一起换。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { promises as fs } from 'fs';
|
|
18
|
+
import JSZip from 'jszip';
|
|
19
|
+
import { walkTaskFiles, loadIgnore } from '../../lib/task-scan.js';
|
|
20
|
+
import { KIND_SITE } from '../../lib/artifact-target.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 共享 handoff 打包逻辑 —— HTTP 路由 + MCP tool(export_handoff)共用。
|
|
24
|
+
*
|
|
25
|
+
* @param {string} sessionRoot sessions/<sid>/ 绝对路径(canvas/spec 在这)
|
|
26
|
+
* @param {string} sharedRoot shared/ 绝对路径(assets 在这)
|
|
27
|
+
*/
|
|
28
|
+
export async function buildHandoffZip(sessionRoot, sharedRoot, { projectId, projectName, skillId, sessionId, runs = [], deckPath = 'canvas.html', kind = null } = {}) {
|
|
29
|
+
const zip = new JSZip();
|
|
30
|
+
const isSite = kind === KIND_SITE;
|
|
31
|
+
|
|
32
|
+
if (isSite) {
|
|
33
|
+
// 站点:产物根整个进 design/,保留文件名与子目录 —— 只留入口页并改名叫
|
|
34
|
+
// canvas.html 的话,子页和 style.css 全丢,页间相对链接必然断。
|
|
35
|
+
// dirname(入口) 就是产物根(手写 = 任务根,构建型 = dist/);忽略规则
|
|
36
|
+
// 从任务根读(.ndignore 住那),试作 `_drafts/` 不进交付包。
|
|
37
|
+
const artifactDirAbs = path.dirname(path.resolve(sessionRoot, deckPath));
|
|
38
|
+
// 忽略规则(.ndignore)住工作区根;构建型站点的产物根是 dist/,
|
|
39
|
+
// 规则却写在源那边,所以这两个目录必须分开取
|
|
40
|
+
const taskRootAbs = path.resolve(sessionRoot);
|
|
41
|
+
const siteFiles = await walkTaskFiles(artifactDirAbs, {
|
|
42
|
+
maxDepth: 6,
|
|
43
|
+
ignore: await loadIgnore(taskRootAbs),
|
|
44
|
+
ignoreBase: taskRootAbs,
|
|
45
|
+
});
|
|
46
|
+
for (const f of siteFiles) {
|
|
47
|
+
try { zip.file(`design/${f.rel}`, await fs.readFile(f.abs)); } catch { /* 中途被删就跳过 */ }
|
|
48
|
+
}
|
|
49
|
+
// 站内 html/css 的 `../../assets/` 归一(zip 布局是 design/<页面> + design/assets/)
|
|
50
|
+
for (const rel of Object.keys(zip.files)) {
|
|
51
|
+
if (zip.files[rel].dir || !/\.(html?|css)$/i.test(rel)) continue;
|
|
52
|
+
const depth = rel.split('/').length - 2; // design/ 之下还有几层
|
|
53
|
+
const up = '../'.repeat(Math.max(0, depth));
|
|
54
|
+
const text = await zip.files[rel].async('string');
|
|
55
|
+
zip.file(rel, text.replace(/(["'(])(?:\.\.\/)+assets\//g, `$1${up}assets/`));
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
try {
|
|
59
|
+
// deckPath 相对 sessionRoot(任务模型下是 tasks/<任务>/canvas.html)
|
|
60
|
+
const raw = await fs.readFile(path.resolve(sessionRoot, deckPath), 'utf8');
|
|
61
|
+
// zip 里的布局是 design/canvas.html + design/assets/…,而任务 deck 写的是
|
|
62
|
+
// `../../assets/generated/x.png`(相对它在 workspace 里的位置)—— 不改写的话
|
|
63
|
+
// 解压出来图全裂。统一压成 `assets/…`。
|
|
64
|
+
const html = raw.replace(/(["'(])(?:\.\.\/)+assets\//g, '$1assets/');
|
|
65
|
+
zip.file('design/canvas.html', html);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err.code !== 'ENOENT') throw err;
|
|
68
|
+
zip.file('design/canvas.html', '<!-- canvas.html not yet generated -->');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const spec = await fs.readFile(path.join(sessionRoot, 'spec.json'), 'utf8');
|
|
74
|
+
zip.file('design/spec.json', spec);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err.code !== 'ENOENT') throw err;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// assets 来自 shared/(跨 session 共享)—— 递归走子目录,
|
|
80
|
+
// 主要是 assets/generated/(generate_image MCP 落档处)必须进 zip,
|
|
81
|
+
// 否则 canvas.html 里的 <img src="assets/generated/..."> 在打开导出时全 404。
|
|
82
|
+
const assetsDir = path.join(sharedRoot, 'assets');
|
|
83
|
+
await zipDirRecursive(zip, assetsDir, 'design/assets');
|
|
84
|
+
|
|
85
|
+
const chatHistory = (runs || []).map((row) => ({ runId: row.id }));
|
|
86
|
+
zip.file('chat-history.json', JSON.stringify({ projectId, sessionId, runs: chatHistory }, null, 2));
|
|
87
|
+
|
|
88
|
+
zip.file('prompt.txt', '');
|
|
89
|
+
zip.file('README.md', renderReadme({ id: projectId, name: projectName, skillId, sessionId, kind }));
|
|
90
|
+
|
|
91
|
+
return zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 递归把 srcDir 下所有文件加进 zip(保留相对路径),dst 是 zip 内根前缀。
|
|
96
|
+
* srcDir 不存在时静默 noop(fail-soft)。子目录中的 dotfile / 软链按需可扩展。
|
|
97
|
+
*/
|
|
98
|
+
async function zipDirRecursive(zip, srcDir, dstPrefix, { skipDotfiles = false } = {}) {
|
|
99
|
+
let entries;
|
|
100
|
+
try {
|
|
101
|
+
entries = await fs.readdir(srcDir, { withFileTypes: true });
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (err.code === 'ENOENT') return;
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
for (const e of entries) {
|
|
107
|
+
if (skipDotfiles && e.name.startsWith('.')) continue;
|
|
108
|
+
const srcAbs = path.join(srcDir, e.name);
|
|
109
|
+
const dstRel = `${dstPrefix}/${e.name}`;
|
|
110
|
+
if (e.isDirectory()) {
|
|
111
|
+
await zipDirRecursive(zip, srcAbs, dstRel, { skipDotfiles });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!e.isFile()) continue; // 跳软链 / fifo 等
|
|
115
|
+
const buf = await fs.readFile(srcAbs);
|
|
116
|
+
zip.file(dstRel, buf);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderReadme(project) {
|
|
121
|
+
return `# ${project.name}
|
|
122
|
+
|
|
123
|
+
NoDesign 工程交付包。
|
|
124
|
+
|
|
125
|
+
## 文件结构
|
|
126
|
+
|
|
127
|
+
${project.kind === 'site' ? `- \`design/\` — 站点全部文件(保留原目录结构与文件名)
|
|
128
|
+
- \`design/${'index.html'}\` — 入口页
|
|
129
|
+
- \`design/assets/\` — 站点引用到的项目素材` : `- \`design/canvas.html\` — 单文件 self-contained HTML,主产物
|
|
130
|
+
- \`design/assets/\` — 项目共享素材`}
|
|
131
|
+
- \`design/spec.json\` — 设计意图档案(agent 私域记忆)
|
|
132
|
+
- \`chat-history.json\` — runs 摘要
|
|
133
|
+
- \`prompt.txt\` — 占位
|
|
134
|
+
|
|
135
|
+
## 怎么用
|
|
136
|
+
|
|
137
|
+
${project.kind === 'site' ? `把 \`design/\` 整个目录当站点根目录发布(任何静态托管都行),或者直接双击
|
|
138
|
+
\`design/index.html\` 在本地浏览 —— 页面之间是相对链接,不依赖服务器。` : `直接在浏览器打开 \`design/canvas.html\` 看 deck。
|
|
139
|
+
导出 PDF:用浏览器打印(${'${DECK.width}'}×${'${DECK.height}'} 视口最佳)。`}
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
导出时间:${new Date().toISOString()}
|
|
143
|
+
项目 ID:${project.id}
|
|
144
|
+
Session ID:${project.sessionId}
|
|
145
|
+
Skill:${project.skillId}
|
|
146
|
+
`;
|
|
147
|
+
}
|