@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,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/site-publish.js — 站点发布核心(2026-08-02)
|
|
3
|
+
*
|
|
4
|
+
* HTTP 路由(api/publish.js)和 agent 的 MCP 工具(publish_site)共用这一套:
|
|
5
|
+
* 闸门(试用号 403 / 每人 2 个)、staging、wrangler deploy、custom domain、记账。
|
|
6
|
+
* agent 走的是**项目 owner** 的额度与权限 —— 谁的项目算谁的,不看是谁触发。
|
|
7
|
+
*
|
|
8
|
+
* # custom domain(<slug>.share.xiaobuyu.trade 这一层)
|
|
9
|
+
* 配置驱动,两个 env 都在才启用,缺一个就退回 <cf_project>.pages.dev:
|
|
10
|
+
* NODESIGN_PUBLISH_DOMAIN 如 share.xiaobuyu.trade(发布域后缀)
|
|
11
|
+
* CLOUDFLARE_API_TOKEN scope:Account.Pages:Edit + Zone.DNS:Edit + Zone:Read
|
|
12
|
+
* 探针实测(2026-08-02):Pages 给四级域名单独排证书(certificate_authority:
|
|
13
|
+
* google),Universal SSL 只盖一级泛域这件事由 Pages custom domain 流程自己解决;
|
|
14
|
+
* wrangler 的 OAuth 挂 domain 够用但建 DNS 记录 403 —— 所以要独立 token。
|
|
15
|
+
* 域名步骤失败不拉倒整次发布:deploy 已成功就返 pages.dev 地址 + warning。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { promises as fs } from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import os from 'os';
|
|
21
|
+
import { can, DENIAL } from '../auth/tier.js';
|
|
22
|
+
import { execFile } from 'child_process';
|
|
23
|
+
import { promisify } from 'util';
|
|
24
|
+
import { getSharedDir } from '../projects/workspace.js';
|
|
25
|
+
import { taskManifest } from './kinds/index.js';
|
|
26
|
+
import { walkTaskFiles, loadIgnore } from './task-scan.js';
|
|
27
|
+
import { resolveBinary } from '../runtime/capabilities.js';
|
|
28
|
+
import {
|
|
29
|
+
getPublished, upsertPublished, removePublished,
|
|
30
|
+
countPublishedByUser, cfProjectNameFor, getByCustomDomain, slugify,
|
|
31
|
+
} from './publish-store.js';
|
|
32
|
+
|
|
33
|
+
const execFileAsync = promisify(execFile);
|
|
34
|
+
const DEPLOY_TIMEOUT_MS = 180_000;
|
|
35
|
+
|
|
36
|
+
export function publishLimit() {
|
|
37
|
+
const v = Number(process.env.NODESIGN_USER_PUBLISH_LIMIT);
|
|
38
|
+
return Number.isFinite(v) && v >= 0 ? v : 2;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validTaskName(name) {
|
|
42
|
+
// `.` 是工作区根上那个站的 key(扁平化后最常见的一种),单独放行;
|
|
43
|
+
// 其余仍然是单层目录名。
|
|
44
|
+
if (name === '.') return true;
|
|
45
|
+
return typeof name === 'string' && name.length > 0
|
|
46
|
+
&& !name.includes('/') && !name.includes('..') && !name.startsWith('.');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fail(status, message) {
|
|
50
|
+
return Object.assign(new Error(message), { status });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** wrangler 跟着服务进程的 node 走(nvm 全局包和 node 同目录),pm2 的 PATH 不可靠 */
|
|
54
|
+
function wranglerBin() {
|
|
55
|
+
// 启动探测(runtime/capabilities.js 'publish')找到的优先(PATH 或 node 同目录);没探到仍按老规矩猜 node 同目录
|
|
56
|
+
return resolveBinary('publish', path.join(path.dirname(process.execPath), 'wrangler'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function runWrangler(args) {
|
|
60
|
+
return execFileAsync(wranglerBin(), args, {
|
|
61
|
+
timeout: DEPLOY_TIMEOUT_MS,
|
|
62
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
63
|
+
env: { ...process.env, CI: '1', WRANGLER_SEND_METRICS: 'false' },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 找要发布的目录站点产物(单页试作不可发布)。
|
|
69
|
+
*
|
|
70
|
+
* `key` 扁平化之后是**站点的产物根**:`.` = 工作区根上那个站,`v2` 之类 =
|
|
71
|
+
* 子目录站。发布记录表里的存量数据存的还是旧任务名,那些记录仍然能下线
|
|
72
|
+
* (unpublish 走存下来的 host,不需要重新寻址),但重发布会认不出来 ——
|
|
73
|
+
* 这是可接受的:重发布本来就要用户在界面上点一下当前那个站。
|
|
74
|
+
*
|
|
75
|
+
* root 消歧(main 39ca348 合入,语义照搬):多个平行站点时不猜。
|
|
76
|
+
* - 显式传 root('.' 表示工作区根)→ 精确匹配,匹配不上 400 并列出现有的
|
|
77
|
+
* - 没传 root:key 匹配 || 只有一个站兜底(旧发布记录兼容)|| 409 列候选
|
|
78
|
+
*/
|
|
79
|
+
async function resolveSiteRoot(pid, key, root) {
|
|
80
|
+
const workspaceRoot = getSharedDir(pid);
|
|
81
|
+
try { await fs.access(workspaceRoot); } catch { return null; }
|
|
82
|
+
const manifest = await taskManifest(workspaceRoot);
|
|
83
|
+
const sites = (manifest?.artifacts || []).filter(a => a.kind === 'site' && !a.single);
|
|
84
|
+
if (!sites.length) return null;
|
|
85
|
+
const label = (a) => a.root || '.';
|
|
86
|
+
let inst;
|
|
87
|
+
if (typeof root === 'string' && root !== '') {
|
|
88
|
+
const want = root === '.' ? '' : root.replace(/\/+$/, '');
|
|
89
|
+
inst = sites.find(a => (a.root || '') === want);
|
|
90
|
+
if (!inst) {
|
|
91
|
+
throw fail(400, `没有 root 为「${root}」的站点,现有:${sites.map(label).join('、')}`);
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
const wanted = key === '.' || key === '' || key == null ? '' : String(key).replace(/\/+$/, '');
|
|
95
|
+
inst = sites.find(a => (a.root || '') === wanted)
|
|
96
|
+
|| (sites.length === 1 ? sites[0] : null);
|
|
97
|
+
if (!inst) {
|
|
98
|
+
throw fail(409, `工作区里有 ${sites.length} 个平行站点:${sites.map(label).join('、')}`
|
|
99
|
+
+ ' —— 用 root 参数指定要发布哪个(工作区根传 "."),不能替你猜');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
taskDir: workspaceRoot,
|
|
104
|
+
root: inst.root || '',
|
|
105
|
+
rootAbs: inst.root ? path.join(workspaceRoot, inst.root) : workspaceRoot,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** staging:与整站 zip 同语义(产物根 + .ndignore + assets 副本 + 相对路径改写) */
|
|
110
|
+
async function stageSite(pid, { taskDir, root, rootAbs }) {
|
|
111
|
+
const stage = await fs.mkdtemp(path.join(os.tmpdir(), 'nd-publish-'));
|
|
112
|
+
const ignore = await loadIgnore(taskDir);
|
|
113
|
+
const files = await walkTaskFiles(rootAbs, { maxDepth: 6, ignore, ignoreBase: taskDir });
|
|
114
|
+
let staged = 0;
|
|
115
|
+
for (const f of files) {
|
|
116
|
+
if (!root && f.rel === 'canvas.html') continue; // 根站排 deck 保留名
|
|
117
|
+
if (f.rel === '.nd-project.json') continue;
|
|
118
|
+
const dest = path.join(stage, f.rel);
|
|
119
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
120
|
+
if (/\.(html?|css)$/i.test(f.rel)) {
|
|
121
|
+
const depth = f.rel.split('/').length - 1;
|
|
122
|
+
const up = '../'.repeat(depth);
|
|
123
|
+
const text = await fs.readFile(f.abs, 'utf8');
|
|
124
|
+
await fs.writeFile(dest, text.replace(/(["'(])(?:\.\.\/)+assets\//g, `$1${up}assets/`));
|
|
125
|
+
} else {
|
|
126
|
+
await fs.copyFile(f.abs, dest);
|
|
127
|
+
}
|
|
128
|
+
staged += 1;
|
|
129
|
+
}
|
|
130
|
+
if (!staged) throw fail(400, '站点没有可发布的文件');
|
|
131
|
+
// 入口:Pages 认 index.html;没有的话把第一个 html 报出来(也是"发了什么"的一部分)
|
|
132
|
+
const entry = files.some(f => f.rel === 'index.html')
|
|
133
|
+
? 'index.html'
|
|
134
|
+
: (files.find(f => /\.html?$/i.test(f.rel))?.rel || null);
|
|
135
|
+
const assetsDir = path.join(getSharedDir(pid), 'assets');
|
|
136
|
+
try {
|
|
137
|
+
await fs.cp(assetsDir, path.join(stage, 'assets'), { recursive: true, force: true });
|
|
138
|
+
} catch { /* 没有素材目录就不带 */ }
|
|
139
|
+
// 兜底 404(2026-08-07):Pages 没有 404.html 时按 SPA 处理,任意路径都回退
|
|
140
|
+
// index.html 返 200 —— 断链和发错站点完全隐形(agent 报障的另一半病根)。
|
|
141
|
+
// NoDesign 的站点是静态多页,不需要 SPA 回退;站点自带 404.html 则不动。
|
|
142
|
+
try {
|
|
143
|
+
await fs.access(path.join(stage, '404.html'));
|
|
144
|
+
} catch {
|
|
145
|
+
await fs.writeFile(path.join(stage, '404.html'),
|
|
146
|
+
'<!doctype html><meta charset="utf-8"><title>404</title>'
|
|
147
|
+
+ '<style>body{font-family:system-ui;display:flex;min-height:100vh;margin:0;'
|
|
148
|
+
+ 'align-items:center;justify-content:center;color:#5F5142;background:#F5F0E4}</style>'
|
|
149
|
+
+ '<p>404 · 这个地址下没有页面</p>');
|
|
150
|
+
}
|
|
151
|
+
return { dir: stage, count: staged, entry };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Cloudflare API(custom domain 专用;deploy 本身走 wrangler)──
|
|
155
|
+
|
|
156
|
+
function domainConfig() {
|
|
157
|
+
const suffix = process.env.NODESIGN_PUBLISH_DOMAIN;
|
|
158
|
+
const token = process.env.CLOUDFLARE_API_TOKEN;
|
|
159
|
+
if (!suffix || !token) return null;
|
|
160
|
+
return {
|
|
161
|
+
suffix,
|
|
162
|
+
token,
|
|
163
|
+
accountId: process.env.NODESIGN_CF_ACCOUNT_ID || 'd49a09c02b1c8f62936fb1db7417b7ca',
|
|
164
|
+
apex: suffix.split('.').slice(-2).join('.'),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function cfApi(cfg, method, urlPath, body) {
|
|
169
|
+
const res = await fetch(`https://api.cloudflare.com/client/v4${urlPath}`, {
|
|
170
|
+
method,
|
|
171
|
+
headers: { Authorization: `Bearer ${cfg.token}`, 'Content-Type': 'application/json' },
|
|
172
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
173
|
+
});
|
|
174
|
+
const data = await res.json().catch(() => ({}));
|
|
175
|
+
if (!data.success) {
|
|
176
|
+
const msg = (data.errors || []).map(e => e.message).join('; ') || `HTTP ${res.status}`;
|
|
177
|
+
throw Object.assign(new Error(msg), { cfErrors: data.errors });
|
|
178
|
+
}
|
|
179
|
+
return data.result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let zoneIdCache = null;
|
|
183
|
+
async function zoneId(cfg) {
|
|
184
|
+
if (zoneIdCache) return zoneIdCache;
|
|
185
|
+
const zones = await cfApi(cfg, 'GET', `/zones?name=${cfg.apex}`);
|
|
186
|
+
zoneIdCache = zones?.[0]?.id || null;
|
|
187
|
+
if (!zoneIdCache) throw new Error(`zone ${cfg.apex} not found`);
|
|
188
|
+
return zoneIdCache;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 域名 slug 与 Pages 项目名 slug 用**同一份** slugify(publish-store.js)——
|
|
192
|
+
// 这两处以前是两份正则、两个截断长度(32 / 24),典型的一件事两份实现。
|
|
193
|
+
function hostSlug(name) {
|
|
194
|
+
return slugify(name, 32);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 用户/agent 自选的域名 slug:小写字母开头、只含小写字母数字连字符、3-32 位 */
|
|
198
|
+
export const SLUG_RE = /^[a-z][a-z0-9-]{1,30}[a-z0-9]$/;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 给已 deploy 的站点挂 <host>.<suffix>:Pages custom domain + 代理 CNAME。
|
|
202
|
+
* host 首选任务名 slug(中文任务名 slug 化常剩空串,退回 hash);被别的站占了
|
|
203
|
+
* 就带 hash 后缀。幂等:published 记录里已有 custom_domain 就只补挂不换名。
|
|
204
|
+
*/
|
|
205
|
+
async function ensureCustomDomain({ pid, storeKey, cfProject, existingDomain, slug: wantSlug }) {
|
|
206
|
+
const cfg = domainConfig();
|
|
207
|
+
if (!cfg) return null;
|
|
208
|
+
const hash = cfProject.slice(-6);
|
|
209
|
+
let host;
|
|
210
|
+
if (wantSlug) {
|
|
211
|
+
// 显式点名的域名:撞车就明确报错让人换,**不静默加后缀** —— 用户说"发成 rin"
|
|
212
|
+
// 结果拿到 rin-a1b2c3 是最难受的那种"成功"
|
|
213
|
+
const bare = `${wantSlug}.${cfg.suffix}`;
|
|
214
|
+
const taken = getByCustomDomain(bare);
|
|
215
|
+
if (taken && !(taken.projectId === pid && taken.task === storeKey)) {
|
|
216
|
+
throw fail(409, `域名 ${bare} 已经被别的站点占了,换一个 slug`);
|
|
217
|
+
}
|
|
218
|
+
host = bare;
|
|
219
|
+
} else if (existingDomain) {
|
|
220
|
+
host = existingDomain;
|
|
221
|
+
} else {
|
|
222
|
+
const slug = hostSlug(storeKey === '.' ? '' : storeKey);
|
|
223
|
+
const bare = slug ? `${slug}.${cfg.suffix}` : `${hash}.${cfg.suffix}`;
|
|
224
|
+
const taken = getByCustomDomain(bare);
|
|
225
|
+
host = (taken && !(taken.projectId === pid && taken.task === storeKey))
|
|
226
|
+
? `${slug ? `${slug}-` : ''}${hash}.${cfg.suffix}`
|
|
227
|
+
: bare;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ① Pages 项目挂域名(已挂过会报错 —— 幂等吞掉)
|
|
231
|
+
try {
|
|
232
|
+
await cfApi(cfg, 'POST', `/accounts/${cfg.accountId}/pages/projects/${cfProject}/domains`, { name: host });
|
|
233
|
+
} catch (err) {
|
|
234
|
+
if (!/already|exists|8000015/i.test(err.message)) throw err;
|
|
235
|
+
}
|
|
236
|
+
// ② 代理 CNAME → <cfProject>.pages.dev(已存在同名记录就不重建)
|
|
237
|
+
const zid = await zoneId(cfg);
|
|
238
|
+
const existing = await cfApi(cfg, 'GET', `/zones/${zid}/dns_records?type=CNAME&name=${host}`);
|
|
239
|
+
if (!existing?.length) {
|
|
240
|
+
await cfApi(cfg, 'POST', `/zones/${zid}/dns_records`, {
|
|
241
|
+
type: 'CNAME', name: host, content: `${cfProject}.pages.dev`, proxied: true,
|
|
242
|
+
comment: 'nodesign publish',
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ③ 换域名:先挂新的、确认挂上了,**再摘旧的**(2026-08-18)。
|
|
247
|
+
// 只挂不摘的话旧域名会变成孤儿 —— CF 上还挂着、DNS 记录还在、还能访问,
|
|
248
|
+
// 而我们的账上已经不认它了。次序不能反:先摘旧的万一新的挂失败,站就没入口了。
|
|
249
|
+
if (existingDomain && existingDomain !== host) {
|
|
250
|
+
try {
|
|
251
|
+
await cfApi(cfg, 'DELETE',
|
|
252
|
+
`/accounts/${cfg.accountId}/pages/projects/${cfProject}/domains/${existingDomain}`);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
if (!/not found|does not exist/i.test(err.message)) throw err;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
await removeCustomDomainDns({ customDomain: existingDomain });
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.warn('[publish] 旧域名 DNS 没清干净(新域名已生效):', err.message);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return host;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 等证书就位(2026-08-18)。
|
|
267
|
+
*
|
|
268
|
+
* 四级域名(`x.share.example.com`)Pages 会单独排一张证书,签发之前浏览器和
|
|
269
|
+
* playwright 拿到的是 `ERR_SSL_VERSION_OR_CIPHER_MISMATCH` —— 发布完立刻自检
|
|
270
|
+
* 必然撞上,问题库里两个不同 agent 各撞了一次,而以前这里连响应都不看
|
|
271
|
+
* (`await cfApi(...)` 返回值整个丢掉),只能靠文案里写死一句"等一两分钟"。
|
|
272
|
+
*
|
|
273
|
+
* 2026-08-18 实测形状(真跑 chenxi.share.xiaobuyu.trade 抓的):
|
|
274
|
+
* { status: 'initializing' → 'pending' → 'active',
|
|
275
|
+
* validation_data: { status: 'initializing' → 'pending' → 'active', method: 'http' },
|
|
276
|
+
* verification_data: { status: 'pending' → 'active' },
|
|
277
|
+
* certificate_authority: 'google' }
|
|
278
|
+
* 全程约 **2.5 分钟**(所以预算给 150s,不是拍脑袋的 45s)。重新发布沿用旧域名时
|
|
279
|
+
* 第一次查就是 active,不会白等。
|
|
280
|
+
* ⚠️ 判据只认 active:认不出的形状一律当"没就绪",绝不把"我看不懂"说成"已就绪"。
|
|
281
|
+
*
|
|
282
|
+
* @returns {Promise<boolean>} true = 确认可访问;false = 超时/形状不认识
|
|
283
|
+
*/
|
|
284
|
+
async function waitForCertificate(cfg, cfProject, host, { budgetMs = 150_000, everyMs = 10_000 } = {}) {
|
|
285
|
+
const deadline = Date.now() + budgetMs;
|
|
286
|
+
let loggedShape = false;
|
|
287
|
+
while (Date.now() < deadline) {
|
|
288
|
+
try {
|
|
289
|
+
const info = await cfApi(cfg, 'GET',
|
|
290
|
+
`/accounts/${cfg.accountId}/pages/projects/${cfProject}/domains/${host}`);
|
|
291
|
+
if (!loggedShape) {
|
|
292
|
+
loggedShape = true;
|
|
293
|
+
console.log('[publish] domain status shape:', JSON.stringify(info)?.slice(0, 400));
|
|
294
|
+
}
|
|
295
|
+
const status = String(info?.status || info?.certificate_authority_status || '').toLowerCase();
|
|
296
|
+
const vs = String(info?.validation_data?.status || '').toLowerCase();
|
|
297
|
+
if (status === 'active' && (!vs || vs === 'active' || vs === 'success')) return true;
|
|
298
|
+
} catch { /* 查不到就当还没好,继续等 */ }
|
|
299
|
+
await new Promise(r => setTimeout(r, everyMs));
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** 从 Pages 项目上摘域名。**必须在删项目之前**:挂着 custom domain 的项目
|
|
305
|
+
* CF 拒删(code 8000028,2026-08-02 实测踩的)。 */
|
|
306
|
+
async function detachCustomDomain(site) {
|
|
307
|
+
const cfg = domainConfig();
|
|
308
|
+
if (!cfg || !site.customDomain) return;
|
|
309
|
+
try {
|
|
310
|
+
await cfApi(cfg, 'DELETE',
|
|
311
|
+
`/accounts/${cfg.accountId}/pages/projects/${site.cfProject}/domains/${site.customDomain}`);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
if (!/not found|does not exist/i.test(err.message)) throw err;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function removeCustomDomainDns(site) {
|
|
318
|
+
const cfg = domainConfig();
|
|
319
|
+
if (!cfg || !site.customDomain) return;
|
|
320
|
+
const zid = await zoneId(cfg);
|
|
321
|
+
const records = await cfApi(cfg, 'GET', `/zones/${zid}/dns_records?type=CNAME&name=${site.customDomain}`);
|
|
322
|
+
for (const r of records || []) {
|
|
323
|
+
await cfApi(cfg, 'DELETE', `/zones/${zid}/dns_records/${r.id}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── 主流程 ──
|
|
328
|
+
|
|
329
|
+
// 同一站点同时只允许一个 deploy 在飞(双击/双开/agent 与人撞车防抖)
|
|
330
|
+
const inFlight = new Set();
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* @param {object} p
|
|
334
|
+
* @param {string} p.projectId
|
|
335
|
+
* @param {string} p.task
|
|
336
|
+
* @param {string} [p.root] 多站点任务点名要发哪个('.' = 任务根);单站点可省
|
|
337
|
+
* @param {object} p.user 额度与权限按这个用户算(HTTP = 请求者,MCP = 项目 owner)
|
|
338
|
+
* @returns {{ site, warning: string|null }}
|
|
339
|
+
*/
|
|
340
|
+
export async function publishSite({ projectId, task, root, slug, user }) {
|
|
341
|
+
if (!validTaskName(task)) throw fail(400, 'invalid task');
|
|
342
|
+
if (slug != null && slug !== '' && !SLUG_RE.test(String(slug))) {
|
|
343
|
+
throw fail(400, `slug 只能是小写字母开头的 3-32 位小写字母/数字/连字符,收到的是「${slug}」`);
|
|
344
|
+
}
|
|
345
|
+
// 档位闸(auth/tier.js):basic 档(公开注册)不开发布;无主(user 为空)fail-closed。
|
|
346
|
+
// 08-21 前这里猜的是 lifetimeCostLimitUsd(试用码),开放注册号那字段是 null 被放行了。
|
|
347
|
+
if (!can(user, 'publishSite')) throw fail(403, DENIAL.publishSite);
|
|
348
|
+
|
|
349
|
+
// ⭐ 2026-08-18 身份收敛:**先寻址,再定 key**。
|
|
350
|
+
// 以前 key 直接用调用方传的 `task`,而 task 在扁平化之后既不参与寻址、也没有
|
|
351
|
+
// 任何校验 —— 于是 agent 把项目 ID 填进去也一路绿灯,落库落出一行
|
|
352
|
+
// task='proj_xxx' 的记录,域名跟着变成 proj-xxx.share.…,而界面按站点根去查
|
|
353
|
+
// 又查不到这一行(显示"未上线",再点一次就会造出第二个 CF 项目 + 第二个域名)。
|
|
354
|
+
// 收敛之后 key 只有一个来源:**站点的产物根**。task 降级成寻址提示。
|
|
355
|
+
const resolved = await resolveSiteRoot(projectId, task, root);
|
|
356
|
+
if (!resolved) throw fail(400, '这个项目里没有可发布的目录站点');
|
|
357
|
+
const storeKey = resolved.root || '.';
|
|
358
|
+
|
|
359
|
+
// slug 撞车要**在部署之前**查出来。ensureCustomDomain 里那道检查跑在 deploy
|
|
360
|
+
// 之后,报错时用户已经白等了一整次 wrangler 部署(59s 起)。那道留着当兜底,
|
|
361
|
+
// 这道负责早失败。
|
|
362
|
+
if (slug) {
|
|
363
|
+
const cfg = domainConfig();
|
|
364
|
+
if (cfg) {
|
|
365
|
+
const want = `${slug}.${cfg.suffix}`;
|
|
366
|
+
const taken = getByCustomDomain(want);
|
|
367
|
+
if (taken && !(taken.projectId === projectId && taken.task === storeKey)) {
|
|
368
|
+
throw fail(409, `域名 ${want} 已经被别的站点占了,换一个 slug`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const existing = getPublished(projectId, storeKey);
|
|
374
|
+
if (!existing && user?.role !== 'admin') {
|
|
375
|
+
const used = countPublishedByUser(user?.id);
|
|
376
|
+
if (used >= publishLimit()) {
|
|
377
|
+
throw fail(403, `你已发布 ${used} 个站点(上限 ${publishLimit()}),先下线一个再发`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const key = `${projectId}/${storeKey}`;
|
|
382
|
+
if (inFlight.has(key)) throw fail(409, '这个站点正在发布中,稍等');
|
|
383
|
+
inFlight.add(key);
|
|
384
|
+
let stage = null;
|
|
385
|
+
try {
|
|
386
|
+
const staged = await stageSite(projectId, resolved);
|
|
387
|
+
stage = staged.dir;
|
|
388
|
+
const name = existing?.cfProject || cfProjectNameFor(projectId, storeKey);
|
|
389
|
+
try {
|
|
390
|
+
await runWrangler(['pages', 'project', 'create', name, '--production-branch', 'main']);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
const msg = `${err.stdout || ''}${err.stderr || ''}${err.message || ''}`;
|
|
393
|
+
if (!/already exists|8000007/i.test(msg)) throw err;
|
|
394
|
+
}
|
|
395
|
+
await runWrangler(['pages', 'deploy', stage, '--project-name', name, '--branch', 'main', '--commit-dirty=true']);
|
|
396
|
+
|
|
397
|
+
let customDomain = existing?.customDomain || null;
|
|
398
|
+
let warning = null;
|
|
399
|
+
let certReady = null;
|
|
400
|
+
try {
|
|
401
|
+
customDomain = await ensureCustomDomain({
|
|
402
|
+
pid: projectId, storeKey, cfProject: name, existingDomain: customDomain, slug,
|
|
403
|
+
});
|
|
404
|
+
if (customDomain) {
|
|
405
|
+
const cfg = domainConfig();
|
|
406
|
+
certReady = cfg ? await waitForCertificate(cfg, name, customDomain) : null;
|
|
407
|
+
}
|
|
408
|
+
} catch (err) {
|
|
409
|
+
if (err.status === 409) throw err; // slug 撞车是调用方要处理的,不能吞
|
|
410
|
+
console.warn('[publish] custom domain failed (deploy 本身成功):', err.message);
|
|
411
|
+
warning = '专属域名没挂上,先用 pages.dev 地址;重新发布会再试';
|
|
412
|
+
}
|
|
413
|
+
const site = upsertPublished({
|
|
414
|
+
projectId, task: storeKey, userId: user?.id ?? null,
|
|
415
|
+
cfProject: name,
|
|
416
|
+
url: customDomain ? `https://${customDomain}` : `https://${name}.pages.dev`,
|
|
417
|
+
customDomain,
|
|
418
|
+
});
|
|
419
|
+
// root / 文件数 / 入口以前都算出来就丢了 —— agent 拿不到"我到底发了哪个站",
|
|
420
|
+
// 只能 curl 反推(问题库 iss_msc44jfb_bqh6 第 3 条诉求)
|
|
421
|
+
return {
|
|
422
|
+
site, warning, certReady,
|
|
423
|
+
root: storeKey, files: staged.count, entry: staged.entry,
|
|
424
|
+
};
|
|
425
|
+
} finally {
|
|
426
|
+
inFlight.delete(key);
|
|
427
|
+
if (stage) fs.rm(stage, { recursive: true, force: true }).catch(() => { /* 临时目录清理失败不致命 */ });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* 按 key 找已发布记录。**下线路径不重新寻址** —— 站点文件可能已经被删了,
|
|
433
|
+
* 但线上那份还挂着,必须还能摘掉。所以先直查,查不到再试寻址出来的 key
|
|
434
|
+
* (收敛前落的老记录、或者 agent 传了个别名)。
|
|
435
|
+
*/
|
|
436
|
+
export async function lookupPublished(projectId, key) {
|
|
437
|
+
const direct = getPublished(projectId, key);
|
|
438
|
+
if (direct) return direct;
|
|
439
|
+
try {
|
|
440
|
+
const resolved = await resolveSiteRoot(projectId, key, undefined);
|
|
441
|
+
if (resolved) return getPublished(projectId, resolved.root || '.');
|
|
442
|
+
} catch { /* 寻址不出来就是没有 */ }
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** @returns {boolean} false = 本来就没发布 */
|
|
447
|
+
export async function unpublishSite({ projectId, task }) {
|
|
448
|
+
if (!validTaskName(task)) throw fail(400, 'invalid task');
|
|
449
|
+
const site = await lookupPublished(projectId, task);
|
|
450
|
+
if (!site) return false;
|
|
451
|
+
await detachCustomDomain(site); // 摘域名必须先于删项目(8000028)
|
|
452
|
+
try {
|
|
453
|
+
await runWrangler(['pages', 'project', 'delete', site.cfProject, '--yes']);
|
|
454
|
+
} catch (err) {
|
|
455
|
+
const msg = `${err.stdout || ''}${err.stderr || ''}${err.message || ''}`;
|
|
456
|
+
if (!/not found|does not exist|8000007/i.test(msg)) throw err;
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
await removeCustomDomainDns(site);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
console.warn('[publish] DNS 清理失败(Pages 项目已删,入口已死):', err.message);
|
|
462
|
+
}
|
|
463
|
+
removePublished(projectId, site.task); // 按记录里的 key 删,不按调用方传的
|
|
464
|
+
return true;
|
|
465
|
+
}
|