@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,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp/tools/pin_to_board — 把一件东西摆到用户画布上的某个位置。
|
|
3
|
+
*
|
|
4
|
+
* 写 board.json(board-store 单锁原子操作,与前端 PATCH 互不覆盖),然后广播
|
|
5
|
+
* board.updated(sessionId: null → project 全连接都收到,前端整份重拉)。
|
|
6
|
+
*
|
|
7
|
+
* 物件 id 约定(与前端 BoardCanvas 派生一致):**id = kind 前缀 + 工作区相对路径**
|
|
8
|
+
* - 普通文件(图片 / 便签 / 数据):路径本身,`assets/generated/a.webp`
|
|
9
|
+
* - deck:`deck:<路径>`,如 `deck:稿件/主稿.html`
|
|
10
|
+
* - 站点:`site:<目录>`
|
|
11
|
+
* - 项目文档:`doc:_root`(记忆)/ `doc:brand`(品牌档案)
|
|
12
|
+
*
|
|
13
|
+
* ## 2026-08-13:这个工具的职权范围缩小了
|
|
14
|
+
*
|
|
15
|
+
* 以前它有个 `zone` 参数,语义是"放进哪块工作区"。**在 id = 路径的模型下这件事
|
|
16
|
+
* 不成立** —— 一个物件属于哪个文件夹,答案就写在它的路径里。让工具单独写一个
|
|
17
|
+
* "显式归属"字段,等于允许画布说"它在 A 文件夹"而磁盘说"它在 B 文件夹",
|
|
18
|
+
* 正是 2026-07-28 把「拖出工作区解绑」停用掉的那个理由(两边对不上,且很容易
|
|
19
|
+
* 误触)。要换文件夹就 `mv` —— 那是真的搬,画布跟着走。
|
|
20
|
+
*
|
|
21
|
+
* 所以现在它只剩一件事:**给这件东西一个位置并置顶**("把它摆到用户眼前")。
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ 顺带修掉的两个僵尸:`zone` 原来被校验成 `/^[A-Za-z0-9-]{8,64}$/`(= 一个
|
|
24
|
+
* sessionId),于是任何中文文件夹名、任何带斜杠的嵌套路径都过不了,一律静默
|
|
25
|
+
* 回落到 sessionId —— 然后在 board.json 里新建一块根本不存在的"工作区"。
|
|
26
|
+
* agent 每帮一次忙就制造一条死数据。
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
import path from 'path';
|
|
32
|
+
import { promises as fs } from 'fs';
|
|
33
|
+
import { pinToZone } from '../../../projects/board-store.js';
|
|
34
|
+
|
|
35
|
+
/** 物件 id → 它住在哪个文件夹(与前端 stage.js 的 zoneOfObjectId 同一套规则) */
|
|
36
|
+
function folderOfObjectId(objectId) {
|
|
37
|
+
if (objectId.startsWith('doc:')) return '';
|
|
38
|
+
const c = objectId.indexOf(':');
|
|
39
|
+
const p = (c > 0 && /^[a-z]+$/.test(objectId.slice(0, c))) ? objectId.slice(c + 1) : objectId;
|
|
40
|
+
const i = p.lastIndexOf('/');
|
|
41
|
+
return i > 0 ? p.slice(0, i) : '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {object} deps
|
|
46
|
+
* @param {string} [deps.sharedRoot] 工作区根(校验文件存在用)
|
|
47
|
+
* @param {string} [deps.projectId]
|
|
48
|
+
* @param {import('../../agent/context.js').AgentContext} [deps.ctx]
|
|
49
|
+
*/
|
|
50
|
+
export function makePinToBoardTool({ sharedRoot, projectId, ctx }) {
|
|
51
|
+
return tool(
|
|
52
|
+
'pin_to_board',
|
|
53
|
+
`Bring an item to the front of the user's canvas, at a free spot in whatever
|
|
54
|
+
folder it lives in. The canvas is their desktop: whatever you write appears
|
|
55
|
+
there automatically — you do NOT need this tool for your own outputs.
|
|
56
|
+
Use it only to deliberately surface something:
|
|
57
|
+
|
|
58
|
+
- Pull a reference (an uploaded asset, the brand doc, an older image) into view
|
|
59
|
+
- Restore something the user dragged off-screen, when they ask for it back
|
|
60
|
+
|
|
61
|
+
This does NOT change which folder the item belongs to — that is decided by
|
|
62
|
+
where the file is on disk. To move it, \`mv\` the file; the canvas follows.
|
|
63
|
+
|
|
64
|
+
Paths are workspace-relative, exactly as they are on disk. Accepted forms:
|
|
65
|
+
- any file path: 'assets/generated/hero.webp', 'notes/灵感.md', '稿件/数据.csv'
|
|
66
|
+
- a deck: 'deck:<path>.html' a site: 'site:<dir>'
|
|
67
|
+
(a bare '<path>.html' is read as a deck)
|
|
68
|
+
- '.claude/agent-memory/memory.md' (project memory) / '.../brand/memory.md' (brand doc)`,
|
|
69
|
+
{
|
|
70
|
+
path: z
|
|
71
|
+
.string()
|
|
72
|
+
.min(1)
|
|
73
|
+
.max(300)
|
|
74
|
+
.describe('Item to surface — see accepted forms in the tool description'),
|
|
75
|
+
},
|
|
76
|
+
async ({ path: rawPath }) => {
|
|
77
|
+
try {
|
|
78
|
+
if (!projectId) {
|
|
79
|
+
return { content: [{ type: 'text', text: 'No project bound; cannot pin.' }], isError: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 归一化成前端物件 id(id = kind 前缀 + 工作区相对路径)
|
|
83
|
+
let objectId = String(rawPath).trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '');
|
|
84
|
+
if (!objectId || objectId.includes('..')) {
|
|
85
|
+
return { content: [{ type: 'text', text: 'Invalid path.' }], isError: true };
|
|
86
|
+
}
|
|
87
|
+
if (objectId.endsWith('agent-memory/brand/memory.md')) objectId = 'doc:brand';
|
|
88
|
+
else if (objectId.endsWith('agent-memory/memory.md')) objectId = 'doc:_root';
|
|
89
|
+
else if (!/^(deck|site|doc):/.test(objectId)) {
|
|
90
|
+
// 裸路径:.html 是一份 deck,其余(图片 / 便签 / 数据文件)id 就是路径。
|
|
91
|
+
// 站点的目录要显式写 `site:` —— 光看路径分不出
|
|
92
|
+
// "一个收纳文件夹"和"一件目录型产物",猜错了钉上去的是个不存在的 id。
|
|
93
|
+
if (sharedRoot) {
|
|
94
|
+
const abs = path.join(sharedRoot, objectId);
|
|
95
|
+
try { await fs.access(abs); } catch {
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: 'text', text: `File not found: ${objectId}. Paths are workspace-relative, same as on disk.` }],
|
|
98
|
+
isError: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (/\.html?$/i.test(objectId)) objectId = `deck:${objectId}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const zoneId = folderOfObjectId(objectId);
|
|
106
|
+
const { zone: placedZone, placed } = await pinToZone(projectId, { objectId, zoneId });
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
ctx?.emit?.({
|
|
110
|
+
type: 'board.updated',
|
|
111
|
+
sessionId: null, // project 级广播:显式压掉 ctx 的 sessionId enrich
|
|
112
|
+
objectId,
|
|
113
|
+
zoneId,
|
|
114
|
+
summary: zoneId ? `已把 ${objectId} 摆到「${zoneId}」里` : `已把 ${objectId} 摆到桌面上`,
|
|
115
|
+
});
|
|
116
|
+
} catch { /* emit fail-safe */ }
|
|
117
|
+
|
|
118
|
+
const where = placedZone?.id ? `in ${placedZone.id}` : 'on the desktop';
|
|
119
|
+
return {
|
|
120
|
+
content: [{
|
|
121
|
+
type: 'text',
|
|
122
|
+
text: `Surfaced ${objectId} ${where} at (${placed.x}, ${placed.y}). The user's canvas updates live.`,
|
|
123
|
+
}],
|
|
124
|
+
};
|
|
125
|
+
} catch (err) {
|
|
126
|
+
return { content: [{ type: 'text', text: `pin_to_board failed: ${err.message}` }], isError: true };
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp/tools/preview-deck.js — preview_deck MCP tool(2026-07-28)
|
|
3
|
+
*
|
|
4
|
+
* agent 主动把做好的 deck 摊到用户眼前:等价于用户在工作台上双击那张 deck 卡
|
|
5
|
+
* ——收起态就展开成内嵌渲染,已经展开就开成画布内的最大化窗口。
|
|
6
|
+
*
|
|
7
|
+
* 实现:纯 emit run.deck_preview 事件(跟 navigate_to_page 同款控制层套路),
|
|
8
|
+
* 路径归一在前端做(lib/stage.js 的 resolveObjectId 已经认得 tasks/<任务>/canvas.html
|
|
9
|
+
* 和会话 cwd 的 canvas.html 两种形态)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { Events } from '../../agent/events.js';
|
|
15
|
+
import { setActiveDeck } from '../../../lib/artifact-target.js';
|
|
16
|
+
import fs from 'node:fs/promises';
|
|
17
|
+
import nodePath from 'node:path';
|
|
18
|
+
import { taskManifest } from '../../../lib/kinds/index.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} deps
|
|
22
|
+
* @param {import('../../agent/context.js').AgentContext} [deps.ctx]
|
|
23
|
+
*/
|
|
24
|
+
export function makePreviewDeckTool({ ctx, sessionId, workspaceRoot }) {
|
|
25
|
+
return tool(
|
|
26
|
+
'preview_deck',
|
|
27
|
+
`Show a deck to the user on their workbench — the same thing that happens
|
|
28
|
+
when they double-click the deck card themselves: a collapsed card expands into
|
|
29
|
+
a live embedded preview, an already-expanded one opens as a maximized window
|
|
30
|
+
over the desktop.
|
|
31
|
+
|
|
32
|
+
Use this when:
|
|
33
|
+
- You just finished (or substantially changed) a deck and want the user to look
|
|
34
|
+
- The user asks "show me" / "let me see it"
|
|
35
|
+
- You're about to discuss a deck and want it on screen first
|
|
36
|
+
|
|
37
|
+
Do NOT use it to spam attention mid-work — one call when the thing is worth
|
|
38
|
+
looking at. This does not replace screenshot_canvas: that one is for **you** to
|
|
39
|
+
see the render, this one is for **the user**.`,
|
|
40
|
+
{
|
|
41
|
+
path: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe(
|
|
45
|
+
"Deck to show, workspace-relative (e.g. 'canvas.html' or '稿件/主稿.html'). Omit for the active artifact.",
|
|
46
|
+
),
|
|
47
|
+
},
|
|
48
|
+
async ({ path: rawPath }) => {
|
|
49
|
+
try {
|
|
50
|
+
const p = typeof rawPath === 'string' ? rawPath.trim().replace(/^\.\//, '') : '';
|
|
51
|
+
// 绝对路径同拒:这个 path 直发前端当寻址依据(home 幽灵族的家规)
|
|
52
|
+
if (p.includes('..') || p.startsWith('/')) {
|
|
53
|
+
return { content: [{ type: 'text', text: 'Invalid path (workspace-relative only).' }], isError: true };
|
|
54
|
+
}
|
|
55
|
+
// ⭐ 2026-08-18:先查这个文件在不在。
|
|
56
|
+
// 以前这里只挡 `..` 和绝对路径,然后无条件 emit + 回一句 "Opened …"。
|
|
57
|
+
// 传一个不存在的路径、或者传站点的子页时,用户桌面上打开的是**本会话的
|
|
58
|
+
// 空 canvas.html**,而 agent 拿到的是成功 —— 它没有任何办法察觉用户
|
|
59
|
+
// 正对着一块空白(问题库 iss_msr8oki7_z9su:「静默成功是最伤的部分」)。
|
|
60
|
+
let siteNote = null;
|
|
61
|
+
if (p) {
|
|
62
|
+
const abs = nodePath.join(workspaceRoot || '', p);
|
|
63
|
+
try {
|
|
64
|
+
await fs.access(abs);
|
|
65
|
+
} catch {
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: 'text', text:
|
|
68
|
+
`path not found: ${p} —— 先写出这个文件,或者用 list_pages 看清楚现有产物。`
|
|
69
|
+
+ '(没有打开任何东西,用户桌面没变。)' }],
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// 站点的子页:站点是整站一扇窗,打开的是站不是那一页 —— 如实说
|
|
74
|
+
try {
|
|
75
|
+
const manifest = await taskManifest(workspaceRoot);
|
|
76
|
+
const arts = manifest?.artifacts || [];
|
|
77
|
+
// ⛔ 原判据是 `(a.root ? p.startsWith(`${a.root}/`) : true)`。**根站的
|
|
78
|
+
// root 是空字符串**(site.js 里 `out.push({ srcRoot:'', root, ... })`),
|
|
79
|
+
// 于是三元退化成"任意路径都算这个站的一页"。后果是 `preview_deck("报告.docx")`
|
|
80
|
+
// 和 `preview_deck("_drafts/试作.html")` 都被回一句自信的假话
|
|
81
|
+
// 「还不在这个站的页面清单里…用户看到的是入口 index.html」——
|
|
82
|
+
// 两句都假:docx 开在 DocxWindow,草稿开在自己那扇 single:true 的窗。
|
|
83
|
+
// 改动本意是消灭"静默成功",结果换成了反向的自信错话。
|
|
84
|
+
//
|
|
85
|
+
// 先看有没有**别的产物正好就是这个路径**(单页站、docx、deck…)——
|
|
86
|
+
// 有就是它自己开,跟根站无关。
|
|
87
|
+
const ownedByOther = arts.some(a => a.entryRel === p || a.file === p
|
|
88
|
+
|| (a.single && (a.pages || []).includes(p)));
|
|
89
|
+
const site = ownedByOther ? null : arts.find((a) => {
|
|
90
|
+
if (a.kind !== 'site' || a.single) return false;
|
|
91
|
+
if (a.root) return p.startsWith(`${a.root}/`); // 目录即边界,前缀够用
|
|
92
|
+
// 根站没有目录边界可用,只能按"是不是一张网页"+页面清单判
|
|
93
|
+
if (!/\.html?$/i.test(p)) return false;
|
|
94
|
+
return true;
|
|
95
|
+
});
|
|
96
|
+
if (site && p !== (site.entryRel || 'index.html')) {
|
|
97
|
+
// 2026-08-18 起子页**真的**开在那一页上(前端 entry 一路传到 SiteWindow)。
|
|
98
|
+
// 但只有在产物清单认得这一页时才成立 —— 不在清单里(比如刚写出来还没
|
|
99
|
+
// 重拉)就还是退回入口,所以这句话要如实分两种说。
|
|
100
|
+
const known = (site.pages || []).includes(
|
|
101
|
+
site.root ? p.slice(site.root.length + 1) : p);
|
|
102
|
+
siteNote = known
|
|
103
|
+
? `打开的是站点「${site.root || '工作区根'}」并停在 ${p} 这一页(整站一扇窗,用户可以在窗里翻别的页)。`
|
|
104
|
+
: `注意:${p} 还不在这个站的页面清单里(产物列表下次重拉才认它),`
|
|
105
|
+
+ `所以用户看到的是入口 ${site.entryRel || 'index.html'}。`
|
|
106
|
+
+ '站内给它加一条真链接,或者等一轮再试。';
|
|
107
|
+
}
|
|
108
|
+
} catch { /* manifest 读不出来不挡预览 */ }
|
|
109
|
+
setActiveDeck(sessionId, p); // 摊给用户看的那份就是"当前 deck"
|
|
110
|
+
}
|
|
111
|
+
ctx?.emit?.(Events.deckPreview(p));
|
|
112
|
+
return {
|
|
113
|
+
content: [{
|
|
114
|
+
type: 'text',
|
|
115
|
+
text: [
|
|
116
|
+
p ? `Opened ${p} on the user's workbench.`
|
|
117
|
+
: "Opened this session's deck on the user's workbench.",
|
|
118
|
+
siteNote,
|
|
119
|
+
].filter(Boolean).join('\n'),
|
|
120
|
+
}],
|
|
121
|
+
};
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: 'text', text: `preview_deck failed: ${err?.message || String(err)}` }],
|
|
125
|
+
isError: true,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp/tools/profile-scroll.js — profile_scroll(2026-08-18)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么有这个工具
|
|
5
|
+
*
|
|
6
|
+
* 用户说"往下滑动一顿一顿的",这在设计工具里是最常见的反馈之一。而在这之前
|
|
7
|
+
* 平台给 agent 的全部观测能力是「截一张静态图」—— 帧时、掉帧、图片体积、
|
|
8
|
+
* 长任务一个都看不到。一个 agent 的实际绕路是:起 `python3 -m http.server`
|
|
9
|
+
* → 从仓库 node_modules 直接 import playwright → 手写 rAF 采样器 → 逐项关掉
|
|
10
|
+
* 嫌疑元素做 A/B。它量出来的结论一个都猜不出来:基线 63% 掉帧,单独关视差
|
|
11
|
+
* 64%(没用),单独关全屏胶片颗粒层 40%,两个都关 5% —— 是**叠加效应**,
|
|
12
|
+
* 真凶是「33.7MB 的 PNG + 一个带 mix-blend-mode 的全屏 fixed 层」这对组合。
|
|
13
|
+
* 没有测量,它大概率只会把用户新抱怨的那个功能拆掉交差,剩下 40% 的掉帧
|
|
14
|
+
* 原封不动留在站里。
|
|
15
|
+
*
|
|
16
|
+
* ## 数字怎么读
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ 这台机器 1 vCPU,而且截图 worker 跟 server 抢同一颗核。**绝对值不代表
|
|
19
|
+
* 用户设备上的体验**,它的用处是 A/B:改一处、再量一次、看方向。返回文本里
|
|
20
|
+
* 会重复这句话,别让 agent 拿绝对数字去跟用户汇报。
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
24
|
+
import { z } from 'zod';
|
|
25
|
+
import { resolveCanvasTarget, CANVAS_PATH_DESC, requireBrowsable } from '../../../lib/artifact-target.js';
|
|
26
|
+
import { openArtifactPage, launchPerceptionBrowser, degradedNote } from './helpers/perception-page.js';
|
|
27
|
+
|
|
28
|
+
const DEVICE_W = { desktop: 1440, tablet: 834, mobile: 390 };
|
|
29
|
+
const DEVICE_H = { desktop: 900, tablet: 1112, mobile: 844 };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 页面里跑的采样器:**只测量,不自己滚**。
|
|
33
|
+
*
|
|
34
|
+
* ⛔ 前两版都是页面自己 `window.scrollBy` 推进的,两次都测错,原因不同:
|
|
35
|
+
* v1「大跨步 + 每步 sleep」——合成器在步与步之间有空闲,坏页面照样报 3% 掉帧。
|
|
36
|
+
* v2「rAF 里逐帧小步推」——压力对了,但**滚动方式还是假的**:程序化滚动
|
|
37
|
+
* 不派发 wheel 事件,于是一整类最常见的"一顿一顿"完全隐形:
|
|
38
|
+
* wheel handler 里做视差、滚动劫持(scrolljacking)、平滑滚库
|
|
39
|
+
* (Locomotive/Lenis 那种自己接管滚动的)。用户的手指走的是 wheel 那条路。
|
|
40
|
+
* 而且 `scroll-behavior: smooth` 会把程序化滚动变成动画,实测让量测范围
|
|
41
|
+
* 悄悄缩到 3.5% —— 看着"滚完了",其实几乎没动。
|
|
42
|
+
* 现在:滚动由外面用 CDP 派发**真 wheel 事件**(≈60Hz),这里只逐帧记 rAF 间隔。
|
|
43
|
+
*
|
|
44
|
+
* ⭐ 顺带白拿一个诊断:派发了多少像素 vs 页面实际走了多少像素。差得远
|
|
45
|
+
* = 滚动被劫持了,这本身就是答案。
|
|
46
|
+
*/
|
|
47
|
+
/* eslint-disable no-undef */
|
|
48
|
+
function installSampler() {
|
|
49
|
+
const st = {
|
|
50
|
+
frames: [], longTasks: [], po: null, raf: 0, running: true,
|
|
51
|
+
startedAt: performance.now(), last: performance.now(),
|
|
52
|
+
container: null, startTop: 0,
|
|
53
|
+
};
|
|
54
|
+
window.__ndProf = st;
|
|
55
|
+
try {
|
|
56
|
+
st.po = new PerformanceObserver((l) => { for (const e of l.getEntries()) st.longTasks.push(e.duration); });
|
|
57
|
+
st.po.observe({ entryTypes: ['longtask'] });
|
|
58
|
+
} catch { /* 不支持就没有长任务数据 */ }
|
|
59
|
+
|
|
60
|
+
// 滚的可能不是 window:应用式版面常常是某个内层容器在滚。wheel 事件落在
|
|
61
|
+
// 指针下面那个元素上,所以内层容器**会**滚 —— 但 window.scrollY 不动,
|
|
62
|
+
// 按它判断就会误报"这页滚不动"。先认出真正在滚的那个。
|
|
63
|
+
const pickContainer = () => {
|
|
64
|
+
const cx = Math.floor(window.innerWidth / 2);
|
|
65
|
+
const cy = Math.floor(window.innerHeight / 2);
|
|
66
|
+
let el = document.elementFromPoint(cx, cy);
|
|
67
|
+
while (el && el !== document.body && el !== document.documentElement) {
|
|
68
|
+
const cs = getComputedStyle(el);
|
|
69
|
+
const scrollable = /(auto|scroll|overlay)/.test(`${cs.overflowY}`) && el.scrollHeight > el.clientHeight + 4;
|
|
70
|
+
if (scrollable) return el;
|
|
71
|
+
el = el.parentElement;
|
|
72
|
+
}
|
|
73
|
+
return document.scrollingElement || document.documentElement;
|
|
74
|
+
};
|
|
75
|
+
st.container = pickContainer();
|
|
76
|
+
st.startTop = st.container.scrollTop;
|
|
77
|
+
|
|
78
|
+
const tick = () => {
|
|
79
|
+
if (!st.running) return;
|
|
80
|
+
const now = performance.now();
|
|
81
|
+
st.frames.push(now - st.last);
|
|
82
|
+
st.last = now;
|
|
83
|
+
st.raf = requestAnimationFrame(tick);
|
|
84
|
+
};
|
|
85
|
+
st.raf = requestAnimationFrame(tick);
|
|
86
|
+
return {
|
|
87
|
+
scrollHeight: st.container.scrollHeight,
|
|
88
|
+
clientHeight: st.container.clientHeight,
|
|
89
|
+
isWindow: st.container === (document.scrollingElement || document.documentElement),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function collectSampler() {
|
|
94
|
+
const st = window.__ndProf;
|
|
95
|
+
if (!st) return null;
|
|
96
|
+
st.running = false;
|
|
97
|
+
cancelAnimationFrame(st.raf);
|
|
98
|
+
try { st.po?.disconnect(); } catch { /* */ }
|
|
99
|
+
|
|
100
|
+
const imgs = performance.getEntriesByType('resource')
|
|
101
|
+
.filter(e => e.initiatorType === 'img' || /\.(png|jpe?g|webp|avif|gif|svg)(\?|$)/i.test(e.name))
|
|
102
|
+
.map(e => ({
|
|
103
|
+
name: decodeURIComponent(e.name.split('?')[0].split('/').pop() || '').slice(0, 60),
|
|
104
|
+
// encodedBodySize 是**这次传输**的字节;transferSize 命中缓存时是 0。
|
|
105
|
+
// 感知通道现在带 X-ND-Raw,拿的是磁盘原图,所以这个数字就是交付站点的数字。
|
|
106
|
+
bytes: e.encodedBodySize || e.transferSize || 0,
|
|
107
|
+
}));
|
|
108
|
+
imgs.sort((a, b) => b.bytes - a.bytes);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
frames: st.frames.slice(2), // 头两帧含启动抖动
|
|
112
|
+
longTasks: st.longTasks,
|
|
113
|
+
elapsedMs: performance.now() - st.startedAt,
|
|
114
|
+
scrollHeight: st.container.scrollHeight,
|
|
115
|
+
clientHeight: st.container.clientHeight,
|
|
116
|
+
movedPx: st.container.scrollTop - st.startTop,
|
|
117
|
+
imgCount: imgs.length,
|
|
118
|
+
imgBytes: imgs.reduce((a, b) => a + b.bytes, 0),
|
|
119
|
+
topImgs: imgs.slice(0, 5),
|
|
120
|
+
layers: (() => {
|
|
121
|
+
// 「合成层嫌疑」:全屏定位 + 带混合模式/滤镜/半透明的那些,
|
|
122
|
+
// 就是那个真实案例里的另一半凶手
|
|
123
|
+
const out = [];
|
|
124
|
+
for (const el of document.querySelectorAll('*')) {
|
|
125
|
+
const cs = getComputedStyle(el);
|
|
126
|
+
if (cs.position !== 'fixed' && cs.position !== 'sticky') continue;
|
|
127
|
+
const r = el.getBoundingClientRect();
|
|
128
|
+
const big = r.width * r.height > window.innerWidth * window.innerHeight * 0.5;
|
|
129
|
+
const heavy = cs.mixBlendMode !== 'normal' || cs.filter !== 'none'
|
|
130
|
+
|| cs.backdropFilter !== 'none' || (cs.opacity !== '1' && cs.opacity !== '');
|
|
131
|
+
if (big && heavy) {
|
|
132
|
+
const id = el.id ? `#${el.id}` : (typeof el.className === 'string' && el.className.trim()
|
|
133
|
+
? `.${el.className.trim().split(/\s+/)[0]}` : el.tagName.toLowerCase());
|
|
134
|
+
out.push(`${id} [${cs.position}, blend=${cs.mixBlendMode}, filter=${cs.filter === 'none' ? '-' : 'yes'}, backdrop=${cs.backdropFilter === 'none' ? '-' : 'yes'}, opacity=${cs.opacity}]`);
|
|
135
|
+
}
|
|
136
|
+
if (out.length >= 5) break;
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
})(),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/* eslint-enable no-undef */
|
|
143
|
+
|
|
144
|
+
const pct = (arr, p) => {
|
|
145
|
+
if (!arr.length) return 0;
|
|
146
|
+
const s = [...arr].sort((a, b) => a - b);
|
|
147
|
+
return s[Math.min(s.length - 1, Math.floor((p / 100) * s.length))];
|
|
148
|
+
};
|
|
149
|
+
const kb = (b) => (b >= 1024 * 1024 ? `${(b / 1024 / 1024).toFixed(1)}MB` : `${Math.round(b / 1024)}KB`);
|
|
150
|
+
|
|
151
|
+
export function makeProfileScrollTool({ workspaceRoot, projectId, sessionId }) {
|
|
152
|
+
return tool(
|
|
153
|
+
'profile_scroll',
|
|
154
|
+
`Measure how a page actually performs while scrolling. Loads it over http
|
|
155
|
+
(same origin as the user preview), scrolls programmatically, and returns frame
|
|
156
|
+
timing, dropped-frame ratio, long tasks, total image bytes and the heaviest
|
|
157
|
+
images, plus any full-screen fixed/sticky layers with blend modes or filters.
|
|
158
|
+
|
|
159
|
+
Scrolling is driven by REAL wheel events, so this also catches the other common
|
|
160
|
+
cause of "one frame at a time": the page eating the wheel and scrolling itself
|
|
161
|
+
(scroll hijacking, Lenis/Locomotive-style smooth scroll). The report says how
|
|
162
|
+
many pixels were dispatched vs how many the page actually moved — if those
|
|
163
|
+
disagree, that is your answer and frame times are beside the point.
|
|
164
|
+
|
|
165
|
+
Use this the moment a user says scrolling feels heavy, janky, laggy or "one
|
|
166
|
+
frame at a time" — do NOT start deleting features to guess which one it is.
|
|
167
|
+
The one real case on record: baseline 63% dropped frames; killing the parallax
|
|
168
|
+
alone changed nothing (64%); killing the full-screen grain layer alone got it to
|
|
169
|
+
40%; killing both got 5%; hiding images got 3%. It was an additive effect
|
|
170
|
+
between 33.7MB of PNGs and one blended full-screen layer. Guessing would have
|
|
171
|
+
removed the wrong thing and left most of the jank in place.
|
|
172
|
+
|
|
173
|
+
Also worth running before you hand over any image-heavy page: total image bytes
|
|
174
|
+
is the single most common cause, and generate_image now writes a .webp sibling
|
|
175
|
+
next to every PNG for exactly this reason.
|
|
176
|
+
|
|
177
|
+
⚠️ This machine has 1 vCPU and the browser competes with the server for it. The
|
|
178
|
+
absolute numbers are NOT what the user's device will see. Use them for A/B:
|
|
179
|
+
measure, change one thing, measure again, compare direction. Never report the
|
|
180
|
+
raw percentage to the user as if it were their experience.`,
|
|
181
|
+
{
|
|
182
|
+
path: z.string().optional().describe(CANVAS_PATH_DESC),
|
|
183
|
+
device: z.enum(['desktop', 'tablet', 'mobile']).optional()
|
|
184
|
+
.describe('Viewport preset (default desktop 1440×900). Mobile is where jank shows up worst.'),
|
|
185
|
+
frames: z.number().int().min(30).max(300).optional()
|
|
186
|
+
.describe('How many wheel steps to scroll across (default 120 ≈ a 2-second scroll at 60Hz). Real wheel events are dispatched at ~60Hz, the same rate a trackpad fires — so wheel handlers, scroll hijacking and smooth-scroll libraries are all in the measurement.'),
|
|
187
|
+
},
|
|
188
|
+
async ({ path: relPath, device, frames: framesArg }) => {
|
|
189
|
+
const asText = (text) => ({ content: [{ type: 'text', text }] });
|
|
190
|
+
const target = await resolveCanvasTarget(workspaceRoot, relPath, sessionId);
|
|
191
|
+
if (!target) return { content: [{ type: 'text', text: 'No page found to profile.' }], isError: true };
|
|
192
|
+
const guard = requireBrowsable(target);
|
|
193
|
+
if (guard) return { content: [{ type: 'text', text: guard }], isError: true };
|
|
194
|
+
|
|
195
|
+
const dev = device || 'desktop';
|
|
196
|
+
const viewport = { width: DEVICE_W[dev], height: DEVICE_H[dev] };
|
|
197
|
+
const nFrames = framesArg ?? 120;
|
|
198
|
+
|
|
199
|
+
let browser;
|
|
200
|
+
try {
|
|
201
|
+
browser = await launchPerceptionBrowser();
|
|
202
|
+
const opened = await openArtifactPage(browser, {
|
|
203
|
+
projectId, workspaceRoot, absPath: target.absPath, viewport,
|
|
204
|
+
});
|
|
205
|
+
await opened.goto();
|
|
206
|
+
const page = opened.page;
|
|
207
|
+
|
|
208
|
+
// 采样器先装上(它顺手认出真正在滚的那个容器),再从外面派发真 wheel
|
|
209
|
+
const geom = await page.evaluate(installSampler);
|
|
210
|
+
const scrollable = Math.max(0, geom.scrollHeight - geom.clientHeight);
|
|
211
|
+
// 每帧推进量:把可滚范围在 nFrames 帧里走完。下限 6px —— 太小的话短页面
|
|
212
|
+
// 一直停在原地,样本全是静止帧(那种"满分"没有意义)。
|
|
213
|
+
const perFrame = Math.max(6, Math.round(scrollable / nFrames));
|
|
214
|
+
|
|
215
|
+
// ⭐ 真 wheel,配速 ≈60Hz。配速是必要的:不配速的话 CDP 一秒能塞几百个
|
|
216
|
+
// wheel,任何页面都会显得卡(假警报),而真人的滚轮/触摸板就是 60-120Hz。
|
|
217
|
+
await page.mouse.move(Math.floor(viewport.width / 2), Math.floor(viewport.height / 2));
|
|
218
|
+
const t0 = Date.now();
|
|
219
|
+
for (let i = 0; i < nFrames; i += 1) {
|
|
220
|
+
await page.mouse.wheel(0, perFrame);
|
|
221
|
+
const wait = (t0 + (i + 1) * 16) - Date.now();
|
|
222
|
+
if (wait > 1) await page.waitForTimeout(wait);
|
|
223
|
+
}
|
|
224
|
+
await page.waitForTimeout(120); // 让最后几帧和长任务落账
|
|
225
|
+
const r = await page.evaluate(collectSampler);
|
|
226
|
+
|
|
227
|
+
// ⛔ **不再过滤长帧**。原来是 `frames.filter(f => f > 0 && f < 2000)`,
|
|
228
|
+
// 于是一个 3017ms 的冻结**被当成脏数据剔掉**,剩下的帧算出 `dropped 0%`
|
|
229
|
+
// 且不作任何提示 —— 越卡越可能被删。A/B 甚至会反过来(把页面改得更卡
|
|
230
|
+
// 反而报得更好)。长帧正是要找的东西。
|
|
231
|
+
const frames = r.frames.filter(f => f > 0);
|
|
232
|
+
const dropped = frames.filter(f => f > 16.7 * 1.5).length;
|
|
233
|
+
const dropRatio = frames.length ? (dropped / frames.length) * 100 : 0;
|
|
234
|
+
const worst = frames.length ? Math.max(...frames) : 0;
|
|
235
|
+
const freezes = frames.filter(f => f > 250).length;
|
|
236
|
+
// 派发了多少 vs 真走了多少。差很多 = 滚动被劫持/被平滑滚库接管,
|
|
237
|
+
// 这本身就是"一顿一顿"的答案,不必再往下猜。
|
|
238
|
+
const askedPx = perFrame * nFrames;
|
|
239
|
+
const movedPx = Math.round(r.movedPx);
|
|
240
|
+
const followRatio = askedPx ? (movedPx / askedPx) * 100 : 100;
|
|
241
|
+
// 样本不足就说不足。原来这种情况报的是 `p50 0.0ms / dropped 0%` ——
|
|
242
|
+
// 一句"满分",而真相是**什么都没量到**(溢出面板、短页、滚不动的页)。
|
|
243
|
+
const tooFew = frames.length < 20;
|
|
244
|
+
// 判据是**跟随率**不是绝对位移:实测那个劫持页走了 960px(不算"没动"),
|
|
245
|
+
// 但只跟上了派发量的 13% —— 用户手上的感觉就是"滚了半天没动"。
|
|
246
|
+
// 60% 是留给平滑滚库正常惯性的余量。
|
|
247
|
+
const hijacked = askedPx > 400 && followRatio < 60;
|
|
248
|
+
|
|
249
|
+
// 契约:note 非空必须写进返回文本。这个工具尤其不能漏 —— 它的描述铁口
|
|
250
|
+
// 直断"走 http 同源",退回 file:// 后它按 Resource Timing 报的
|
|
251
|
+
// 「images 0 files 0KB」正好是它主诊断的反面。
|
|
252
|
+
const degraded = degradedNote(opened);
|
|
253
|
+
const lines = [
|
|
254
|
+
...(degraded ? [degraded, ''] : []),
|
|
255
|
+
`profile_scroll — ${target.relPath} @ ${dev} ${viewport.width}×${viewport.height}`,
|
|
256
|
+
`scroll container ${geom.isWindow ? 'window' : 'an inner element (overflow:auto)'}`
|
|
257
|
+
+ `, scrollable ${scrollable}px, real wheel events ${perFrame}px × ${nFrames} @60Hz`,
|
|
258
|
+
`moved ${movedPx}px of ${askedPx}px dispatched (${followRatio.toFixed(0)}% followed)`,
|
|
259
|
+
'',
|
|
260
|
+
...(tooFew ? [
|
|
261
|
+
`⚠ ONLY ${frames.length} frames sampled — that is not enough to say anything.`,
|
|
262
|
+
' Nothing below is a verdict. Usual causes: the page is barely taller than the',
|
|
263
|
+
' viewport, or the thing that scrolls is not under the centre of the screen.',
|
|
264
|
+
'',
|
|
265
|
+
] : []),
|
|
266
|
+
...(hijacked ? [
|
|
267
|
+
`⚠ ${nFrames} real wheel events asked for ${askedPx}px; the page moved ${movedPx}px (${followRatio.toFixed(0)}%).`,
|
|
268
|
+
' Something is EATING the scroll — a wheel handler doing custom scrolling,',
|
|
269
|
+
' scroll hijacking, or a smooth-scroll library (Lenis/Locomotive). That IS the',
|
|
270
|
+
' jank the user feels: their wheel and the page no longer agree. Look at wheel',
|
|
271
|
+
' listeners before you look at images or layers.',
|
|
272
|
+
'',
|
|
273
|
+
] : []),
|
|
274
|
+
`frame time p50 ${pct(frames, 50).toFixed(1)}ms p90 ${pct(frames, 90).toFixed(1)}ms p99 ${pct(frames, 99).toFixed(1)}ms (16.7ms = 60fps)`,
|
|
275
|
+
`dropped ${dropRatio.toFixed(0)}% of ${frames.length} frames took >25ms`,
|
|
276
|
+
`worst frame ${worst.toFixed(0)}ms${freezes ? ` · ${freezes} frame(s) over 250ms = a visible freeze` : ''}`,
|
|
277
|
+
`long tasks ${r.longTasks.length}${r.longTasks.length ? ` (worst ${Math.max(...r.longTasks).toFixed(0)}ms)` : ''}`,
|
|
278
|
+
'',
|
|
279
|
+
`images ${r.imgCount} files, ${kb(r.imgBytes)} total (as delivered: originals, not preview variants)`,
|
|
280
|
+
...(r.topImgs.length
|
|
281
|
+
? [`heaviest ${r.topImgs.map(i => `${i.name} ${kb(i.bytes)}`).join(' · ')}`]
|
|
282
|
+
: []),
|
|
283
|
+
...(r.layers.length
|
|
284
|
+
? ['', 'full-screen composited layers (a classic other half of the problem):',
|
|
285
|
+
...r.layers.map(l => ` ${l}`)]
|
|
286
|
+
: []),
|
|
287
|
+
'',
|
|
288
|
+
r.imgBytes > 5 * 1024 * 1024
|
|
289
|
+
? `⚠ ${kb(r.imgBytes)} of images is the first thing to fix — reference the .webp siblings instead of the PNG masters.`
|
|
290
|
+
: null,
|
|
291
|
+
dropRatio > 25 && r.layers.length
|
|
292
|
+
? '⚠ Both heavy layers and dropped frames present. These are usually ADDITIVE — test removing them one at a time AND together before concluding.'
|
|
293
|
+
: null,
|
|
294
|
+
'',
|
|
295
|
+
'Numbers are for A/B comparison on this 1-vCPU box, not the user\'s device.',
|
|
296
|
+
].filter(l => l !== null);
|
|
297
|
+
return asText(lines.join('\n'));
|
|
298
|
+
} catch (err) {
|
|
299
|
+
return { content: [{ type: 'text', text: `profile_scroll failed: ${err?.message || String(err)}` }], isError: true };
|
|
300
|
+
} finally {
|
|
301
|
+
await browser?.close().catch(() => {});
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
);
|
|
305
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp/tools/publish-site.js — publish_site MCP tool(2026-08-02)
|
|
3
|
+
*
|
|
4
|
+
* agent 版的「一键上线」:把任务里的目录站点发到 Cloudflare Pages 公网。
|
|
5
|
+
* 与站点窗的上线按钮共用 lib/site-publish.js 的全部闸门 —— 额度和权限按
|
|
6
|
+
* **项目 owner** 算(basic 档不能发、pro 档每人 2 个;档位见 auth/tier.js),不是"谁触发算谁"。
|
|
7
|
+
*
|
|
8
|
+
* 发到公网是外发动作:工具描述里写死"用户明确要求才调"。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { getProject } from '../../../projects/store.js';
|
|
14
|
+
import { getUserById } from '../../../auth/users-store.js';
|
|
15
|
+
import { publishSite, unpublishSite, lookupPublished } from '../../../lib/site-publish.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} deps
|
|
19
|
+
* @param {string} [deps.projectId]
|
|
20
|
+
*/
|
|
21
|
+
export function makePublishSiteTool({ projectId }) {
|
|
22
|
+
return tool(
|
|
23
|
+
'publish_site',
|
|
24
|
+
`Publish a site task to the public web (Cloudflare Pages), update an already
|
|
25
|
+
published site, or take it offline.
|
|
26
|
+
|
|
27
|
+
Call this ONLY when the user explicitly asks to put the site online / update the
|
|
28
|
+
live version / take it down. Never publish unprompted — going public is the
|
|
29
|
+
user's call, not yours.
|
|
30
|
+
|
|
31
|
+
action:
|
|
32
|
+
- "publish": deploy the current files. First publish returns the permanent URL
|
|
33
|
+
(a brand-new domain may take a minute or two to start resolving); republishing
|
|
34
|
+
keeps the same URL.
|
|
35
|
+
- "unpublish": delete the deployment; the public URL dies immediately.
|
|
36
|
+
- "status": just report whether this task is published and at which URL.
|
|
37
|
+
|
|
38
|
+
If the workspace contains more than one site, the tool refuses to guess and lists
|
|
39
|
+
the candidates — pass "root" to name the one to publish ("." = the workspace root).
|
|
40
|
+
Each site holds ONE public URL, keyed by its own root directory.
|
|
41
|
+
|
|
42
|
+
**The public domain**: by default it is derived from the site's root directory
|
|
43
|
+
name; Chinese names slug down to nothing, so those fall back to a short hash
|
|
44
|
+
("a1b2c3.share..."). Pass "slug" to choose a readable one — do that whenever the
|
|
45
|
+
site has a name worth showing, because the URL is what the user hands to other
|
|
46
|
+
people. A slug is claimed on first publish and reused on every republish.
|
|
47
|
+
|
|
48
|
+
**Verifying a fresh publish**: a brand-new domain needs a certificate issued
|
|
49
|
+
before https works; until then screenshot_url fails with an SSL error. This tool
|
|
50
|
+
now waits for it and tells you whether it is ready. Also note Cloudflare Pages
|
|
51
|
+
serves the site's own 404.html for unknown paths — an HTTP 200 does NOT prove a
|
|
52
|
+
path exists, check the content type and body.
|
|
53
|
+
|
|
54
|
+
The user's publish quota and permissions apply (basic-tier accounts cannot publish;
|
|
55
|
+
pro accounts have a per-user site limit). If the tool returns a quota or
|
|
56
|
+
permission error, relay it as-is — do not retry.`,
|
|
57
|
+
{
|
|
58
|
+
task: z.string().describe(
|
|
59
|
+
'站点标识:站点根目录的相对路径(工作区根上那个站传 ".")。'
|
|
60
|
+
+ '不确定就传 "." —— 只有一个站时工具会自己认出来'),
|
|
61
|
+
action: z.enum(['publish', 'unpublish', 'status']).default('publish'),
|
|
62
|
+
root: z.string().optional().describe(
|
|
63
|
+
'有多个平行站点时点名要发布哪个(相对工作区根,根上那个站传 ".")。单站点省略'),
|
|
64
|
+
slug: z.string().optional().describe(
|
|
65
|
+
'自选公网域名前缀,如 "chenxi" → chenxi.share.<域>。'
|
|
66
|
+
+ '小写字母开头、3-32 位小写字母/数字/连字符。被占用会明确报错让你换一个'),
|
|
67
|
+
},
|
|
68
|
+
async ({ task, action, root, slug }) => {
|
|
69
|
+
const asText = (text) => ({ content: [{ type: 'text', text }] });
|
|
70
|
+
try {
|
|
71
|
+
const project = getProject(projectId);
|
|
72
|
+
if (!project) return asText('错误:项目不存在');
|
|
73
|
+
const owner = project.ownerId ? getUserById(project.ownerId) : null;
|
|
74
|
+
if (!owner) return asText('错误:找不到项目归属用户,不能发布');
|
|
75
|
+
|
|
76
|
+
if (action === 'status') {
|
|
77
|
+
const site = await lookupPublished(projectId, task);
|
|
78
|
+
return asText(site
|
|
79
|
+
? `已发布:${site.url}(站点根 ${site.task},最近发布 ${site.lastPublishedAt})`
|
|
80
|
+
: '未发布');
|
|
81
|
+
}
|
|
82
|
+
if (action === 'unpublish') {
|
|
83
|
+
const removed = await unpublishSite({ projectId, task });
|
|
84
|
+
return asText(removed ? '已下线,公网地址即刻失效。' : '本来就没有发布。');
|
|
85
|
+
}
|
|
86
|
+
const r = await publishSite({ projectId, task, root, slug, user: owner });
|
|
87
|
+
return asText([
|
|
88
|
+
`已上线:${r.site.url}`,
|
|
89
|
+
`发布的是 ${r.root === '.' ? '工作区根' : `${r.root}/`},${r.files} 个文件,入口 ${r.entry || '(无 html)'}`,
|
|
90
|
+
r.certReady === true
|
|
91
|
+
? '证书已就绪,现在就能 screenshot_url 自检。'
|
|
92
|
+
: (r.certReady === false
|
|
93
|
+
? '⚠ 证书还没签发完(等了 150 秒,通常 2-3 分钟)—— 现在截图会拿到 SSL 错误,过一会儿再试。'
|
|
94
|
+
: '重新发布地址不变。'),
|
|
95
|
+
r.warning ? `注意:${r.warning}` : null,
|
|
96
|
+
].filter(Boolean).join('\n'));
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return asText(`发布操作失败:${err.message}`);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
}
|