@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,842 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp/tools/generate-image.js — generate_image MCP tool
|
|
3
|
+
*
|
|
4
|
+
* 调用 Gemini 3.1 Flash Image Preview(Nano Banana 2)通过 NoDesk passthrough
|
|
5
|
+
* 网关 → DMXAPI 落点。给主 agent 的"画图"能力,让 deck/landing 类产物里
|
|
6
|
+
* 能塞 hero / cover / bg / icon / decoration / portrait / illustration
|
|
7
|
+
* / quote-backdrop / section-divider / pattern。
|
|
8
|
+
*
|
|
9
|
+
* 调用约定(agent 端):
|
|
10
|
+
* mcp__nodesign__generate_image
|
|
11
|
+
* prompt: string 自然描述场景(不堆关键词)
|
|
12
|
+
* aspectRatio?: enum 14 种官方比例,default '16:9'
|
|
13
|
+
* imageSize?: '512'|'1K'|'2K'|'4K' default '1K'
|
|
14
|
+
* referenceImages?: string[] workspace 相对路径,max 14
|
|
15
|
+
* (Gemini 3.1 Flash 文档:人物 ≤4、物体 ≤10)
|
|
16
|
+
* assetRole?: enum 落档语义类,影响 default 命名 + emit 字段
|
|
17
|
+
* outputName?: string 不带后缀;default `gen-${ts}-${role}`
|
|
18
|
+
* thinkingLevel?: 'minimal'|'high' default 'minimal'(latency 优先)
|
|
19
|
+
* responseModalities?: array default ['IMAGE']
|
|
20
|
+
*
|
|
21
|
+
* 返回 CallToolResult:
|
|
22
|
+
* content: [
|
|
23
|
+
* { type: 'text', text: 'Generated <name>.png at assets/generated/<name>.png ...' },
|
|
24
|
+
* { type: 'image', data: <base64>, mimeType: 'image/png' },
|
|
25
|
+
* ]
|
|
26
|
+
*
|
|
27
|
+
* 落地:
|
|
28
|
+
* 优先 <sharedRoot>/assets/generated/<name>.png(跨 session 复用 + 软链让
|
|
29
|
+
* sessions/<sid>/assets/ 直接看见),fallback <workspaceRoot>/assets/generated/。
|
|
30
|
+
* 从 sessions/<sid>/canvas.html 引用即 `assets/generated/<name>.png`。
|
|
31
|
+
*
|
|
32
|
+
* 网关:
|
|
33
|
+
* POST <NODESIGN_GATEWAY_URL>/default/passthrough
|
|
34
|
+
* Authorization: Bearer <NODESIGN_GATEWAY_KEY>
|
|
35
|
+
* body 顶层注入 channel="DMX" + channel_url=<DMXAPI base>/v1beta/models/<model>:generateContent
|
|
36
|
+
* 剩下字段是 Gemini 标准 generateContent 协议(contents / generationConfig)
|
|
37
|
+
*
|
|
38
|
+
* 不复用 binary-fixup-proxy:那个只接 /v1/messages(Anthropic 协议),
|
|
39
|
+
* Gemini 走 /v1beta/...。MCP tool 在 server 进程内跑,直接 fetch 最干净。
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import path from 'node:path';
|
|
43
|
+
import fs from 'node:fs/promises';
|
|
44
|
+
import { spawn } from 'node:child_process';
|
|
45
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
46
|
+
import { Events } from '../../agent/events.js';
|
|
47
|
+
import { z } from 'zod';
|
|
48
|
+
import sharp from 'sharp';
|
|
49
|
+
import {
|
|
50
|
+
THUMBNAIL_MAX_DIM, THUMBNAIL_QUALITY, writeWebpSibling, // 预热由 writeWebpSibling 顺带做
|
|
51
|
+
} from '../../../lib/image-variant.js';
|
|
52
|
+
|
|
53
|
+
// Thumbnail 配置(env 可调)。**原图不动**——保留 Gemini 输出的全分辨率(通常
|
|
54
|
+
// 1080×1920+ PNG,6-8MB)让用户最终交付不损失质量。仅生成低清 thumbnail 给
|
|
55
|
+
// chat 缩略图 + WS 推送用,避免单条 message 8MB+ 让浏览器 parse 卡。
|
|
56
|
+
// 长边 512 + JPEG q80 → ~50KB,chat / WS 流畅。原图通过 HTTP /api/.../assets/...
|
|
57
|
+
// 按需加载(iframe 引用原图,用户点查看大图也加载原图)。
|
|
58
|
+
/**
|
|
59
|
+
* 用 sharp 生成低清 thumbnail(不动原图)。
|
|
60
|
+
* 长边 ≤ THUMBNAIL_MAX_DIM;统一 webp 输出。
|
|
61
|
+
*
|
|
62
|
+
* 2026-07-31 从 JPEG 换成 webp:同观感小三成,而且 webp 有 alpha,抠图产物
|
|
63
|
+
* 不用再平铺白底 —— 原来那圈白底在预览里是真能看见的。
|
|
64
|
+
* 规格常量从 lib/image-variant.js 来:资源路由给老图现补缩略图时用的是同一份,
|
|
65
|
+
* 两边各写各的数字只会表现为某些图偶尔糊一点,查不出来。
|
|
66
|
+
*
|
|
67
|
+
* fail-soft:sharp 抛错返 null 让调用方降级。
|
|
68
|
+
*
|
|
69
|
+
* @param {Buffer} rawBuf
|
|
70
|
+
* @returns {Promise<{ buf: Buffer, mimeType: string }|null>}
|
|
71
|
+
*/
|
|
72
|
+
async function makeThumbnail(rawBuf) {
|
|
73
|
+
try {
|
|
74
|
+
const meta = await sharp(rawBuf).metadata();
|
|
75
|
+
const w = meta.width || 0;
|
|
76
|
+
const h = meta.height || 0;
|
|
77
|
+
let pipeline = sharp(rawBuf);
|
|
78
|
+
const longEdge = Math.max(w, h);
|
|
79
|
+
if (longEdge > THUMBNAIL_MAX_DIM) {
|
|
80
|
+
pipeline = pipeline.resize({
|
|
81
|
+
width: w >= h ? THUMBNAIL_MAX_DIM : null,
|
|
82
|
+
height: h > w ? THUMBNAIL_MAX_DIM : null,
|
|
83
|
+
fit: 'inside',
|
|
84
|
+
withoutEnlargement: true,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const buf = await pipeline.webp({ quality: THUMBNAIL_QUALITY }).toBuffer();
|
|
88
|
+
return { buf, mimeType: 'image/webp' };
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.warn(`[generate-image] thumbnail failed (${err.message}), chat will use raw or skip`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Model 路由:默认 flash (NB2);anchor 类关键图(cover / character bible
|
|
96
|
+
// identity sheet / brand mockup hero)可升 pro 拿 commercial-grade 质量。
|
|
97
|
+
// Pro 比 Flash 慢 + 贵 ~2-3×,但质量提升对"会被复用为 referenceImages 种子"
|
|
98
|
+
// 的图值得——种子错了下游全漂、整个 deck 返工成本更高。
|
|
99
|
+
// spike 实测 NoDesk + DMXAPI 两个 model id 都通。
|
|
100
|
+
const MODELS = {
|
|
101
|
+
flash: 'gemini-3.1-flash-image-preview',
|
|
102
|
+
pro: 'gemini-3-pro-image-preview',
|
|
103
|
+
};
|
|
104
|
+
const DEFAULT_MODEL = 'flash';
|
|
105
|
+
|
|
106
|
+
// 14 种官方比例(Gemini 3.1 Flash Image Preview 文档)
|
|
107
|
+
const ASPECT_RATIOS = [
|
|
108
|
+
'1:1', '16:9', '9:16', '3:2', '2:3', '4:5', '5:4',
|
|
109
|
+
'21:9', '4:1', '1:4', '8:1', '1:8', '3:4', '4:3',
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const IMAGE_SIZES = ['512', '1K', '2K', '4K'];
|
|
113
|
+
|
|
114
|
+
const ASSET_ROLES = [
|
|
115
|
+
'hero', 'cover', 'bg', 'frame', 'icon', 'decoration',
|
|
116
|
+
'portrait', 'illustration', 'quote-backdrop', 'section-divider', 'pattern',
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const RESPONSE_MODALITIES = ['IMAGE', 'TEXT'];
|
|
120
|
+
|
|
121
|
+
const MIME_BY_EXT = {
|
|
122
|
+
'.png': 'image/png',
|
|
123
|
+
'.jpg': 'image/jpeg',
|
|
124
|
+
'.jpeg': 'image/jpeg',
|
|
125
|
+
'.webp': 'image/webp',
|
|
126
|
+
'.gif': 'image/gif',
|
|
127
|
+
// PDF:NB2 支持文档输入(generateContent inline_data application/pdf)。
|
|
128
|
+
// spike 实测 NoDesk + DMXAPI 透传通,且 NB2 真读 PDF 文本生成准确数据
|
|
129
|
+
// 可视化(Q3 sales report PDF → 4 stat card 信息图,数字一一对上)。
|
|
130
|
+
// 用例详见 cookbook § K Document-to-visual。
|
|
131
|
+
'.pdf': 'application/pdf',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const DEFAULT_NODESK_URL = 'https://llm-gateway-api.nodesk.tech';
|
|
135
|
+
const DEFAULT_DMXAPI_BASE = 'https://www.dmxapi.cn';
|
|
136
|
+
const DEFAULT_CHANNEL = 'DMX';
|
|
137
|
+
|
|
138
|
+
// ── codex 生图桥(2026-07-27:NoDesk 网关退役,codex 成为默认 provider)──
|
|
139
|
+
// 骑 codex CLI 订阅(零 API 费):spawn `codex exec` 让它调自带图像生成工具,
|
|
140
|
+
// 图直接落到我们指定的绝对路径。实测单张 ~45-60s,参考图走 -i 附件(同样实测
|
|
141
|
+
// 风格参照有效)。桥接 prompt 必须写死"逐字传递零改写"——codex agent 默认会
|
|
142
|
+
// 按自己的 Augmentation rules 润色 prompt。
|
|
143
|
+
const IMAGE_PROVIDER = () => (process.env.NODESIGN_IMAGE_PROVIDER || 'codex').toLowerCase();
|
|
144
|
+
const CODEX_BIN = process.env.NODESIGN_CODEX_BIN || 'codex';
|
|
145
|
+
const CODEX_IMAGE_TIMEOUT_MS = Number(process.env.NODESIGN_CODEX_IMAGE_TIMEOUT_MS) || 240_000;
|
|
146
|
+
|
|
147
|
+
function buildCodexBridgePrompt({ prompt, aspectRatio, absOut, refCount }) {
|
|
148
|
+
return [
|
|
149
|
+
'你是图像生成管道的执行端,只做下面几件事,不做任何多余动作:',
|
|
150
|
+
'1. 调用你的图像生成工具生成一张图。<image-prompt> 标签内的内容必须逐字作为生成 prompt,禁止改写、增删、翻译或润色。',
|
|
151
|
+
`2. 输出比例:${aspectRatio}。优先用工具的比例/尺寸参数;工具没有对应参数时,作为补充说明传给工具,但不修改 <image-prompt> 原文。`,
|
|
152
|
+
refCount > 0
|
|
153
|
+
? `3. 本消息附带 ${refCount} 张参考图,把它们作为图像生成的参考输入(风格 / 主体一致性参照)。`
|
|
154
|
+
: '3. 本次无参考图。',
|
|
155
|
+
`4. 生成后把图片文件复制到精确路径 ${absOut}(目录已存在)。`,
|
|
156
|
+
'5. 最后只回复该绝对路径。',
|
|
157
|
+
'<image-prompt>',
|
|
158
|
+
prompt,
|
|
159
|
+
'</image-prompt>',
|
|
160
|
+
].join('\n');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 跑一次 codex exec 生图,以目标文件落盘为成功标准(codex 的文本回复不可信),
|
|
165
|
+
* 失败自动重试一次。abort signal / 超时都 SIGKILL 子进程。
|
|
166
|
+
*/
|
|
167
|
+
async function runCodexImageGen({ bridgePrompt, refPaths, cwd, signal, expectFile, timeoutMs = CODEX_IMAGE_TIMEOUT_MS }) {
|
|
168
|
+
const args = ['exec', '--skip-git-repo-check', '-s', 'workspace-write', '-C', cwd, bridgePrompt];
|
|
169
|
+
for (const p of refPaths) args.push('-i', p);
|
|
170
|
+
|
|
171
|
+
const runOnce = () => new Promise((resolve, reject) => {
|
|
172
|
+
const child = spawn(CODEX_BIN, args, { stdio: ['ignore', 'pipe', 'pipe'], env: process.env });
|
|
173
|
+
let stderrTail = '';
|
|
174
|
+
child.stdout.on('data', () => { /* 排空防背压 */ });
|
|
175
|
+
child.stderr.on('data', (d) => { stderrTail = (stderrTail + d.toString()).slice(-2000); });
|
|
176
|
+
const killTimer = setTimeout(() => {
|
|
177
|
+
try { child.kill('SIGKILL'); } catch { /* */ }
|
|
178
|
+
reject(new Error(`codex exec timeout after ${Math.round(timeoutMs / 1000)}s`));
|
|
179
|
+
}, timeoutMs);
|
|
180
|
+
const onAbort = () => { try { child.kill('SIGKILL'); } catch { /* */ } };
|
|
181
|
+
signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
182
|
+
child.on('error', (err) => { clearTimeout(killTimer); reject(err); });
|
|
183
|
+
child.on('close', (code) => {
|
|
184
|
+
clearTimeout(killTimer);
|
|
185
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
186
|
+
if (signal?.aborted) return reject(new Error('aborted'));
|
|
187
|
+
if (code !== 0) return reject(new Error(`codex exec exited ${code}: ${stderrTail.slice(-300) || 'no stderr'}`));
|
|
188
|
+
resolve();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
193
|
+
try {
|
|
194
|
+
await runOnce();
|
|
195
|
+
const st = await fs.stat(expectFile).catch(() => null);
|
|
196
|
+
if (st && st.size > 0) return;
|
|
197
|
+
throw new Error(`codex finished but target file missing/empty: ${expectFile}`);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (attempt === 2 || signal?.aborted) throw err;
|
|
200
|
+
console.warn(`[generate-image] codex attempt ${attempt} failed (${err.message}), retrying once`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const PASSTHROUGH_PATH = '/default/passthrough';
|
|
206
|
+
// model id 在 callGateway 时动态拼,因为支持 flash / pro 路由
|
|
207
|
+
const generateContentPathFor = (modelId) => `/v1beta/models/${modelId}:generateContent`;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 把 referenceImages 路径解析到 sharedRoot/workspaceRoot 之一。防止 traversal。
|
|
211
|
+
*
|
|
212
|
+
* @param {string} relPath
|
|
213
|
+
* @param {string} workspaceRoot
|
|
214
|
+
* @param {string|null} sharedRoot
|
|
215
|
+
* @returns {Promise<{ abs: string, mimeType: string }>}
|
|
216
|
+
* @throws {Error} 路径越界 / 文件不存在 / 不支持的 mime
|
|
217
|
+
*/
|
|
218
|
+
async function resolveReferenceImage(relPath, workspaceRoot, sharedRoot) {
|
|
219
|
+
if (path.isAbsolute(relPath)) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`referenceImages must be relative paths inside the workspace; got absolute: ${relPath}`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const candidates = [workspaceRoot];
|
|
226
|
+
if (sharedRoot) candidates.push(sharedRoot);
|
|
227
|
+
|
|
228
|
+
let absResolved = null;
|
|
229
|
+
let baseUsed = null;
|
|
230
|
+
for (const base of candidates) {
|
|
231
|
+
const candidate = path.resolve(base, relPath);
|
|
232
|
+
// 防 traversal:resolved path 必须在 base 之内(含 base 本身)
|
|
233
|
+
if (candidate === base || candidate.startsWith(base + path.sep)) {
|
|
234
|
+
absResolved = candidate;
|
|
235
|
+
baseUsed = base;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (!absResolved) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`referenceImages path escapes workspace/shared roots: ${relPath}`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 真实存在 + 可读
|
|
246
|
+
await fs.access(absResolved);
|
|
247
|
+
|
|
248
|
+
const ext = path.extname(absResolved).toLowerCase();
|
|
249
|
+
const mimeType = MIME_BY_EXT[ext];
|
|
250
|
+
if (!mimeType) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`Unsupported reference format ${ext} (allowed: png/jpg/jpeg/webp/gif/pdf): ${relPath}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return { abs: absResolved, mimeType, baseUsed };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 调网关返回 Gemini 响应 body(已 parse)。
|
|
260
|
+
*
|
|
261
|
+
* @returns {Promise<object>} parsed JSON
|
|
262
|
+
* @throws {Error} 401/HTTP 错误 / 网络错误
|
|
263
|
+
*/
|
|
264
|
+
async function callGateway(payload, { gatewayUrl, gatewayKey, channel, channelBase, modelId, signal }) {
|
|
265
|
+
const passthroughUrl = gatewayUrl.replace(/\/$/, '') + PASSTHROUGH_PATH;
|
|
266
|
+
const channelUrl = channelBase.replace(/\/$/, '') + generateContentPathFor(modelId);
|
|
267
|
+
|
|
268
|
+
const wrapped = {
|
|
269
|
+
channel,
|
|
270
|
+
channel_url: channelUrl,
|
|
271
|
+
...payload,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const res = await fetch(passthroughUrl, {
|
|
275
|
+
method: 'POST',
|
|
276
|
+
headers: {
|
|
277
|
+
'Authorization': `Bearer ${gatewayKey}`,
|
|
278
|
+
'Content-Type': 'application/json',
|
|
279
|
+
},
|
|
280
|
+
body: JSON.stringify(wrapped),
|
|
281
|
+
signal,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (!res.ok) {
|
|
285
|
+
const text = await res.text().catch(() => '');
|
|
286
|
+
const snippet = text.slice(0, 400);
|
|
287
|
+
const hint =
|
|
288
|
+
res.status === 401 || res.status === 403
|
|
289
|
+
? ' (auth failed — check NODESIGN_GATEWAY_KEY)'
|
|
290
|
+
: res.status === 429
|
|
291
|
+
? ' (rate limit / quota — try again later)'
|
|
292
|
+
: '';
|
|
293
|
+
throw new Error(`gateway HTTP ${res.status}${hint}: ${snippet}`);
|
|
294
|
+
}
|
|
295
|
+
return await res.json();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 从 Gemini 响应里提第一张图(base64 PNG)。多张时只取第一。
|
|
300
|
+
* 有些响应 model 会在 thought 阶段产中间图(thought:true)—— 跳掉那些,
|
|
301
|
+
* 取 final(无 thought 标记的)image part。
|
|
302
|
+
*
|
|
303
|
+
* @returns {{ base64: string, mimeType: string, accompanyText: string }}
|
|
304
|
+
* @throws {Error} 无 image part
|
|
305
|
+
*/
|
|
306
|
+
function extractFinalImage(response) {
|
|
307
|
+
const parts = response?.candidates?.[0]?.content?.parts || [];
|
|
308
|
+
if (!Array.isArray(parts) || parts.length === 0) {
|
|
309
|
+
throw new Error('Gemini response has no parts');
|
|
310
|
+
}
|
|
311
|
+
let lastImage = null;
|
|
312
|
+
let firstFinalImage = null;
|
|
313
|
+
const accompanyTexts = [];
|
|
314
|
+
for (const p of parts) {
|
|
315
|
+
if (p.inlineData?.data) {
|
|
316
|
+
lastImage = p.inlineData;
|
|
317
|
+
if (!p.thought && !firstFinalImage) firstFinalImage = p.inlineData;
|
|
318
|
+
} else if (p.inline_data?.data) {
|
|
319
|
+
lastImage = p.inline_data;
|
|
320
|
+
if (!p.thought && !firstFinalImage) firstFinalImage = p.inline_data;
|
|
321
|
+
} else if (p.text && !p.thought) {
|
|
322
|
+
accompanyTexts.push(p.text);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const chosen = firstFinalImage || lastImage;
|
|
326
|
+
if (!chosen) throw new Error('Gemini response has no image data');
|
|
327
|
+
return {
|
|
328
|
+
base64: chosen.data,
|
|
329
|
+
mimeType: chosen.mimeType || chosen.mime_type || 'image/png',
|
|
330
|
+
accompanyText: accompanyTexts.join('\n').trim(),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function safeBaseName(s) {
|
|
335
|
+
return String(s || '')
|
|
336
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
337
|
+
.replace(/^-+|-+$/g, '')
|
|
338
|
+
.slice(0, 64);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function buildOutputName(outputName, assetRole) {
|
|
342
|
+
if (outputName) {
|
|
343
|
+
const safe = safeBaseName(outputName);
|
|
344
|
+
if (safe) return safe;
|
|
345
|
+
}
|
|
346
|
+
const ts = Date.now();
|
|
347
|
+
const role = safeBaseName(assetRole || 'image');
|
|
348
|
+
return `gen-${ts}-${role}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @param {object} deps
|
|
353
|
+
* @param {string} deps.workspaceRoot agent cwd(sessions/<sid>/ 模式或老 runId 模式)
|
|
354
|
+
* @param {string|null} [deps.sharedRoot] project shared/,存在时优先落档于此
|
|
355
|
+
* @param {import('../../agent/context.js').AgentContext} [deps.ctx]
|
|
356
|
+
*/
|
|
357
|
+
export function makeGenerateImageTool({ workspaceRoot, sharedRoot = null, ctx } = {}) {
|
|
358
|
+
return tool(
|
|
359
|
+
'generate_image',
|
|
360
|
+
`Generate a high-quality image.
|
|
361
|
+
Use this to add hero / cover / background / frame / icon / decoration / portrait
|
|
362
|
+
/ illustration / quote-backdrop / section-divider / pattern visuals to canvas.html.
|
|
363
|
+
|
|
364
|
+
BACKEND: codex-imagegen (subscription) -> gpt-image-2. What actually works is
|
|
365
|
+
prompt + aspectRatio + referenceImages (+ assetRole / outputName for naming).
|
|
366
|
+
imageSize / thinkingLevel / responseModalities / model / useGrounding are
|
|
367
|
+
Gemini-gateway-only and SILENTLY IGNORED — do not spend effort on them.
|
|
368
|
+
PDF referenceImages are NOT supported (images only). One call produces exactly
|
|
369
|
+
ONE image; there is no "3 variations in one prompt". Expect ~45-60s per image —
|
|
370
|
+
prefer one good anchor shot over many speculative variants.
|
|
371
|
+
|
|
372
|
+
Saves the image to assets/generated/<name>.png inside the workspace (visible
|
|
373
|
+
across sessions via the shared/ softlink). Returns the image as an inline
|
|
374
|
+
content block so you can vision-check it immediately.
|
|
375
|
+
|
|
376
|
+
PROMPT WRITING:
|
|
377
|
+
- Picture the finished frame first, then describe what you see, in order.
|
|
378
|
+
Positive description beats piling up negations.
|
|
379
|
+
- Describe the scene narratively, don't list keywords.
|
|
380
|
+
- If the user's prompt is already detailed, normalize it — do not expand it
|
|
381
|
+
with creative additions they did not ask for.
|
|
382
|
+
- For photorealism use camera language: 85mm lens, wide-angle, macro,
|
|
383
|
+
golden-hour lighting, three-point softbox, etc.
|
|
384
|
+
- For icons / stickers: explicitly say "white background" (transparent is
|
|
385
|
+
not supported; use remove_background afterwards if you need alpha).
|
|
386
|
+
- For text-in-image: quote the exact text and state the font style
|
|
387
|
+
("clean sans-serif", "bold serif headline").
|
|
388
|
+
- Do NOT write size or ratio into the prompt body ("in 4K", "16:9
|
|
389
|
+
widescreen") — that only makes the backend resize once for nothing.
|
|
390
|
+
Ratio goes in aspectRatio.
|
|
391
|
+
|
|
392
|
+
ASPECT RATIO defaults by use:
|
|
393
|
+
- cover/hero/landscape: 16:9 or 21:9
|
|
394
|
+
- portrait/avatar: 4:5 or 2:3
|
|
395
|
+
- icon/sticker/pattern: 1:1
|
|
396
|
+
- vertical banner: 9:16
|
|
397
|
+
HARD LIMIT: gpt-image-2 cannot exceed a 3:1 long-to-short ratio. 4:1, 1:4,
|
|
398
|
+
8:1 and 1:8 are accepted by the schema but CANNOT be produced natively — for
|
|
399
|
+
a thin banner, render 16:9 and crop it with CSS object-fit instead.
|
|
400
|
+
|
|
401
|
+
REFERENCES:
|
|
402
|
+
Pass workspace-relative paths (e.g., 'assets/photo.jpg' or
|
|
403
|
+
'assets/generated/prev.png'). Image formats: png/jpg/jpeg/webp/gif.
|
|
404
|
+
HTTP urls are rejected. Pass the 1-2 most on-point images — feeding many
|
|
405
|
+
dilutes the anchor. Label each image's role in the prompt text
|
|
406
|
+
("Image 1: edit target, Image 2: style reference").
|
|
407
|
+
Use cases:
|
|
408
|
+
- Style transfer: pass an image, describe the new style
|
|
409
|
+
- Character consistency: pass 1-2 portraits across multi-page deck
|
|
410
|
+
- Composition / mockup: pass logo + model image, describe how they combine
|
|
411
|
+
- Inpainting: pass the canvas screenshot, describe what to change
|
|
412
|
+
NOTE: the backend is stateless — it does not remember earlier images or this
|
|
413
|
+
conversation. To iterate on a previous image, pass it back in referenceImages
|
|
414
|
+
and restate the invariants every round.
|
|
415
|
+
|
|
416
|
+
WHEN TO USE:
|
|
417
|
+
- You're building a deck / landing / report and want real imagery
|
|
418
|
+
- You need a backdrop that pure CSS gradient can't achieve
|
|
419
|
+
- You want a sample image to align style with the user before batch-generating
|
|
420
|
+
- You have user-uploaded reference and need to extend / restyle / combine
|
|
421
|
+
|
|
422
|
+
WHEN NOT TO USE:
|
|
423
|
+
- Pure UI controls (buttons, form fields) — use Tailwind + shadcn instead
|
|
424
|
+
- Data charts — use Recharts/ECharts/Mermaid via React mount
|
|
425
|
+
- Simple inline icons (≤5 per page) — use lucide-react inline SVG
|
|
426
|
+
|
|
427
|
+
ALWAYS pair generation with mcp__nodesign__record_decision so the prompt + role
|
|
428
|
+
land on the shared decision sticky (notes/decisions). Its params are title +
|
|
429
|
+
rationale (both required), scope, alternatives — there is no "topic" param.`,
|
|
430
|
+
{
|
|
431
|
+
prompt: z
|
|
432
|
+
.string()
|
|
433
|
+
.min(4)
|
|
434
|
+
.max(3500)
|
|
435
|
+
.describe('Natural-language scene description. Describe, don\'t list keywords.'),
|
|
436
|
+
aspectRatio: z
|
|
437
|
+
.enum(ASPECT_RATIOS)
|
|
438
|
+
.optional()
|
|
439
|
+
.describe('Output aspect ratio; default 16:9. See doc for use-case mapping.'),
|
|
440
|
+
imageSize: z
|
|
441
|
+
.enum(IMAGE_SIZES)
|
|
442
|
+
.optional()
|
|
443
|
+
.describe('Resolution tier; default 1K. 4K only when print-grade detail required.'),
|
|
444
|
+
referenceImages: z
|
|
445
|
+
.array(z.string().min(1))
|
|
446
|
+
.max(14)
|
|
447
|
+
.optional()
|
|
448
|
+
.describe('Workspace-relative paths to references (png/jpg/webp/gif image OR .pdf document). Max 14 (≤4 character + ≤10 object). Use for style transfer / character consistency / inpainting / document-to-visual (cookbook § E + § K).'),
|
|
449
|
+
assetRole: z
|
|
450
|
+
.enum(ASSET_ROLES)
|
|
451
|
+
.optional()
|
|
452
|
+
.describe('Semantic role; affects default output name + UI badge. One of hero/cover/bg/frame/icon/decoration/portrait/illustration/quote-backdrop/section-divider/pattern.'),
|
|
453
|
+
outputName: z
|
|
454
|
+
.string()
|
|
455
|
+
.max(64)
|
|
456
|
+
.optional()
|
|
457
|
+
.describe('Output filename without extension. Auto-generated if omitted (gen-<timestamp>-<role>).'),
|
|
458
|
+
thinkingLevel: z
|
|
459
|
+
.enum(['minimal', 'high'])
|
|
460
|
+
.optional()
|
|
461
|
+
.describe('Gemini thinking budget; "minimal" (default) for low latency, "high" for complex composition.'),
|
|
462
|
+
responseModalities: z
|
|
463
|
+
.array(z.enum(RESPONSE_MODALITIES))
|
|
464
|
+
.min(1)
|
|
465
|
+
.max(2)
|
|
466
|
+
.optional()
|
|
467
|
+
.describe('Output modalities; default ["IMAGE"]. Add "TEXT" if you want the model\'s commentary alongside the image.'),
|
|
468
|
+
model: z
|
|
469
|
+
.enum(['flash', 'pro'])
|
|
470
|
+
.optional()
|
|
471
|
+
.describe('NB2 model tier; "flash" (default, gemini-3.1-flash-image-preview) for most images. "pro" (gemini-3-pro-image-preview, ~2-3× slower & costlier) only for anchor shots that become referenceImages seeds for downstream pages — cover hero / character bible identity sheet / brand mockup hero. See cookbook § H model routing.'),
|
|
472
|
+
useGrounding: z
|
|
473
|
+
.boolean()
|
|
474
|
+
.optional()
|
|
475
|
+
.describe('Enable Google Image Search grounding for real-world subjects (landmarks / cities / products / nature / specific brands). Default false. When true, model can pull real images from web during generation to anchor visual fidelity. Adds ~60-90s latency. Model auto-skips for people/character queries (Google guardrail). Sources saved to <name>.grounding.json sidecar. See cookbook § L.'),
|
|
476
|
+
},
|
|
477
|
+
async ({
|
|
478
|
+
prompt,
|
|
479
|
+
aspectRatio = '16:9',
|
|
480
|
+
imageSize = '1K',
|
|
481
|
+
referenceImages,
|
|
482
|
+
assetRole,
|
|
483
|
+
outputName,
|
|
484
|
+
thinkingLevel = 'minimal',
|
|
485
|
+
responseModalities = ['IMAGE'],
|
|
486
|
+
model = DEFAULT_MODEL,
|
|
487
|
+
useGrounding = false,
|
|
488
|
+
}) => {
|
|
489
|
+
const modelId = MODELS[model];
|
|
490
|
+
if (!modelId) {
|
|
491
|
+
return {
|
|
492
|
+
content: [{
|
|
493
|
+
type: 'text',
|
|
494
|
+
text: `generate_image failed: unknown model '${model}'. Use 'flash' or 'pro'.`,
|
|
495
|
+
}],
|
|
496
|
+
isError: true,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
// 1. provider 分流(2026-07-27:NoDesk 网关退役,默认 codex 订阅生图;
|
|
500
|
+
// gateway 分支保留给显式 NODESIGN_IMAGE_PROVIDER=gateway 的场景)
|
|
501
|
+
const provider = IMAGE_PROVIDER();
|
|
502
|
+
|
|
503
|
+
// 输出命名 + 目录提前定:codex 分支需要先有确定的目标路径让 codex 落盘
|
|
504
|
+
const finalName = buildOutputName(outputName, assetRole);
|
|
505
|
+
const useShared = !!sharedRoot;
|
|
506
|
+
const outDir = path.join(
|
|
507
|
+
useShared ? sharedRoot : workspaceRoot,
|
|
508
|
+
'assets',
|
|
509
|
+
'generated',
|
|
510
|
+
);
|
|
511
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
512
|
+
|
|
513
|
+
// 2. 解析 referenceImages(fail-fast;两个 provider 共用解析,消费方式不同:
|
|
514
|
+
// codex 用 abs 路径走 -i 附件,gateway 读文件转 base64 inline parts)
|
|
515
|
+
const resolvedRefs = [];
|
|
516
|
+
if (referenceImages && referenceImages.length > 0) {
|
|
517
|
+
for (const rel of referenceImages) {
|
|
518
|
+
try {
|
|
519
|
+
resolvedRefs.push(await resolveReferenceImage(rel, workspaceRoot, sharedRoot));
|
|
520
|
+
} catch (err) {
|
|
521
|
+
return {
|
|
522
|
+
content: [{
|
|
523
|
+
type: 'text',
|
|
524
|
+
text: `generate_image failed resolving referenceImages[${rel}]: ${err.message}`,
|
|
525
|
+
}],
|
|
526
|
+
isError: true,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
let imgBuf;
|
|
533
|
+
let outMime = 'image/png';
|
|
534
|
+
let accompanyText = null;
|
|
535
|
+
let response = null; // gateway 分支才有(grounding metadata 从这取)
|
|
536
|
+
let fileName;
|
|
537
|
+
let absOut;
|
|
538
|
+
|
|
539
|
+
if (provider === 'codex') {
|
|
540
|
+
const pdfRef = resolvedRefs.find((r) => r.mimeType === 'application/pdf');
|
|
541
|
+
if (pdfRef) {
|
|
542
|
+
return {
|
|
543
|
+
content: [{
|
|
544
|
+
type: 'text',
|
|
545
|
+
text: 'generate_image failed: codex provider 不支持 PDF reference(-i 只收图片)。先把 PDF 内容转述进 prompt,或截图后当图片 reference。',
|
|
546
|
+
}],
|
|
547
|
+
isError: true,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
fileName = `${finalName}.png`;
|
|
551
|
+
absOut = path.join(outDir, fileName);
|
|
552
|
+
const bridgePrompt = buildCodexBridgePrompt({
|
|
553
|
+
prompt, aspectRatio, absOut, refCount: resolvedRefs.length,
|
|
554
|
+
});
|
|
555
|
+
try {
|
|
556
|
+
await runCodexImageGen({
|
|
557
|
+
bridgePrompt,
|
|
558
|
+
refPaths: resolvedRefs.map((r) => r.abs),
|
|
559
|
+
cwd: outDir,
|
|
560
|
+
signal: ctx?.abortController?.signal,
|
|
561
|
+
expectFile: absOut,
|
|
562
|
+
});
|
|
563
|
+
} catch (err) {
|
|
564
|
+
return {
|
|
565
|
+
content: [{
|
|
566
|
+
type: 'text',
|
|
567
|
+
text: `generate_image codex error: ${err?.message || String(err)}`,
|
|
568
|
+
}],
|
|
569
|
+
isError: true,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
imgBuf = await fs.readFile(absOut);
|
|
573
|
+
} else {
|
|
574
|
+
// ── gateway 分支(显式 opt-in)──
|
|
575
|
+
const gatewayUrl = process.env.NODESIGN_GATEWAY_URL || DEFAULT_NODESK_URL;
|
|
576
|
+
const gatewayKey = process.env.NODESIGN_GATEWAY_KEY;
|
|
577
|
+
if (!gatewayKey) {
|
|
578
|
+
return {
|
|
579
|
+
content: [{
|
|
580
|
+
type: 'text',
|
|
581
|
+
text: 'generate_image failed: NODESIGN_IMAGE_PROVIDER=gateway 但 NODESIGN_GATEWAY_KEY 未设。',
|
|
582
|
+
}],
|
|
583
|
+
isError: true,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
const channel = process.env.NODESIGN_GATEWAY_CHANNEL || DEFAULT_CHANNEL;
|
|
587
|
+
const channelBase =
|
|
588
|
+
process.env.NODESIGN_GATEWAY_CHANNEL_URL_BASE || DEFAULT_DMXAPI_BASE;
|
|
589
|
+
|
|
590
|
+
const inlineImageParts = [];
|
|
591
|
+
for (const resolved of resolvedRefs) {
|
|
592
|
+
const buf = await fs.readFile(resolved.abs);
|
|
593
|
+
inlineImageParts.push({
|
|
594
|
+
inline_data: {
|
|
595
|
+
mime_type: resolved.mimeType,
|
|
596
|
+
data: buf.toString('base64'),
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Gemini generateContent payload
|
|
602
|
+
const parts = [{ text: prompt }, ...inlineImageParts];
|
|
603
|
+
const payload = {
|
|
604
|
+
contents: [{ parts }],
|
|
605
|
+
generationConfig: {
|
|
606
|
+
responseModalities,
|
|
607
|
+
imageConfig: {
|
|
608
|
+
aspectRatio,
|
|
609
|
+
imageSize,
|
|
610
|
+
},
|
|
611
|
+
thinkingConfig: {
|
|
612
|
+
thinkingLevel: thinkingLevel === 'high' ? 'High' : 'Minimal',
|
|
613
|
+
includeThoughts: false,
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
// Image Search Grounding:opt-in。NB2 自决要不要真用(人物 query
|
|
617
|
+
// 模型自动跳过,Google guardrail;地标/产品/真实场景才会触发)。
|
|
618
|
+
...(useGrounding ? { tools: [{ googleSearch: {} }] } : {}),
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
response = await callGateway(payload, {
|
|
623
|
+
gatewayUrl,
|
|
624
|
+
gatewayKey,
|
|
625
|
+
channel,
|
|
626
|
+
channelBase,
|
|
627
|
+
modelId,
|
|
628
|
+
signal: ctx?.abortController?.signal,
|
|
629
|
+
});
|
|
630
|
+
} catch (err) {
|
|
631
|
+
return {
|
|
632
|
+
content: [{
|
|
633
|
+
type: 'text',
|
|
634
|
+
text: `generate_image gateway error: ${err?.message || String(err)}`,
|
|
635
|
+
}],
|
|
636
|
+
isError: true,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
let extracted;
|
|
641
|
+
try {
|
|
642
|
+
extracted = extractFinalImage(response);
|
|
643
|
+
} catch (err) {
|
|
644
|
+
return {
|
|
645
|
+
content: [{
|
|
646
|
+
type: 'text',
|
|
647
|
+
text:
|
|
648
|
+
`generate_image failed: ${err.message}. `
|
|
649
|
+
+ `Response keys: ${Object.keys(response || {}).join(', ')}. `
|
|
650
|
+
+ `Try refining the prompt or check gateway logs.`,
|
|
651
|
+
}],
|
|
652
|
+
isError: true,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// 扩展名跟 Gemini 返回的 mimeType 走(实测 Gemini 3.1 Flash Image
|
|
657
|
+
// 经常返 image/jpeg 而不是 png,硬写 .png 会让文件名和真实编码不一致)
|
|
658
|
+
const ext = (() => {
|
|
659
|
+
switch ((extracted.mimeType || '').toLowerCase()) {
|
|
660
|
+
case 'image/jpeg': case 'image/jpg': return '.jpg';
|
|
661
|
+
case 'image/webp': return '.webp';
|
|
662
|
+
case 'image/gif': return '.gif';
|
|
663
|
+
case 'image/png':
|
|
664
|
+
default: return '.png';
|
|
665
|
+
}
|
|
666
|
+
})();
|
|
667
|
+
imgBuf = Buffer.from(extracted.base64, 'base64');
|
|
668
|
+
outMime = extracted.mimeType || 'image/png';
|
|
669
|
+
accompanyText = extracted.accompanyText || null;
|
|
670
|
+
fileName = `${finalName}${ext}`;
|
|
671
|
+
absOut = path.join(outDir, fileName);
|
|
672
|
+
// 原图不压缩——保留全分辨率给最终交付(导出 / iframe 引用)
|
|
673
|
+
await fs.writeFile(absOut, imgBuf);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// 额外生成 thumbnail(仅给 chat 缩略图 / WS 推送用,原图保留)
|
|
677
|
+
// 落到 .thumbnails/ 子目录,agent 通常不引用(隐藏目录命名暗示),但能被
|
|
678
|
+
// /api/.../assets/.thumbnails/foo.thumb.webp 路径访问(assets endpoint 不限子树)
|
|
679
|
+
const thumbDir = path.join(outDir, '.thumbnails');
|
|
680
|
+
await fs.mkdir(thumbDir, { recursive: true });
|
|
681
|
+
const thumbName = `${finalName}.thumb.webp`;
|
|
682
|
+
const absThumb = path.join(thumbDir, thumbName);
|
|
683
|
+
const thumb = await makeThumbnail(imgBuf);
|
|
684
|
+
if (thumb) {
|
|
685
|
+
await fs.writeFile(absThumb, thumb.buf);
|
|
686
|
+
console.log(`[generate-image] saved ${fileName} ${imgBuf.length}B + thumb ${thumb.buf.length}B`);
|
|
687
|
+
} else {
|
|
688
|
+
console.log(`[generate-image] saved ${fileName} ${imgBuf.length}B (thumb skipped)`);
|
|
689
|
+
}
|
|
690
|
+
const thumbAgentRelPath = thumb ? path.posix.join('assets', 'generated', '.thumbnails', thumbName) : null;
|
|
691
|
+
|
|
692
|
+
// 兄弟 webp(2026-08-18):页面里引它,PNG 是母版留给用户下载和再编辑。
|
|
693
|
+
// 实现在 image-variant.js(跟派生层同一个 q82),它**顺带预热派生档**。
|
|
694
|
+
const webp = await writeWebpSibling(absOut, imgBuf, 'assets/generated');
|
|
695
|
+
|
|
696
|
+
// 语义 sidecar(2026-07-27 工作台):.meta/<name>.json 记录物件来历,
|
|
697
|
+
// /api/.../artifacts 清单合并给产物墙显示(prompt / 角色 / 来源 run)。
|
|
698
|
+
// fail-soft:写不进不影响生图主流程。
|
|
699
|
+
try {
|
|
700
|
+
const metaDir = path.join(outDir, '.meta');
|
|
701
|
+
await fs.mkdir(metaDir, { recursive: true });
|
|
702
|
+
await fs.writeFile(path.join(metaDir, `${finalName}.json`), JSON.stringify({
|
|
703
|
+
prompt,
|
|
704
|
+
assetRole: assetRole || null,
|
|
705
|
+
aspectRatio,
|
|
706
|
+
provider,
|
|
707
|
+
model: provider === 'codex' ? 'codex' : model,
|
|
708
|
+
referenceImageCount: resolvedRefs.length,
|
|
709
|
+
sessionId: ctx?.sessionId || null,
|
|
710
|
+
runId: ctx?.runId || null,
|
|
711
|
+
ts: new Date().toISOString(),
|
|
712
|
+
}, null, 2));
|
|
713
|
+
} catch (err) {
|
|
714
|
+
console.warn(`[generate-image] meta sidecar write failed: ${err.message}`);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Path the agent sees relative to its cwd (sessions/<sid>/) — when
|
|
718
|
+
// sharedRoot is in play, sessions/<sid>/assets is a softlink to
|
|
719
|
+
// shared/assets, so relative path is the same either way.
|
|
720
|
+
const agentRelPath = path.posix.join('assets', 'generated', fileName);
|
|
721
|
+
|
|
722
|
+
// 6.5 提 grounding metadata(仅 useGrounding=true 且 model 真触发了搜索时存在)
|
|
723
|
+
// 落 sidecar `<name>.grounding.json` 给前端 attribution UI / spec.json 审计用。
|
|
724
|
+
// 模型对人物 query 自动跳过 grounding,那时这块为空——不落 sidecar,行为同普通生图。
|
|
725
|
+
const candidate = response?.candidates?.[0] || {};
|
|
726
|
+
const groundingMetadata = candidate.groundingMetadata || candidate.grounding_metadata;
|
|
727
|
+
let groundingPath = null;
|
|
728
|
+
let groundingSourceCount = 0;
|
|
729
|
+
let groundingQueries = [];
|
|
730
|
+
let groundingTopSources = [];
|
|
731
|
+
if (groundingMetadata) {
|
|
732
|
+
const sidecarName = `${finalName}.grounding.json`;
|
|
733
|
+
const absSidecar = path.join(outDir, sidecarName);
|
|
734
|
+
try {
|
|
735
|
+
await fs.writeFile(absSidecar, JSON.stringify(groundingMetadata, null, 2));
|
|
736
|
+
groundingPath = path.posix.join('assets', 'generated', sidecarName);
|
|
737
|
+
} catch (err) {
|
|
738
|
+
console.warn(`[generate-image] grounding sidecar write failed: ${err.message}`);
|
|
739
|
+
}
|
|
740
|
+
const chunks = groundingMetadata.groundingChunks || [];
|
|
741
|
+
groundingSourceCount = chunks.length;
|
|
742
|
+
groundingQueries = (groundingMetadata.webSearchQueries || []).slice(0, 5);
|
|
743
|
+
groundingTopSources = chunks.slice(0, 5).map((c) => ({
|
|
744
|
+
title: c.web?.title || null,
|
|
745
|
+
uri: c.web?.uri || null,
|
|
746
|
+
}));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// 7a. emit file_changed —— 让图**当场**上墙。
|
|
750
|
+
//
|
|
751
|
+
// MCP 工具写盘不走 PostToolUse(Write|Edit) 那条 file_changed 直发(matcher
|
|
752
|
+
// 匹配不到 mcp__nodesign__* 工具名),所以生成的图在这一发之前对前端是不存在的:
|
|
753
|
+
// 产物墙只在 listVersion / boardVersion 变化时才重拉 /artifacts,而这两个都要等
|
|
754
|
+
// run.done 的兜底刷新。结果就是"图生完了,要等这一轮跑完才出现在任务文件夹里"。
|
|
755
|
+
// record-decision.js 早补过同样一发。⚠️发相对路径不发 absOut:绝对路径会在前端孵出「home」影子文件夹(stage.js 拒收注释详述)
|
|
756
|
+
try {
|
|
757
|
+
ctx?.emit?.(Events.fileChanged(agentRelPath, 'add'));
|
|
758
|
+
} catch { /* fail-safe */ }
|
|
759
|
+
|
|
760
|
+
// 7b. emit run.image_generated(前端可显 thumbnail / 加 timeline 节点)
|
|
761
|
+
try {
|
|
762
|
+
ctx?.emit?.({
|
|
763
|
+
type: 'run.image_generated',
|
|
764
|
+
path: agentRelPath, // 原图路径(agent 引用 + 前端"查看大图"链接)
|
|
765
|
+
thumbnailPath: thumbAgentRelPath, // null 时表示 thumbnail 生成失败
|
|
766
|
+
absPath: absOut,
|
|
767
|
+
sizeBytes: imgBuf.length,
|
|
768
|
+
thumbnailSizeBytes: thumb?.buf.length || null,
|
|
769
|
+
prompt,
|
|
770
|
+
assetRole: assetRole || null,
|
|
771
|
+
aspectRatio,
|
|
772
|
+
imageSize,
|
|
773
|
+
model: provider === 'codex' ? 'codex' : model, // 前端 badge 显示 + spec.json 审计
|
|
774
|
+
referenceImageCount: resolvedRefs.length,
|
|
775
|
+
accompanyText,
|
|
776
|
+
groundingUsed: groundingPath !== null, // model 真触发了搜索
|
|
777
|
+
groundingSourceCount,
|
|
778
|
+
groundingPath, // sidecar 相对路径,前端读 attribution HTML
|
|
779
|
+
});
|
|
780
|
+
} catch { /* fail-safe */ }
|
|
781
|
+
|
|
782
|
+
// 8. 返回 CallToolResult — text caption + image content block
|
|
783
|
+
const captionParts = [
|
|
784
|
+
`Generated ${fileName}`,
|
|
785
|
+
`at ${agentRelPath}`,
|
|
786
|
+
provider === 'codex'
|
|
787
|
+
? `(${aspectRatio}, codex-imagegen, ${(imgBuf.length / 1024).toFixed(1)} KB)`
|
|
788
|
+
: `(${aspectRatio}, ${imageSize}, ${model}, ${(imgBuf.length / 1024).toFixed(1)} KB)`,
|
|
789
|
+
];
|
|
790
|
+
if (webp) {
|
|
791
|
+
captionParts.push(`— 页面里引 ${webp.rel}(${(webp.bytes / 1024).toFixed(0)} KB,`
|
|
792
|
+
+ `比 PNG 母版小 ${Math.max(1, Math.round(imgBuf.length / Math.max(webp.bytes, 1)))}×);`
|
|
793
|
+
+ 'PNG 是母版,别往页面里引。');
|
|
794
|
+
}
|
|
795
|
+
if (assetRole) captionParts.push(`role=${assetRole}`);
|
|
796
|
+
if (resolvedRefs.length > 0) {
|
|
797
|
+
captionParts.push(`with ${resolvedRefs.length} reference image${resolvedRefs.length > 1 ? 's' : ''}`);
|
|
798
|
+
}
|
|
799
|
+
if (groundingPath) {
|
|
800
|
+
captionParts.push(`grounded with ${groundingSourceCount} source${groundingSourceCount > 1 ? 's' : ''}`);
|
|
801
|
+
} else if (useGrounding) {
|
|
802
|
+
captionParts.push('(grounding requested but model didn\'t fire — likely person/character query, see cookbook § L)');
|
|
803
|
+
}
|
|
804
|
+
const caption = captionParts.join(' ');
|
|
805
|
+
|
|
806
|
+
const content = [{ type: 'text', text: caption }];
|
|
807
|
+
if (accompanyText) {
|
|
808
|
+
content.push({ type: 'text', text: `Model commentary: ${accompanyText}` });
|
|
809
|
+
}
|
|
810
|
+
if (groundingPath) {
|
|
811
|
+
// 给 agent 看到本次 grounding 用的搜索 + top sources,方便它在回话里
|
|
812
|
+
// 简短报给用户("grounded with 5 sources from <queries>");完整 attribution
|
|
813
|
+
// HTML 在 sidecar 里供前端 chip UI 读。
|
|
814
|
+
const sourceLines = groundingTopSources
|
|
815
|
+
.filter((s) => s.title || s.uri)
|
|
816
|
+
.map((s, i) => ` [${i + 1}] ${s.title || ''} ${s.uri || ''}`.trim())
|
|
817
|
+
.join('\n');
|
|
818
|
+
content.push({
|
|
819
|
+
type: 'text',
|
|
820
|
+
text:
|
|
821
|
+
`Image Search Grounding active.\n`
|
|
822
|
+
+ `Queries: ${groundingQueries.join(' | ') || '(none)'}\n`
|
|
823
|
+
+ `Top sources (${groundingSourceCount} total):\n${sourceLines || ' (none)'}\n`
|
|
824
|
+
+ `Full attribution metadata: ${groundingPath}`,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
// image content block 用 thumbnail base64(原图通过 HTTP 按需加载,不走 WS):
|
|
828
|
+
// 原图 base64 化后单条 WS message 8MB+ 让浏览器 parse 卡 / nginx upstream 也痛苦。
|
|
829
|
+
// thumbnail ~50KB 推 chat 缩略图够清晰,用户看大图点开走 HTTP /api/.../assets/...
|
|
830
|
+
// agent 仍能通过 caption 里的 agentRelPath 引用原图(`<img src="assets/generated/foo.png">`)。
|
|
831
|
+
// thumbnail 失败时降级回原 base64(保险,agent 至少能看到图)。
|
|
832
|
+
const imageBlockData = thumb ? thumb.buf.toString('base64') : imgBuf.toString('base64');
|
|
833
|
+
const imageBlockMime = thumb ? thumb.mimeType : outMime;
|
|
834
|
+
content.push({
|
|
835
|
+
type: 'image',
|
|
836
|
+
data: imageBlockData,
|
|
837
|
+
mimeType: imageBlockMime,
|
|
838
|
+
});
|
|
839
|
+
return { content };
|
|
840
|
+
},
|
|
841
|
+
);
|
|
842
|
+
}
|