@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,640 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/projects/board-store.js — 工作台画布布局的唯一读写方(2026-07-27 分区版)
|
|
3
|
+
*
|
|
4
|
+
* HTTP 路由(api/board.js)和 MCP 工具(pin-to-board)都经这里读写,
|
|
5
|
+
* 带 per-project 写锁串行化读改写,避免前端 PATCH 与 agent 写入互相覆盖。
|
|
6
|
+
*
|
|
7
|
+
* schema(shared/board.json):
|
|
8
|
+
* {
|
|
9
|
+
* size: { w, h },
|
|
10
|
+
* zones: { [zoneId]: { x, y } }, // zoneId = 文件夹的工作区相对路径(#14 只剩坐标)
|
|
11
|
+
* objects: { [objectId]: { x, y, z, w?, h?, expanded? } },
|
|
12
|
+
* bindings: { [bindingId]: { type, from, to, label?, by? } } // 关系线
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* 布局只存摆放,物件本体由 artifacts / sessions / memory 数据源派生。
|
|
16
|
+
* **关系是例外**:bindings 不是任何数据源的派生,board.json 就是它的真相
|
|
17
|
+
* (2026-08-07 加,词汇表见 server/lib/binding-types.js)。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { promises as fs } from 'fs';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import { getSharedDir, ensureProjectWorkspace, gitRenamesSince } from './workspace.js';
|
|
23
|
+
import { isBindingType } from '../lib/binding-types.js';
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_BOARD_SIZE = { w: 4000, h: 2600 };
|
|
26
|
+
export const MAX_BOARD_BYTES = 512 * 1024;
|
|
27
|
+
export const MAX_OBJECTS = 2000;
|
|
28
|
+
export const MAX_ZONES = 200;
|
|
29
|
+
// 关系线上限。取值理由:一块板上人能看懂的线远少于这个数,1000 是防脱缰
|
|
30
|
+
// (agent 循环里连画)的闸门,不是设计目标。超了直接不收,不做淘汰 ——
|
|
31
|
+
// 静默丢最旧的会让"我明明画了"变成玄学。
|
|
32
|
+
export const MAX_BINDINGS = 1000;
|
|
33
|
+
|
|
34
|
+
// 分区自动铺位常数 —— 与前端 BoardCanvas 的 ZONE_* 保持一致(数值约定,非共享代码)
|
|
35
|
+
// ZONE_DEFAULTS 已删(#14):它是"文件夹=版面上一整条带"时代的默认矩形,
|
|
36
|
+
// zones 瘦身后连兜底都用不上了。前端 board-geometry.js 的注释别再指着它对齐。
|
|
37
|
+
|
|
38
|
+
function boardPath(pid) {
|
|
39
|
+
return path.join(getSharedDir(pid), 'board.json');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── per-project 写锁(进程内串行化)──
|
|
43
|
+
const locks = new Map();
|
|
44
|
+
function withBoardLock(pid, fn) {
|
|
45
|
+
const prev = locks.get(pid) || Promise.resolve();
|
|
46
|
+
const run = prev.catch(() => {}).then(fn);
|
|
47
|
+
locks.set(pid, run.catch(() => {}));
|
|
48
|
+
return run;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function clampNum(v, min, max, fallback) {
|
|
52
|
+
const n = Number(v);
|
|
53
|
+
if (!Number.isFinite(n)) return fallback;
|
|
54
|
+
return Math.min(max, Math.max(min, Math.round(n)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sanitizeSize(raw) {
|
|
58
|
+
return {
|
|
59
|
+
w: clampNum(raw?.w, 1000, 20000, DEFAULT_BOARD_SIZE.w),
|
|
60
|
+
h: clampNum(raw?.h, 800, 20000, DEFAULT_BOARD_SIZE.h),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* **画布原生**物件的形态白名单。
|
|
66
|
+
*
|
|
67
|
+
* 绝大多数画布物件是磁盘产物的影子:board.json 只存它摆在哪,本体是文件
|
|
68
|
+
* (所以 agent 读得到、能进上下文、删文件即消失)。涂鸦不一样 —— 它没有
|
|
69
|
+
* 有意义的文件形态,board.json 就是它的**本体**。这类物件必须显式登记,
|
|
70
|
+
* 否则任何人往 objects 里塞一个 kind 就能造出一个不受形态表管的东西。
|
|
71
|
+
*
|
|
72
|
+
* 2026-08-08 加进 `text`。在那之前画布上打的字一律落成 `.md` 便签,理由是
|
|
73
|
+
* "agent 读得到"。但用户要的是**白板**:在工程文件旁边随手写一句、画一笔,
|
|
74
|
+
* 那是给自己的记号,不是给 agent 的输入。想让 agent 看见的写便利贴 ——
|
|
75
|
+
* 那条路还在,挪到了右键菜单里。
|
|
76
|
+
*/
|
|
77
|
+
const CANVAS_NATIVE_KINDS = new Set(['scribble', 'text']);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 坐标夹持上限。画布 2026-08-13 起全向无限,坐标不再由 board.size 夹住 ——
|
|
81
|
+
* 这个数只挡非有限值和纯属事故的数字(±100 万世界像素之外没有正常操作能到)。
|
|
82
|
+
*/
|
|
83
|
+
const COORD_LIMIT = 1e6;
|
|
84
|
+
|
|
85
|
+
/** 涂鸦路径串上限。一条随手画的线约 300~800 字符,8000 够长且撑不爆 board.json */
|
|
86
|
+
const MAX_SCRIBBLE_PATH = 8000;
|
|
87
|
+
/**
|
|
88
|
+
* 画布文字的字数上限。
|
|
89
|
+
*
|
|
90
|
+
* 它是"写在白板上的一句话",不是文档 —— 长东西该写成 .md(那是便利贴,
|
|
91
|
+
* agent 读得到)。2000 字够写一段说明,也撑不爆 board.json。
|
|
92
|
+
*/
|
|
93
|
+
const MAX_TEXT_LEN = 2000;
|
|
94
|
+
|
|
95
|
+
/** 画布文字可选的字体。**白名单而不是自由字符串** —— 这个值会进 CSS */
|
|
96
|
+
export const TEXT_FONTS = ['pen', 'kai', 'sans', 'serif', 'mono'];
|
|
97
|
+
const TEXT_SIZES = ['sm', 'md', 'lg', 'xl'];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 变换字段(2026-08-13,选中态控制器)。缺省不落字段 —— 没转过没缩过的物件
|
|
101
|
+
* 别背两个恒等值,board.json 的 diff 要能一眼看出"谁被动过"。
|
|
102
|
+
*/
|
|
103
|
+
function sanitizeTransform(data) {
|
|
104
|
+
const out = {};
|
|
105
|
+
const rot = Number(data?.rotation);
|
|
106
|
+
if (Number.isFinite(rot) && rot !== 0) out.rotation = clampNum(rot, -360, 360, 0);
|
|
107
|
+
const sc = Number(data?.scale);
|
|
108
|
+
if (Number.isFinite(sc) && sc !== 1) out.scale = clampNum(sc, 0.2, 10, 1);
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sanitizeCanvasData(kind, data) {
|
|
113
|
+
if (kind === 'text') {
|
|
114
|
+
const t = typeof data?.t === 'string' ? data.t.slice(0, MAX_TEXT_LEN) : '';
|
|
115
|
+
if (!t.trim()) return null;
|
|
116
|
+
return {
|
|
117
|
+
t,
|
|
118
|
+
font: TEXT_FONTS.includes(data?.font) ? data.font : 'kai',
|
|
119
|
+
size: TEXT_SIZES.includes(data?.size) ? data.size : 'md',
|
|
120
|
+
color: ['ink', 'red', 'pencil', 'brass'].includes(data?.color) ? data.color : 'ink',
|
|
121
|
+
...sanitizeTransform(data),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (kind !== 'scribble') return null;
|
|
125
|
+
const d = typeof data?.d === 'string' ? data.d.slice(0, MAX_SCRIBBLE_PATH) : '';
|
|
126
|
+
// 只收 SVG path 里合法的那几个字符,挡住任何往 DOM 里塞东西的尝试
|
|
127
|
+
if (!d || !/^[\dMLQCZ ,.\-eE]+$/.test(d)) return null;
|
|
128
|
+
return {
|
|
129
|
+
d,
|
|
130
|
+
color: ['ink', 'red', 'pencil', 'brass'].includes(data.color) ? data.color : 'ink',
|
|
131
|
+
width: clampNum(data.width, 1, 24, 2),
|
|
132
|
+
...sanitizeTransform(data),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function sanitizeObject(o, size) {
|
|
137
|
+
if (!o || typeof o !== 'object') return null;
|
|
138
|
+
const kind = typeof o.kind === 'string' && CANVAS_NATIVE_KINDS.has(o.kind) ? o.kind : null;
|
|
139
|
+
const data = kind ? sanitizeCanvasData(kind, o.data) : null;
|
|
140
|
+
// 登记了 kind 却给不出合法内容 → 整条丢弃。留一个空壳会在画布上变成
|
|
141
|
+
// 一个看不见也删不掉的幽灵物件。
|
|
142
|
+
if (kind && !data) return null;
|
|
143
|
+
return {
|
|
144
|
+
// 画布 2026-08-13 起全向无限:坐标不再被桌面尺寸夹持(原来非 native 只许
|
|
145
|
+
// 正区间、native 也出不了 ±size)。这里只挡非有限值和纯属事故的数字 ——
|
|
146
|
+
// 夹得再紧一点都意味着"用户摆在那儿的东西刷新后跳走",那是静默数据损坏。
|
|
147
|
+
x: clampNum(o.x, -COORD_LIMIT, COORD_LIMIT, 0),
|
|
148
|
+
y: clampNum(o.y, -COORD_LIMIT, COORD_LIMIT, 0),
|
|
149
|
+
z: clampNum(o.z, 0, 1e6, 0),
|
|
150
|
+
...(Number.isFinite(Number(o.w)) ? { w: clampNum(o.w, 4, COORD_LIMIT, 200) } : {}),
|
|
151
|
+
...(Number.isFinite(Number(o.h)) ? { h: clampNum(o.h, 4, COORD_LIMIT, 200) } : {}),
|
|
152
|
+
...(o.expanded ? { expanded: true } : {}),
|
|
153
|
+
// 显式归属:'' = 明确无归属(覆盖 sid 派生),非空 = 所属工作区 id
|
|
154
|
+
...(typeof o.zone === 'string' && o.zone.length <= 300 ? { zone: o.zone } : {}),
|
|
155
|
+
// 出处(2026-08-14 agent 摆位/建元素):agent 落的座/写的字标出来,
|
|
156
|
+
// 用户的是默认不标 —— 跟关系线的 by 同一条纪律
|
|
157
|
+
...(o.by === 'agent' ? { by: 'agent' } : {}),
|
|
158
|
+
...(kind ? { kind, data } : {}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 关系线。**不存坐标** —— 端点是 object id 或 zone id,线跟着端点走。
|
|
164
|
+
*
|
|
165
|
+
* 词汇表在 `server/lib/binding-types.js`(前端画线那份视觉映射要跟它对齐,
|
|
166
|
+
* 有 parity 断言看着)。不认识的 type 一律丢弃:宁可少画一条线,也不要在
|
|
167
|
+
* 画布上留一条没人知道什么意思的连线。
|
|
168
|
+
*
|
|
169
|
+
* 自环(from === to)也丢:它画不出来,且多半是 agent 传错了 id。
|
|
170
|
+
*/
|
|
171
|
+
function sanitizeBinding(b) {
|
|
172
|
+
if (!b || typeof b !== 'object') return null;
|
|
173
|
+
if (!isBindingType(b.type)) return null;
|
|
174
|
+
const from = typeof b.from === 'string' ? b.from.slice(0, 300) : '';
|
|
175
|
+
const to = typeof b.to === 'string' ? b.to.slice(0, 300) : '';
|
|
176
|
+
if (!from || !to || from === to) return null;
|
|
177
|
+
return {
|
|
178
|
+
type: b.type,
|
|
179
|
+
from,
|
|
180
|
+
to,
|
|
181
|
+
// 线上的字。没写就渲染时回落到词汇表的默认词,不在这里补 —— 存了默认词
|
|
182
|
+
// 之后改词汇表就改不动存量了。
|
|
183
|
+
...(typeof b.label === 'string' && b.label.trim()
|
|
184
|
+
? { label: b.label.trim().slice(0, 60) }
|
|
185
|
+
: {}),
|
|
186
|
+
// 谁画的。用户画的线 agent 不该擅自删,反过来也一样;auto = 机器可证的
|
|
187
|
+
// 引用关系(auto-relations.js 对账层专属,只有它增删自家 b:auto:* 条目)。
|
|
188
|
+
...(b.by === 'agent' || b.by === 'user' || b.by === 'auto' ? { by: b.by } : {}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* zones 一行只剩坐标(2026-08-13 瘦身,#14)。
|
|
194
|
+
*
|
|
195
|
+
* 逐字段的下场:
|
|
196
|
+
* - `w`/`h` —— 文件夹变方卡后前端视图**强制** FOLDER_CARD 尺寸,存的数字
|
|
197
|
+
* 没人读,还会成为"画布上 288 宽、存档里 1340"那种自相矛盾的证据。
|
|
198
|
+
* - `title` —— 名字从路径读(id 就是路径),存一份就是第二个真相源,
|
|
199
|
+
* 改名后立刻过期(实测过)。
|
|
200
|
+
* - `collapsed` —— 收起/展开两态 2026-08-13 随"当前目录"模型退役。
|
|
201
|
+
* - `pinned` —— 纵向堆叠 2026-08-08 退役,字段没有了对立面。
|
|
202
|
+
* 存量数据里这些字段读进来直接丢,下次写盘自然消失。
|
|
203
|
+
*/
|
|
204
|
+
function sanitizeZone(z) {
|
|
205
|
+
if (!z || typeof z !== 'object') return null;
|
|
206
|
+
return {
|
|
207
|
+
// 同 sanitizeObject:无限画布,文件夹卡也能摆在任何地方(含负坐标)
|
|
208
|
+
x: clampNum(z.x, -COORD_LIMIT, COORD_LIMIT, 0),
|
|
209
|
+
y: clampNum(z.y, -COORD_LIMIT, COORD_LIMIT, 0),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function sanitizeBoard(raw) {
|
|
214
|
+
const size = sanitizeSize(raw?.size);
|
|
215
|
+
const objects = {};
|
|
216
|
+
const zones = {};
|
|
217
|
+
const bindings = {};
|
|
218
|
+
let count = 0;
|
|
219
|
+
for (const [id, o] of Object.entries(raw?.objects && typeof raw.objects === 'object' ? raw.objects : {})) {
|
|
220
|
+
if (count >= MAX_OBJECTS) break;
|
|
221
|
+
if (typeof id !== 'string' || id.length > 300) continue;
|
|
222
|
+
const s = sanitizeObject(o, size);
|
|
223
|
+
if (s) { objects[id] = s; count += 1; }
|
|
224
|
+
}
|
|
225
|
+
let zCount = 0;
|
|
226
|
+
for (const [id, z] of Object.entries(raw?.zones && typeof raw.zones === 'object' ? raw.zones : {})) {
|
|
227
|
+
if (zCount >= MAX_ZONES) break;
|
|
228
|
+
if (typeof id !== 'string' || id.length > 300) continue;
|
|
229
|
+
const s = sanitizeZone(z);
|
|
230
|
+
if (s) { zones[id] = s; zCount += 1; }
|
|
231
|
+
}
|
|
232
|
+
let bCount = 0;
|
|
233
|
+
for (const [id, b] of Object.entries(raw?.bindings && typeof raw.bindings === 'object' ? raw.bindings : {})) {
|
|
234
|
+
if (bCount >= MAX_BINDINGS) break;
|
|
235
|
+
if (typeof id !== 'string' || id.length > 300) continue;
|
|
236
|
+
const s = sanitizeBinding(b);
|
|
237
|
+
if (s) { bindings[id] = s; bCount += 1; }
|
|
238
|
+
}
|
|
239
|
+
// 主角覆盖(2026-08-14 agent 摆位):显式立的主角压过前端 pickHero 的推断。
|
|
240
|
+
// 存 id 不存理由 —— 理由在会话里,画布只要知道谁站 C 位
|
|
241
|
+
const hero = typeof raw?.hero === 'string' && raw.hero.length <= 300 ? raw.hero : null;
|
|
242
|
+
return { size, zones, objects, bindings, ...(hero ? { hero } : {}) };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function readBoard(pid) {
|
|
246
|
+
try {
|
|
247
|
+
const raw = await fs.readFile(boardPath(pid), 'utf8');
|
|
248
|
+
return sanitizeBoard(JSON.parse(raw));
|
|
249
|
+
} catch {
|
|
250
|
+
return { size: { ...DEFAULT_BOARD_SIZE }, zones: {}, objects: {}, bindings: {} };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function writeBoard(pid, board) {
|
|
255
|
+
const json = JSON.stringify(board);
|
|
256
|
+
if (Buffer.byteLength(json) > MAX_BOARD_BYTES) {
|
|
257
|
+
const err = new Error('board layout too large');
|
|
258
|
+
err.status = 400;
|
|
259
|
+
throw err;
|
|
260
|
+
}
|
|
261
|
+
await ensureProjectWorkspace(pid);
|
|
262
|
+
await fs.writeFile(boardPath(pid), json, 'utf8');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 全量替换(前端 reset / 兼容旧 PUT)。 */
|
|
266
|
+
export function replaceBoard(pid, raw) {
|
|
267
|
+
return withBoardLock(pid, async () => {
|
|
268
|
+
const board = sanitizeBoard(raw);
|
|
269
|
+
await writeBoard(pid, board);
|
|
270
|
+
return board;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* diff 式合并写:{ size?, objects?: {id: obj|null}, zones?: {id: zone|null} }
|
|
276
|
+
* null 值 = 删除该条目。返回合并后的完整 board。
|
|
277
|
+
*/
|
|
278
|
+
export function patchBoard(pid, patch) {
|
|
279
|
+
return withBoardLock(pid, async () => {
|
|
280
|
+
const board = await readBoard(pid);
|
|
281
|
+
if (patch?.size) board.size = sanitizeSize(patch.size);
|
|
282
|
+
// 本次被显式删掉的端点 id。**不能拿 board.objects 的成员资格当存在性判据**:
|
|
283
|
+
// 它是稀疏的(只存被拖过 / pin 过的物件,没动过的产物压根没有条目),
|
|
284
|
+
// 那样会把连向"还没被摆过的产物"的线全误删。只清确实删了的。
|
|
285
|
+
const removed = new Set();
|
|
286
|
+
// 拿着旧 id 迟到的写入要落到新名字上(前端 800ms 防抖 / agent 本轮的旧路径)。
|
|
287
|
+
// 不转发的话它会把刚改完名的旧条目**重新插回来**,画布上多出一张回到默认
|
|
288
|
+
// 位置的重影,而且不报错。见 renameBoardPaths 上面那段。
|
|
289
|
+
const fwd = (id) => forwardId(pid, id);
|
|
290
|
+
if (patch?.zones && typeof patch.zones === 'object') {
|
|
291
|
+
for (const [rawId, z] of Object.entries(patch.zones)) {
|
|
292
|
+
if (typeof rawId !== 'string' || rawId.length > 300) continue;
|
|
293
|
+
const id = fwd(rawId);
|
|
294
|
+
if (z === null) { delete board.zones[id]; removed.add(id); continue; }
|
|
295
|
+
const s = sanitizeZone(z);
|
|
296
|
+
if (s && (board.zones[id] || Object.keys(board.zones).length < MAX_ZONES)) board.zones[id] = s;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (patch?.objects && typeof patch.objects === 'object') {
|
|
300
|
+
for (const [rawId, o] of Object.entries(patch.objects)) {
|
|
301
|
+
if (typeof rawId !== 'string' || rawId.length > 300) continue;
|
|
302
|
+
const id = fwd(rawId);
|
|
303
|
+
if (o === null) { delete board.objects[id]; removed.add(id); continue; }
|
|
304
|
+
const s = sanitizeObject(o, board.size);
|
|
305
|
+
if (s && (board.objects[id] || Object.keys(board.objects).length < MAX_OBJECTS)) board.objects[id] = s;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (patch?.bindings && typeof patch.bindings === 'object') {
|
|
309
|
+
for (const [id, b] of Object.entries(patch.bindings)) {
|
|
310
|
+
if (typeof id !== 'string' || id.length > 300) continue;
|
|
311
|
+
if (b === null) { delete board.bindings[id]; continue; }
|
|
312
|
+
const s = sanitizeBinding(b);
|
|
313
|
+
if (!s) continue;
|
|
314
|
+
// 端点也要转发:agent 本轮拿旧路径连的线,落下来必须连到新名字上
|
|
315
|
+
const fixed = { ...s, from: fwd(s.from), to: fwd(s.to) };
|
|
316
|
+
if (board.bindings[id] || Object.keys(board.bindings).length < MAX_BINDINGS) board.bindings[id] = fixed;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// 主角覆盖:null = 撤销(回到 pickHero 自动推断),字符串 = 显式立主角
|
|
320
|
+
if (patch?.hero !== undefined) {
|
|
321
|
+
if (patch.hero === null) delete board.hero;
|
|
322
|
+
else if (typeof patch.hero === 'string' && patch.hero.length <= 300) board.hero = fwd(patch.hero);
|
|
323
|
+
}
|
|
324
|
+
// 端点被删掉的线一起清掉,否则画布上留一条连向虚空的线。放在最后:
|
|
325
|
+
// 这一趟才看得到本次删除的全貌(同一个 patch 里可能既删物件又加线)。
|
|
326
|
+
if (removed.size) {
|
|
327
|
+
for (const [id, b] of Object.entries(board.bindings)) {
|
|
328
|
+
if (removed.has(b.from) || removed.has(b.to)) delete board.bindings[id];
|
|
329
|
+
}
|
|
330
|
+
if (board.hero && removed.has(board.hero)) delete board.hero;
|
|
331
|
+
}
|
|
332
|
+
await writeBoard(pid, board);
|
|
333
|
+
return board;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ── 改名(2026-08-08)────────────────────────────────────────────────────
|
|
338
|
+
//
|
|
339
|
+
// id 是**工作区相对路径**,所以文件一动,画布上的身份就跟着变。移动从罕见事件
|
|
340
|
+
// 变成日常动作(拖进文件夹 = 真 mv)之后,这件事必须有一等公民的地位。
|
|
341
|
+
//
|
|
342
|
+
// ## 为什么不能用 patchBoard 删旧插新
|
|
343
|
+
//
|
|
344
|
+
// patchBoard 末尾那段"端点被删的线一起清掉"会把指向旧 id 的 `annotates` 线
|
|
345
|
+
// 一并删掉 —— 也就是**每拖一次卡,挂在它上面的评论就没了**。删除和改名是
|
|
346
|
+
// 两件事:删除意味着"这东西不在了,连着它的线也没意义了",改名意味着
|
|
347
|
+
// "还是它,换了个名字"。用同一个动词表达,必然丢掉后者的语义。
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* 改名的转发表:**旧 id → 新 id**,带 TTL。
|
|
351
|
+
*
|
|
352
|
+
* 改名不是一次写入,是三个并发写方都得学会的一件事:
|
|
353
|
+
* ① 服务端(这里)——立刻知道
|
|
354
|
+
* ② 前端画布 —— scheduleSave 是 800ms 防抖,从 layoutRef 按**旧 id** 组 patch,
|
|
355
|
+
* 改完名之后那一发迟到的 flush 会把 objects[旧id] 重新插回来
|
|
356
|
+
* ③ agent —— 这一轮的上下文里还是旧路径,会继续往旧 id 上 pin、上批注
|
|
357
|
+
*
|
|
358
|
+
* 不转发的话,表现是"拖进文件夹时灵时不灵"(迟到的写入把旧条目复活,画布上
|
|
359
|
+
* 就多出一张回到默认位置的重影)。这类症状最难查,因为它**不报错**。
|
|
360
|
+
*
|
|
361
|
+
* 内存表 + TTL 够用:它只需要盖住"改完名之后还有人拿着旧 id"那个窗口。
|
|
362
|
+
* 进程重启、以及 agent 在画布背后自己 mv 的情况,由 git 改名对账兜底
|
|
363
|
+
* (见 workspace.js 的 reconcileBoardRenames)。
|
|
364
|
+
*/
|
|
365
|
+
const RENAME_TTL_MS = 5 * 60 * 1000;
|
|
366
|
+
/** @type {Map<string, Map<string, { to: string, at: number }>>} pid → (旧 id → 新 id) */
|
|
367
|
+
const renameJournal = new Map();
|
|
368
|
+
|
|
369
|
+
function journalOf(pid) {
|
|
370
|
+
let m = renameJournal.get(pid);
|
|
371
|
+
if (!m) { m = new Map(); renameJournal.set(pid, m); }
|
|
372
|
+
return m;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function noteRenames(pid, pairs, now) {
|
|
376
|
+
const j = journalOf(pid);
|
|
377
|
+
for (const [from, to] of pairs) j.set(from, { to, at: now });
|
|
378
|
+
for (const [k, v] of j) if (now - v.at > RENAME_TTL_MS) j.delete(k);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* 把一个可能过期的 id 换成它现在的名字。
|
|
383
|
+
* 顺着链走(`a→b` 之后又 `b→c`,拿着 a 的迟到写入要落到 c 上)。
|
|
384
|
+
*/
|
|
385
|
+
export function forwardId(pid, id, now = Date.now()) {
|
|
386
|
+
const j = renameJournal.get(pid);
|
|
387
|
+
if (!j || typeof id !== 'string') return id;
|
|
388
|
+
let cur = id;
|
|
389
|
+
for (let hop = 0; hop < 8; hop++) {
|
|
390
|
+
const e = j.get(cur);
|
|
391
|
+
if (!e || now - e.at > RENAME_TTL_MS) return cur;
|
|
392
|
+
cur = e.to;
|
|
393
|
+
}
|
|
394
|
+
return cur;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 把一个**文件路径**换成它现在的位置 —— forwardId 的前缀感知版(#4)。
|
|
399
|
+
*
|
|
400
|
+
* forwardId 只做精确匹配,够画布用(物件 id 整个换)。但 artifact-file
|
|
401
|
+
* 拿的是文件路径:文件夹 `稿件` 改成 `定稿` 之后,开着的窗还在请求
|
|
402
|
+
* `稿件/主稿.html` —— 表里只有 `稿件 → 定稿` 这一条,精确匹配永远打不中。
|
|
403
|
+
* 这里按**最长前缀**找(全路径 → 上级 → 再上级),命中就把前缀换掉接着走链。
|
|
404
|
+
*/
|
|
405
|
+
export function forwardPath(pid, rel, now = Date.now()) {
|
|
406
|
+
const j = renameJournal.get(pid);
|
|
407
|
+
if (!j || typeof rel !== 'string' || !rel) return rel;
|
|
408
|
+
let cur = rel;
|
|
409
|
+
for (let hop = 0; hop < 8; hop++) {
|
|
410
|
+
let next = null;
|
|
411
|
+
const segs = cur.split('/');
|
|
412
|
+
for (let i = segs.length; i > 0; i--) {
|
|
413
|
+
const prefix = segs.slice(0, i).join('/');
|
|
414
|
+
const e = j.get(prefix);
|
|
415
|
+
if (e && now - e.at <= RENAME_TTL_MS) { next = e.to + cur.slice(prefix.length); break; }
|
|
416
|
+
}
|
|
417
|
+
if (!next || next === cur) return cur;
|
|
418
|
+
cur = next;
|
|
419
|
+
}
|
|
420
|
+
return cur;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** 只在测试里用:清掉转发表,免得用例之间串味 */
|
|
424
|
+
export function _resetRenameJournal() { renameJournal.clear(); }
|
|
425
|
+
/** 只在测试里用:往转发表里记一笔(noteRenames 不导出,真路径走 renameBoardPaths) */
|
|
426
|
+
export function _noteRenamesForTest(pid, pairs, now = Date.now()) { noteRenames(pid, pairs, now); }
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* 一次 id 改名:物件、文件夹、以及所有关系线端点一起改。
|
|
430
|
+
*
|
|
431
|
+
* 文件夹改名是**前缀改名** —— `稿件` → `定稿` 要连带它下面的一切:
|
|
432
|
+
* 子文件夹 `稿件/初稿`、物件 `deck:稿件/主稿.html`、裸路径 `稿件/说明.md`。
|
|
433
|
+
* 而且同一个目录**同时**可能是文件夹(zones['稿件'])和产物(objects['site:稿件']),
|
|
434
|
+
* 两个命名空间都得一致地改 —— 今天 rewriteBoardIds 的 mapEnd 就在这个形状上
|
|
435
|
+
* 栽过一次(文件夹端点掉进物件那条分支,被原样放行成了断头线)。
|
|
436
|
+
*
|
|
437
|
+
* @param {string} pid
|
|
438
|
+
* @param {Array<[string, string]>} pairs [旧路径, 新路径],**路径不带 kind 前缀**
|
|
439
|
+
* @returns {Promise<{ board: object, renamed: number }>}
|
|
440
|
+
*/
|
|
441
|
+
export function renameBoardPaths(pid, pairs) {
|
|
442
|
+
const clean = (pairs || [])
|
|
443
|
+
.map(([a, b]) => [String(a || '').replace(/\/+$/, ''), String(b || '').replace(/\/+$/, '')])
|
|
444
|
+
.filter(([a, b]) => a && b && a !== b);
|
|
445
|
+
if (!clean.length) return Promise.resolve({ board: null, renamed: 0 });
|
|
446
|
+
|
|
447
|
+
return withBoardLock(pid, async () => {
|
|
448
|
+
const board = await readBoard(pid);
|
|
449
|
+
const now = Date.now();
|
|
450
|
+
const applied = [];
|
|
451
|
+
|
|
452
|
+
/** 一个 id(可能带 `deck:` 之类前缀)在这批改名下的新名字;没动就返回原值 */
|
|
453
|
+
const mapId = (id) => {
|
|
454
|
+
if (typeof id !== 'string' || !id) return id;
|
|
455
|
+
const c = id.indexOf(':');
|
|
456
|
+
// kind 前缀只认字母(`deck:` `site:`)—— 路径里的冒号不算前缀
|
|
457
|
+
const prefix = c > 0 && /^[a-z]+$/.test(id.slice(0, c)) ? id.slice(0, c + 1) : '';
|
|
458
|
+
const p = id.slice(prefix.length);
|
|
459
|
+
for (const [from, to] of clean) {
|
|
460
|
+
if (p === from) return prefix + to;
|
|
461
|
+
if (p.startsWith(from + '/')) return prefix + to + p.slice(from.length);
|
|
462
|
+
}
|
|
463
|
+
return id;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const remap = (bag) => {
|
|
467
|
+
const src = bag || {};
|
|
468
|
+
const next = {};
|
|
469
|
+
// **先把没改名的放进去占位**,再放改了名的。撞名时该让路的是"搬过来的
|
|
470
|
+
// 那个",不是原地那个 —— 按插入顺序一趟写的话,改名源恰好排在前面就会
|
|
471
|
+
// 把活着的条目静默顶掉,而顺序是 JSON 键序,纯属看运气。
|
|
472
|
+
for (const [id, v] of Object.entries(src)) if (mapId(id) === id) next[id] = v;
|
|
473
|
+
for (const [id, v] of Object.entries(src)) {
|
|
474
|
+
const n = mapId(id);
|
|
475
|
+
if (n === id) continue;
|
|
476
|
+
if (next[n]) {
|
|
477
|
+
// 目标位置有活的 —— 搬不过去就**留在原地**,不能一声不响地丢掉。
|
|
478
|
+
// (只写 continue 的话这条从 next 里彻底消失,用户的卡凭空没了。)
|
|
479
|
+
if (!next[id]) next[id] = v;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
next[n] = v;
|
|
483
|
+
applied.push([id, n]);
|
|
484
|
+
}
|
|
485
|
+
return next;
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
board.objects = remap(board.objects);
|
|
489
|
+
board.zones = remap(board.zones);
|
|
490
|
+
|
|
491
|
+
// 显式归属字段也要跟着改名。`objects[*].zone` 是活的:pin_to_board 写它、
|
|
492
|
+
// 拖进文件夹写它。只改键不改这个值的话,文件夹一改名,所有显式归属的成员
|
|
493
|
+
// 就指向一个不存在的文件夹(画布上表现为"卡片从文件夹里掉出来了")。
|
|
494
|
+
let touchedField = false;
|
|
495
|
+
for (const o of Object.values(board.objects)) {
|
|
496
|
+
if (typeof o.zone !== 'string' || !o.zone) continue;
|
|
497
|
+
const n = mapId(o.zone);
|
|
498
|
+
if (n !== o.zone) { o.zone = n; touchedField = true; }
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 关系线端点。**这里改了也要算数** —— board.objects 是稀疏的,一条线完全
|
|
502
|
+
// 可以指向一个没有坐标条目的产物,那种情况下 applied 是空的,可对账位点
|
|
503
|
+
// 照样往前推:不落盘 = 这次改名永久丢失,下次再也算不出来。
|
|
504
|
+
let touchedBinding = false;
|
|
505
|
+
for (const b of Object.values(board.bindings || {})) {
|
|
506
|
+
const from = mapId(b.from);
|
|
507
|
+
const to = mapId(b.to);
|
|
508
|
+
if (from !== b.from || to !== b.to) { b.from = from; b.to = to; touchedBinding = true; }
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (applied.length || touchedField || touchedBinding) {
|
|
512
|
+
await writeBoard(pid, board);
|
|
513
|
+
}
|
|
514
|
+
// 转发表**永远**记裸路径对,不只记 applied:applied 是画布条目的改名
|
|
515
|
+
//(deck 带 `deck:` 前缀、没上画布的文件根本不在里面),而 artifact-file
|
|
516
|
+
// 的改名转发(#4)查的是文件路径 —— 只记 applied 的话,改一个 deck 或者
|
|
517
|
+
// 一个没有画布条目的文件,开着的窗照样 404。
|
|
518
|
+
noteRenames(pid, [...clean, ...applied], now);
|
|
519
|
+
return { board, renamed: applied.length };
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* 对账:把 agent 在画布背后做的改名,补写进 board.json。
|
|
525
|
+
*
|
|
526
|
+
* 转发表(上面那个)盖的是"刚改完名、还有人拿着旧 id"的那几分钟窗口,活在内存里。
|
|
527
|
+
* 这一条盖的是另一半:**画布根本没参与的移动** —— agent 一句 `mv`、一次
|
|
528
|
+
* 重构目录,进程重启,都在它的覆盖范围里。
|
|
529
|
+
*
|
|
530
|
+
* 跑在扫产物清单的路上(打开项目必调),所以用户看到的第一帧就是对齐过的。
|
|
531
|
+
* 没有新 commit 时是一次 rev-parse 的事。
|
|
532
|
+
*
|
|
533
|
+
* 同步位点存在 `.nd/board-sync.json`:`.nd/` 是 gitignore 的,这个位点记的是
|
|
534
|
+
* "我的画布追到哪了",属于本地状态,不该进项目历史被别的会话读到。
|
|
535
|
+
*/
|
|
536
|
+
const SYNC_FILE = 'board-sync.json';
|
|
537
|
+
|
|
538
|
+
function syncPath(pid) {
|
|
539
|
+
return path.join(getSharedDir(pid), '.nd', SYNC_FILE);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export async function reconcileBoardRenames(pid) {
|
|
543
|
+
let seen = null;
|
|
544
|
+
try { seen = JSON.parse(await fs.readFile(syncPath(pid), 'utf8'))?.commit || null; } catch { /* 首次 */ }
|
|
545
|
+
|
|
546
|
+
const { head, renames } = await gitRenamesSince(pid, seen);
|
|
547
|
+
if (!head) return { renamed: 0 };
|
|
548
|
+
|
|
549
|
+
let renamed = 0;
|
|
550
|
+
if (renames.length) {
|
|
551
|
+
({ renamed } = await renameBoardPaths(pid, renames));
|
|
552
|
+
if (renamed) console.log(`[board] ${pid} 跟上 ${renames.length} 个改名,改了 ${renamed} 条画布条目`);
|
|
553
|
+
}
|
|
554
|
+
// 位点无条件推进(哪怕这次没有改名)——否则每次都要重算同一段 diff。
|
|
555
|
+
// 首次跑时 seen 是 null,gitRenamesSince 直接返回空,等于"从现在开始跟",
|
|
556
|
+
// 不去回放整段历史:历史里的改名早就体现在当前 board.json 里了。
|
|
557
|
+
if (seen !== head) {
|
|
558
|
+
try {
|
|
559
|
+
await fs.mkdir(path.dirname(syncPath(pid)), { recursive: true });
|
|
560
|
+
await fs.writeFile(syncPath(pid), JSON.stringify({ commit: head }), 'utf8');
|
|
561
|
+
} catch { /* 写不了位点:下次重算,不影响正确性 */ }
|
|
562
|
+
}
|
|
563
|
+
return { renamed };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// `nextZoneRect`(给新建的工作区自动铺位)2026-08-13 删除:服务端不再新建
|
|
567
|
+
// 文件夹条目 —— 文件夹的权威是磁盘扫描,前端按 newStackedZoneRect 给它安排
|
|
568
|
+
// 位置。服务端这份自动铺位是当年 pinToZone 会凭空造区留下的。
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* 把一个物件摆到画布上的一个空位并置顶(`pin_to_board` 用)。
|
|
572
|
+
* 单锁原子操作。槽位按 244×210 网格估算(服务端不知道物件真实尺寸,取最大卡片脚印)。
|
|
573
|
+
*
|
|
574
|
+
* ## 2026-08-13 改了三件事
|
|
575
|
+
*
|
|
576
|
+
* 1. **两个 id 都过转发表。** 以前直接 `board.objects[objectId] = …`,agent 在
|
|
577
|
+
* 改名窗口(TTL 5 分钟)里 pin 一下,就往 board.json 里插一条指向旧路径的
|
|
578
|
+
* 条目 —— 画布上多一张回到默认位置的重影,而且不报错。这正是
|
|
579
|
+
* `renameBoardPaths` 上面那段注释拼死在防的东西,这条写入口是唯一的漏网。
|
|
580
|
+
* 2. **不再写 `zone` 显式归属字段。** id = 路径之后,"它属于哪个文件夹"由路径
|
|
581
|
+
* 回答,再写一个字段只会让画布和磁盘各执一词。
|
|
582
|
+
* 3. **不再凭空新建文件夹条目。** 文件夹的权威是磁盘扫描;board.json 里那条
|
|
583
|
+
* 只是坐标。造一条磁盘上不存在的出来,就是一块剪不掉的僵尸框。
|
|
584
|
+
* 文件夹没坐标(还没被摆过)时就摆到桌面上,前端下一轮会给它安排位置。
|
|
585
|
+
*/
|
|
586
|
+
export function pinToZone(pid, { objectId, zoneId = '' }) {
|
|
587
|
+
return withBoardLock(pid, async () => {
|
|
588
|
+
const board = await readBoard(pid);
|
|
589
|
+
const now = Date.now();
|
|
590
|
+
const oid = forwardId(pid, objectId, now);
|
|
591
|
+
const zid = zoneId ? forwardId(pid, zoneId, now) : '';
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* 找一个空位。
|
|
595
|
+
*
|
|
596
|
+
* ⚠️ 2026-08-13 重写。原来这里按「区矩形 1120×640 + 40px 标题栏 + 244×210
|
|
597
|
+
* 网格」算槽位 —— 那是「文件夹是版面上摊开的一块地」时代的几何,画布上
|
|
598
|
+
* 早就没有这些东西了(区内坐标系随刀 3 整段退役)。算出来的位置跟前端的
|
|
599
|
+
* 排布不在一个坐标语言里。
|
|
600
|
+
*
|
|
601
|
+
* 现在跟前端对齐成同一件事:**一层就是一片桌面,卡按格子铺**。服务端不知道
|
|
602
|
+
* 每张卡多大(那是前端形态表的事),所以取一个够大的格子(最宽的卡 640 ×
|
|
603
|
+
* 一行的常见高度 220)做保守估算 —— 宁可空一点,也不要压在别人身上。
|
|
604
|
+
*
|
|
605
|
+
* 只跟**同一层**的东西比位置:别的文件夹里的卡不在这块桌面上。
|
|
606
|
+
*/
|
|
607
|
+
const CELL_W = 320; const CELL_H = 220; const PAD = 10;
|
|
608
|
+
const dirOf = (id) => {
|
|
609
|
+
if (typeof id !== 'string' || id.startsWith('doc:')) return '';
|
|
610
|
+
const c = id.indexOf(':');
|
|
611
|
+
const path = (c > 0 && /^[a-z]+$/.test(id.slice(0, c))) ? id.slice(c + 1) : id;
|
|
612
|
+
const i = path.lastIndexOf('/');
|
|
613
|
+
return i > 0 ? path.slice(0, i) : '';
|
|
614
|
+
};
|
|
615
|
+
const sameLayer = Object.entries(board.objects)
|
|
616
|
+
.filter(([id]) => id !== oid && dirOf(id) === zid)
|
|
617
|
+
.map(([, o]) => o);
|
|
618
|
+
|
|
619
|
+
const cols = 4;
|
|
620
|
+
let slot = null;
|
|
621
|
+
for (let i = 0; i < 200 && !slot; i += 1) {
|
|
622
|
+
const x = PAD + (i % cols) * CELL_W;
|
|
623
|
+
const y = PAD + Math.floor(i / cols) * CELL_H;
|
|
624
|
+
const taken = sameLayer.some(o => Math.abs((o.x || 0) - x) < CELL_W / 2
|
|
625
|
+
&& Math.abs((o.y || 0) - y) < CELL_H / 2);
|
|
626
|
+
if (!taken) slot = { x, y };
|
|
627
|
+
}
|
|
628
|
+
if (!slot) slot = { x: PAD, y: PAD };
|
|
629
|
+
|
|
630
|
+
const zMax = Math.max(10, ...Object.values(board.objects).map(o => o.z || 0));
|
|
631
|
+
board.objects[oid] = {
|
|
632
|
+
...(board.objects[oid] || {}),
|
|
633
|
+
x: clampNum(slot.x, 0, board.size.w, PAD),
|
|
634
|
+
y: clampNum(slot.y, 0, board.size.h, PAD),
|
|
635
|
+
z: zMax + 1,
|
|
636
|
+
};
|
|
637
|
+
await writeBoard(pid, board);
|
|
638
|
+
return { board, zone: zid ? { id: zid } : null, placed: board.objects[oid] };
|
|
639
|
+
});
|
|
640
|
+
}
|