@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,289 +1,289 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/export-collect.js — 按「产物卡」收集要导出的东西(2026-08-17)
|
|
3
|
-
*
|
|
4
|
-
* 导出的寻址单位从「会话当前产物」换成**画布上的那张卡**:卡的类型决定收什么。
|
|
5
|
-
* site 卡收站点那棵树、图片卡收那张图,以此类推。
|
|
6
|
-
*
|
|
7
|
-
* 为什么按卡 id 就够、不用再造一份枚举:
|
|
8
|
-
* 卡 id 本身**就是地址**(`site:<产物根>` / `deck:<html路径>` / 文件卡是裸路径),
|
|
9
|
-
* 前端 `BoardCanvas` 拼它、服务端 `board-store.mapId` 认它,用的是同一套前缀判据。
|
|
10
|
-
* 这里只解析地址、落到磁盘上收,权威清单仍然只有 `/artifacts` 那一份。
|
|
11
|
-
*
|
|
12
|
-
* 素材只收**这份产物真正引用到的**(`asset-refs.js` 扫出来的),不再像旧交付包那样
|
|
13
|
-
* 把整个项目级 `shared/assets` 一锅端 —— 那会把别的任务的图一起交出去,生产上最大的
|
|
14
|
-
* 项目那个目录有 280MB。
|
|
15
|
-
*
|
|
16
|
-
* ── 2026-08-17 评审后修的四个洞(都有单测钉着)──
|
|
17
|
-
* 1. **根级站点卡**(`site:`,rel 为空 —— 扁平化后「根 index.html = 一个站」是常态)
|
|
18
|
-
* 从工作区根盲走,把 `assets/`、`exports/`、`board.json`、别的任务全打进包,
|
|
19
|
-
* 280MB 病原样复活。
|
|
20
|
-
* 2. **单页站点卡**(`site:任务/_drafts/试作.html`)整类 400 —— 它是画布上真实存在
|
|
21
|
-
* 的一种卡,收集器却要求 site 必须是目录。
|
|
22
|
-
* 3. **`.ndignore` 基准**从卡 id 字符串上切第一段,只有「深度 1 的任务文件夹」这一
|
|
23
|
-
* 种布局碰巧对;根级构建站、嵌套站全取错,忽略规则静默失效。
|
|
24
|
-
* 4. **软链逃逸**:`safeResolve` 只做词法检查,工作区里一个指向外面的软链能把
|
|
25
|
-
* 工作区外的文件收进包(实测读出过 /tmp 下的文件内容)。
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import path from 'node:path';
|
|
29
|
-
import fs from 'node:fs/promises';
|
|
30
|
-
import { walkTaskFiles, loadIgnore, RESERVED_DIRS, isReservedFile } from './task-scan.js';
|
|
31
|
-
import { collectAssetRefs } from './asset-refs.js';
|
|
32
|
-
import { kindDef, taskManifest, can } from './kinds/index.js';
|
|
33
|
-
import { fileKindOfPath, fileKindDef } from './kinds/file-kinds.js';
|
|
34
|
-
// 越界判据收在一处(原来这里有第二份实现,docx 页图路由抄它的形状时抄漏了 realpath)
|
|
35
|
-
import { safeResolveRead as safeResolve } from './safe-path.js';
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* 卡 id → { kind, rel }。
|
|
39
|
-
*
|
|
40
|
-
* ⚠️ 前缀只认纯字母(`deck:` `site:`)—— 路径里本来就可能有冒号,判据必须跟
|
|
41
|
-
* 前端 `stage.js` 的 `zoneOfObjectId` 和服务端 `board-store.mapId` 保持一致。
|
|
42
|
-
* 没有合法前缀 = 文件卡,类型按路径推。
|
|
43
|
-
* 认哪些前缀问注册表(kindDef)—— 原来是两个手写 Set,加形态时这里必漏,
|
|
44
|
-
* 新形态的卡会被当成裸文件路径去解析(「写死表家族」第 5 处,2026-08-18 收)。
|
|
45
|
-
*
|
|
46
|
-
* ⚠️ 已知歧义:根层一个叫 `final:v2.png` 的文件,卡 id 就是裸路径,这里会把
|
|
47
|
-
* `final` 当前缀剥掉。三处判据是**一致地**这样,改判据要三处一起改;collectCard
|
|
48
|
-
* 里对这种情况有一次「整串当路径」的回退。
|
|
49
|
-
*/
|
|
50
|
-
export function parseCardId(cardId) {
|
|
51
|
-
if (!cardId || typeof cardId !== 'string') return null;
|
|
52
|
-
const c = cardId.indexOf(':');
|
|
53
|
-
if (c > 0 && /^[a-z]+$/.test(cardId.slice(0, c))) {
|
|
54
|
-
const kind = cardId.slice(0, c);
|
|
55
|
-
const rel = cardId.slice(c + 1).replace(/\\/g, '/');
|
|
56
|
-
if (kindDef(kind)) return { kind, rel };
|
|
57
|
-
return { kind: fileKindOfPath(rel), rel };
|
|
58
|
-
}
|
|
59
|
-
const rel = cardId.replace(/\\/g, '/');
|
|
60
|
-
return { kind: fileKindOfPath(rel), rel };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
/** 这种形态能导出成什么。single = 单页产物,没有「整站 zip」可言(跟 taskManifest 的 decorate 同一条剥除) */
|
|
65
|
-
export function exportFormatsFor(kind, { single = false } = {}) {
|
|
66
|
-
const list = kindDef(kind)?.exportFormats || fileKindDef(kind)?.exportFormats || [];
|
|
67
|
-
return single ? list.filter(f => f !== 'site') : list;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 忽略规则的基准目录 = **`.ndignore` 自己所在的那一层**,从产物根往上找第一个。
|
|
72
|
-
*
|
|
73
|
-
* 这跟 `.gitignore` 是同一条语义:规则文件的作用域就是它自己的目录。
|
|
74
|
-
*
|
|
75
|
-
* 曾经从卡 id 字符串上切第一段(`rel.split('/')[0]`)当基准,只有「深度 1 的任务
|
|
76
|
-
* 文件夹」这一种布局碰巧对:根级构建站(`site:dist`)会切出 `dist`、嵌套站
|
|
77
|
-
* (`site:客户/官网`)会切出 `客户`,两种情况下忽略规则都**静默失效**。
|
|
78
|
-
* 也试过问 `taskManifest` 谁认领了这个产物根,但构建站的 `dist/` 自己看起来也像
|
|
79
|
-
* 个站,从深往浅问会停在 `dist` 上 —— 而 `.ndignore` 明明在更外面。
|
|
80
|
-
* 按规则文件自己的位置找,三种布局一次都不会错。
|
|
81
|
-
*/
|
|
82
|
-
async function ignoreBaseFor(workspaceRoot, rel) {
|
|
83
|
-
const segs = rel ? rel.split('/') : [];
|
|
84
|
-
for (let i = segs.length; i >= 0; i--) {
|
|
85
|
-
const cand = segs.slice(0, i).join('/');
|
|
86
|
-
const dir = cand ? path.join(workspaceRoot, cand) : workspaceRoot;
|
|
87
|
-
try {
|
|
88
|
-
await fs.access(path.join(dir, '.ndignore'));
|
|
89
|
-
return cand;
|
|
90
|
-
} catch { /* 这层没有,往上找 */ }
|
|
91
|
-
}
|
|
92
|
-
return ''; // 一个都没有:基准取工作区根(规则为空,等价于不过滤)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* 收一张卡。
|
|
97
|
-
*
|
|
98
|
-
* @returns {Promise<{cardId, kind, rel, title, single, files, assets, missing, unresolved, exportFormats}>}
|
|
99
|
-
* files 产物自身的文件(rel 相对工作区根)
|
|
100
|
-
* assets 它真正引用到、且磁盘上确实存在的素材
|
|
101
|
-
* missing 引用了但磁盘上找不到的(裂图预警)
|
|
102
|
-
* unresolved 算不出来的引用(动态拼接 / 脚本字面量 / 树外引用 / `<base href>`)
|
|
103
|
-
*/
|
|
104
|
-
export async function collectCard({ workspaceRoot, cardId }) {
|
|
105
|
-
const parsed = parseCardId(cardId);
|
|
106
|
-
if (!parsed) throw Object.assign(new Error(`认不出这张卡:${cardId}`), { status: 400 });
|
|
107
|
-
|
|
108
|
-
let { kind, rel } = parsed;
|
|
109
|
-
let abs = await safeResolve(workspaceRoot, rel);
|
|
110
|
-
let stat = null;
|
|
111
|
-
if (abs) { try { stat = await fs.stat(abs); } catch { /* 下面回退 */ } }
|
|
112
|
-
|
|
113
|
-
// 前缀剥错了的回退:`final:v2.png` 这种文件名会被当成 `final:` 前缀
|
|
114
|
-
if (!stat && cardId !== rel) {
|
|
115
|
-
const whole = cardId.replace(/\\/g, '/');
|
|
116
|
-
const absWhole = await safeResolve(workspaceRoot, whole);
|
|
117
|
-
if (absWhole) {
|
|
118
|
-
try {
|
|
119
|
-
stat = await fs.stat(absWhole);
|
|
120
|
-
abs = absWhole; rel = whole; kind = fileKindOfPath(whole);
|
|
121
|
-
} catch { /* 仍然没有 */ }
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (!abs) throw Object.assign(new Error(`路径越界或指向工作区外:${rel}`), { status: 400 });
|
|
125
|
-
if (!stat) throw Object.assign(new Error(`产物不存在:${rel}`), { status: 404 });
|
|
126
|
-
|
|
127
|
-
const files = [];
|
|
128
|
-
// 「这种形态有没有目录型」问注册表(kind 条目声明 directory)。整树打包只对
|
|
129
|
-
// browsable 的目录型成立(站点 = 一棵互相引用的树);word 文件夹也是目录型,
|
|
130
|
-
// 但 .docx 成员各自自包含,卡级导出走下面挑主成员那条,不走树
|
|
131
|
-
const dirKind = !!kindDef(kind)?.directory;
|
|
132
|
-
// 单页站点(`_drafts/试作.html` 这类)也是 site 卡,只是它是一个文件不是一棵树
|
|
133
|
-
const single = dirKind && can(kind, 'browsable') && stat.isFile();
|
|
134
|
-
const isTree = dirKind && can(kind, 'browsable') && stat.isDirectory();
|
|
135
|
-
|
|
136
|
-
const deepWarn = [];
|
|
137
|
-
if (isTree && !rel) {
|
|
138
|
-
// ⭐**根站不盲走。** 扁平化后「根 index.html = 一个站」是常态,而根站的「树」
|
|
139
|
-
// 就是整个工作区 —— 盲走会把项目基础设施和**别的任务**一起卷进来。
|
|
140
|
-
// 正解是问权威解析器要页面清单:`taskManifest` 的根站条目(site / 非 single /
|
|
141
|
-
// 无 srcRoot,跟 assets.js 判 rootSite 同一口径)里的 `pages` 就是它认领了哪些页。
|
|
142
|
-
// 别的任务的 index.html 不在这份 pages 里,天然被排除;页面引用到的 css/js/图
|
|
143
|
-
// 由下面的 refs 收。这比我自己拿一张保留目录表去猜要靠谱 —— 谁属于这个站,
|
|
144
|
-
// 只有解析器说了算。
|
|
145
|
-
const m = await taskManifest(workspaceRoot).catch(() => null);
|
|
146
|
-
const rootSite = (m?.artifacts || []).find(a => a.kind === 'site' && !a.single && !a.srcRoot);
|
|
147
|
-
for (const pg of (rootSite?.pages || [])) {
|
|
148
|
-
const pRel = String(pg).replace(/\\/g, '/');
|
|
149
|
-
const pAbs = await safeResolve(workspaceRoot, pRel);
|
|
150
|
-
if (!pAbs) continue;
|
|
151
|
-
try { if ((await fs.stat(pAbs)).isFile()) files.push({ abs: pAbs, rel: pRel }); } catch { /* 页面刚被删 */ }
|
|
152
|
-
}
|
|
153
|
-
if (!files.length) {
|
|
154
|
-
// 解析器认不出根站(还没写出 index.html 之类)→ 退回入口文件本身,
|
|
155
|
-
// 绝不退回「盲走整个工作区」
|
|
156
|
-
const entryAbs = await safeResolve(workspaceRoot, 'index.html');
|
|
157
|
-
if (entryAbs) {
|
|
158
|
-
try { if ((await fs.stat(entryAbs)).isFile()) files.push({ abs: entryAbs, rel: 'index.html' }); } catch { /* */ }
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
} else if (isTree) {
|
|
162
|
-
const ignoreBaseRel = await ignoreBaseFor(workspaceRoot, rel);
|
|
163
|
-
const ignoreBaseAbs = (await safeResolve(workspaceRoot, ignoreBaseRel)) || workspaceRoot;
|
|
164
|
-
const ignore = await loadIgnore(ignoreBaseAbs);
|
|
165
|
-
const walked = await walkTaskFiles(abs, { maxDepth: 6, ignore, ignoreBase: ignoreBaseAbs });
|
|
166
|
-
for (const f of walked) {
|
|
167
|
-
const r = path.relative(workspaceRoot, f.abs).replace(/\\/g, '/');
|
|
168
|
-
files.push({ abs: f.abs, rel: r });
|
|
169
|
-
// maxDepth 撞顶是静默截断的(walkTaskFiles 不给信号)。贴着上限的文件说明
|
|
170
|
-
// 这棵树很深,更深的东西可能没进包 —— 报出来,别让人以为齐了。
|
|
171
|
-
// ⚠️ 深度要按 **walk 相对卡根** 的路径算(`f.rel`),不是工作区相对的 `r`:
|
|
172
|
-
// 嵌套站 `客户/官网/a/b/c/x.png` 在工作区坐标下有 6 段,但 walk 只走了 4 层,
|
|
173
|
-
// 离截断远着 —— 用错坐标系会往交付物里印假警报。
|
|
174
|
-
if (f.rel.split('/').length >= 6) deepWarn.push(r);
|
|
175
|
-
}
|
|
176
|
-
} else if (kind === 'docx' && stat.isDirectory()) {
|
|
177
|
-
// word 文件夹卡(`docx:报告`):地址是文件夹,导出对象是主成员 —— 窗里的
|
|
178
|
-
// 导出按钮会点名具体成员(`docx:报告/文档v2.docx`),走上面的单文件分支;
|
|
179
|
-
// 这条是卡级导出的兜底。挑主成员的判据在 kinds/docx.js,别在这儿抄第二份。
|
|
180
|
-
let names = [];
|
|
181
|
-
try { names = kindDef('docx').sortDocxNames(await fs.readdir(abs)); } catch { /* 下面报 404 */ }
|
|
182
|
-
if (!names.length) throw Object.assign(new Error(`${rel} 里没有 .docx`), { status: 404 });
|
|
183
|
-
rel = `${rel}/${names[0]}`;
|
|
184
|
-
files.push({ abs: path.join(abs, names[0]), rel });
|
|
185
|
-
} else {
|
|
186
|
-
if (!stat.isFile()) throw Object.assign(new Error(`${rel} 不是文件`), { status: 400 });
|
|
187
|
-
files.push({ abs, rel });
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (!files.length) {
|
|
191
|
-
throw Object.assign(new Error(`${rel} 里没有可导出的文件`), { status: 400 });
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// 树外引用只认 `assets/` 和产物自己的目录(旧 /exports/site 有这道闸,别丢)。
|
|
195
|
-
// 根层产物的自有前缀是空串 = 不设闸:它的树本来就是整个工作区,这条认账。
|
|
196
|
-
const ownPrefix = isTree ? (rel ? `${rel}/` : '') : (path.posix.dirname(rel) === '.' ? '' : `${path.posix.dirname(rel)}/`);
|
|
197
|
-
// 只有**标记语言**产物需要跟着引用扫下去。`browsable` 正好是这条线:能用浏览器
|
|
198
|
-
// 打开 = 由 html/css 组成 = 有外部引用要带。docx 是自包含的 zip(图片字体都在
|
|
199
|
-
// 包里),拿二进制去跑引用扫描器是白费力气,还可能在压缩字节里撞出假路径。
|
|
200
|
-
// 问注册表和能力,不问形态名 —— 加第四种形态时这行不用动。
|
|
201
|
-
const scannable = !!kindDef(kind) && can(kind, 'browsable');
|
|
202
|
-
let refs = []; let unresolved = []; let candidates = [];
|
|
203
|
-
if (scannable) {
|
|
204
|
-
// ⭐**传递引用要跟着扫下去。** html 引 css、css 再引图 —— 只扫产物自身的话,
|
|
205
|
-
// 那张图既不进包也不进清单(用户拍的规矩里最坏的状态),解压即裂图且零预警。
|
|
206
|
-
// deck 卡 / 单页站 / 根站三类全中,因为它们的 files 里只有页面本身。
|
|
207
|
-
// 定点到不动为止,最多四轮(防环、也防超深链把单次导出拖垮)。
|
|
208
|
-
const allow = ownPrefix === '' ? [] : [ownPrefix, 'assets/'];
|
|
209
|
-
const scanned = new Set(files.map(f => f.rel));
|
|
210
|
-
let batch = files;
|
|
211
|
-
for (let round = 0; round < 4 && batch.length; round++) {
|
|
212
|
-
const r = await collectAssetRefs({ files: batch, baseRoot: workspaceRoot, allowPrefixes: allow });
|
|
213
|
-
refs.push(...r.refs); unresolved.push(...r.unresolved); candidates.push(...r.candidates);
|
|
214
|
-
batch = [];
|
|
215
|
-
for (const rr of r.refs) {
|
|
216
|
-
if (scanned.has(rr) || !/\.(html?|css|m?js|cjs)$/i.test(rr)) continue;
|
|
217
|
-
scanned.add(rr);
|
|
218
|
-
const a = await safeResolve(workspaceRoot, rr);
|
|
219
|
-
if (a) batch.push({ abs: a, rel: rr });
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
refs = [...new Set(refs)];
|
|
223
|
-
}
|
|
224
|
-
if (deepWarn.length) {
|
|
225
|
-
unresolved.push({
|
|
226
|
-
from: rel || '(工作区根)',
|
|
227
|
-
why: `目录深度贴到上限 6 层,更深的文件没有进包(命中 ${deepWarn.length} 个)`,
|
|
228
|
-
snippet: deepWarn.slice(0, 3).join(' / '),
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const assets = []; const missing = [];
|
|
233
|
-
const ownSet = new Set(files.map(f => f.rel));
|
|
234
|
-
// 脚本里的完整字面量路径(画廊 / lightbox 那种):**磁盘上真有这个文件就不是猜**,
|
|
235
|
-
// 提升成素材;解析不到的仍然只进清单。这不违反「扫不到的不多塞」——
|
|
236
|
-
// 它扫得到,只是来源是脚本而不是标签。
|
|
237
|
-
for (const c of candidates) {
|
|
238
|
-
const a = await safeResolve(workspaceRoot, c.rel);
|
|
239
|
-
if (!a || ownSet.has(c.rel)) continue;
|
|
240
|
-
try {
|
|
241
|
-
if ((await fs.stat(a)).isFile()) { refs.push(c.rel); continue; }
|
|
242
|
-
} catch { /* 不存在 */ }
|
|
243
|
-
unresolved.push({ from: c.from, why: '脚本里的字符串字面量(磁盘上没找到对应文件)', snippet: c.snippet });
|
|
244
|
-
}
|
|
245
|
-
refs = [...new Set(refs)].sort();
|
|
246
|
-
for (const r of refs) {
|
|
247
|
-
const a = await safeResolve(workspaceRoot, r);
|
|
248
|
-
if (!a) continue; // 软链逃逸的引用直接不收
|
|
249
|
-
let st = null;
|
|
250
|
-
try { st = await fs.stat(a); } catch { missing.push(r); continue; }
|
|
251
|
-
if (st.isFile()) { assets.push({ abs: a, rel: r }); continue; }
|
|
252
|
-
if (st.isDirectory()) {
|
|
253
|
-
// pretty-URL(`href="about/"`)指的是 `about/index.html`,不是裂图。
|
|
254
|
-
// 不处理的话每个多页站导出都会带一堆假的缺失警报。
|
|
255
|
-
const idxRel = `${r.replace(/\/$/, '')}/index.html`;
|
|
256
|
-
if (ownSet.has(idxRel)) continue;
|
|
257
|
-
const idxAbs = await safeResolve(workspaceRoot, idxRel);
|
|
258
|
-
if (idxAbs) {
|
|
259
|
-
try {
|
|
260
|
-
if ((await fs.stat(idxAbs)).isFile()) { assets.push({ abs: idxAbs, rel: idxRel }); continue; }
|
|
261
|
-
} catch { /* 落 missing */ }
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
missing.push(r);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return {
|
|
268
|
-
cardId, kind, rel, single,
|
|
269
|
-
title: path.basename(rel) || rel || '产物',
|
|
270
|
-
files, assets, missing, unresolved,
|
|
271
|
-
exportFormats: exportFormatsFor(kind, { single }),
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* 按类型收一批卡(「导出全部图片」这种)。
|
|
277
|
-
* 单张失败不拖累整批 —— 收不到的记进 skipped,让人看见少了什么。
|
|
278
|
-
*/
|
|
279
|
-
export async function collectCards({ workspaceRoot, cardIds }) {
|
|
280
|
-
const bundles = []; const skipped = [];
|
|
281
|
-
for (const id of cardIds) {
|
|
282
|
-
try {
|
|
283
|
-
bundles.push(await collectCard({ workspaceRoot, cardId: id }));
|
|
284
|
-
} catch (err) {
|
|
285
|
-
skipped.push({ cardId: id, reason: err.message, status: err.status || 500 });
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return { bundles, skipped };
|
|
289
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* lib/export-collect.js — 按「产物卡」收集要导出的东西(2026-08-17)
|
|
3
|
+
*
|
|
4
|
+
* 导出的寻址单位从「会话当前产物」换成**画布上的那张卡**:卡的类型决定收什么。
|
|
5
|
+
* site 卡收站点那棵树、图片卡收那张图,以此类推。
|
|
6
|
+
*
|
|
7
|
+
* 为什么按卡 id 就够、不用再造一份枚举:
|
|
8
|
+
* 卡 id 本身**就是地址**(`site:<产物根>` / `deck:<html路径>` / 文件卡是裸路径),
|
|
9
|
+
* 前端 `BoardCanvas` 拼它、服务端 `board-store.mapId` 认它,用的是同一套前缀判据。
|
|
10
|
+
* 这里只解析地址、落到磁盘上收,权威清单仍然只有 `/artifacts` 那一份。
|
|
11
|
+
*
|
|
12
|
+
* 素材只收**这份产物真正引用到的**(`asset-refs.js` 扫出来的),不再像旧交付包那样
|
|
13
|
+
* 把整个项目级 `shared/assets` 一锅端 —— 那会把别的任务的图一起交出去,生产上最大的
|
|
14
|
+
* 项目那个目录有 280MB。
|
|
15
|
+
*
|
|
16
|
+
* ── 2026-08-17 评审后修的四个洞(都有单测钉着)──
|
|
17
|
+
* 1. **根级站点卡**(`site:`,rel 为空 —— 扁平化后「根 index.html = 一个站」是常态)
|
|
18
|
+
* 从工作区根盲走,把 `assets/`、`exports/`、`board.json`、别的任务全打进包,
|
|
19
|
+
* 280MB 病原样复活。
|
|
20
|
+
* 2. **单页站点卡**(`site:任务/_drafts/试作.html`)整类 400 —— 它是画布上真实存在
|
|
21
|
+
* 的一种卡,收集器却要求 site 必须是目录。
|
|
22
|
+
* 3. **`.ndignore` 基准**从卡 id 字符串上切第一段,只有「深度 1 的任务文件夹」这一
|
|
23
|
+
* 种布局碰巧对;根级构建站、嵌套站全取错,忽略规则静默失效。
|
|
24
|
+
* 4. **软链逃逸**:`safeResolve` 只做词法检查,工作区里一个指向外面的软链能把
|
|
25
|
+
* 工作区外的文件收进包(实测读出过 /tmp 下的文件内容)。
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import fs from 'node:fs/promises';
|
|
30
|
+
import { walkTaskFiles, loadIgnore, RESERVED_DIRS, isReservedFile } from './task-scan.js';
|
|
31
|
+
import { collectAssetRefs } from './asset-refs.js';
|
|
32
|
+
import { kindDef, taskManifest, can } from './kinds/index.js';
|
|
33
|
+
import { fileKindOfPath, fileKindDef } from './kinds/file-kinds.js';
|
|
34
|
+
// 越界判据收在一处(原来这里有第二份实现,docx 页图路由抄它的形状时抄漏了 realpath)
|
|
35
|
+
import { safeResolveRead as safeResolve } from './safe-path.js';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 卡 id → { kind, rel }。
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ 前缀只认纯字母(`deck:` `site:`)—— 路径里本来就可能有冒号,判据必须跟
|
|
41
|
+
* 前端 `stage.js` 的 `zoneOfObjectId` 和服务端 `board-store.mapId` 保持一致。
|
|
42
|
+
* 没有合法前缀 = 文件卡,类型按路径推。
|
|
43
|
+
* 认哪些前缀问注册表(kindDef)—— 原来是两个手写 Set,加形态时这里必漏,
|
|
44
|
+
* 新形态的卡会被当成裸文件路径去解析(「写死表家族」第 5 处,2026-08-18 收)。
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ 已知歧义:根层一个叫 `final:v2.png` 的文件,卡 id 就是裸路径,这里会把
|
|
47
|
+
* `final` 当前缀剥掉。三处判据是**一致地**这样,改判据要三处一起改;collectCard
|
|
48
|
+
* 里对这种情况有一次「整串当路径」的回退。
|
|
49
|
+
*/
|
|
50
|
+
export function parseCardId(cardId) {
|
|
51
|
+
if (!cardId || typeof cardId !== 'string') return null;
|
|
52
|
+
const c = cardId.indexOf(':');
|
|
53
|
+
if (c > 0 && /^[a-z]+$/.test(cardId.slice(0, c))) {
|
|
54
|
+
const kind = cardId.slice(0, c);
|
|
55
|
+
const rel = cardId.slice(c + 1).replace(/\\/g, '/');
|
|
56
|
+
if (kindDef(kind)) return { kind, rel };
|
|
57
|
+
return { kind: fileKindOfPath(rel), rel };
|
|
58
|
+
}
|
|
59
|
+
const rel = cardId.replace(/\\/g, '/');
|
|
60
|
+
return { kind: fileKindOfPath(rel), rel };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
/** 这种形态能导出成什么。single = 单页产物,没有「整站 zip」可言(跟 taskManifest 的 decorate 同一条剥除) */
|
|
65
|
+
export function exportFormatsFor(kind, { single = false } = {}) {
|
|
66
|
+
const list = kindDef(kind)?.exportFormats || fileKindDef(kind)?.exportFormats || [];
|
|
67
|
+
return single ? list.filter(f => f !== 'site') : list;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 忽略规则的基准目录 = **`.ndignore` 自己所在的那一层**,从产物根往上找第一个。
|
|
72
|
+
*
|
|
73
|
+
* 这跟 `.gitignore` 是同一条语义:规则文件的作用域就是它自己的目录。
|
|
74
|
+
*
|
|
75
|
+
* 曾经从卡 id 字符串上切第一段(`rel.split('/')[0]`)当基准,只有「深度 1 的任务
|
|
76
|
+
* 文件夹」这一种布局碰巧对:根级构建站(`site:dist`)会切出 `dist`、嵌套站
|
|
77
|
+
* (`site:客户/官网`)会切出 `客户`,两种情况下忽略规则都**静默失效**。
|
|
78
|
+
* 也试过问 `taskManifest` 谁认领了这个产物根,但构建站的 `dist/` 自己看起来也像
|
|
79
|
+
* 个站,从深往浅问会停在 `dist` 上 —— 而 `.ndignore` 明明在更外面。
|
|
80
|
+
* 按规则文件自己的位置找,三种布局一次都不会错。
|
|
81
|
+
*/
|
|
82
|
+
async function ignoreBaseFor(workspaceRoot, rel) {
|
|
83
|
+
const segs = rel ? rel.split('/') : [];
|
|
84
|
+
for (let i = segs.length; i >= 0; i--) {
|
|
85
|
+
const cand = segs.slice(0, i).join('/');
|
|
86
|
+
const dir = cand ? path.join(workspaceRoot, cand) : workspaceRoot;
|
|
87
|
+
try {
|
|
88
|
+
await fs.access(path.join(dir, '.ndignore'));
|
|
89
|
+
return cand;
|
|
90
|
+
} catch { /* 这层没有,往上找 */ }
|
|
91
|
+
}
|
|
92
|
+
return ''; // 一个都没有:基准取工作区根(规则为空,等价于不过滤)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 收一张卡。
|
|
97
|
+
*
|
|
98
|
+
* @returns {Promise<{cardId, kind, rel, title, single, files, assets, missing, unresolved, exportFormats}>}
|
|
99
|
+
* files 产物自身的文件(rel 相对工作区根)
|
|
100
|
+
* assets 它真正引用到、且磁盘上确实存在的素材
|
|
101
|
+
* missing 引用了但磁盘上找不到的(裂图预警)
|
|
102
|
+
* unresolved 算不出来的引用(动态拼接 / 脚本字面量 / 树外引用 / `<base href>`)
|
|
103
|
+
*/
|
|
104
|
+
export async function collectCard({ workspaceRoot, cardId }) {
|
|
105
|
+
const parsed = parseCardId(cardId);
|
|
106
|
+
if (!parsed) throw Object.assign(new Error(`认不出这张卡:${cardId}`), { status: 400 });
|
|
107
|
+
|
|
108
|
+
let { kind, rel } = parsed;
|
|
109
|
+
let abs = await safeResolve(workspaceRoot, rel);
|
|
110
|
+
let stat = null;
|
|
111
|
+
if (abs) { try { stat = await fs.stat(abs); } catch { /* 下面回退 */ } }
|
|
112
|
+
|
|
113
|
+
// 前缀剥错了的回退:`final:v2.png` 这种文件名会被当成 `final:` 前缀
|
|
114
|
+
if (!stat && cardId !== rel) {
|
|
115
|
+
const whole = cardId.replace(/\\/g, '/');
|
|
116
|
+
const absWhole = await safeResolve(workspaceRoot, whole);
|
|
117
|
+
if (absWhole) {
|
|
118
|
+
try {
|
|
119
|
+
stat = await fs.stat(absWhole);
|
|
120
|
+
abs = absWhole; rel = whole; kind = fileKindOfPath(whole);
|
|
121
|
+
} catch { /* 仍然没有 */ }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!abs) throw Object.assign(new Error(`路径越界或指向工作区外:${rel}`), { status: 400 });
|
|
125
|
+
if (!stat) throw Object.assign(new Error(`产物不存在:${rel}`), { status: 404 });
|
|
126
|
+
|
|
127
|
+
const files = [];
|
|
128
|
+
// 「这种形态有没有目录型」问注册表(kind 条目声明 directory)。整树打包只对
|
|
129
|
+
// browsable 的目录型成立(站点 = 一棵互相引用的树);word 文件夹也是目录型,
|
|
130
|
+
// 但 .docx 成员各自自包含,卡级导出走下面挑主成员那条,不走树
|
|
131
|
+
const dirKind = !!kindDef(kind)?.directory;
|
|
132
|
+
// 单页站点(`_drafts/试作.html` 这类)也是 site 卡,只是它是一个文件不是一棵树
|
|
133
|
+
const single = dirKind && can(kind, 'browsable') && stat.isFile();
|
|
134
|
+
const isTree = dirKind && can(kind, 'browsable') && stat.isDirectory();
|
|
135
|
+
|
|
136
|
+
const deepWarn = [];
|
|
137
|
+
if (isTree && !rel) {
|
|
138
|
+
// ⭐**根站不盲走。** 扁平化后「根 index.html = 一个站」是常态,而根站的「树」
|
|
139
|
+
// 就是整个工作区 —— 盲走会把项目基础设施和**别的任务**一起卷进来。
|
|
140
|
+
// 正解是问权威解析器要页面清单:`taskManifest` 的根站条目(site / 非 single /
|
|
141
|
+
// 无 srcRoot,跟 assets.js 判 rootSite 同一口径)里的 `pages` 就是它认领了哪些页。
|
|
142
|
+
// 别的任务的 index.html 不在这份 pages 里,天然被排除;页面引用到的 css/js/图
|
|
143
|
+
// 由下面的 refs 收。这比我自己拿一张保留目录表去猜要靠谱 —— 谁属于这个站,
|
|
144
|
+
// 只有解析器说了算。
|
|
145
|
+
const m = await taskManifest(workspaceRoot).catch(() => null);
|
|
146
|
+
const rootSite = (m?.artifacts || []).find(a => a.kind === 'site' && !a.single && !a.srcRoot);
|
|
147
|
+
for (const pg of (rootSite?.pages || [])) {
|
|
148
|
+
const pRel = String(pg).replace(/\\/g, '/');
|
|
149
|
+
const pAbs = await safeResolve(workspaceRoot, pRel);
|
|
150
|
+
if (!pAbs) continue;
|
|
151
|
+
try { if ((await fs.stat(pAbs)).isFile()) files.push({ abs: pAbs, rel: pRel }); } catch { /* 页面刚被删 */ }
|
|
152
|
+
}
|
|
153
|
+
if (!files.length) {
|
|
154
|
+
// 解析器认不出根站(还没写出 index.html 之类)→ 退回入口文件本身,
|
|
155
|
+
// 绝不退回「盲走整个工作区」
|
|
156
|
+
const entryAbs = await safeResolve(workspaceRoot, 'index.html');
|
|
157
|
+
if (entryAbs) {
|
|
158
|
+
try { if ((await fs.stat(entryAbs)).isFile()) files.push({ abs: entryAbs, rel: 'index.html' }); } catch { /* */ }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} else if (isTree) {
|
|
162
|
+
const ignoreBaseRel = await ignoreBaseFor(workspaceRoot, rel);
|
|
163
|
+
const ignoreBaseAbs = (await safeResolve(workspaceRoot, ignoreBaseRel)) || workspaceRoot;
|
|
164
|
+
const ignore = await loadIgnore(ignoreBaseAbs);
|
|
165
|
+
const walked = await walkTaskFiles(abs, { maxDepth: 6, ignore, ignoreBase: ignoreBaseAbs });
|
|
166
|
+
for (const f of walked) {
|
|
167
|
+
const r = path.relative(workspaceRoot, f.abs).replace(/\\/g, '/');
|
|
168
|
+
files.push({ abs: f.abs, rel: r });
|
|
169
|
+
// maxDepth 撞顶是静默截断的(walkTaskFiles 不给信号)。贴着上限的文件说明
|
|
170
|
+
// 这棵树很深,更深的东西可能没进包 —— 报出来,别让人以为齐了。
|
|
171
|
+
// ⚠️ 深度要按 **walk 相对卡根** 的路径算(`f.rel`),不是工作区相对的 `r`:
|
|
172
|
+
// 嵌套站 `客户/官网/a/b/c/x.png` 在工作区坐标下有 6 段,但 walk 只走了 4 层,
|
|
173
|
+
// 离截断远着 —— 用错坐标系会往交付物里印假警报。
|
|
174
|
+
if (f.rel.split('/').length >= 6) deepWarn.push(r);
|
|
175
|
+
}
|
|
176
|
+
} else if (kind === 'docx' && stat.isDirectory()) {
|
|
177
|
+
// word 文件夹卡(`docx:报告`):地址是文件夹,导出对象是主成员 —— 窗里的
|
|
178
|
+
// 导出按钮会点名具体成员(`docx:报告/文档v2.docx`),走上面的单文件分支;
|
|
179
|
+
// 这条是卡级导出的兜底。挑主成员的判据在 kinds/docx.js,别在这儿抄第二份。
|
|
180
|
+
let names = [];
|
|
181
|
+
try { names = kindDef('docx').sortDocxNames(await fs.readdir(abs)); } catch { /* 下面报 404 */ }
|
|
182
|
+
if (!names.length) throw Object.assign(new Error(`${rel} 里没有 .docx`), { status: 404 });
|
|
183
|
+
rel = `${rel}/${names[0]}`;
|
|
184
|
+
files.push({ abs: path.join(abs, names[0]), rel });
|
|
185
|
+
} else {
|
|
186
|
+
if (!stat.isFile()) throw Object.assign(new Error(`${rel} 不是文件`), { status: 400 });
|
|
187
|
+
files.push({ abs, rel });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!files.length) {
|
|
191
|
+
throw Object.assign(new Error(`${rel} 里没有可导出的文件`), { status: 400 });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 树外引用只认 `assets/` 和产物自己的目录(旧 /exports/site 有这道闸,别丢)。
|
|
195
|
+
// 根层产物的自有前缀是空串 = 不设闸:它的树本来就是整个工作区,这条认账。
|
|
196
|
+
const ownPrefix = isTree ? (rel ? `${rel}/` : '') : (path.posix.dirname(rel) === '.' ? '' : `${path.posix.dirname(rel)}/`);
|
|
197
|
+
// 只有**标记语言**产物需要跟着引用扫下去。`browsable` 正好是这条线:能用浏览器
|
|
198
|
+
// 打开 = 由 html/css 组成 = 有外部引用要带。docx 是自包含的 zip(图片字体都在
|
|
199
|
+
// 包里),拿二进制去跑引用扫描器是白费力气,还可能在压缩字节里撞出假路径。
|
|
200
|
+
// 问注册表和能力,不问形态名 —— 加第四种形态时这行不用动。
|
|
201
|
+
const scannable = !!kindDef(kind) && can(kind, 'browsable');
|
|
202
|
+
let refs = []; let unresolved = []; let candidates = [];
|
|
203
|
+
if (scannable) {
|
|
204
|
+
// ⭐**传递引用要跟着扫下去。** html 引 css、css 再引图 —— 只扫产物自身的话,
|
|
205
|
+
// 那张图既不进包也不进清单(用户拍的规矩里最坏的状态),解压即裂图且零预警。
|
|
206
|
+
// deck 卡 / 单页站 / 根站三类全中,因为它们的 files 里只有页面本身。
|
|
207
|
+
// 定点到不动为止,最多四轮(防环、也防超深链把单次导出拖垮)。
|
|
208
|
+
const allow = ownPrefix === '' ? [] : [ownPrefix, 'assets/'];
|
|
209
|
+
const scanned = new Set(files.map(f => f.rel));
|
|
210
|
+
let batch = files;
|
|
211
|
+
for (let round = 0; round < 4 && batch.length; round++) {
|
|
212
|
+
const r = await collectAssetRefs({ files: batch, baseRoot: workspaceRoot, allowPrefixes: allow });
|
|
213
|
+
refs.push(...r.refs); unresolved.push(...r.unresolved); candidates.push(...r.candidates);
|
|
214
|
+
batch = [];
|
|
215
|
+
for (const rr of r.refs) {
|
|
216
|
+
if (scanned.has(rr) || !/\.(html?|css|m?js|cjs)$/i.test(rr)) continue;
|
|
217
|
+
scanned.add(rr);
|
|
218
|
+
const a = await safeResolve(workspaceRoot, rr);
|
|
219
|
+
if (a) batch.push({ abs: a, rel: rr });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
refs = [...new Set(refs)];
|
|
223
|
+
}
|
|
224
|
+
if (deepWarn.length) {
|
|
225
|
+
unresolved.push({
|
|
226
|
+
from: rel || '(工作区根)',
|
|
227
|
+
why: `目录深度贴到上限 6 层,更深的文件没有进包(命中 ${deepWarn.length} 个)`,
|
|
228
|
+
snippet: deepWarn.slice(0, 3).join(' / '),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const assets = []; const missing = [];
|
|
233
|
+
const ownSet = new Set(files.map(f => f.rel));
|
|
234
|
+
// 脚本里的完整字面量路径(画廊 / lightbox 那种):**磁盘上真有这个文件就不是猜**,
|
|
235
|
+
// 提升成素材;解析不到的仍然只进清单。这不违反「扫不到的不多塞」——
|
|
236
|
+
// 它扫得到,只是来源是脚本而不是标签。
|
|
237
|
+
for (const c of candidates) {
|
|
238
|
+
const a = await safeResolve(workspaceRoot, c.rel);
|
|
239
|
+
if (!a || ownSet.has(c.rel)) continue;
|
|
240
|
+
try {
|
|
241
|
+
if ((await fs.stat(a)).isFile()) { refs.push(c.rel); continue; }
|
|
242
|
+
} catch { /* 不存在 */ }
|
|
243
|
+
unresolved.push({ from: c.from, why: '脚本里的字符串字面量(磁盘上没找到对应文件)', snippet: c.snippet });
|
|
244
|
+
}
|
|
245
|
+
refs = [...new Set(refs)].sort();
|
|
246
|
+
for (const r of refs) {
|
|
247
|
+
const a = await safeResolve(workspaceRoot, r);
|
|
248
|
+
if (!a) continue; // 软链逃逸的引用直接不收
|
|
249
|
+
let st = null;
|
|
250
|
+
try { st = await fs.stat(a); } catch { missing.push(r); continue; }
|
|
251
|
+
if (st.isFile()) { assets.push({ abs: a, rel: r }); continue; }
|
|
252
|
+
if (st.isDirectory()) {
|
|
253
|
+
// pretty-URL(`href="about/"`)指的是 `about/index.html`,不是裂图。
|
|
254
|
+
// 不处理的话每个多页站导出都会带一堆假的缺失警报。
|
|
255
|
+
const idxRel = `${r.replace(/\/$/, '')}/index.html`;
|
|
256
|
+
if (ownSet.has(idxRel)) continue;
|
|
257
|
+
const idxAbs = await safeResolve(workspaceRoot, idxRel);
|
|
258
|
+
if (idxAbs) {
|
|
259
|
+
try {
|
|
260
|
+
if ((await fs.stat(idxAbs)).isFile()) { assets.push({ abs: idxAbs, rel: idxRel }); continue; }
|
|
261
|
+
} catch { /* 落 missing */ }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
missing.push(r);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
cardId, kind, rel, single,
|
|
269
|
+
title: path.basename(rel) || rel || '产物',
|
|
270
|
+
files, assets, missing, unresolved,
|
|
271
|
+
exportFormats: exportFormatsFor(kind, { single }),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 按类型收一批卡(「导出全部图片」这种)。
|
|
277
|
+
* 单张失败不拖累整批 —— 收不到的记进 skipped,让人看见少了什么。
|
|
278
|
+
*/
|
|
279
|
+
export async function collectCards({ workspaceRoot, cardIds }) {
|
|
280
|
+
const bundles = []; const skipped = [];
|
|
281
|
+
for (const id of cardIds) {
|
|
282
|
+
try {
|
|
283
|
+
bundles.push(await collectCard({ workspaceRoot, cardId: id }));
|
|
284
|
+
} catch (err) {
|
|
285
|
+
skipped.push({ cardId: id, reason: err.message, status: err.status || 500 });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return { bundles, skipped };
|
|
289
|
+
}
|