@xiaobuyu/nodesign 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +140 -0
- package/bin/nodesign.js +176 -0
- package/package.json +104 -0
- package/server/api/README.md +45 -0
- package/server/api/_guard.js +75 -0
- package/server/api/admin.js +187 -0
- package/server/api/assets/docx-page.js +103 -0
- package/server/api/assets/helpers.js +22 -0
- package/server/api/assets/notes.js +144 -0
- package/server/api/assets.js +896 -0
- package/server/api/board.js +57 -0
- package/server/api/browse.js +138 -0
- package/server/api/canvas.js +444 -0
- package/server/api/chatai.js +324 -0
- package/server/api/exports/build-standalone.js +980 -0
- package/server/api/exports/cards.js +118 -0
- package/server/api/exports/docx-pdf.js +46 -0
- package/server/api/exports/export-page.js +142 -0
- package/server/api/exports/handoff.js +147 -0
- package/server/api/exports.js +690 -0
- package/server/api/instruction.js +75 -0
- package/server/api/local.js +115 -0
- package/server/api/me.js +116 -0
- package/server/api/memory.js +185 -0
- package/server/api/pending-changes.js +366 -0
- package/server/api/plugins.js +164 -0
- package/server/api/projects.js +220 -0
- package/server/api/publish.js +64 -0
- package/server/api/recent.js +73 -0
- package/server/api/sessions.js +589 -0
- package/server/api/skills.js +77 -0
- package/server/api/standalone-fit.js +168 -0
- package/server/api/turn-compose.js +180 -0
- package/server/api/turn-inflight.js +44 -0
- package/server/api/turn.js +669 -0
- package/server/auth/README.md +17 -0
- package/server/auth/middleware.js +138 -0
- package/server/auth/origin-guard.js +71 -0
- package/server/auth/session.js +108 -0
- package/server/auth/tier.js +113 -0
- package/server/auth/users-store.js +348 -0
- package/server/edit/README.md +29 -0
- package/server/engine/README.md +113 -0
- package/server/engine/agent/agent-shared.js +550 -0
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -0
- package/server/engine/agent/auto-mode-rules.js +78 -0
- package/server/engine/agent/context.js +418 -0
- package/server/engine/agent/events.js +464 -0
- package/server/engine/agent/hooks/canvas-validate.js +199 -0
- package/server/engine/agent/hooks/failure.js +120 -0
- package/server/engine/agent/hooks/file-events.js +75 -0
- package/server/engine/agent/hooks/lifecycle.js +233 -0
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -0
- package/server/engine/agent/hooks/post-guidance.js +206 -0
- package/server/engine/agent/hooks/post-subagent-report.js +104 -0
- package/server/engine/agent/hooks/post-trim.js +58 -0
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -0
- package/server/engine/agent/hooks/pre-defaults.js +84 -0
- package/server/engine/agent/hooks/pre-injectors.js +277 -0
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -0
- package/server/engine/agent/hooks/pre-starter-files.js +61 -0
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -0
- package/server/engine/agent/hooks/site-validate.js +94 -0
- package/server/engine/agent/hooks/tool-prompts.js +31 -0
- package/server/engine/agent/hooks/turn-state-memory.js +56 -0
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -0
- package/server/engine/agent/hooks.js +354 -0
- package/server/engine/agent/init-contract.js +69 -0
- package/server/engine/agent/isolation.js +159 -0
- package/server/engine/agent/model-context.js +359 -0
- package/server/engine/agent/model-table.js +292 -0
- package/server/engine/agent/plugin-loader.js +266 -0
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -0
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -0
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -0
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -0
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -0
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -0
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -0
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -0
- package/server/engine/agent/prompts/tools/site-reference.md +106 -0
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -0
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -0
- package/server/engine/agent/session-loop.js +1024 -0
- package/server/engine/agent/session-model.js +164 -0
- package/server/engine/agent/skill.js +204 -0
- package/server/engine/agent/system-prompts.js +86 -0
- package/server/engine/agent/task-events.js +78 -0
- package/server/engine/agents/ds-extractor.md +132 -0
- package/server/engine/agents/explorer.md +190 -0
- package/server/engine/agents/index.js +230 -0
- package/server/engine/agents/schemas/design-system.json +156 -0
- package/server/engine/agents/schemas/tweak-schema.json +113 -0
- package/server/engine/agents/tweak-proposer.md +127 -0
- package/server/engine/agents/vision-checker.md +254 -0
- package/server/engine/browse/capture.js +422 -0
- package/server/engine/browse/card.js +137 -0
- package/server/engine/browse/handover.js +59 -0
- package/server/engine/browse/page-digest.js +119 -0
- package/server/engine/browse/refs.js +196 -0
- package/server/engine/browse/registry.js +307 -0
- package/server/engine/browse/screencast.js +170 -0
- package/server/engine/browse/state.js +121 -0
- package/server/engine/chatai/chat-log.js +79 -0
- package/server/engine/chatai/index.js +193 -0
- package/server/engine/chatai/openai-compat.js +152 -0
- package/server/engine/chatai/orchestrate.js +309 -0
- package/server/engine/chatai/perform.js +40 -0
- package/server/engine/chatai/summarize.js +79 -0
- package/server/engine/mcp/capability-gate.js +52 -0
- package/server/engine/mcp/index.js +339 -0
- package/server/engine/mcp/param-sanitizer.js +142 -0
- package/server/engine/mcp/tools/arrange-on-board.js +124 -0
- package/server/engine/mcp/tools/artifact-session.js +278 -0
- package/server/engine/mcp/tools/browse-computer.js +421 -0
- package/server/engine/mcp/tools/browse-find-batch.js +183 -0
- package/server/engine/mcp/tools/browse-screenshot.js +157 -0
- package/server/engine/mcp/tools/browse.js +480 -0
- package/server/engine/mcp/tools/build-docx.js +101 -0
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -0
- package/server/engine/mcp/tools/create-on-board.js +155 -0
- package/server/engine/mcp/tools/crystallize-skill.js +168 -0
- package/server/engine/mcp/tools/deliver-files.js +0 -0
- package/server/engine/mcp/tools/explain-style.js +237 -0
- package/server/engine/mcp/tools/export-handoff.js +133 -0
- package/server/engine/mcp/tools/expose-tweaks.js +149 -0
- package/server/engine/mcp/tools/generate-image.js +842 -0
- package/server/engine/mcp/tools/get-computed-styles.js +164 -0
- package/server/engine/mcp/tools/get-pending-changes.js +205 -0
- package/server/engine/mcp/tools/h3box-ssh.js +84 -0
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -0
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -0
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -0
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -0
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -0
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -0
- package/server/engine/mcp/tools/helpers/rembg.js +273 -0
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -0
- package/server/engine/mcp/tools/highlight.js +68 -0
- package/server/engine/mcp/tools/list-pages.js +222 -0
- package/server/engine/mcp/tools/lookup-tags.js +59 -0
- package/server/engine/mcp/tools/navigate-to-page.js +60 -0
- package/server/engine/mcp/tools/organize-board.js +64 -0
- package/server/engine/mcp/tools/paint-still.js +434 -0
- package/server/engine/mcp/tools/pin-to-board.js +130 -0
- package/server/engine/mcp/tools/preview-deck.js +130 -0
- package/server/engine/mcp/tools/profile-scroll.js +305 -0
- package/server/engine/mcp/tools/publish-site.js +102 -0
- package/server/engine/mcp/tools/query-elements.js +195 -0
- package/server/engine/mcp/tools/read-board.js +103 -0
- package/server/engine/mcp/tools/read-document.js +93 -0
- package/server/engine/mcp/tools/read-page.js +264 -0
- package/server/engine/mcp/tools/read-tavern-json.js +143 -0
- package/server/engine/mcp/tools/record-decision.js +140 -0
- package/server/engine/mcp/tools/relate-on-board.js +110 -0
- package/server/engine/mcp/tools/remove-background.js +391 -0
- package/server/engine/mcp/tools/report-issue.js +115 -0
- package/server/engine/mcp/tools/roll-film.js +266 -0
- package/server/engine/mcp/tools/screenshot-docx.js +138 -0
- package/server/engine/mcp/tools/screenshot-url.js +195 -0
- package/server/engine/mcp/tools/screenshot.js +583 -0
- package/server/engine/mcp/tools/tier-gate.js +96 -0
- package/server/engine/mcp/tools/trace-motion.js +215 -0
- package/server/engine/mcp/tools/web-search.js +548 -0
- package/server/engine/motion/inventory.js +349 -0
- package/server/engine/perception/session.js +235 -0
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -0
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -0
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -0
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -0
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -0
- package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -0
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -0
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -0
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -0
- package/server/engine/runs/active-runs.js +698 -0
- package/server/engine/runs/live-turn.js +252 -0
- package/server/engine/runs/store.js +364 -0
- package/server/engine/runs/turn-relay.js +217 -0
- package/server/engine/runtime/workspace.js +164 -0
- package/server/index.js +235 -0
- package/server/lib/artifact-file-path.js +80 -0
- package/server/lib/artifact-target.js +284 -0
- package/server/lib/asset-refs.js +239 -0
- package/server/lib/async-queue.js +104 -0
- package/server/lib/auto-relations.js +130 -0
- package/server/lib/binding-types.js +60 -0
- package/server/lib/board-kind-sizes.js +62 -0
- package/server/lib/board-relations.js +133 -0
- package/server/lib/browse-proxy.js +180 -0
- package/server/lib/canvas-id.js +36 -0
- package/server/lib/cover.js +227 -0
- package/server/lib/danbooru-tags.js +291 -0
- package/server/lib/doc-extract.js +156 -0
- package/server/lib/docx/build-from-source.js +260 -0
- package/server/lib/docx/build.js +490 -0
- package/server/lib/docx/dump-styles.js +287 -0
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -0
- package/server/lib/docx/merge-runs.js +129 -0
- package/server/lib/docx/numbering.js +218 -0
- package/server/lib/docx/order.js +152 -0
- package/server/lib/docx/rawzip.js +172 -0
- package/server/lib/docx/render.js +92 -0
- package/server/lib/docx/text-lint.js +225 -0
- package/server/lib/docx/tokens.js +335 -0
- package/server/lib/docx/units.js +29 -0
- package/server/lib/docx/xml.js +291 -0
- package/server/lib/docx-pages.js +187 -0
- package/server/lib/export-collect.js +289 -0
- package/server/lib/export-package.js +210 -0
- package/server/lib/html-srcset.js +145 -0
- package/server/lib/image-variant.js +460 -0
- package/server/lib/ingress/forward-openai-chat.js +305 -0
- package/server/lib/ingress/openai-chat.js +491 -0
- package/server/lib/ingress/session-notice.js +66 -0
- package/server/lib/ingress/session-routes.js +68 -0
- package/server/lib/ingress/slot-probe.js +130 -0
- package/server/lib/ingress/upstream-billing.js +52 -0
- package/server/lib/ingress/upstream-fail-streak.js +72 -0
- package/server/lib/ingress/upstream-truncation.js +48 -0
- package/server/lib/issues-store.js +182 -0
- package/server/lib/kinds/deck.js +72 -0
- package/server/lib/kinds/docx.js +283 -0
- package/server/lib/kinds/file-kinds.js +113 -0
- package/server/lib/kinds/index.js +202 -0
- package/server/lib/kinds/site.js +161 -0
- package/server/lib/model-ingress.js +543 -0
- package/server/lib/moderation.js +255 -0
- package/server/lib/notice-store.js +103 -0
- package/server/lib/plugin-install.js +104 -0
- package/server/lib/plugin-validator.js +721 -0
- package/server/lib/publish-store.js +115 -0
- package/server/lib/quick-summary.js +52 -0
- package/server/lib/quota.js +270 -0
- package/server/lib/rate-window.js +29 -0
- package/server/lib/reference-assets.js +92 -0
- package/server/lib/region-shot.js +135 -0
- package/server/lib/safe-path.js +73 -0
- package/server/lib/sdk-session.js +33 -0
- package/server/lib/showcase-store.js +92 -0
- package/server/lib/site-publish.js +465 -0
- package/server/lib/ssrf-guard.js +432 -0
- package/server/lib/task-scan.js +158 -0
- package/server/lib/tavern-json.js +154 -0
- package/server/lib/video-variant.js +237 -0
- package/server/lib/workspace-path.js +33 -0
- package/server/ops/fix-sdk-musl.mjs +40 -0
- package/server/ops/install-macos-fonts.sh +114 -0
- package/server/ops/macos-fonts.conf +336 -0
- package/server/ops/sandbox-shim/bwrap +57 -0
- package/server/projects/assets-summary.js +119 -0
- package/server/projects/auto-name.js +53 -0
- package/server/projects/board-store.js +640 -0
- package/server/projects/move-entry.js +103 -0
- package/server/projects/store.js +270 -0
- package/server/projects/ui-config.js +54 -0
- package/server/projects/workspace-templates.js +53 -0
- package/server/projects/workspace.js +1025 -0
- package/server/runtime/capabilities.js +153 -0
- package/server/runtime/local-config.js +177 -0
- package/server/runtime/local-env.js +88 -0
- package/server/runtime/platform.js +342 -0
- package/server/runtime/profile.js +72 -0
- package/server/services/rembg-launcher.js +313 -0
- package/server/services/rembg-service.py +334 -0
- package/server/shared/README.md +25 -0
- package/server/shared/deck.js +54 -0
- package/server/shared/time.js +49 -0
- package/server/style-pipeline/README.md +53 -0
- package/server/ws/broker.js +36 -0
- package/server/ws/browse-channel.js +177 -0
- package/server/ws/index.js +386 -0
- package/web/README.md +89 -0
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +11 -0
- package/web/dist/assets/DeckWindow-4G0xX7P3.js +16 -0
- package/web/dist/assets/DocxWindow-BXQsoPkh.js +16 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/SiteWindow-D6eS-2gM.js +18 -0
- package/web/dist/assets/again-D8tfgKKO.webp +0 -0
- package/web/dist/assets/caveat-nd-CP6HlsNg.woff2 +0 -0
- package/web/dist/assets/clock-xJhvfYpS.webp +0 -0
- package/web/dist/assets/film-cut-BBKMwH7q.webp +0 -0
- package/web/dist/assets/film-still-BZ_DL9Tt.webp +0 -0
- package/web/dist/assets/index-C0KUf6Wd.css +1 -0
- package/web/dist/assets/index-Cbl3ATvp.js +2125 -0
- package/web/dist/assets/ink-desk-BRbxeDsR.webp +0 -0
- package/web/dist/assets/ink-night-Bz4BT3XH.webp +0 -0
- package/web/dist/assets/ink-portrait-C0DM4V8Q.webp +0 -0
- package/web/dist/assets/longcang-regular-C0uvTuKa.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
- package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
- package/web/dist/assets/pending-edit-apply-DTjo2geW.js +31 -0
- package/web/dist/assets/question-8H4HSqxh.webp +0 -0
- package/web/dist/assets/reject-BaPJqElj.webp +0 -0
- package/web/dist/assets/rotate-cw-LMT3x1Zj.js +11 -0
- package/web/dist/assets/rp-portrait-CbrGpew2.webp +0 -0
- package/web/dist/assets/rp-street-C4jVKjxx.webp +0 -0
- package/web/dist/assets/square-dashed-mouse-pointer-CBoAkfhi.js +11 -0
- package/web/dist/assets/tangle-DIxHVNuT.webp +0 -0
- package/web/dist/assets/thisone-BylZKglV.webp +0 -0
- package/web/dist/assets/thumb-DO1wfK1I.webp +0 -0
- package/web/dist/index.html +18 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/async-queue.js — push/pull 解耦的异步队列
|
|
3
|
+
*
|
|
4
|
+
* 用途:SDK Query streamInput 模式下,runAgent 需要一个 AsyncIterable<SDKUserMessage>
|
|
5
|
+
* 持续 yield user message 让 query 一直活着;前端 POST /turn 把 message push 进
|
|
6
|
+
* 队列,generator 那头 pull 后 yield 给 SDK。
|
|
7
|
+
*
|
|
8
|
+
* 行为:
|
|
9
|
+
* - push(item) 立即返回;waiter 等着就唤醒一个,否则进 items 数组
|
|
10
|
+
* - next() 返回 Promise;items 有就立刻 resolve,否则 Promise 挂着等 push / close
|
|
11
|
+
* - close() 终结流;所有 pending waiter 收到 { done: true };后续 push 抛错
|
|
12
|
+
* - return() 自动调 close(让 for-await-of 的 break 能正确收尾)
|
|
13
|
+
*
|
|
14
|
+
* 不是通用线程安全 queue —— 单事件循环内用,order 保证 FIFO。
|
|
15
|
+
*
|
|
16
|
+
* @template T
|
|
17
|
+
*/
|
|
18
|
+
export class AsyncQueue {
|
|
19
|
+
constructor() {
|
|
20
|
+
/** @type {T[]} */
|
|
21
|
+
this.items = [];
|
|
22
|
+
/** @type {Array<(r: IteratorResult<T>) => void>} */
|
|
23
|
+
this.waiters = [];
|
|
24
|
+
this.closed = false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 当前积压 item 数量(已 push 未 pull 的)。
|
|
29
|
+
* 给 UI "已排队 N 条" 显示用。
|
|
30
|
+
*/
|
|
31
|
+
get size() {
|
|
32
|
+
return this.items.length;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 是否还有 active waiter 在等(agent 已 pull 完积压,正等下一条)。
|
|
37
|
+
* 给"agent idle"判定用。
|
|
38
|
+
*/
|
|
39
|
+
get isPending() {
|
|
40
|
+
return this.waiters.length > 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 推一个 item 进队列。
|
|
45
|
+
* @param {T} item
|
|
46
|
+
* @throws {Error} 已 close 时抛
|
|
47
|
+
*/
|
|
48
|
+
push(item) {
|
|
49
|
+
if (this.closed) {
|
|
50
|
+
throw new Error('AsyncQueue: push after close');
|
|
51
|
+
}
|
|
52
|
+
if (this.waiters.length > 0) {
|
|
53
|
+
const w = this.waiters.shift();
|
|
54
|
+
w({ value: item, done: false });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.items.push(item);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 终结流。pending waiter 全部收到 done,items 里残留的 item 仍可 pull
|
|
62
|
+
* 直到清空,再之后 next() 返回 done。
|
|
63
|
+
*
|
|
64
|
+
* 幂等:多次调用 noop。
|
|
65
|
+
*/
|
|
66
|
+
close() {
|
|
67
|
+
if (this.closed) return;
|
|
68
|
+
this.closed = true;
|
|
69
|
+
while (this.waiters.length > 0) {
|
|
70
|
+
const w = this.waiters.shift();
|
|
71
|
+
w({ value: undefined, done: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── AsyncIterator protocol ──
|
|
76
|
+
|
|
77
|
+
[Symbol.asyncIterator]() {
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @returns {Promise<IteratorResult<T>>}
|
|
83
|
+
*/
|
|
84
|
+
next() {
|
|
85
|
+
if (this.items.length > 0) {
|
|
86
|
+
return Promise.resolve({ value: this.items.shift(), done: false });
|
|
87
|
+
}
|
|
88
|
+
if (this.closed) {
|
|
89
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
90
|
+
}
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
this.waiters.push(resolve);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* for-await-of 提前 break / throw 时被 runtime 调;触发 close。
|
|
98
|
+
* @returns {Promise<IteratorResult<T>>}
|
|
99
|
+
*/
|
|
100
|
+
return() {
|
|
101
|
+
this.close();
|
|
102
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/auto-relations.js — 自动落线层(2026-08-14,北极星路线 4)
|
|
3
|
+
*
|
|
4
|
+
* 有一类关系**不需要人或 agent 判断,机器可证**:html 里 `<img src>` 真实
|
|
5
|
+
* 指向工作区里的一张图,就是铁打的「取材」。这层把它们落成 `by:'auto'` 的
|
|
6
|
+
* ref 边 —— 关系密度从「靠自觉」变成「有底仓」,用户和 agent 的手画线在
|
|
7
|
+
* 底仓之上做增量。
|
|
8
|
+
*
|
|
9
|
+
* 三条纪律:
|
|
10
|
+
* 1. **只动自家的边**。自动边 id 恒为 `b:auto:ref:<hash>`(确定性 → 幂等),
|
|
11
|
+
* 对账时新增缺的、删掉失效的,**其余 id 一根手指都不碰** —— 用户/agent
|
|
12
|
+
* 画的线不归这层管。
|
|
13
|
+
* 2. **引用得真实存在**才落线。悬空 src 是裂图体检的事,不是关系。
|
|
14
|
+
* 3. **不画**。BindingLayer 对 auto+ref 不渲染 —— 一个站引三十张图,画出来
|
|
15
|
+
* 是蜘蛛网不是版面。它们的消费方是 agent 上下文(digest 收拢成一行)和
|
|
16
|
+
* 将来的主角判断(被 ref 指着的 = 素材,不该当 hero)。
|
|
17
|
+
*
|
|
18
|
+
* 触发:挂在 GET /artifacts 尾巴上 fire-and-forget(那里 manifests 现成),
|
|
19
|
+
* 每项目 30s 节流。产物身份的拼法与 BoardCanvas 的物件 id 同构 ——
|
|
20
|
+
* `deck:<file>` / `site:<root||''>` / 单页 `site:<entryRel>`。
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from 'node:fs/promises';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import crypto from 'node:crypto';
|
|
26
|
+
import { readBoard, patchBoard } from '../projects/board-store.js';
|
|
27
|
+
|
|
28
|
+
const MAX_HTML_BYTES = 512 * 1024;
|
|
29
|
+
const AUTO_REF_PREFIX = 'b:auto:ref:';
|
|
30
|
+
/** 每个项目对账节流(ms)—— /artifacts 是热接口,别把它变成扫盘器 */
|
|
31
|
+
const THROTTLE_MS = 30_000;
|
|
32
|
+
const lastRun = new Map();
|
|
33
|
+
|
|
34
|
+
const hashId = (from, to) =>
|
|
35
|
+
AUTO_REF_PREFIX + crypto.createHash('sha1').update(`${from}|${to}`).digest('hex').slice(0, 12);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 从一段 html 里抠出本地资源引用(src/href/poster + 内联 url())。
|
|
39
|
+
* 返回**相对 html 所在目录**的原始引用串(未归一)。
|
|
40
|
+
*/
|
|
41
|
+
export function extractRefs(html) {
|
|
42
|
+
const out = new Set();
|
|
43
|
+
const push = (raw) => {
|
|
44
|
+
if (!raw) return;
|
|
45
|
+
const v = raw.trim().replace(/^['"]|['"]$/g, '').split(/[?#]/)[0];
|
|
46
|
+
if (!v || /^(https?:|data:|blob:|mailto:|javascript:|\/\/)/i.test(v)) return;
|
|
47
|
+
out.add(v);
|
|
48
|
+
};
|
|
49
|
+
for (const m of html.matchAll(/<(?:img|video|audio|source|embed)\b[^>]*?\s(?:src|poster)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi)) push(m[1]);
|
|
50
|
+
for (const m of html.matchAll(/url\(\s*([^)]+?)\s*\)/gi)) push(m[1]);
|
|
51
|
+
return [...out];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 引用串 → 工作区相对路径(越界/绝对路径丢弃)。baseDir 是 html 所在目录(工作区相对)。 */
|
|
55
|
+
export function resolveRef(raw, baseDir) {
|
|
56
|
+
try {
|
|
57
|
+
const decoded = decodeURIComponent(raw);
|
|
58
|
+
if (decoded.startsWith('/')) return null; // 站内绝对路径按约定不出现,出现也不猜
|
|
59
|
+
const joined = path.posix.normalize(path.posix.join(baseDir || '.', decoded));
|
|
60
|
+
if (joined.startsWith('..') || path.posix.isAbsolute(joined)) return null;
|
|
61
|
+
return joined === '.' ? null : joined;
|
|
62
|
+
} catch { return null; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 对账一个项目的自动 ref 边。
|
|
67
|
+
*
|
|
68
|
+
* @param {string} pid
|
|
69
|
+
* @param {string} workspaceRoot 绝对路径
|
|
70
|
+
* @param {Array} tasks /artifacts 已算好的 tasks(id/artifacts 同构前端)
|
|
71
|
+
*/
|
|
72
|
+
export async function reconcileAutoRefs(pid, workspaceRoot, tasks) {
|
|
73
|
+
// 期望集:{ bid: { from, to } }
|
|
74
|
+
const desired = new Map();
|
|
75
|
+
for (const t of tasks || []) {
|
|
76
|
+
for (const a of t.artifacts || []) {
|
|
77
|
+
// 产物身份(与 BoardCanvas 的物件 id 同一拼法)
|
|
78
|
+
// ⚠️ 卡 id 按**形态**派生,别用「不是 deck 就是 site」的二分(2026-08-18 修)。
|
|
79
|
+
// docx 上线之后那个三元把 .docx 产物算成了 `site:` 前缀 —— id 对不上,
|
|
80
|
+
// 它的自动关系边落在一个不存在的端点上。规则跟前端 cardIdOf
|
|
81
|
+
// (web/src/lib/board-kinds.js)必须一致,两边岔开就是"同一件东西两份实现"。
|
|
82
|
+
const fromId = a.kind === 'site'
|
|
83
|
+
? `site:${a.single ? a.entryRel : (a.root || t.id)}`
|
|
84
|
+
: `${a.kind || 'deck'}:${a.file}`;
|
|
85
|
+
// 这件产物的 html 们(deck 一份;站点 = 各页,路径相对站根)
|
|
86
|
+
const pages = a.kind === 'deck'
|
|
87
|
+
? [a.file]
|
|
88
|
+
: (a.single ? [a.entryRel] : (a.pages || []).map(p => (a.root ? `${a.root}/${p}` : p)));
|
|
89
|
+
for (const rel of pages) {
|
|
90
|
+
if (!rel || !/\.html?$/i.test(rel)) continue;
|
|
91
|
+
let html;
|
|
92
|
+
try {
|
|
93
|
+
const abs = path.join(workspaceRoot, rel);
|
|
94
|
+
const st = await fs.stat(abs);
|
|
95
|
+
if (st.size > MAX_HTML_BYTES) continue;
|
|
96
|
+
html = await fs.readFile(abs, 'utf8');
|
|
97
|
+
} catch { continue; }
|
|
98
|
+
const baseDir = path.posix.dirname(rel.replace(/\\/g, '/'));
|
|
99
|
+
for (const raw of extractRefs(html)) {
|
|
100
|
+
const to = resolveRef(raw, baseDir === '.' ? '' : baseDir);
|
|
101
|
+
if (!to || to === rel) continue;
|
|
102
|
+
try { await fs.access(path.join(workspaceRoot, to)); } catch { continue; } // 悬空不落线
|
|
103
|
+
desired.set(hashId(fromId, to), { from: fromId, to });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 现状 → diff。只碰 b:auto:ref:* 的条目。
|
|
110
|
+
const board = await readBoard(pid);
|
|
111
|
+
const patch = {};
|
|
112
|
+
for (const [bid, b] of Object.entries(board.bindings || {})) {
|
|
113
|
+
if (bid.startsWith(AUTO_REF_PREFIX) && !desired.has(bid)) patch[bid] = null;
|
|
114
|
+
}
|
|
115
|
+
for (const [bid, { from, to }] of desired) {
|
|
116
|
+
const cur = board.bindings?.[bid];
|
|
117
|
+
if (cur && cur.from === from && cur.to === to) continue;
|
|
118
|
+
patch[bid] = { type: 'ref', from, to, by: 'auto' };
|
|
119
|
+
}
|
|
120
|
+
if (Object.keys(patch).length) await patchBoard(pid, { bindings: patch });
|
|
121
|
+
return { desired: desired.size, changed: Object.keys(patch).length };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 节流版:/artifacts 尾巴上 fire-and-forget 用 */
|
|
125
|
+
export function reconcileAutoRefsThrottled(pid, workspaceRoot, tasks) {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
if (now - (lastRun.get(pid) || 0) < THROTTLE_MS) return;
|
|
128
|
+
lastRun.set(pid, now);
|
|
129
|
+
reconcileAutoRefs(pid, workspaceRoot, tasks).catch(() => { /* fail-soft:对账绝不拖累清单接口 */ });
|
|
130
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 关系线的语义词汇表 —— 画布上一条线「是什么意思」的**唯一真相**。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么关系要单独存一张表
|
|
5
|
+
*
|
|
6
|
+
* 北极星是「任务文件夹能排出登录墙那种版面」。手摆之所以好看,是因为每张纸的
|
|
7
|
+
* 位置在回答「它和旁边那张什么关系」。而系统此前只知道每个产物的**尺寸和
|
|
8
|
+
* mtime** —— 关系数据不到位,再好的布局算法也只能排出「错落有致的网格」。
|
|
9
|
+
*
|
|
10
|
+
* 所以关系是 board.json 里的一等公民,跟 objects / zones 平级:
|
|
11
|
+
*
|
|
12
|
+
* bindings: { "<id>": { type, from, to, label?, by? } }
|
|
13
|
+
*
|
|
14
|
+
* 端点 `from` / `to` 是 **object id 或 zone id**(用户明确要求文件夹之间也能连线)。
|
|
15
|
+
* 线本身不存坐标 —— 端点一移动,线自己跟着走。这是 tldraw `TLBinding` 的思路:
|
|
16
|
+
* 关系是独立记录,不是形状的属性,箭头形状本身不存端点坐标。**抄的是思路,
|
|
17
|
+
* 不是代码**(tldraw 是专有许可证)。
|
|
18
|
+
*
|
|
19
|
+
* ## 为什么是这五种
|
|
20
|
+
*
|
|
21
|
+
* 挑的标准是「这条关系能不能改变布局决定」。能影响摆放的才收,纯装饰的不收。
|
|
22
|
+
* 每一种都对应创作过程里一件真实发生的事:
|
|
23
|
+
*
|
|
24
|
+
* - **改自**:一个产物是另一个的下一版。这是版本谱系,最该被看见的一条 ——
|
|
25
|
+
* 用户翻旧版找的就是它。
|
|
26
|
+
* - **批注**:一段文字在说某个东西。annotation 形态存在的理由。
|
|
27
|
+
* - **顺序**:分镜、章节、流程的先后。它决定读序,也就决定排布方向。
|
|
28
|
+
* - **取材**:这张图用在那个 deck 里 / 这份资料喂给了那篇稿。跨任务的引用。
|
|
29
|
+
* - **对照**:并列比较(proto-暖调 vs proto-冷调)。无向,且**明确要求并排**,
|
|
30
|
+
* 是唯一一条直接给布局下指令的关系。
|
|
31
|
+
*
|
|
32
|
+
* ## 明确不做成线的:「成组」
|
|
33
|
+
*
|
|
34
|
+
* 「这几件属于同一批」的正确画法是**圈一个框**,不是两两连线 —— n 个成员
|
|
35
|
+
* 两两连线是 n(n-1)/2 条,五件就是十条,画面直接糊掉。成组用已有的 zone
|
|
36
|
+
* (工作区 / 文件夹)表达,agent 建文件夹收纳就是在表达成组。
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 词汇表。`directed` 决定渲染时画不画箭头,也决定 from/to 能不能互换。
|
|
41
|
+
* `label` 是没写自定义文字时线上显示的默认词。
|
|
42
|
+
*/
|
|
43
|
+
export const BINDING_TYPES = {
|
|
44
|
+
'derives-from': { label: '改自', directed: true },
|
|
45
|
+
annotates: { label: '批注', directed: true },
|
|
46
|
+
flow: { label: '接着', directed: true },
|
|
47
|
+
ref: { label: '取材', directed: true },
|
|
48
|
+
contrast: { label: '对照', directed: false },
|
|
49
|
+
// 关联(2026-08-14,手动连线上线时加):五种预定义都不合身时的逃生舱。
|
|
50
|
+
// 含义由线上的 label 定义(用户写一句,agent 逐字读)。收它不违反
|
|
51
|
+
// 「纯装饰不收」——它对布局说「摆近点」(affinity),对 agent 说 label 那句话。
|
|
52
|
+
link: { label: '关联', directed: false },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** 词汇表 id 列表,校验和 parity 断言都用它。 */
|
|
56
|
+
export const BINDING_TYPE_IDS = Object.keys(BINDING_TYPES);
|
|
57
|
+
|
|
58
|
+
export function isBindingType(t) {
|
|
59
|
+
return Object.prototype.hasOwnProperty.call(BINDING_TYPES, t);
|
|
60
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* board-kind-sizes —— 画布物件身位的服务端镜像(2026-08-14,agent 摆位批)
|
|
3
|
+
*
|
|
4
|
+
* read_board / arrange_on_board / create_on_board 要在服务端估算卡片矩形
|
|
5
|
+
* (挨着谁摆、会不会压到谁),而身位真相住在前端 `web/src/lib/board-kinds.js`
|
|
6
|
+
* (渲染方)。这里抄一份**常量**,web 侧有 parity 测试钉着两边一致 ——
|
|
7
|
+
* 改那边忘了改这边,测试直接红(跟 binding-types 的双份表同一套纪律)。
|
|
8
|
+
*
|
|
9
|
+
* 文字/涂鸦不在表里:它们的 w/h 是逐条存在 board.json 里的(本体即数据)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { KINDS } from './kinds/index.js';
|
|
13
|
+
|
|
14
|
+
export const DECK_EMBED_W = 640;
|
|
15
|
+
export const ARTIFACT_HEADER_H = 28;
|
|
16
|
+
export const ARTIFACT_PREVIEW_H = { deck: 360, site: 400, docx: 420, browse: 360 };
|
|
17
|
+
|
|
18
|
+
// file 是 224x32 的细条卡(parity 测试上岗第一天就逮住我猜成 160x120 ——
|
|
19
|
+
// 那是涂鸦的默认身位。别猜,抄表)
|
|
20
|
+
export const KIND_SIZES = {
|
|
21
|
+
doc: { w: 200, h: 96 },
|
|
22
|
+
image: { w: 200, h: 176 },
|
|
23
|
+
video: { w: 240, h: 160 },
|
|
24
|
+
note: { w: 200, h: 148 },
|
|
25
|
+
file: { w: 224, h: 32 },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// 前缀表从注册表派生(「写死表家族」第 4 处,2026-08-18 收):手写
|
|
29
|
+
// `deck|site|docx` 的话加形态必漏,新形态的卡掉到 file 兜底(224×32 细条),
|
|
30
|
+
// agent 摆位按错矩形算。⚠️ 新形态要同步给 ARTIFACT_PREVIEW_H 加一行
|
|
31
|
+
//(两边都有 parity 测试钉着前端那份)。
|
|
32
|
+
const KIND_PREFIX_RE = new RegExp(`^(${Object.keys(KINDS).join('|')}):`);
|
|
33
|
+
|
|
34
|
+
const IMG_EXT = /\.(png|jpe?g|webp|gif|svg|avif)$/i;
|
|
35
|
+
const VIDEO_EXT = /\.(mp4|webm|mov)$/i;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 按 id + board.json 条目估身位。优先条目里存的 w/h(文字/涂鸦,或将来任何
|
|
39
|
+
* 显式尺寸),其次按 id 形态推:kind 前缀 → 产物卡,扩展名 → 图/视频/便签/文件。
|
|
40
|
+
*/
|
|
41
|
+
export function estimateSize(id, entry) {
|
|
42
|
+
if (entry && Number.isFinite(entry.w) && Number.isFinite(entry.h)) {
|
|
43
|
+
return { w: entry.w, h: entry.h };
|
|
44
|
+
}
|
|
45
|
+
const s = String(id || '');
|
|
46
|
+
if (s.startsWith('doc:')) return KIND_SIZES.doc;
|
|
47
|
+
// 浏览器卡是**单例**,id 就是 'browse'(没有路径可挂 —— 它背后不是文件)。
|
|
48
|
+
// 不给这条分支的话它会掉到最后的 file 兜底(224×32 的细条),agent 摆位时
|
|
49
|
+
// 就会拿一个错的矩形去算"挨着谁摆、会不会压到谁"。
|
|
50
|
+
if (s === 'browse') {
|
|
51
|
+
return { w: DECK_EMBED_W, h: ARTIFACT_HEADER_H + ARTIFACT_PREVIEW_H.browse };
|
|
52
|
+
}
|
|
53
|
+
const m = KIND_PREFIX_RE.exec(s);
|
|
54
|
+
if (m) {
|
|
55
|
+
const t = m[1];
|
|
56
|
+
return { w: DECK_EMBED_W, h: ARTIFACT_HEADER_H + ARTIFACT_PREVIEW_H[t] };
|
|
57
|
+
}
|
|
58
|
+
if (IMG_EXT.test(s)) return KIND_SIZES.image;
|
|
59
|
+
if (VIDEO_EXT.test(s)) return KIND_SIZES.video;
|
|
60
|
+
if (/\.(md|txt)$/i.test(s)) return KIND_SIZES.note;
|
|
61
|
+
return KIND_SIZES.file;
|
|
62
|
+
}
|
|
@@ -0,0 +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, 80);
|
|
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
|
+
}
|
|
@@ -0,0 +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 };
|