@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,133 +1,133 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* server/lib/board-relations.js — 关系线的 agent 读侧(2026-08-14,切片③)
|
|
3
|
-
*
|
|
4
|
-
* 在此之前 bindings 是**只写不读**的:agent 有 relate_on_board 能画线、用户
|
|
5
|
-
* 在画布上看得见,但没有任何一层把「用户画了什么关系」喂回 agent —— 线是
|
|
6
|
-
* 装饰。这份文件是读侧的唯一实现,两个消费口都从这儿取:
|
|
7
|
-
*
|
|
8
|
-
* 1. UserPromptSubmit 摘要(relationsDigest):每轮把全图压成 ≤N 行,
|
|
9
|
-
* 用户画的排前面 —— 他专门画的线就是他想让你知道的事。
|
|
10
|
-
* 2. PreToolUse 邻域(fileNeighborhood):agent 摸某个文件时,只注连着
|
|
11
|
-
* 这个文件的边(摘要被截断时的精确补充)。
|
|
12
|
-
*
|
|
13
|
-
* 端点是画布 id(= kind 前缀 + 工作区相对路径,或画布原生 id `text:`/
|
|
14
|
-
* `scribble:`)。手写字端点渲染**内容本身**——这是「标注作用全局」的关键一步:
|
|
15
|
-
* 用户在画布上写的字经由一条线变成 agent 眼里的上下文。涂鸦端点诚实占位
|
|
16
|
-
* (笔画内容看不见,要看得截图),不假装理解。
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { readBoard } from '../projects/board-store.js';
|
|
20
|
-
import { BINDING_TYPES } from './binding-types.js';
|
|
21
|
-
import { KINDS } from './kinds/index.js';
|
|
22
|
-
|
|
23
|
-
// ⚠️ **从注册表派生,别手写**(2026-08-18 修)。这行原来写死 `['deck:','site:']`,
|
|
24
|
-
// 于是 docx 上线之后 `docx:` 前缀的卡不在表里 —— **它们身上的关系线 agent 一条也
|
|
25
|
-
// 看不见**(endpointMatchesRel 永远不匹配),而且错得很安静。
|
|
26
|
-
// 这是「加形态时最容易漏的写死处」家族的第三处(前两处是前端 cardIdOf 和
|
|
27
|
-
// BoardCanvas 的 type:'deck',08-17 已修)。
|
|
28
|
-
const KIND_PREFIXES = Object.keys(KINDS).map(id => `${id}:`);
|
|
29
|
-
|
|
30
|
-
// 目录型形态的前缀(kind 条目声明了 directory 的那些)。2026-08-18 补修:
|
|
31
|
-
// 下面 endpointMatchesRel 的收敛循环原来写死 `['site:']` —— 上面 KIND_PREFIXES
|
|
32
|
-
// 修掉写死表的同一天,这半个同款病漏掉了:word 文件夹卡(docx:报告)身上的边,
|
|
33
|
-
// agent 摸成员文件(报告/终稿v2.docx)时注不进 fileNeighborhood。
|
|
34
|
-
const DIR_KIND_PREFIXES = Object.entries(KINDS)
|
|
35
|
-
.filter(([, def]) => def.directory).map(([id]) => `${id}:`);
|
|
36
|
-
|
|
37
|
-
/** 端点 id → 给 agent 看的一小段描述 */
|
|
38
|
-
export function describeEndpoint(end, board) {
|
|
39
|
-
const obj = board?.objects?.[end];
|
|
40
|
-
if (obj?.kind === 'text') {
|
|
41
|
-
const t = String(obj.data?.t || '').replace(/\s+/g, ' ').trim().slice(0,
|
|
42
|
-
return `手写字「${t}」`;
|
|
43
|
-
}
|
|
44
|
-
if (obj?.kind === 'scribble') return '一笔涂鸦(笔画内容要看得截图画布)';
|
|
45
|
-
for (const p of KIND_PREFIXES) {
|
|
46
|
-
if (end.startsWith(p)) {
|
|
47
|
-
const rel = end.slice(p.length);
|
|
48
|
-
const kind = p.slice(0, -1);
|
|
49
|
-
return rel ? `${rel}(${kind})` : `工作区根上的${kind}`;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return end; // 裸文件路径或文件夹路径
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** 一条边 → 一行中文。方向用箭头表达,无向用平线。 */
|
|
56
|
-
export function bindingLine(b, board) {
|
|
57
|
-
const t = BINDING_TYPES[b.type];
|
|
58
|
-
const word = b.label || t?.label || b.type;
|
|
59
|
-
const joint = t?.directed ? `─${word}→` : `─${word}─`;
|
|
60
|
-
const who = b.by === 'user' ? '〔用户画的〕' : b.by === 'agent' ? '〔你画的〕' : '';
|
|
61
|
-
return `${describeEndpoint(b.from, board)} ${joint} ${describeEndpoint(b.to, board)}${who}`;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* 全图摘要。用户画的排前面(他专门画的 = 他想让你知道的),截断报余量。
|
|
66
|
-
* 没有任何线 → null(沉默,不占 prompt)。
|
|
67
|
-
*/
|
|
68
|
-
export async function relationsDigest(pid, { limit = 12 } = {}) {
|
|
69
|
-
if (!pid) return null;
|
|
70
|
-
const board = await readBoard(pid);
|
|
71
|
-
const all = Object.values(board.bindings || {});
|
|
72
|
-
if (!all.length) return null;
|
|
73
|
-
// 手画的逐条列;自动 ref(内容引用对账出来的底仓)**收拢成每源一行**——
|
|
74
|
-
// 一个站引三十张图,铺开就把摘要刷没了。优先级 user > agent > auto。
|
|
75
|
-
const manual = [
|
|
76
|
-
...all.filter(b => b.by === 'user'),
|
|
77
|
-
...all.filter(b => b.by === 'agent'),
|
|
78
|
-
...all.filter(b => b.by !== 'user' && b.by !== 'agent' && !(b.by === 'auto' && b.type === 'ref')),
|
|
79
|
-
];
|
|
80
|
-
const autoRefs = all.filter(b => b.by === 'auto' && b.type === 'ref');
|
|
81
|
-
const lines = manual.slice(0, limit).map(b => ` ${bindingLine(b, board)}`);
|
|
82
|
-
const more = manual.length - lines.length;
|
|
83
|
-
if (more > 0) lines.push(` …还有 ${more} 条(摸到相关文件时会单独提示)`);
|
|
84
|
-
if (autoRefs.length) {
|
|
85
|
-
const bySrc = new Map();
|
|
86
|
-
for (const b of autoRefs) {
|
|
87
|
-
if (!bySrc.has(b.from)) bySrc.set(b.from, []);
|
|
88
|
-
bySrc.get(b.from).push(b.to);
|
|
89
|
-
}
|
|
90
|
-
for (const [from, tos] of [...bySrc].slice(0, 4)) {
|
|
91
|
-
const sample = tos.slice(0, 3).join('、');
|
|
92
|
-
lines.push(` ${describeEndpoint(from, board)} ─取材─ ${tos.length} 件素材(自动对账:${sample}${tos.length > 3 ? ' 等' : ''})`);
|
|
93
|
-
}
|
|
94
|
-
if (bySrc.size > 4) lines.push(` …另有 ${bySrc.size - 4} 件产物的取材清单省略`);
|
|
95
|
-
}
|
|
96
|
-
return lines.join('\n');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** 某个端点 id 是否指向这个工作区相对路径(裸路径或任一 kind 前缀形态)。
|
|
100
|
-
* 导出仅供测试钉规则(fileNeighborhood 是唯一业务调用方)。 */
|
|
101
|
-
export function endpointMatchesRel(end, rel) {
|
|
102
|
-
if (end === rel) return true;
|
|
103
|
-
for (const p of KIND_PREFIXES) if (end === p + rel) return true;
|
|
104
|
-
// 目录型产物的收敛(2026-08-14 根站病族普查补上的):站点/word 文件夹的边
|
|
105
|
-
// 挂在**根卡**上,agent 摸的是里面的文件 —— 原来只做精确匹配,等于站点页的
|
|
106
|
-
// 一跳邻域从来注入不出来。按"住在这个根里"匹配;根站(root=空串)收根层
|
|
107
|
-
// 散文件(.md 除外)—— 与前端 resolveObjectId 同一条规则,两边别岔开。
|
|
108
|
-
for (const p of DIR_KIND_PREFIXES) {
|
|
109
|
-
if (!end.startsWith(p)) continue;
|
|
110
|
-
const root = end.slice(p.length);
|
|
111
|
-
if (root === '') {
|
|
112
|
-
if (!rel.includes('/') && !/\.md$/i.test(rel)) return true;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (rel === root || rel.startsWith(`${root}/`)) return true;
|
|
116
|
-
}
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* 一跳邻域:连着某个文件/文件夹的全部边。没有 → null。
|
|
122
|
-
* PreToolUse 注入用 —— agent 正要摸的东西,它身上的线最相关。
|
|
123
|
-
*/
|
|
124
|
-
export async function fileNeighborhood(pid, rel, { limit = 6 } = {}) {
|
|
125
|
-
if (!pid || !rel) return null;
|
|
126
|
-
const board = await readBoard(pid);
|
|
127
|
-
const hits = Object.values(board.bindings || {})
|
|
128
|
-
.filter(b => endpointMatchesRel(b.from, rel) || endpointMatchesRel(b.to, rel));
|
|
129
|
-
if (!hits.length) return null;
|
|
130
|
-
const lines = hits.slice(0, limit).map(b => ` ${bindingLine(b, board)}`);
|
|
131
|
-
if (hits.length > limit) lines.push(` …还有 ${hits.length - limit} 条`);
|
|
132
|
-
return lines.join('\n');
|
|
133
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/board-relations.js — 关系线的 agent 读侧(2026-08-14,切片③)
|
|
3
|
+
*
|
|
4
|
+
* 在此之前 bindings 是**只写不读**的:agent 有 relate_on_board 能画线、用户
|
|
5
|
+
* 在画布上看得见,但没有任何一层把「用户画了什么关系」喂回 agent —— 线是
|
|
6
|
+
* 装饰。这份文件是读侧的唯一实现,两个消费口都从这儿取:
|
|
7
|
+
*
|
|
8
|
+
* 1. UserPromptSubmit 摘要(relationsDigest):每轮把全图压成 ≤N 行,
|
|
9
|
+
* 用户画的排前面 —— 他专门画的线就是他想让你知道的事。
|
|
10
|
+
* 2. PreToolUse 邻域(fileNeighborhood):agent 摸某个文件时,只注连着
|
|
11
|
+
* 这个文件的边(摘要被截断时的精确补充)。
|
|
12
|
+
*
|
|
13
|
+
* 端点是画布 id(= kind 前缀 + 工作区相对路径,或画布原生 id `text:`/
|
|
14
|
+
* `scribble:`)。手写字端点渲染**内容本身**——这是「标注作用全局」的关键一步:
|
|
15
|
+
* 用户在画布上写的字经由一条线变成 agent 眼里的上下文。涂鸦端点诚实占位
|
|
16
|
+
* (笔画内容看不见,要看得截图),不假装理解。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readBoard } from '../projects/board-store.js';
|
|
20
|
+
import { BINDING_TYPES } from './binding-types.js';
|
|
21
|
+
import { KINDS } from './kinds/index.js';
|
|
22
|
+
|
|
23
|
+
// ⚠️ **从注册表派生,别手写**(2026-08-18 修)。这行原来写死 `['deck:','site:']`,
|
|
24
|
+
// 于是 docx 上线之后 `docx:` 前缀的卡不在表里 —— **它们身上的关系线 agent 一条也
|
|
25
|
+
// 看不见**(endpointMatchesRel 永远不匹配),而且错得很安静。
|
|
26
|
+
// 这是「加形态时最容易漏的写死处」家族的第三处(前两处是前端 cardIdOf 和
|
|
27
|
+
// BoardCanvas 的 type:'deck',08-17 已修)。
|
|
28
|
+
const KIND_PREFIXES = Object.keys(KINDS).map(id => `${id}:`);
|
|
29
|
+
|
|
30
|
+
// 目录型形态的前缀(kind 条目声明了 directory 的那些)。2026-08-18 补修:
|
|
31
|
+
// 下面 endpointMatchesRel 的收敛循环原来写死 `['site:']` —— 上面 KIND_PREFIXES
|
|
32
|
+
// 修掉写死表的同一天,这半个同款病漏掉了:word 文件夹卡(docx:报告)身上的边,
|
|
33
|
+
// agent 摸成员文件(报告/终稿v2.docx)时注不进 fileNeighborhood。
|
|
34
|
+
const DIR_KIND_PREFIXES = Object.entries(KINDS)
|
|
35
|
+
.filter(([, def]) => def.directory).map(([id]) => `${id}:`);
|
|
36
|
+
|
|
37
|
+
/** 端点 id → 给 agent 看的一小段描述 */
|
|
38
|
+
export function describeEndpoint(end, board) {
|
|
39
|
+
const obj = board?.objects?.[end];
|
|
40
|
+
if (obj?.kind === 'text') {
|
|
41
|
+
const t = String(obj.data?.t || '').replace(/\s+/g, ' ').trim().slice(0, 48);
|
|
42
|
+
return `手写字「${t}」`;
|
|
43
|
+
}
|
|
44
|
+
if (obj?.kind === 'scribble') return '一笔涂鸦(笔画内容要看得截图画布)';
|
|
45
|
+
for (const p of KIND_PREFIXES) {
|
|
46
|
+
if (end.startsWith(p)) {
|
|
47
|
+
const rel = end.slice(p.length);
|
|
48
|
+
const kind = p.slice(0, -1);
|
|
49
|
+
return rel ? `${rel}(${kind})` : `工作区根上的${kind}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return end; // 裸文件路径或文件夹路径
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 一条边 → 一行中文。方向用箭头表达,无向用平线。 */
|
|
56
|
+
export function bindingLine(b, board) {
|
|
57
|
+
const t = BINDING_TYPES[b.type];
|
|
58
|
+
const word = b.label || t?.label || b.type;
|
|
59
|
+
const joint = t?.directed ? `─${word}→` : `─${word}─`;
|
|
60
|
+
const who = b.by === 'user' ? '〔用户画的〕' : b.by === 'agent' ? '〔你画的〕' : '';
|
|
61
|
+
return `${describeEndpoint(b.from, board)} ${joint} ${describeEndpoint(b.to, board)}${who}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 全图摘要。用户画的排前面(他专门画的 = 他想让你知道的),截断报余量。
|
|
66
|
+
* 没有任何线 → null(沉默,不占 prompt)。
|
|
67
|
+
*/
|
|
68
|
+
export async function relationsDigest(pid, { limit = 12 } = {}) {
|
|
69
|
+
if (!pid) return null;
|
|
70
|
+
const board = await readBoard(pid);
|
|
71
|
+
const all = Object.values(board.bindings || {});
|
|
72
|
+
if (!all.length) return null;
|
|
73
|
+
// 手画的逐条列;自动 ref(内容引用对账出来的底仓)**收拢成每源一行**——
|
|
74
|
+
// 一个站引三十张图,铺开就把摘要刷没了。优先级 user > agent > auto。
|
|
75
|
+
const manual = [
|
|
76
|
+
...all.filter(b => b.by === 'user'),
|
|
77
|
+
...all.filter(b => b.by === 'agent'),
|
|
78
|
+
...all.filter(b => b.by !== 'user' && b.by !== 'agent' && !(b.by === 'auto' && b.type === 'ref')),
|
|
79
|
+
];
|
|
80
|
+
const autoRefs = all.filter(b => b.by === 'auto' && b.type === 'ref');
|
|
81
|
+
const lines = manual.slice(0, limit).map(b => ` ${bindingLine(b, board)}`);
|
|
82
|
+
const more = manual.length - lines.length;
|
|
83
|
+
if (more > 0) lines.push(` …还有 ${more} 条(摸到相关文件时会单独提示)`);
|
|
84
|
+
if (autoRefs.length) {
|
|
85
|
+
const bySrc = new Map();
|
|
86
|
+
for (const b of autoRefs) {
|
|
87
|
+
if (!bySrc.has(b.from)) bySrc.set(b.from, []);
|
|
88
|
+
bySrc.get(b.from).push(b.to);
|
|
89
|
+
}
|
|
90
|
+
for (const [from, tos] of [...bySrc].slice(0, 4)) {
|
|
91
|
+
const sample = tos.slice(0, 3).join('、');
|
|
92
|
+
lines.push(` ${describeEndpoint(from, board)} ─取材─ ${tos.length} 件素材(自动对账:${sample}${tos.length > 3 ? ' 等' : ''})`);
|
|
93
|
+
}
|
|
94
|
+
if (bySrc.size > 4) lines.push(` …另有 ${bySrc.size - 4} 件产物的取材清单省略`);
|
|
95
|
+
}
|
|
96
|
+
return lines.join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 某个端点 id 是否指向这个工作区相对路径(裸路径或任一 kind 前缀形态)。
|
|
100
|
+
* 导出仅供测试钉规则(fileNeighborhood 是唯一业务调用方)。 */
|
|
101
|
+
export function endpointMatchesRel(end, rel) {
|
|
102
|
+
if (end === rel) return true;
|
|
103
|
+
for (const p of KIND_PREFIXES) if (end === p + rel) return true;
|
|
104
|
+
// 目录型产物的收敛(2026-08-14 根站病族普查补上的):站点/word 文件夹的边
|
|
105
|
+
// 挂在**根卡**上,agent 摸的是里面的文件 —— 原来只做精确匹配,等于站点页的
|
|
106
|
+
// 一跳邻域从来注入不出来。按"住在这个根里"匹配;根站(root=空串)收根层
|
|
107
|
+
// 散文件(.md 除外)—— 与前端 resolveObjectId 同一条规则,两边别岔开。
|
|
108
|
+
for (const p of DIR_KIND_PREFIXES) {
|
|
109
|
+
if (!end.startsWith(p)) continue;
|
|
110
|
+
const root = end.slice(p.length);
|
|
111
|
+
if (root === '') {
|
|
112
|
+
if (!rel.includes('/') && !/\.md$/i.test(rel)) return true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (rel === root || rel.startsWith(`${root}/`)) return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 一跳邻域:连着某个文件/文件夹的全部边。没有 → null。
|
|
122
|
+
* PreToolUse 注入用 —— agent 正要摸的东西,它身上的线最相关。
|
|
123
|
+
*/
|
|
124
|
+
export async function fileNeighborhood(pid, rel, { limit = 6 } = {}) {
|
|
125
|
+
if (!pid || !rel) return null;
|
|
126
|
+
const board = await readBoard(pid);
|
|
127
|
+
const hits = Object.values(board.bindings || {})
|
|
128
|
+
.filter(b => endpointMatchesRel(b.from, rel) || endpointMatchesRel(b.to, rel));
|
|
129
|
+
if (!hits.length) return null;
|
|
130
|
+
const lines = hits.slice(0, limit).map(b => ` ${bindingLine(b, board)}`);
|
|
131
|
+
if (hits.length > limit) lines.push(` …还有 ${hits.length - limit} 条`);
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
}
|
|
@@ -1,180 +1,180 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* server/lib/browse-proxy.js — 浏览通道的出网闸,装在**代理层**(2026-08-18)
|
|
3
|
-
*
|
|
4
|
-
* ## 为什么闸从 CDP 挪到代理
|
|
5
|
-
*
|
|
6
|
-
* 第一版把闸装在 CDP 的 `Fetch.requestPaused` 上。它拦得住导航、子资源、跳转每一跳、
|
|
7
|
-
* 跨源 iframe —— 但审查真攻出来四条它**根本看不见**的路:
|
|
8
|
-
*
|
|
9
|
-
* - **WebSocket 握手**(`ws://127.0.0.1:4001` 双向读到了数据)
|
|
10
|
-
* - **`<link rel=prefetch>` / speculation rules**(打到了生产 :4001)
|
|
11
|
-
* - **`navigator.sendBeacon`**
|
|
12
|
-
* - **还没装上闸的弹窗**(`window.open` / `target=_blank`,在那 250ms 窗口里
|
|
13
|
-
* 能发 GET 和 POST,还能循环开)
|
|
14
|
-
*
|
|
15
|
-
* 共同点是它们都不走 `Fetch` 那个阶段。**逐条补是补不完的** —— 下一个 web 平台特性
|
|
16
|
-
* 又会开一条。所以闸挪到**所有出网都必经的那一层**:给这个浏览器挂一个我们自己的
|
|
17
|
-
* HTTP 代理,`--proxy-server` 之后 chromium 的每一次连接都从这儿走。
|
|
18
|
-
* 实测代理确实看得到上面四条全部(包括 WS 的 `CONNECT 127.0.0.1:4001`)。
|
|
19
|
-
*
|
|
20
|
-
* ## 附带白拿的两件
|
|
21
|
-
*
|
|
22
|
-
* 1. ⭐ **DNS 重绑定被根除,不再是"缓解"。** 我们自己解析、自己**连那个验过的 IP**
|
|
23
|
-
* (pin),浏览器拿不到第二次解析的机会。CDP 那版只能是"解析一次→浏览器再解析
|
|
24
|
-
* 一次",中间有 TOCTOU 窗口,而且主机名缓存把窗口变成了保证 30 秒的洞。
|
|
25
|
-
* 2. **没有初始化竞态。** 代理在浏览器启动参数里,第一个字节之前就生效;
|
|
26
|
-
* CDP 那版要等 `Fetch.enable` 握手完,弹窗根本等不到。
|
|
27
|
-
*
|
|
28
|
-
* CDP 那道闸**保留**当纵深(它能给出漂亮的按请求记账),但不再是唯一一道。
|
|
29
|
-
*
|
|
30
|
-
* ## 感知通道不挂代理
|
|
31
|
-
*
|
|
32
|
-
* 感知通道(看自己产物)**故意**走 loopback 的 artifact-file,它必须能到 127.0.0.1。
|
|
33
|
-
* 所以只有浏览通道的浏览器带 `--proxy-server`,两条通道在这一层物理分开。
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
import http from 'node:http';
|
|
37
|
-
import net from 'node:net';
|
|
38
|
-
import dns from 'node:dns/promises';
|
|
39
|
-
import { blockReason } from './ssrf-guard.js';
|
|
40
|
-
|
|
41
|
-
/** 允许连的端口。80/443 之外的公网端口极少是设计参考站,而放开等于多一片攻击面。 */
|
|
42
|
-
const ALLOWED_PORTS = new Set([80, 443, 8080, 8443]);
|
|
43
|
-
const RESOLVE_TIMEOUT_MS = 4000;
|
|
44
|
-
const CONNECT_TIMEOUT_MS = 15_000;
|
|
45
|
-
|
|
46
|
-
let server = null;
|
|
47
|
-
let port = 0;
|
|
48
|
-
const blocked = [];
|
|
49
|
-
|
|
50
|
-
function note(target, reason) {
|
|
51
|
-
blocked.push({ target, reason, at: Date.now() });
|
|
52
|
-
if (blocked.length > 200) blocked.shift();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 解析 + 判定,返回可以连的那个 IP(**pin 住它**,不把主机名交给下游再解析一次)。
|
|
57
|
-
* @returns {Promise<{ip: string, family: number} | {deny: string}>}
|
|
58
|
-
*/
|
|
59
|
-
async function resolveAllowed(host, portNum) {
|
|
60
|
-
if (!ALLOWED_PORTS.has(portNum)) {
|
|
61
|
-
return { deny: `port ${portNum} is not allowed (only ${[...ALLOWED_PORTS].join('/')})` };
|
|
62
|
-
}
|
|
63
|
-
const bare = host.toLowerCase().replace(/^\[|\]$/g, '');
|
|
64
|
-
if (bare === 'localhost' || bare.endsWith('.localhost') || bare.endsWith('.local')) {
|
|
65
|
-
return { deny: `${bare} resolves inside the local network` };
|
|
66
|
-
}
|
|
67
|
-
// 字面量 IP 不劳烦 DNS
|
|
68
|
-
if (/^\d+\.\d+\.\d+\.\d+$/.test(bare) || bare.includes(':')) {
|
|
69
|
-
const why = blockReason(bare);
|
|
70
|
-
return why ? { deny: why } : { ip: bare, family: bare.includes(':') ? 6 : 4 };
|
|
71
|
-
}
|
|
72
|
-
let addrs;
|
|
73
|
-
try {
|
|
74
|
-
addrs = await Promise.race([
|
|
75
|
-
dns.lookup(bare, { all: true, verbatim: true }),
|
|
76
|
-
new Promise((_, rej) => setTimeout(() => rej(new Error('dns timeout')), RESOLVE_TIMEOUT_MS)),
|
|
77
|
-
]);
|
|
78
|
-
} catch (err) {
|
|
79
|
-
// ⚠️ kind:'dns' 是给 denyBody 分文案用的:解析失败**不是策略拦截**。
|
|
80
|
-
// 第一版对 NXDOMAIN 也甩「内网与本机地址是硬边界」,agent 上报时据此
|
|
81
|
-
// 推理出一整套"代理有两个解析器"的错误理论 —— 错误提示比没提示更贵。
|
|
82
|
-
return { deny: `cannot resolve ${bare}: ${err.message}`, kind: 'dns' };
|
|
83
|
-
}
|
|
84
|
-
if (!addrs.length) return { deny: `${bare} resolved to nothing`, kind: 'dns' };
|
|
85
|
-
// **任一**地址落在禁止段就整个拒 —— 多 A 记录里混一条内网是标准起手式,
|
|
86
|
-
// "挑一个能用的"等于自己开门
|
|
87
|
-
for (const a of addrs) {
|
|
88
|
-
const why = blockReason(a.address);
|
|
89
|
-
if (why) return { deny: `${bare} → ${why}` };
|
|
90
|
-
}
|
|
91
|
-
return { ip: addrs[0].address, family: addrs[0].family };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 按拒因分文案:DNS 解析失败要说清"这个域名现在指不到任何地方",
|
|
95
|
-
// 策略拦截才配那句硬边界 —— 两种拒混着说,agent 会朝错误的方向排障。
|
|
96
|
-
const denyTail = (kind) => (kind === 'dns'
|
|
97
|
-
? '这是域名解析失败(域名可能不存在、拼错或已下线),不是出网策略拦截。'
|
|
98
|
-
: '内网与本机地址是硬边界,不是可配置项。');
|
|
99
|
-
|
|
100
|
-
const denyBody = (reason, kind) => {
|
|
101
|
-
const body = `拒绝出网:${reason}\n\n${denyTail(kind)}`;
|
|
102
|
-
return `HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain; charset=utf-8\r\n`
|
|
103
|
-
+ `Content-Length: ${Buffer.byteLength(body)}\r\nConnection: close\r\n\r\n${body}`;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
/** 启动(幂等)。@returns {Promise<{port:number, blocked:Array}>} */
|
|
107
|
-
export async function startBrowseProxy() {
|
|
108
|
-
if (server) return { port, blocked };
|
|
109
|
-
|
|
110
|
-
server = http.createServer();
|
|
111
|
-
|
|
112
|
-
// 明文 http:绝对形式的请求行(`GET http://host/path`)
|
|
113
|
-
server.on('request', async (req, res) => {
|
|
114
|
-
let u;
|
|
115
|
-
try { u = new URL(req.url); } catch {
|
|
116
|
-
res.writeHead(400); return res.end('proxy: bad absolute-form request');
|
|
117
|
-
}
|
|
118
|
-
const p = Number(u.port || (u.protocol === 'https:' ? 443 : 80));
|
|
119
|
-
const verdict = await resolveAllowed(u.hostname, p);
|
|
120
|
-
if (verdict.deny) {
|
|
121
|
-
note(`${u.hostname}:${p}`, verdict.deny);
|
|
122
|
-
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
123
|
-
return res.end(`拒绝出网:${verdict.deny}\n\n${denyTail(verdict.kind)}`);
|
|
124
|
-
}
|
|
125
|
-
// 连**验过的那个 IP**,Host 头保留原主机名(虚拟主机才认得出)
|
|
126
|
-
const upstream = http.request({
|
|
127
|
-
host: verdict.ip, port: p, method: req.method,
|
|
128
|
-
path: u.pathname + u.search,
|
|
129
|
-
headers: { ...req.headers, host: u.host },
|
|
130
|
-
timeout: CONNECT_TIMEOUT_MS,
|
|
131
|
-
setHost: false,
|
|
132
|
-
}, (up) => {
|
|
133
|
-
res.writeHead(up.statusCode || 502, up.headers);
|
|
134
|
-
up.pipe(res);
|
|
135
|
-
});
|
|
136
|
-
upstream.on('error', (err) => {
|
|
137
|
-
if (!res.headersSent) res.writeHead(502, { 'Content-Type': 'text/plain' });
|
|
138
|
-
res.end(`proxy upstream error: ${err.message}`);
|
|
139
|
-
});
|
|
140
|
-
req.pipe(upstream);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// CONNECT:https / wss 都走这条(**WS 就是靠它被盖住的**)
|
|
144
|
-
server.on('connect', async (req, socket, head) => {
|
|
145
|
-
const [rawHost, rawPort] = String(req.url || '').split(':');
|
|
146
|
-
const p = Number(rawPort || 443);
|
|
147
|
-
const verdict = await resolveAllowed(rawHost || '', p);
|
|
148
|
-
if (verdict.deny) {
|
|
149
|
-
note(`${rawHost}:${p}`, verdict.deny);
|
|
150
|
-
socket.write(denyBody(verdict.deny, verdict.kind));
|
|
151
|
-
return socket.destroy();
|
|
152
|
-
}
|
|
153
|
-
const up = net.connect({ host: verdict.ip, port: p }, () => {
|
|
154
|
-
socket.write('HTTP/1.1 200 Connection Established\r\nProxy-Agent: nodesign-browse\r\n\r\n');
|
|
155
|
-
if (head?.length) up.write(head);
|
|
156
|
-
socket.pipe(up); up.pipe(socket);
|
|
157
|
-
});
|
|
158
|
-
up.setTimeout(CONNECT_TIMEOUT_MS, () => up.destroy());
|
|
159
|
-
up.on('error', () => { try { socket.destroy(); } catch { /* */ } });
|
|
160
|
-
socket.on('error', () => { try { up.destroy(); } catch { /* */ } });
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
await new Promise((resolve, reject) => {
|
|
164
|
-
server.once('error', reject);
|
|
165
|
-
server.listen(0, '127.0.0.1', resolve);
|
|
166
|
-
});
|
|
167
|
-
port = server.address().port;
|
|
168
|
-
server.unref?.();
|
|
169
|
-
console.log(`[browse-proxy] listening on 127.0.0.1:${port}`);
|
|
170
|
-
return { port, blocked };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** 给工具的返回值用:这次调用期间拦了什么 */
|
|
174
|
-
export function blockedSince(n) {
|
|
175
|
-
return blocked.slice(n);
|
|
176
|
-
}
|
|
177
|
-
export function blockedCount() {
|
|
178
|
-
return blocked.length;
|
|
179
|
-
}
|
|
180
|
-
export const _proxy = { ALLOWED_PORTS };
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/browse-proxy.js — 浏览通道的出网闸,装在**代理层**(2026-08-18)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么闸从 CDP 挪到代理
|
|
5
|
+
*
|
|
6
|
+
* 第一版把闸装在 CDP 的 `Fetch.requestPaused` 上。它拦得住导航、子资源、跳转每一跳、
|
|
7
|
+
* 跨源 iframe —— 但审查真攻出来四条它**根本看不见**的路:
|
|
8
|
+
*
|
|
9
|
+
* - **WebSocket 握手**(`ws://127.0.0.1:4001` 双向读到了数据)
|
|
10
|
+
* - **`<link rel=prefetch>` / speculation rules**(打到了生产 :4001)
|
|
11
|
+
* - **`navigator.sendBeacon`**
|
|
12
|
+
* - **还没装上闸的弹窗**(`window.open` / `target=_blank`,在那 250ms 窗口里
|
|
13
|
+
* 能发 GET 和 POST,还能循环开)
|
|
14
|
+
*
|
|
15
|
+
* 共同点是它们都不走 `Fetch` 那个阶段。**逐条补是补不完的** —— 下一个 web 平台特性
|
|
16
|
+
* 又会开一条。所以闸挪到**所有出网都必经的那一层**:给这个浏览器挂一个我们自己的
|
|
17
|
+
* HTTP 代理,`--proxy-server` 之后 chromium 的每一次连接都从这儿走。
|
|
18
|
+
* 实测代理确实看得到上面四条全部(包括 WS 的 `CONNECT 127.0.0.1:4001`)。
|
|
19
|
+
*
|
|
20
|
+
* ## 附带白拿的两件
|
|
21
|
+
*
|
|
22
|
+
* 1. ⭐ **DNS 重绑定被根除,不再是"缓解"。** 我们自己解析、自己**连那个验过的 IP**
|
|
23
|
+
* (pin),浏览器拿不到第二次解析的机会。CDP 那版只能是"解析一次→浏览器再解析
|
|
24
|
+
* 一次",中间有 TOCTOU 窗口,而且主机名缓存把窗口变成了保证 30 秒的洞。
|
|
25
|
+
* 2. **没有初始化竞态。** 代理在浏览器启动参数里,第一个字节之前就生效;
|
|
26
|
+
* CDP 那版要等 `Fetch.enable` 握手完,弹窗根本等不到。
|
|
27
|
+
*
|
|
28
|
+
* CDP 那道闸**保留**当纵深(它能给出漂亮的按请求记账),但不再是唯一一道。
|
|
29
|
+
*
|
|
30
|
+
* ## 感知通道不挂代理
|
|
31
|
+
*
|
|
32
|
+
* 感知通道(看自己产物)**故意**走 loopback 的 artifact-file,它必须能到 127.0.0.1。
|
|
33
|
+
* 所以只有浏览通道的浏览器带 `--proxy-server`,两条通道在这一层物理分开。
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import http from 'node:http';
|
|
37
|
+
import net from 'node:net';
|
|
38
|
+
import dns from 'node:dns/promises';
|
|
39
|
+
import { blockReason } from './ssrf-guard.js';
|
|
40
|
+
|
|
41
|
+
/** 允许连的端口。80/443 之外的公网端口极少是设计参考站,而放开等于多一片攻击面。 */
|
|
42
|
+
const ALLOWED_PORTS = new Set([80, 443, 8080, 8443]);
|
|
43
|
+
const RESOLVE_TIMEOUT_MS = 4000;
|
|
44
|
+
const CONNECT_TIMEOUT_MS = 15_000;
|
|
45
|
+
|
|
46
|
+
let server = null;
|
|
47
|
+
let port = 0;
|
|
48
|
+
const blocked = [];
|
|
49
|
+
|
|
50
|
+
function note(target, reason) {
|
|
51
|
+
blocked.push({ target, reason, at: Date.now() });
|
|
52
|
+
if (blocked.length > 200) blocked.shift();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 解析 + 判定,返回可以连的那个 IP(**pin 住它**,不把主机名交给下游再解析一次)。
|
|
57
|
+
* @returns {Promise<{ip: string, family: number} | {deny: string}>}
|
|
58
|
+
*/
|
|
59
|
+
async function resolveAllowed(host, portNum) {
|
|
60
|
+
if (!ALLOWED_PORTS.has(portNum)) {
|
|
61
|
+
return { deny: `port ${portNum} is not allowed (only ${[...ALLOWED_PORTS].join('/')})` };
|
|
62
|
+
}
|
|
63
|
+
const bare = host.toLowerCase().replace(/^\[|\]$/g, '');
|
|
64
|
+
if (bare === 'localhost' || bare.endsWith('.localhost') || bare.endsWith('.local')) {
|
|
65
|
+
return { deny: `${bare} resolves inside the local network` };
|
|
66
|
+
}
|
|
67
|
+
// 字面量 IP 不劳烦 DNS
|
|
68
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(bare) || bare.includes(':')) {
|
|
69
|
+
const why = blockReason(bare);
|
|
70
|
+
return why ? { deny: why } : { ip: bare, family: bare.includes(':') ? 6 : 4 };
|
|
71
|
+
}
|
|
72
|
+
let addrs;
|
|
73
|
+
try {
|
|
74
|
+
addrs = await Promise.race([
|
|
75
|
+
dns.lookup(bare, { all: true, verbatim: true }),
|
|
76
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('dns timeout')), RESOLVE_TIMEOUT_MS)),
|
|
77
|
+
]);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// ⚠️ kind:'dns' 是给 denyBody 分文案用的:解析失败**不是策略拦截**。
|
|
80
|
+
// 第一版对 NXDOMAIN 也甩「内网与本机地址是硬边界」,agent 上报时据此
|
|
81
|
+
// 推理出一整套"代理有两个解析器"的错误理论 —— 错误提示比没提示更贵。
|
|
82
|
+
return { deny: `cannot resolve ${bare}: ${err.message}`, kind: 'dns' };
|
|
83
|
+
}
|
|
84
|
+
if (!addrs.length) return { deny: `${bare} resolved to nothing`, kind: 'dns' };
|
|
85
|
+
// **任一**地址落在禁止段就整个拒 —— 多 A 记录里混一条内网是标准起手式,
|
|
86
|
+
// "挑一个能用的"等于自己开门
|
|
87
|
+
for (const a of addrs) {
|
|
88
|
+
const why = blockReason(a.address);
|
|
89
|
+
if (why) return { deny: `${bare} → ${why}` };
|
|
90
|
+
}
|
|
91
|
+
return { ip: addrs[0].address, family: addrs[0].family };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 按拒因分文案:DNS 解析失败要说清"这个域名现在指不到任何地方",
|
|
95
|
+
// 策略拦截才配那句硬边界 —— 两种拒混着说,agent 会朝错误的方向排障。
|
|
96
|
+
const denyTail = (kind) => (kind === 'dns'
|
|
97
|
+
? '这是域名解析失败(域名可能不存在、拼错或已下线),不是出网策略拦截。'
|
|
98
|
+
: '内网与本机地址是硬边界,不是可配置项。');
|
|
99
|
+
|
|
100
|
+
const denyBody = (reason, kind) => {
|
|
101
|
+
const body = `拒绝出网:${reason}\n\n${denyTail(kind)}`;
|
|
102
|
+
return `HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain; charset=utf-8\r\n`
|
|
103
|
+
+ `Content-Length: ${Buffer.byteLength(body)}\r\nConnection: close\r\n\r\n${body}`;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** 启动(幂等)。@returns {Promise<{port:number, blocked:Array}>} */
|
|
107
|
+
export async function startBrowseProxy() {
|
|
108
|
+
if (server) return { port, blocked };
|
|
109
|
+
|
|
110
|
+
server = http.createServer();
|
|
111
|
+
|
|
112
|
+
// 明文 http:绝对形式的请求行(`GET http://host/path`)
|
|
113
|
+
server.on('request', async (req, res) => {
|
|
114
|
+
let u;
|
|
115
|
+
try { u = new URL(req.url); } catch {
|
|
116
|
+
res.writeHead(400); return res.end('proxy: bad absolute-form request');
|
|
117
|
+
}
|
|
118
|
+
const p = Number(u.port || (u.protocol === 'https:' ? 443 : 80));
|
|
119
|
+
const verdict = await resolveAllowed(u.hostname, p);
|
|
120
|
+
if (verdict.deny) {
|
|
121
|
+
note(`${u.hostname}:${p}`, verdict.deny);
|
|
122
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
123
|
+
return res.end(`拒绝出网:${verdict.deny}\n\n${denyTail(verdict.kind)}`);
|
|
124
|
+
}
|
|
125
|
+
// 连**验过的那个 IP**,Host 头保留原主机名(虚拟主机才认得出)
|
|
126
|
+
const upstream = http.request({
|
|
127
|
+
host: verdict.ip, port: p, method: req.method,
|
|
128
|
+
path: u.pathname + u.search,
|
|
129
|
+
headers: { ...req.headers, host: u.host },
|
|
130
|
+
timeout: CONNECT_TIMEOUT_MS,
|
|
131
|
+
setHost: false,
|
|
132
|
+
}, (up) => {
|
|
133
|
+
res.writeHead(up.statusCode || 502, up.headers);
|
|
134
|
+
up.pipe(res);
|
|
135
|
+
});
|
|
136
|
+
upstream.on('error', (err) => {
|
|
137
|
+
if (!res.headersSent) res.writeHead(502, { 'Content-Type': 'text/plain' });
|
|
138
|
+
res.end(`proxy upstream error: ${err.message}`);
|
|
139
|
+
});
|
|
140
|
+
req.pipe(upstream);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// CONNECT:https / wss 都走这条(**WS 就是靠它被盖住的**)
|
|
144
|
+
server.on('connect', async (req, socket, head) => {
|
|
145
|
+
const [rawHost, rawPort] = String(req.url || '').split(':');
|
|
146
|
+
const p = Number(rawPort || 443);
|
|
147
|
+
const verdict = await resolveAllowed(rawHost || '', p);
|
|
148
|
+
if (verdict.deny) {
|
|
149
|
+
note(`${rawHost}:${p}`, verdict.deny);
|
|
150
|
+
socket.write(denyBody(verdict.deny, verdict.kind));
|
|
151
|
+
return socket.destroy();
|
|
152
|
+
}
|
|
153
|
+
const up = net.connect({ host: verdict.ip, port: p }, () => {
|
|
154
|
+
socket.write('HTTP/1.1 200 Connection Established\r\nProxy-Agent: nodesign-browse\r\n\r\n');
|
|
155
|
+
if (head?.length) up.write(head);
|
|
156
|
+
socket.pipe(up); up.pipe(socket);
|
|
157
|
+
});
|
|
158
|
+
up.setTimeout(CONNECT_TIMEOUT_MS, () => up.destroy());
|
|
159
|
+
up.on('error', () => { try { socket.destroy(); } catch { /* */ } });
|
|
160
|
+
socket.on('error', () => { try { up.destroy(); } catch { /* */ } });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await new Promise((resolve, reject) => {
|
|
164
|
+
server.once('error', reject);
|
|
165
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
166
|
+
});
|
|
167
|
+
port = server.address().port;
|
|
168
|
+
server.unref?.();
|
|
169
|
+
console.log(`[browse-proxy] listening on 127.0.0.1:${port}`);
|
|
170
|
+
return { port, blocked };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 给工具的返回值用:这次调用期间拦了什么 */
|
|
174
|
+
export function blockedSince(n) {
|
|
175
|
+
return blocked.slice(n);
|
|
176
|
+
}
|
|
177
|
+
export function blockedCount() {
|
|
178
|
+
return blocked.length;
|
|
179
|
+
}
|
|
180
|
+
export const _proxy = { ALLOWED_PORTS };
|