@xiaobuyu/nodesign 0.0.1 → 0.0.3
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/README.md +156 -106
- package/bin/nodesign.js +0 -0
- package/package.json +1 -1
- package/server/api/README.md +45 -45
- package/server/api/_guard.js +75 -75
- package/server/api/admin.js +187 -187
- package/server/api/assets/docx-page.js +103 -103
- package/server/api/assets/helpers.js +42 -22
- package/server/api/assets/notes.js +194 -144
- package/server/api/assets.js +891 -896
- package/server/api/board.js +134 -57
- package/server/api/browse.js +138 -138
- package/server/api/canvas.js +444 -444
- package/server/api/chatai.js +324 -324
- package/server/api/exports/build-standalone.js +980 -980
- package/server/api/exports/cards.js +118 -118
- package/server/api/exports/docx-pdf.js +46 -46
- package/server/api/exports/export-page.js +142 -142
- package/server/api/exports/handoff.js +147 -147
- package/server/api/exports.js +690 -690
- package/server/api/instruction.js +75 -75
- package/server/api/me.js +116 -116
- package/server/api/memory.js +185 -185
- package/server/api/pending-changes.js +366 -366
- package/server/api/plugins.js +164 -164
- package/server/api/projects.js +220 -220
- package/server/api/publish.js +64 -64
- package/server/api/recent.js +73 -73
- package/server/api/sessions.js +589 -589
- package/server/api/skills.js +77 -77
- package/server/api/standalone-fit.js +168 -168
- package/server/api/turn-compose.js +180 -180
- package/server/api/turn-inflight.js +44 -44
- package/server/auth/README.md +17 -17
- package/server/auth/middleware.js +138 -138
- package/server/auth/origin-guard.js +71 -71
- package/server/auth/session.js +108 -108
- package/server/auth/tier.js +113 -113
- package/server/auth/users-store.js +348 -348
- package/server/edit/README.md +29 -29
- package/server/engine/README.md +113 -113
- package/server/engine/agent/agent-shared.js +550 -550
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
- package/server/engine/agent/auto-mode-rules.js +78 -78
- package/server/engine/agent/context.js +418 -418
- package/server/engine/agent/events.js +465 -464
- package/server/engine/agent/hooks/canvas-validate.js +199 -199
- package/server/engine/agent/hooks/failure.js +120 -120
- package/server/engine/agent/hooks/file-events.js +75 -75
- package/server/engine/agent/hooks/lifecycle.js +233 -233
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
- package/server/engine/agent/hooks/post-guidance.js +206 -206
- package/server/engine/agent/hooks/post-subagent-report.js +104 -104
- package/server/engine/agent/hooks/post-trim.js +58 -58
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
- package/server/engine/agent/hooks/pre-defaults.js +84 -84
- package/server/engine/agent/hooks/pre-injectors.js +277 -277
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
- package/server/engine/agent/hooks/pre-starter-files.js +61 -61
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
- package/server/engine/agent/hooks/site-validate.js +94 -94
- package/server/engine/agent/hooks/tool-prompts.js +31 -31
- package/server/engine/agent/hooks/turn-state-memory.js +56 -56
- package/server/engine/agent/hooks/user-prompt-submit.js +258 -215
- package/server/engine/agent/hooks.js +354 -354
- package/server/engine/agent/init-contract.js +69 -69
- package/server/engine/agent/isolation.js +159 -159
- package/server/engine/agent/model-table.js +292 -292
- package/server/engine/agent/plugin-loader.js +266 -266
- package/server/engine/agent/prompts/nodesign-prelude.md +388 -336
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
- package/server/engine/agent/prompts/tools/site-reference.md +106 -106
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
- package/server/engine/agent/session-loop.js +1035 -1024
- package/server/engine/agent/session-model.js +164 -164
- package/server/engine/agent/skill.js +204 -204
- package/server/engine/agent/task-events.js +78 -78
- package/server/engine/agents/ds-extractor.md +132 -132
- package/server/engine/agents/explorer.md +190 -190
- package/server/engine/agents/index.js +230 -230
- package/server/engine/agents/schemas/design-system.json +156 -156
- package/server/engine/agents/schemas/tweak-schema.json +113 -113
- package/server/engine/agents/tweak-proposer.md +127 -127
- package/server/engine/agents/vision-checker.md +254 -254
- package/server/engine/browse/capture.js +422 -422
- package/server/engine/browse/card.js +137 -137
- package/server/engine/browse/handover.js +59 -59
- package/server/engine/browse/page-digest.js +119 -119
- package/server/engine/browse/refs.js +196 -196
- package/server/engine/browse/registry.js +307 -307
- package/server/engine/browse/screencast.js +170 -170
- package/server/engine/browse/state.js +121 -121
- package/server/engine/chatai/chat-log.js +79 -79
- package/server/engine/chatai/index.js +193 -193
- package/server/engine/chatai/openai-compat.js +152 -152
- package/server/engine/chatai/orchestrate.js +309 -309
- package/server/engine/chatai/perform.js +40 -40
- package/server/engine/chatai/summarize.js +79 -79
- package/server/engine/mcp/capability-gate.js +52 -52
- package/server/engine/mcp/index.js +351 -339
- package/server/engine/mcp/param-sanitizer.js +142 -142
- package/server/engine/mcp/tools/arrange-on-board.js +124 -124
- package/server/engine/mcp/tools/artifact-session.js +278 -278
- package/server/engine/mcp/tools/browse-computer.js +421 -421
- package/server/engine/mcp/tools/browse-find-batch.js +183 -183
- package/server/engine/mcp/tools/browse-screenshot.js +157 -157
- package/server/engine/mcp/tools/browse.js +480 -480
- package/server/engine/mcp/tools/build-docx.js +101 -101
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
- package/server/engine/mcp/tools/create-on-board.js +152 -155
- package/server/engine/mcp/tools/crystallize-skill.js +168 -168
- package/server/engine/mcp/tools/edit-sketch.js +167 -0
- package/server/engine/mcp/tools/explain-style.js +237 -237
- package/server/engine/mcp/tools/export-handoff.js +133 -133
- package/server/engine/mcp/tools/expose-tweaks.js +149 -149
- package/server/engine/mcp/tools/generate-image.js +842 -842
- package/server/engine/mcp/tools/get-computed-styles.js +164 -164
- package/server/engine/mcp/tools/get-pending-changes.js +205 -205
- package/server/engine/mcp/tools/h3box-ssh.js +84 -84
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
- package/server/engine/mcp/tools/highlight.js +68 -68
- package/server/engine/mcp/tools/list-pages.js +222 -222
- package/server/engine/mcp/tools/look-at-board.js +124 -0
- package/server/engine/mcp/tools/lookup-tags.js +59 -59
- package/server/engine/mcp/tools/navigate-to-page.js +60 -60
- package/server/engine/mcp/tools/organize-board.js +64 -64
- package/server/engine/mcp/tools/paint-still.js +434 -434
- package/server/engine/mcp/tools/pin-to-board.js +130 -130
- package/server/engine/mcp/tools/preview-deck.js +130 -130
- package/server/engine/mcp/tools/profile-scroll.js +305 -305
- package/server/engine/mcp/tools/publish-site.js +102 -102
- package/server/engine/mcp/tools/query-elements.js +195 -195
- package/server/engine/mcp/tools/read-board.js +190 -103
- package/server/engine/mcp/tools/read-document.js +93 -93
- package/server/engine/mcp/tools/read-page.js +264 -264
- package/server/engine/mcp/tools/read-tavern-json.js +143 -143
- package/server/engine/mcp/tools/read-user-view.js +59 -0
- package/server/engine/mcp/tools/record-decision.js +140 -140
- package/server/engine/mcp/tools/relate-on-board.js +112 -110
- package/server/engine/mcp/tools/report-issue.js +115 -115
- package/server/engine/mcp/tools/roll-film.js +266 -266
- package/server/engine/mcp/tools/screenshot-docx.js +138 -138
- package/server/engine/mcp/tools/screenshot-url.js +195 -195
- package/server/engine/mcp/tools/screenshot.js +583 -583
- package/server/engine/mcp/tools/sketch-on-board.js +337 -0
- package/server/engine/mcp/tools/tier-gate.js +96 -96
- package/server/engine/mcp/tools/trace-motion.js +215 -215
- package/server/engine/mcp/tools/web-search.js +548 -548
- package/server/engine/mcp/tools/write-on-board.js +130 -0
- package/server/engine/motion/inventory.js +349 -349
- package/server/engine/perception/session.js +235 -235
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +199 -192
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +169 -162
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +235 -228
- 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 -78
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +330 -323
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
- package/server/engine/runs/active-runs.js +698 -698
- package/server/engine/runs/board-tasklist.js +130 -0
- package/server/engine/runs/live-turn.js +252 -252
- package/server/engine/runs/store.js +364 -364
- package/server/engine/runs/turn-relay.js +217 -217
- package/server/engine/runtime/workspace.js +164 -164
- package/server/index.js +3 -0
- package/server/lib/artifact-file-path.js +80 -80
- package/server/lib/artifact-target.js +284 -284
- package/server/lib/asset-refs.js +239 -239
- package/server/lib/async-queue.js +104 -104
- package/server/lib/auto-relations.js +130 -130
- package/server/lib/binding-types.js +72 -60
- package/server/lib/board-graph-export.js +191 -0
- package/server/lib/board-groups.js +167 -0
- package/server/lib/board-hero.js +76 -0
- package/server/lib/board-kind-sizes.js +85 -62
- package/server/lib/board-relations.js +133 -133
- package/server/lib/browse-proxy.js +180 -180
- package/server/lib/canvas-id.js +36 -36
- package/server/lib/chalk.js +121 -0
- package/server/lib/cover.js +227 -227
- package/server/lib/danbooru-tags.js +291 -291
- package/server/lib/doc-extract.js +156 -156
- package/server/lib/docx/build-from-source.js +260 -260
- package/server/lib/docx/build.js +490 -490
- package/server/lib/docx/dump-styles.js +287 -287
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
- package/server/lib/docx/merge-runs.js +129 -129
- package/server/lib/docx/numbering.js +218 -218
- package/server/lib/docx/order.js +152 -152
- package/server/lib/docx/rawzip.js +172 -172
- package/server/lib/docx/render.js +92 -92
- package/server/lib/docx/text-lint.js +225 -225
- package/server/lib/docx/tokens.js +335 -335
- package/server/lib/docx/units.js +29 -29
- package/server/lib/docx/xml.js +291 -291
- package/server/lib/docx-pages.js +187 -187
- package/server/lib/export-collect.js +289 -289
- package/server/lib/export-package.js +210 -210
- package/server/lib/html-srcset.js +145 -145
- package/server/lib/image-variant.js +460 -460
- package/server/lib/ingress/forward-openai-chat.js +305 -305
- package/server/lib/ingress/openai-chat.js +491 -491
- package/server/lib/ingress/session-notice.js +66 -66
- package/server/lib/ingress/session-routes.js +68 -68
- package/server/lib/ingress/slot-probe.js +130 -130
- package/server/lib/ingress/upstream-billing.js +52 -52
- package/server/lib/ingress/upstream-fail-streak.js +72 -72
- package/server/lib/ingress/upstream-truncation.js +48 -48
- package/server/lib/issues-store.js +182 -182
- package/server/lib/kinds/deck.js +72 -72
- package/server/lib/kinds/docx.js +283 -283
- package/server/lib/kinds/file-kinds.js +113 -113
- package/server/lib/kinds/index.js +202 -202
- package/server/lib/kinds/site.js +161 -161
- package/server/lib/model-ingress.js +543 -543
- package/server/lib/moderation.js +255 -255
- package/server/lib/notice-store.js +103 -103
- package/server/lib/plugin-install.js +104 -104
- package/server/lib/plugin-validator.js +721 -721
- package/server/lib/publish-store.js +115 -115
- package/server/lib/quick-summary.js +52 -52
- package/server/lib/quota.js +270 -270
- package/server/lib/rate-window.js +29 -29
- package/server/lib/reference-assets.js +92 -92
- package/server/lib/region-shot.js +135 -135
- package/server/lib/safe-path.js +73 -73
- package/server/lib/sdk-session.js +33 -33
- package/server/lib/showcase-store.js +92 -92
- package/server/lib/site-publish.js +465 -465
- package/server/lib/sketch-layout.js +277 -0
- package/server/lib/ssrf-guard.js +432 -432
- package/server/lib/task-scan.js +158 -158
- package/server/lib/tavern-json.js +154 -154
- package/server/lib/video-variant.js +237 -237
- package/server/lib/workspace-path.js +33 -33
- package/server/ops/fix-sdk-musl.mjs +40 -40
- package/server/ops/install-macos-fonts.sh +114 -114
- package/server/ops/macos-fonts.conf +336 -336
- package/server/ops/sandbox-shim/bwrap +57 -57
- package/server/projects/assets-summary.js +119 -119
- package/server/projects/auto-name.js +53 -53
- package/server/projects/board-limits.js +12 -0
- package/server/projects/board-sanitize.js +252 -0
- package/server/projects/board-store.js +507 -640
- package/server/projects/move-entry.js +103 -103
- package/server/projects/store.js +270 -270
- package/server/projects/ui-config.js +60 -54
- package/server/projects/viewpoint-store.js +78 -0
- package/server/projects/workspace-templates.js +53 -53
- package/server/projects/workspace.js +1025 -1025
- package/server/runtime/local-config.js +177 -177
- package/server/runtime/profile.js +72 -72
- package/server/services/rembg-service.py +334 -334
- package/server/shared/README.md +25 -25
- package/server/shared/deck.js +54 -54
- package/server/shared/time.js +49 -49
- package/server/style-pipeline/README.md +53 -53
- package/server/ws/broker.js +39 -36
- package/server/ws/browse-channel.js +177 -177
- package/server/ws/index.js +386 -386
- package/web/README.md +89 -89
- package/web/dist/assets/BrowserWindow-P_vIdh-G.js +11 -0
- package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-BpLv5082.js} +3 -3
- package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-DSAU1-tD.js} +4 -4
- package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
- package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
- package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
- package/web/dist/assets/arc-D_khH_xN.js +1 -0
- package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
- package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
- package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
- package/web/dist/assets/channel-DGkccMO1.js +1 -0
- package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
- package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
- package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
- package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
- package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
- package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
- package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
- package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
- package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
- package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
- package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
- package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
- package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
- package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
- package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
- package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
- package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
- package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
- package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
- package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
- package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
- package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
- package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
- package/web/dist/assets/index-GAamh1p4.js +2152 -0
- package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
- package/web/dist/assets/init-Gi6I4Gst.js +1 -0
- package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
- package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
- package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
- package/web/dist/assets/layout-CnXiF_NB.js +1 -0
- package/web/dist/assets/linear-BMW0u6G6.js +1 -0
- package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
- package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
- package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
- package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
- package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
- package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
- package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
- package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-KAfChbGV.js} +1 -1
- package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
- package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
- package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
- package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
- package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
- package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
- package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
- package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
- package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
- package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
- package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
- package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
- package/web/dist/index.html +17 -17
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
- package/web/dist/assets/SiteWindow-D6eS-2gM.js +0 -18
- package/web/dist/assets/index-Cbl3ATvp.js +0 -2125
|
@@ -1,640 +1,507 @@
|
|
|
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 {
|
|
24
|
-
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*/
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
*
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
}
|
|
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 { CHALK_DIR } from '../lib/chalk.js';
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_BOARD_SIZE, MAX_BOARD_BYTES, MAX_OBJECTS, MAX_ZONES, MAX_BINDINGS,
|
|
27
|
+
} from './board-limits.js';
|
|
28
|
+
export { TEXT_FONTS } from './board-sanitize.js';
|
|
29
|
+
import { DEFAULT_BOARD_SIZE, MAX_BOARD_BYTES, MAX_OBJECTS, MAX_ZONES, MAX_BINDINGS } from './board-limits.js';
|
|
30
|
+
import {
|
|
31
|
+
clampNum, sanitizeSize, sanitizeTag, sanitizeObject, sanitizeBinding, sanitizeZone, sanitizeBoard, isSafeCanvasId,
|
|
32
|
+
} from './board-sanitize.js';
|
|
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
|
+
export async function readBoard(pid) {
|
|
52
|
+
try {
|
|
53
|
+
const raw = await fs.readFile(boardPath(pid), 'utf8');
|
|
54
|
+
return sanitizeBoard(JSON.parse(raw));
|
|
55
|
+
} catch {
|
|
56
|
+
return { size: { ...DEFAULT_BOARD_SIZE }, zones: {}, objects: {}, bindings: {} };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function writeBoard(pid, board) {
|
|
61
|
+
const json = JSON.stringify(board);
|
|
62
|
+
if (Buffer.byteLength(json) > MAX_BOARD_BYTES) {
|
|
63
|
+
const err = new Error('board layout too large');
|
|
64
|
+
err.status = 400;
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
await ensureProjectWorkspace(pid);
|
|
68
|
+
await fs.writeFile(boardPath(pid), json, 'utf8');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 全量替换(前端 reset / 兼容旧 PUT)。 */
|
|
72
|
+
export function replaceBoard(pid, raw) {
|
|
73
|
+
return withBoardLock(pid, async () => {
|
|
74
|
+
const board = sanitizeBoard(raw);
|
|
75
|
+
await writeBoard(pid, board);
|
|
76
|
+
return board;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* diff 式合并写:{ size?, objects?: {id: obj|null}, zones?: {id: zone|null} }
|
|
82
|
+
* null 值 = 删除该条目。返回合并后的完整 board。
|
|
83
|
+
*/
|
|
84
|
+
export function patchBoard(pid, patch) {
|
|
85
|
+
return withBoardLock(pid, async () => {
|
|
86
|
+
const board = await readBoard(pid);
|
|
87
|
+
if (patch?.size) board.size = sanitizeSize(patch.size);
|
|
88
|
+
// 本次被显式删掉的端点 id。**不能拿 board.objects 的成员资格当存在性判据**:
|
|
89
|
+
// 它是稀疏的(只存被拖过 / pin 过的物件,没动过的产物压根没有条目),
|
|
90
|
+
// 那样会把连向"还没被摆过的产物"的线全误删。只清确实删了的。
|
|
91
|
+
const removed = new Set();
|
|
92
|
+
// 拿着旧 id 迟到的写入要落到新名字上(前端 800ms 防抖 / agent 本轮的旧路径)。
|
|
93
|
+
// 不转发的话它会把刚改完名的旧条目**重新插回来**,画布上多出一张回到默认
|
|
94
|
+
// 位置的重影,而且不报错。见 renameBoardPaths 上面那段。
|
|
95
|
+
const fwd = (id) => forwardId(pid, id);
|
|
96
|
+
if (patch?.zones && typeof patch.zones === 'object') {
|
|
97
|
+
for (const [rawId, z] of Object.entries(patch.zones)) {
|
|
98
|
+
if (!isSafeCanvasId(rawId)) continue;
|
|
99
|
+
const id = fwd(rawId);
|
|
100
|
+
if (z === null) { delete board.zones[id]; removed.add(id); continue; }
|
|
101
|
+
const s = sanitizeZone(z);
|
|
102
|
+
if (s && (board.zones[id] || Object.keys(board.zones).length < MAX_ZONES)) board.zones[id] = s;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (patch?.objects && typeof patch.objects === 'object') {
|
|
106
|
+
for (const [rawId, o] of Object.entries(patch.objects)) {
|
|
107
|
+
if (!isSafeCanvasId(rawId)) continue;
|
|
108
|
+
const id = fwd(rawId);
|
|
109
|
+
if (o === null) { delete board.objects[id]; removed.add(id); continue; }
|
|
110
|
+
const s = sanitizeObject(o, board.size);
|
|
111
|
+
if (s && (board.objects[id] || Object.keys(board.objects).length < MAX_OBJECTS)) board.objects[id] = s;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (patch?.bindings && typeof patch.bindings === 'object') {
|
|
115
|
+
for (const [id, b] of Object.entries(patch.bindings)) {
|
|
116
|
+
if (typeof id !== 'string' || id.length > 300) continue;
|
|
117
|
+
if (b === null) { delete board.bindings[id]; continue; }
|
|
118
|
+
const s = sanitizeBinding(b);
|
|
119
|
+
if (!s) continue;
|
|
120
|
+
// 端点也要转发:agent 本轮拿旧路径连的线,落下来必须连到新名字上
|
|
121
|
+
const fixed = { ...s, from: fwd(s.from), to: fwd(s.to) };
|
|
122
|
+
if (board.bindings[id] || Object.keys(board.bindings).length < MAX_BINDINGS) board.bindings[id] = fixed;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// 主角覆盖:null = 撤销(回到 pickHero 自动推断),字符串 = 显式立主角
|
|
126
|
+
if (patch?.hero !== undefined) {
|
|
127
|
+
if (patch.hero === null) delete board.hero;
|
|
128
|
+
else if (typeof patch.hero === 'string' && patch.hero.length <= 300) board.hero = fwd(patch.hero);
|
|
129
|
+
}
|
|
130
|
+
// 端点被删掉的线一起清掉,否则画布上留一条连向虚空的线。放在最后:
|
|
131
|
+
// 这一趟才看得到本次删除的全貌(同一个 patch 里可能既删物件又加线)。
|
|
132
|
+
if (removed.size) {
|
|
133
|
+
for (const [id, b] of Object.entries(board.bindings)) {
|
|
134
|
+
if (removed.has(b.from) || removed.has(b.to)) delete board.bindings[id];
|
|
135
|
+
}
|
|
136
|
+
if (board.hero && removed.has(board.hero)) delete board.hero;
|
|
137
|
+
}
|
|
138
|
+
await writeBoard(pid, board);
|
|
139
|
+
return board;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── 改名(2026-08-08)────────────────────────────────────────────────────
|
|
144
|
+
//
|
|
145
|
+
// id 是**工作区相对路径**,所以文件一动,画布上的身份就跟着变。移动从罕见事件
|
|
146
|
+
// 变成日常动作(拖进文件夹 = 真 mv)之后,这件事必须有一等公民的地位。
|
|
147
|
+
//
|
|
148
|
+
// ## 为什么不能用 patchBoard 删旧插新
|
|
149
|
+
//
|
|
150
|
+
// patchBoard 末尾那段"端点被删的线一起清掉"会把指向旧 id 的 `annotates` 线
|
|
151
|
+
// 一并删掉 —— 也就是**每拖一次卡,挂在它上面的评论就没了**。删除和改名是
|
|
152
|
+
// 两件事:删除意味着"这东西不在了,连着它的线也没意义了",改名意味着
|
|
153
|
+
// "还是它,换了个名字"。用同一个动词表达,必然丢掉后者的语义。
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 改名的转发表:**旧 id → 新 id**,带 TTL。
|
|
157
|
+
*
|
|
158
|
+
* 改名不是一次写入,是三个并发写方都得学会的一件事:
|
|
159
|
+
* ① 服务端(这里)——立刻知道
|
|
160
|
+
* ② 前端画布 —— scheduleSave 是 800ms 防抖,从 layoutRef 按**旧 id** 组 patch,
|
|
161
|
+
* 改完名之后那一发迟到的 flush 会把 objects[旧id] 重新插回来
|
|
162
|
+
* ③ agent —— 这一轮的上下文里还是旧路径,会继续往旧 id 上 pin、上批注
|
|
163
|
+
*
|
|
164
|
+
* 不转发的话,表现是"拖进文件夹时灵时不灵"(迟到的写入把旧条目复活,画布上
|
|
165
|
+
* 就多出一张回到默认位置的重影)。这类症状最难查,因为它**不报错**。
|
|
166
|
+
*
|
|
167
|
+
* 内存表 + TTL 够用:它只需要盖住"改完名之后还有人拿着旧 id"那个窗口。
|
|
168
|
+
* 进程重启、以及 agent 在画布背后自己 mv 的情况,由 git 改名对账兜底
|
|
169
|
+
* (见 workspace.js 的 reconcileBoardRenames)。
|
|
170
|
+
*/
|
|
171
|
+
/**
|
|
172
|
+
* 草稿落定(2026-08-23 黑板):把 staging 位清掉,tag 留着当逻辑分组。
|
|
173
|
+
* 不传 tag = 这个项目上所有草稿一起落定(回合结束的兜底用它)。
|
|
174
|
+
* 返回落定了几件,0 = 没草稿,不写盘。
|
|
175
|
+
*/
|
|
176
|
+
export function commitStaging(pid, { tag = null } = {}) {
|
|
177
|
+
return withBoardLock(pid, async () => {
|
|
178
|
+
const board = await readBoard(pid);
|
|
179
|
+
let n = 0;
|
|
180
|
+
const hit = (e) => e?.staging === true && (!tag || e.tag === tag);
|
|
181
|
+
for (const o of Object.values(board.objects || {})) if (hit(o)) { delete o.staging; n += 1; }
|
|
182
|
+
for (const b of Object.values(board.bindings || {})) if (hit(b)) { delete b.staging; n += 1; }
|
|
183
|
+
if (n) await writeBoard(pid, board);
|
|
184
|
+
return { board, committed: n };
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 按标签整组删除(物件 + 线)。黑板擦 —— 用户或 agent 把一次头脑风暴整块抹掉。
|
|
190
|
+
* 删画布原生物件(text/scribble)和这组的板书文件(notes/板书/,它们是这组自己的话);
|
|
191
|
+
* 带同一标签的产物卡只摘标签不删座位,产物的本体不归黑板擦管。
|
|
192
|
+
*/
|
|
193
|
+
export function removeByTag(pid, tag) {
|
|
194
|
+
const t = sanitizeTag(tag);
|
|
195
|
+
if (!t) return Promise.resolve({ board: null, removed: 0 });
|
|
196
|
+
return withBoardLock(pid, async () => {
|
|
197
|
+
const board = await readBoard(pid);
|
|
198
|
+
let removed = 0;
|
|
199
|
+
const gone = new Set();
|
|
200
|
+
for (const [id, o] of Object.entries(board.objects || {})) {
|
|
201
|
+
if (o.tag !== t) continue;
|
|
202
|
+
if (o.kind) { delete board.objects[id]; gone.add(id); removed += 1; continue; }
|
|
203
|
+
// 板书(notes/板书/*.md)是这组自己的话,擦组连文件一起删;其它文件只摘标签。
|
|
204
|
+
// ⛔ 删前按绝对路径断言落在 notes/板书/ 里且是单段文件名(fable 08-23 审出:id 里塞 ../ 能
|
|
205
|
+
// 以进程身份删任意文件;sanitize 现在也拒 `..`,这里是第二道闸)
|
|
206
|
+
if (id.startsWith(`${CHALK_DIR}/`)) {
|
|
207
|
+
const abs = chalkAbsPath(pid, id);
|
|
208
|
+
if (abs) { try { await fs.unlink(abs); } catch { /* 已经没了 */ } }
|
|
209
|
+
delete board.objects[id]; gone.add(id); removed += 1; continue;
|
|
210
|
+
}
|
|
211
|
+
delete o.tag;
|
|
212
|
+
}
|
|
213
|
+
for (const [id, b] of Object.entries(board.bindings || {})) {
|
|
214
|
+
if (b.tag === t || gone.has(b.from) || gone.has(b.to)) { delete board.bindings[id]; removed += 1; }
|
|
215
|
+
}
|
|
216
|
+
if (board.hero && gone.has(board.hero)) delete board.hero;
|
|
217
|
+
await writeBoard(pid, board);
|
|
218
|
+
return { board, removed };
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** 板书 id → 绝对路径,越界/多段/非 .md 一律 null(删与读两处共用) */
|
|
223
|
+
export function chalkAbsPath(pid, id) {
|
|
224
|
+
if (typeof id !== 'string' || !id.startsWith(`${CHALK_DIR}/`)) return null;
|
|
225
|
+
const name = id.slice(CHALK_DIR.length + 1);
|
|
226
|
+
if (!name || name.includes('/') || name.includes('\\') || name.includes('..') || name.startsWith('.') || !name.endsWith('.md')) return null;
|
|
227
|
+
const base = path.resolve(getSharedDir(pid), CHALK_DIR);
|
|
228
|
+
const abs = path.resolve(base, name);
|
|
229
|
+
return abs.startsWith(base + path.sep) ? abs : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const RENAME_TTL_MS = 5 * 60 * 1000;
|
|
233
|
+
/** @type {Map<string, Map<string, { to: string, at: number }>>} pid → (旧 id → 新 id) */
|
|
234
|
+
const renameJournal = new Map();
|
|
235
|
+
|
|
236
|
+
function journalOf(pid) {
|
|
237
|
+
let m = renameJournal.get(pid);
|
|
238
|
+
if (!m) { m = new Map(); renameJournal.set(pid, m); }
|
|
239
|
+
return m;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function noteRenames(pid, pairs, now) {
|
|
243
|
+
const j = journalOf(pid);
|
|
244
|
+
for (const [from, to] of pairs) j.set(from, { to, at: now });
|
|
245
|
+
for (const [k, v] of j) if (now - v.at > RENAME_TTL_MS) j.delete(k);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 把一个可能过期的 id 换成它现在的名字。
|
|
250
|
+
* 顺着链走(`a→b` 之后又 `b→c`,拿着 a 的迟到写入要落到 c 上)。
|
|
251
|
+
*/
|
|
252
|
+
export function forwardId(pid, id, now = Date.now()) {
|
|
253
|
+
const j = renameJournal.get(pid);
|
|
254
|
+
if (!j || typeof id !== 'string') return id;
|
|
255
|
+
let cur = id;
|
|
256
|
+
for (let hop = 0; hop < 8; hop++) {
|
|
257
|
+
const e = j.get(cur);
|
|
258
|
+
if (!e || now - e.at > RENAME_TTL_MS) return cur;
|
|
259
|
+
cur = e.to;
|
|
260
|
+
}
|
|
261
|
+
return cur;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 把一个**文件路径**换成它现在的位置 —— forwardId 的前缀感知版(#4)。
|
|
266
|
+
*
|
|
267
|
+
* forwardId 只做精确匹配,够画布用(物件 id 整个换)。但 artifact-file
|
|
268
|
+
* 拿的是文件路径:文件夹 `稿件` 改成 `定稿` 之后,开着的窗还在请求
|
|
269
|
+
* `稿件/主稿.html` —— 表里只有 `稿件 → 定稿` 这一条,精确匹配永远打不中。
|
|
270
|
+
* 这里按**最长前缀**找(全路径 → 上级 → 再上级),命中就把前缀换掉接着走链。
|
|
271
|
+
*/
|
|
272
|
+
export function forwardPath(pid, rel, now = Date.now()) {
|
|
273
|
+
const j = renameJournal.get(pid);
|
|
274
|
+
if (!j || typeof rel !== 'string' || !rel) return rel;
|
|
275
|
+
let cur = rel;
|
|
276
|
+
for (let hop = 0; hop < 8; hop++) {
|
|
277
|
+
let next = null;
|
|
278
|
+
const segs = cur.split('/');
|
|
279
|
+
for (let i = segs.length; i > 0; i--) {
|
|
280
|
+
const prefix = segs.slice(0, i).join('/');
|
|
281
|
+
const e = j.get(prefix);
|
|
282
|
+
if (e && now - e.at <= RENAME_TTL_MS) { next = e.to + cur.slice(prefix.length); break; }
|
|
283
|
+
}
|
|
284
|
+
if (!next || next === cur) return cur;
|
|
285
|
+
cur = next;
|
|
286
|
+
}
|
|
287
|
+
return cur;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** 只在测试里用:清掉转发表,免得用例之间串味 */
|
|
291
|
+
export function _resetRenameJournal() { renameJournal.clear(); }
|
|
292
|
+
/** 只在测试里用:往转发表里记一笔(noteRenames 不导出,真路径走 renameBoardPaths) */
|
|
293
|
+
export function _noteRenamesForTest(pid, pairs, now = Date.now()) { noteRenames(pid, pairs, now); }
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 一次 id 改名:物件、文件夹、以及所有关系线端点一起改。
|
|
297
|
+
*
|
|
298
|
+
* 文件夹改名是**前缀改名** —— `稿件` → `定稿` 要连带它下面的一切:
|
|
299
|
+
* 子文件夹 `稿件/初稿`、物件 `deck:稿件/主稿.html`、裸路径 `稿件/说明.md`。
|
|
300
|
+
* 而且同一个目录**同时**可能是文件夹(zones['稿件'])和产物(objects['site:稿件']),
|
|
301
|
+
* 两个命名空间都得一致地改 —— 今天 rewriteBoardIds 的 mapEnd 就在这个形状上
|
|
302
|
+
* 栽过一次(文件夹端点掉进物件那条分支,被原样放行成了断头线)。
|
|
303
|
+
*
|
|
304
|
+
* @param {string} pid
|
|
305
|
+
* @param {Array<[string, string]>} pairs [旧路径, 新路径],**路径不带 kind 前缀**
|
|
306
|
+
* @returns {Promise<{ board: object, renamed: number }>}
|
|
307
|
+
*/
|
|
308
|
+
export function renameBoardPaths(pid, pairs) {
|
|
309
|
+
const clean = (pairs || [])
|
|
310
|
+
.map(([a, b]) => [String(a || '').replace(/\/+$/, ''), String(b || '').replace(/\/+$/, '')])
|
|
311
|
+
.filter(([a, b]) => a && b && a !== b);
|
|
312
|
+
if (!clean.length) return Promise.resolve({ board: null, renamed: 0 });
|
|
313
|
+
|
|
314
|
+
return withBoardLock(pid, async () => {
|
|
315
|
+
const board = await readBoard(pid);
|
|
316
|
+
const now = Date.now();
|
|
317
|
+
const applied = [];
|
|
318
|
+
|
|
319
|
+
/** 一个 id(可能带 `deck:` 之类前缀)在这批改名下的新名字;没动就返回原值 */
|
|
320
|
+
const mapId = (id) => {
|
|
321
|
+
if (typeof id !== 'string' || !id) return id;
|
|
322
|
+
const c = id.indexOf(':');
|
|
323
|
+
// kind 前缀只认字母(`deck:` `site:`)—— 路径里的冒号不算前缀
|
|
324
|
+
const prefix = c > 0 && /^[a-z]+$/.test(id.slice(0, c)) ? id.slice(0, c + 1) : '';
|
|
325
|
+
const p = id.slice(prefix.length);
|
|
326
|
+
for (const [from, to] of clean) {
|
|
327
|
+
if (p === from) return prefix + to;
|
|
328
|
+
if (p.startsWith(from + '/')) return prefix + to + p.slice(from.length);
|
|
329
|
+
}
|
|
330
|
+
return id;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const remap = (bag) => {
|
|
334
|
+
const src = bag || {};
|
|
335
|
+
const next = {};
|
|
336
|
+
// **先把没改名的放进去占位**,再放改了名的。撞名时该让路的是"搬过来的
|
|
337
|
+
// 那个",不是原地那个 —— 按插入顺序一趟写的话,改名源恰好排在前面就会
|
|
338
|
+
// 把活着的条目静默顶掉,而顺序是 JSON 键序,纯属看运气。
|
|
339
|
+
for (const [id, v] of Object.entries(src)) if (mapId(id) === id) next[id] = v;
|
|
340
|
+
for (const [id, v] of Object.entries(src)) {
|
|
341
|
+
const n = mapId(id);
|
|
342
|
+
if (n === id) continue;
|
|
343
|
+
if (next[n]) {
|
|
344
|
+
// 目标位置有活的 —— 搬不过去就**留在原地**,不能一声不响地丢掉。
|
|
345
|
+
// (只写 continue 的话这条从 next 里彻底消失,用户的卡凭空没了。)
|
|
346
|
+
if (!next[id]) next[id] = v;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
next[n] = v;
|
|
350
|
+
applied.push([id, n]);
|
|
351
|
+
}
|
|
352
|
+
return next;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
board.objects = remap(board.objects);
|
|
356
|
+
board.zones = remap(board.zones);
|
|
357
|
+
|
|
358
|
+
// 显式归属字段也要跟着改名。`objects[*].zone` 是活的:pin_to_board 写它、
|
|
359
|
+
// 拖进文件夹写它。只改键不改这个值的话,文件夹一改名,所有显式归属的成员
|
|
360
|
+
// 就指向一个不存在的文件夹(画布上表现为"卡片从文件夹里掉出来了")。
|
|
361
|
+
let touchedField = false;
|
|
362
|
+
for (const o of Object.values(board.objects)) {
|
|
363
|
+
if (typeof o.zone !== 'string' || !o.zone) continue;
|
|
364
|
+
const n = mapId(o.zone);
|
|
365
|
+
if (n !== o.zone) { o.zone = n; touchedField = true; }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 关系线端点。**这里改了也要算数** —— board.objects 是稀疏的,一条线完全
|
|
369
|
+
// 可以指向一个没有坐标条目的产物,那种情况下 applied 是空的,可对账位点
|
|
370
|
+
// 照样往前推:不落盘 = 这次改名永久丢失,下次再也算不出来。
|
|
371
|
+
let touchedBinding = false;
|
|
372
|
+
for (const b of Object.values(board.bindings || {})) {
|
|
373
|
+
const from = mapId(b.from);
|
|
374
|
+
const to = mapId(b.to);
|
|
375
|
+
if (from !== b.from || to !== b.to) { b.from = from; b.to = to; touchedBinding = true; }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (applied.length || touchedField || touchedBinding) {
|
|
379
|
+
await writeBoard(pid, board);
|
|
380
|
+
}
|
|
381
|
+
// 转发表**永远**记裸路径对,不只记 applied:applied 是画布条目的改名
|
|
382
|
+
//(deck 带 `deck:` 前缀、没上画布的文件根本不在里面),而 artifact-file
|
|
383
|
+
// 的改名转发(#4)查的是文件路径 —— 只记 applied 的话,改一个 deck 或者
|
|
384
|
+
// 一个没有画布条目的文件,开着的窗照样 404。
|
|
385
|
+
noteRenames(pid, [...clean, ...applied], now);
|
|
386
|
+
return { board, renamed: applied.length };
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* 对账:把 agent 在画布背后做的改名,补写进 board.json。
|
|
392
|
+
*
|
|
393
|
+
* 转发表(上面那个)盖的是"刚改完名、还有人拿着旧 id"的那几分钟窗口,活在内存里。
|
|
394
|
+
* 这一条盖的是另一半:**画布根本没参与的移动** —— agent 一句 `mv`、一次
|
|
395
|
+
* 重构目录,进程重启,都在它的覆盖范围里。
|
|
396
|
+
*
|
|
397
|
+
* 跑在扫产物清单的路上(打开项目必调),所以用户看到的第一帧就是对齐过的。
|
|
398
|
+
* 没有新 commit 时是一次 rev-parse 的事。
|
|
399
|
+
*
|
|
400
|
+
* 同步位点存在 `.nd/board-sync.json`:`.nd/` 是 gitignore 的,这个位点记的是
|
|
401
|
+
* "我的画布追到哪了",属于本地状态,不该进项目历史被别的会话读到。
|
|
402
|
+
*/
|
|
403
|
+
const SYNC_FILE = 'board-sync.json';
|
|
404
|
+
|
|
405
|
+
function syncPath(pid) {
|
|
406
|
+
return path.join(getSharedDir(pid), '.nd', SYNC_FILE);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export async function reconcileBoardRenames(pid) {
|
|
410
|
+
let seen = null;
|
|
411
|
+
try { seen = JSON.parse(await fs.readFile(syncPath(pid), 'utf8'))?.commit || null; } catch { /* 首次 */ }
|
|
412
|
+
|
|
413
|
+
const { head, renames } = await gitRenamesSince(pid, seen);
|
|
414
|
+
if (!head) return { renamed: 0 };
|
|
415
|
+
|
|
416
|
+
let renamed = 0;
|
|
417
|
+
if (renames.length) {
|
|
418
|
+
({ renamed } = await renameBoardPaths(pid, renames));
|
|
419
|
+
if (renamed) console.log(`[board] ${pid} 跟上 ${renames.length} 个改名,改了 ${renamed} 条画布条目`);
|
|
420
|
+
}
|
|
421
|
+
// 位点无条件推进(哪怕这次没有改名)——否则每次都要重算同一段 diff。
|
|
422
|
+
// 首次跑时 seen 是 null,gitRenamesSince 直接返回空,等于"从现在开始跟",
|
|
423
|
+
// 不去回放整段历史:历史里的改名早就体现在当前 board.json 里了。
|
|
424
|
+
if (seen !== head) {
|
|
425
|
+
try {
|
|
426
|
+
await fs.mkdir(path.dirname(syncPath(pid)), { recursive: true });
|
|
427
|
+
await fs.writeFile(syncPath(pid), JSON.stringify({ commit: head }), 'utf8');
|
|
428
|
+
} catch { /* 写不了位点:下次重算,不影响正确性 */ }
|
|
429
|
+
}
|
|
430
|
+
return { renamed };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// `nextZoneRect`(给新建的工作区自动铺位)2026-08-13 删除:服务端不再新建
|
|
434
|
+
// 文件夹条目 —— 文件夹的权威是磁盘扫描,前端按 newStackedZoneRect 给它安排
|
|
435
|
+
// 位置。服务端这份自动铺位是当年 pinToZone 会凭空造区留下的。
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* 把一个物件摆到画布上的一个空位并置顶(`pin_to_board` 用)。
|
|
439
|
+
* 单锁原子操作。槽位按 244×210 网格估算(服务端不知道物件真实尺寸,取最大卡片脚印)。
|
|
440
|
+
*
|
|
441
|
+
* ## 2026-08-13 改了三件事
|
|
442
|
+
*
|
|
443
|
+
* 1. **两个 id 都过转发表。** 以前直接 `board.objects[objectId] = …`,agent 在
|
|
444
|
+
* 改名窗口(TTL 5 分钟)里 pin 一下,就往 board.json 里插一条指向旧路径的
|
|
445
|
+
* 条目 —— 画布上多一张回到默认位置的重影,而且不报错。这正是
|
|
446
|
+
* `renameBoardPaths` 上面那段注释拼死在防的东西,这条写入口是唯一的漏网。
|
|
447
|
+
* 2. **不再写 `zone` 显式归属字段。** id = 路径之后,"它属于哪个文件夹"由路径
|
|
448
|
+
* 回答,再写一个字段只会让画布和磁盘各执一词。
|
|
449
|
+
* 3. **不再凭空新建文件夹条目。** 文件夹的权威是磁盘扫描;board.json 里那条
|
|
450
|
+
* 只是坐标。造一条磁盘上不存在的出来,就是一块剪不掉的僵尸框。
|
|
451
|
+
* 文件夹没坐标(还没被摆过)时就摆到桌面上,前端下一轮会给它安排位置。
|
|
452
|
+
*/
|
|
453
|
+
export function pinToZone(pid, { objectId, zoneId = '' }) {
|
|
454
|
+
return withBoardLock(pid, async () => {
|
|
455
|
+
const board = await readBoard(pid);
|
|
456
|
+
const now = Date.now();
|
|
457
|
+
const oid = forwardId(pid, objectId, now);
|
|
458
|
+
const zid = zoneId ? forwardId(pid, zoneId, now) : '';
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* 找一个空位。
|
|
462
|
+
*
|
|
463
|
+
* ⚠️ 2026-08-13 重写。原来这里按「区矩形 1120×640 + 40px 标题栏 + 244×210
|
|
464
|
+
* 网格」算槽位 —— 那是「文件夹是版面上摊开的一块地」时代的几何,画布上
|
|
465
|
+
* 早就没有这些东西了(区内坐标系随刀 3 整段退役)。算出来的位置跟前端的
|
|
466
|
+
* 排布不在一个坐标语言里。
|
|
467
|
+
*
|
|
468
|
+
* 现在跟前端对齐成同一件事:**一层就是一片桌面,卡按格子铺**。服务端不知道
|
|
469
|
+
* 每张卡多大(那是前端形态表的事),所以取一个够大的格子(最宽的卡 640 ×
|
|
470
|
+
* 一行的常见高度 220)做保守估算 —— 宁可空一点,也不要压在别人身上。
|
|
471
|
+
*
|
|
472
|
+
* 只跟**同一层**的东西比位置:别的文件夹里的卡不在这块桌面上。
|
|
473
|
+
*/
|
|
474
|
+
const CELL_W = 320; const CELL_H = 220; const PAD = 10;
|
|
475
|
+
const dirOf = (id) => {
|
|
476
|
+
if (typeof id !== 'string' || id.startsWith('doc:')) return '';
|
|
477
|
+
const c = id.indexOf(':');
|
|
478
|
+
const path = (c > 0 && /^[a-z]+$/.test(id.slice(0, c))) ? id.slice(c + 1) : id;
|
|
479
|
+
const i = path.lastIndexOf('/');
|
|
480
|
+
return i > 0 ? path.slice(0, i) : '';
|
|
481
|
+
};
|
|
482
|
+
const sameLayer = Object.entries(board.objects)
|
|
483
|
+
.filter(([id]) => id !== oid && dirOf(id) === zid)
|
|
484
|
+
.map(([, o]) => o);
|
|
485
|
+
|
|
486
|
+
const cols = 4;
|
|
487
|
+
let slot = null;
|
|
488
|
+
for (let i = 0; i < 200 && !slot; i += 1) {
|
|
489
|
+
const x = PAD + (i % cols) * CELL_W;
|
|
490
|
+
const y = PAD + Math.floor(i / cols) * CELL_H;
|
|
491
|
+
const taken = sameLayer.some(o => Math.abs((o.x || 0) - x) < CELL_W / 2
|
|
492
|
+
&& Math.abs((o.y || 0) - y) < CELL_H / 2);
|
|
493
|
+
if (!taken) slot = { x, y };
|
|
494
|
+
}
|
|
495
|
+
if (!slot) slot = { x: PAD, y: PAD };
|
|
496
|
+
|
|
497
|
+
const zMax = Math.max(10, ...Object.values(board.objects).map(o => o.z || 0));
|
|
498
|
+
board.objects[oid] = {
|
|
499
|
+
...(board.objects[oid] || {}),
|
|
500
|
+
x: clampNum(slot.x, 0, board.size.w, PAD),
|
|
501
|
+
y: clampNum(slot.y, 0, board.size.h, PAD),
|
|
502
|
+
z: zMax + 1,
|
|
503
|
+
};
|
|
504
|
+
await writeBoard(pid, board);
|
|
505
|
+
return { board, zone: zid ? { id: zid } : null, placed: board.objects[oid] };
|
|
506
|
+
});
|
|
507
|
+
}
|