@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,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/runtime/capabilities.js — 本机能力位:外部二进制 / 外部服务钥匙 / 本地产线,启动时探一遍,
|
|
3
|
+
* 工具注册与界面都问它(一份表,两个读者)。
|
|
4
|
+
*
|
|
5
|
+
* 为什么需要它(08-22 盘点):仓库里同一件事有三种写法——ffmpeg 有 hasFfmpeg() 探针并降级、rembg 有
|
|
6
|
+
* isAvailable() 返回可操作的装法、playwright 20 个调用点里只有 2 个接住了「Executable doesn't exist」,
|
|
7
|
+
* 其余是 500 或一条裸的工具错误。本地分发版装在别人的机器上,缺依赖是常态不是事故:
|
|
8
|
+
* - 工具层:engine/mcp/capability-gate.js 按这里的结果给工具描述加「⛔ 不可用 + 装法」、调用期直接拦
|
|
9
|
+
* - 界面层:GET /api/local/status 带 capabilities,配置页画成一张表
|
|
10
|
+
* - 日志层:platform.dump 之后打一行汇总
|
|
11
|
+
*
|
|
12
|
+
* 探测只在启动做一次(二进制装好了要重启;配置页的「重启」按钮就是干这个的)。
|
|
13
|
+
* 探不到 ≠ 不存在:whichBinary 按 PATH + 几个已知安装位置找,找不到就如实说 detail,别替用户猜。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { isAvailable as rembgAvailable, REMBG_SETUP_HINT } from '../engine/mcp/tools/helpers/rembg.js';
|
|
20
|
+
|
|
21
|
+
const isWin = process.platform === 'win32';
|
|
22
|
+
|
|
23
|
+
/** 跨平台 which:PATH(Windows 连 PATHEXT)+ 调用方给的额外目录。找不到返回 null */
|
|
24
|
+
export function whichBinary(name, extraDirs = []) {
|
|
25
|
+
if (!name) return null;
|
|
26
|
+
if (path.isAbsolute(name)) return fs.existsSync(name) ? name : null;
|
|
27
|
+
const dirs = [...(process.env.PATH || '').split(path.delimiter).filter(Boolean), ...extraDirs];
|
|
28
|
+
const exts = isWin ? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').map((e) => e.toLowerCase()) : [''];
|
|
29
|
+
for (const dir of dirs) {
|
|
30
|
+
for (const ext of exts) {
|
|
31
|
+
const candidate = path.join(dir, isWin && !name.toLowerCase().endsWith(ext) ? name + ext : name);
|
|
32
|
+
try {
|
|
33
|
+
const st = fs.statSync(candidate);
|
|
34
|
+
if (st.isFile()) return candidate;
|
|
35
|
+
} catch { /* 下一个 */ }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const home = os.homedir();
|
|
42
|
+
const LO_DIRS = isWin
|
|
43
|
+
? ['C:\\Program Files\\LibreOffice\\program', 'C:\\Program Files (x86)\\LibreOffice\\program']
|
|
44
|
+
: process.platform === 'darwin'
|
|
45
|
+
? ['/Applications/LibreOffice.app/Contents/MacOS', path.join(home, 'Applications/LibreOffice.app/Contents/MacOS')]
|
|
46
|
+
: ['/usr/lib/libreoffice/program', '/opt/libreoffice/program', '/snap/bin'];
|
|
47
|
+
const BREW_DIRS = process.platform === 'darwin' ? ['/opt/homebrew/bin', '/usr/local/bin'] : [];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 能力表。kind:binary | key | service。level:'required'(没有就跑不起来)| 'feature'(少一块功能)。
|
|
51
|
+
* fix 写给人看:装法一句话。probe 返回 { available, detail, path? }。
|
|
52
|
+
*/
|
|
53
|
+
export const CAPABILITY_DEFS = Object.freeze([
|
|
54
|
+
{ id: 'git', kind: 'binary', level: 'required', label: 'git', uses: '项目工作区的版本历史(建项目就要用)',
|
|
55
|
+
fix: isWin ? '装 Git for Windows(git-scm.com),装完重开终端' : 'apt/brew install git',
|
|
56
|
+
probe: () => bin('git') },
|
|
57
|
+
{ id: 'chromium', kind: 'binary', level: 'feature', label: 'Chromium(playwright)', uses: '截图自检 / 页面感知 / 浏览器工具 / PDF·PPTX 导出 / 封面',
|
|
58
|
+
fix: 'npx playwright install chromium',
|
|
59
|
+
probe: async () => {
|
|
60
|
+
let pw;
|
|
61
|
+
try { pw = await import('playwright'); } catch (err) { return { available: false, detail: `playwright 包加载失败:${err.message}` }; }
|
|
62
|
+
let p = null;
|
|
63
|
+
try { p = pw.chromium.executablePath(); } catch (err) { return { available: false, detail: `executablePath: ${err.message}` }; }
|
|
64
|
+
return p && fs.existsSync(p) ? { available: true, detail: p, path: p } : { available: false, detail: `浏览器未下载(期望在 ${p || '?'})` };
|
|
65
|
+
} },
|
|
66
|
+
{ id: 'libreoffice', kind: 'binary', level: 'feature', label: 'LibreOffice(soffice)', uses: 'Word/docx 形态:渲页图、缩略图、导出 PDF、build_docx 的页图体检',
|
|
67
|
+
fix: isWin ? '官网装 LibreOffice(默认装到 C:\\Program Files\\LibreOffice)' : process.platform === 'darwin' ? 'brew install --cask libreoffice' : 'apt install libreoffice',
|
|
68
|
+
probe: () => bin('soffice', LO_DIRS) },
|
|
69
|
+
{ id: 'poppler', kind: 'binary', level: 'feature', label: 'poppler(pdftoppm)', uses: 'docx 页图(PDF → PNG)',
|
|
70
|
+
fix: isWin ? '装 poppler for Windows 并把 bin 目录加进 PATH' : process.platform === 'darwin' ? 'brew install poppler' : 'apt install poppler-utils',
|
|
71
|
+
probe: () => bin('pdftoppm', BREW_DIRS) },
|
|
72
|
+
{ id: 'ffmpeg', kind: 'binary', level: 'feature', label: 'ffmpeg', uses: '视频转码(没有就发原片)',
|
|
73
|
+
fix: isWin ? '装 ffmpeg 并加进 PATH' : process.platform === 'darwin' ? 'brew install ffmpeg' : 'apt install ffmpeg',
|
|
74
|
+
probe: () => bin('ffmpeg', BREW_DIRS) },
|
|
75
|
+
{ id: 'rembg', kind: 'service', level: 'feature', label: 'rembg 抠图环境', uses: 'remove_background',
|
|
76
|
+
fix: `${REMBG_SETUP_HINT}(见 server/services/rembg-launcher.js)`,
|
|
77
|
+
probe: async () => { const r = await rembgAvailable(); return { available: r.available, detail: r.available ? `mode=${r.mode}` : r.reason }; } },
|
|
78
|
+
{ id: 'imageGen', kind: 'service', level: 'feature', label: '生图通道', uses: 'generate_image',
|
|
79
|
+
fix: '默认走 codex CLI:npm i -g @openai/codex && codex login;或 NODESIGN_IMAGE_PROVIDER=gateway + NODESIGN_GATEWAY_KEY',
|
|
80
|
+
probe: () => {
|
|
81
|
+
const provider = (process.env.NODESIGN_IMAGE_PROVIDER || 'codex').toLowerCase();
|
|
82
|
+
if (provider === 'gateway') return { available: !!process.env.NODESIGN_GATEWAY_KEY, detail: process.env.NODESIGN_GATEWAY_KEY ? 'gateway(NODESIGN_GATEWAY_KEY 已配)' : 'NODESIGN_GATEWAY_KEY 为空' };
|
|
83
|
+
const r = bin(process.env.NODESIGN_CODEX_BIN || 'codex');
|
|
84
|
+
return { ...r, detail: r.available ? `codex:${r.detail}(是否已 codex login 这里探不到)` : `codex CLI 不在 PATH(${r.detail})` };
|
|
85
|
+
} },
|
|
86
|
+
{ id: 'webSearch', kind: 'key', level: 'feature', label: '联网搜索钥匙', uses: 'web_search',
|
|
87
|
+
fix: '.env 里配 NODESIGN_TAVILY_KEY / NODESIGN_EXA_KEY / NODESIGN_BAIDU_QIANFAN_KEY / NODESIGN_ZHIPU_KEY 任一',
|
|
88
|
+
probe: () => anyEnv(['NODESIGN_TAVILY_KEY', 'NODESIGN_EXA_KEY', 'NODESIGN_BAIDU_QIANFAN_KEY', 'NODESIGN_ZHIPU_KEY']) },
|
|
89
|
+
{ id: 'publish', kind: 'key', level: 'feature', label: 'Cloudflare Pages 发布', uses: 'publish_site(一键上线四级域名)',
|
|
90
|
+
fix: '.env 里配 CLOUDFLARE_API_TOKEN + NODESIGN_PUBLISH_DOMAIN(+ NODESIGN_CF_ACCOUNT_ID),并装 wrangler(npm i -g wrangler)',
|
|
91
|
+
probe: () => {
|
|
92
|
+
const missing = ['CLOUDFLARE_API_TOKEN', 'NODESIGN_PUBLISH_DOMAIN'].filter((k) => !process.env[k]);
|
|
93
|
+
if (missing.length) return { available: false, detail: `${missing.join(', ')} 为空` };
|
|
94
|
+
const w = bin('wrangler', [path.dirname(process.execPath)]);
|
|
95
|
+
return w.available ? { available: true, detail: `wrangler:${w.detail}`, path: w.path } : { available: false, detail: 'wrangler 不在 PATH 也不在 node 同目录' };
|
|
96
|
+
} },
|
|
97
|
+
{ id: 'localBox', kind: 'service', level: 'feature', label: '本地 GPU 盒子(paint_still / roll_film)', uses: '自部署生图 / 文生视频',
|
|
98
|
+
fix: 'NODESIGN_LOCAL_BOX=on + NODESIGN_H3BOX_SSH(站主自己的 5090 盒子;一般用户用不上)',
|
|
99
|
+
probe: () => (process.env.NODESIGN_LOCAL_BOX === 'on' && process.env.NODESIGN_H3BOX_SSH
|
|
100
|
+
? { available: true, detail: process.env.NODESIGN_H3BOX_SSH } : { available: false, detail: process.env.NODESIGN_LOCAL_BOX === 'on' ? 'NODESIGN_H3BOX_SSH 为空' : 'NODESIGN_LOCAL_BOX 未开' }) },
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
function bin(name, extra = []) {
|
|
104
|
+
const p = whichBinary(name, extra);
|
|
105
|
+
return p ? { available: true, detail: p, path: p } : { available: false, detail: `${name} 不在 PATH${extra.length ? '(也不在常见安装位置)' : ''}` };
|
|
106
|
+
}
|
|
107
|
+
function anyEnv(names) {
|
|
108
|
+
const hit = names.find((n) => !!process.env[n]);
|
|
109
|
+
return hit ? { available: true, detail: `${hit} 已配` } : { available: false, detail: `${names.join(' / ')} 都为空` };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** id → { id, kind, level, label, uses, fix, available, detail, path? }。probeCapabilities 之前是空的 */
|
|
113
|
+
const state = new Map();
|
|
114
|
+
let probed = null;
|
|
115
|
+
|
|
116
|
+
export async function probeCapabilities({ force = false } = {}) {
|
|
117
|
+
if (probed && !force) return probed;
|
|
118
|
+
probed = (async () => {
|
|
119
|
+
for (const def of CAPABILITY_DEFS) {
|
|
120
|
+
let r;
|
|
121
|
+
try { r = await def.probe(); } catch (err) { r = { available: false, detail: `探测出错:${err.message}` }; }
|
|
122
|
+
const { probe: _p, ...meta } = def;
|
|
123
|
+
state.set(def.id, Object.freeze({ ...meta, available: !!r.available, detail: r.detail || '', ...(r.path ? { path: r.path } : {}) }));
|
|
124
|
+
}
|
|
125
|
+
return capabilitySnapshot();
|
|
126
|
+
})();
|
|
127
|
+
return probed;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function capabilitySnapshot() {
|
|
131
|
+
return CAPABILITY_DEFS.map((d) => state.get(d.id)).filter(Boolean);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 没探过(单测 / 脚本直接 import 工具)→ null,调用方当「不知道」= 不拦 */
|
|
135
|
+
export function capabilityState(id) {
|
|
136
|
+
return state.get(id) || null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 给 spawn 用的可执行路径:探到了就用绝对路径(Mac/Win 的 LibreOffice 不在 PATH),没探或没探到就原名(让 ENOENT 原样报) */
|
|
140
|
+
export function resolveBinary(capId, fallbackName) {
|
|
141
|
+
const s = state.get(capId);
|
|
142
|
+
return s?.available && s.path ? s.path : fallbackName;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 启动日志一行汇总 */
|
|
146
|
+
export function summarizeCapabilities() {
|
|
147
|
+
const snap = capabilitySnapshot();
|
|
148
|
+
const ok = snap.filter((c) => c.available).map((c) => c.id);
|
|
149
|
+
const missing = snap.filter((c) => !c.available);
|
|
150
|
+
const lines = [`[capabilities] 可用 ${ok.length}/${snap.length}:${ok.join(' ') || '(无)'}`];
|
|
151
|
+
for (const c of missing) lines.push(` ${c.level === 'required' ? '⛔' : '○'} ${c.id}(${c.label}):${c.detail} → ${c.fix}`);
|
|
152
|
+
return lines.join('\n');
|
|
153
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/runtime/local-config.js — 用户自己的模型插槽(本地分发版)。
|
|
3
|
+
*
|
|
4
|
+
* 文件:<dataRoot>/config.json(local profile;hosted 下不读,除非 NODESIGN_MODELS_CONFIG 指了路径——给测试用)。
|
|
5
|
+
* 形状故意跟 model-table.js 的内置行**同一套字段名**:一个事实一种写法,配置页的表单、内置表的注释、
|
|
6
|
+
* 这里的 schema 三处说的是同一个东西。
|
|
7
|
+
*
|
|
8
|
+
* {
|
|
9
|
+
* "upstreams": {
|
|
10
|
+
* "myrelay": { "label": "我的中转站", "baseUrl": "https://api.example.com", "protocol": "anthropic" | "openai-chat",
|
|
11
|
+
* "authStyle": "x-api-key" | "bearer" | "none", "key": "sk-..."(或 "keyEnv": "MY_KEY"), "countTokens": false }
|
|
12
|
+
* },
|
|
13
|
+
* "models": [
|
|
14
|
+
* { "id": "kimi-k2", "label": "Kimi K2", "desc": "…", "window": 262144, "upstream": "myrelay", "wireModel": "kimi-k2-0905",
|
|
15
|
+
* "thinking": "strip", "reasoningEffort": "high", "maxOutput": 32000, "prices": { "input": 0.6, "output": 2.5 },
|
|
16
|
+
* "emptyRetries": 2, "retryBudgetMs": 120000, "fastModel": "kimi-k2", "brand": "custom" }
|
|
17
|
+
* ]
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* 分级校验(用户拍板「断言降级」):内置表的错仍然在 model-context.js 加载时炸;外部行的错**不炸进程**,
|
|
21
|
+
* 整条丢掉并把原因收进 errors(启动日志 + GET /api/local/config 都能看到),别的行照常可用。
|
|
22
|
+
* 配置页保存时走同一个 validate,所以用户看到的红字和启动时日志里的是同一句话。
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { z } from 'zod';
|
|
28
|
+
import { profile } from './profile.js';
|
|
29
|
+
import { BRANDS, UPSTREAMS_BUILTIN, MODELS_BUILTIN } from '../engine/agent/model-table.js';
|
|
30
|
+
|
|
31
|
+
export const PROTOCOLS = Object.freeze(['anthropic', 'openai-chat']);
|
|
32
|
+
export const AUTH_STYLES = Object.freeze(['x-api-key', 'bearer', 'none']);
|
|
33
|
+
export const THINKING_MODES = Object.freeze(['strip', 'enabled8k', 'passthrough']);
|
|
34
|
+
export const REASONING_EFFORTS = Object.freeze(['low', 'medium', 'high', 'max']);
|
|
35
|
+
/** 预算上限:CLI 流式请求总超时 600s − 单发最长挂起 185s(upstream-truncation.test.js 钉的同一条线),留余量取 400s */
|
|
36
|
+
export const MAX_RETRY_BUDGET_MS = 400_000;
|
|
37
|
+
|
|
38
|
+
const ID_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
39
|
+
const RESERVED_UPSTREAMS = new Set(Object.keys(UPSTREAMS_BUILTIN));
|
|
40
|
+
const RESERVED_MODELS = new Set(MODELS_BUILTIN.map((m) => m.id));
|
|
41
|
+
|
|
42
|
+
const PricesSchema = z.object({
|
|
43
|
+
input: z.number().min(0), output: z.number().min(0),
|
|
44
|
+
cacheRead: z.number().min(0).optional(), cacheWrite: z.number().min(0).optional(),
|
|
45
|
+
}).strict();
|
|
46
|
+
|
|
47
|
+
const UpstreamSchema = z.object({
|
|
48
|
+
label: z.string().trim().min(1).max(80).optional(),
|
|
49
|
+
baseUrl: z.string().trim().url().refine((u) => /^https?:\/\//.test(u), 'baseUrl 必须是 http(s)://'),
|
|
50
|
+
protocol: z.enum(PROTOCOLS).default('anthropic'),
|
|
51
|
+
authStyle: z.enum(AUTH_STYLES).optional(),
|
|
52
|
+
key: z.string().trim().min(1).optional(),
|
|
53
|
+
keyEnv: z.string().trim().regex(/^[A-Z][A-Z0-9_]*$/, 'keyEnv 必须是大写 env 变量名').optional(),
|
|
54
|
+
countTokens: z.boolean().default(false),
|
|
55
|
+
imageFormats: z.array(z.string()).optional(),
|
|
56
|
+
}).strict();
|
|
57
|
+
|
|
58
|
+
const ModelSchema = z.object({
|
|
59
|
+
id: z.string().trim().regex(ID_RE, 'id 只能是字母数字 . _ -,64 字以内'),
|
|
60
|
+
label: z.string().trim().min(1).max(60),
|
|
61
|
+
desc: z.string().trim().max(200).default(''),
|
|
62
|
+
brand: z.enum(BRANDS).default('custom'),
|
|
63
|
+
window: z.number().int().min(8_000).max(10_000_000),
|
|
64
|
+
upstream: z.string().trim().min(1),
|
|
65
|
+
wireModel: z.string().trim().min(1),
|
|
66
|
+
fastModel: z.string().trim().min(1).optional(),
|
|
67
|
+
thinking: z.enum(THINKING_MODES).default('strip'),
|
|
68
|
+
reasoningEffort: z.enum(REASONING_EFFORTS).optional(),
|
|
69
|
+
helperReasoningEffort: z.enum(REASONING_EFFORTS).optional(),
|
|
70
|
+
maxOutput: z.number().int().min(256).max(1_000_000).optional(),
|
|
71
|
+
liftImages: z.boolean().default(false),
|
|
72
|
+
prices: PricesSchema.optional(),
|
|
73
|
+
emptyRetries: z.number().int().min(0).max(10).optional(),
|
|
74
|
+
retryBudgetMs: z.number().int().min(0).max(MAX_RETRY_BUDGET_MS).optional(),
|
|
75
|
+
uncensored: z.boolean().default(false),
|
|
76
|
+
}).strict();
|
|
77
|
+
|
|
78
|
+
export const ConfigSchema = z.object({
|
|
79
|
+
upstreams: z.record(z.string(), UpstreamSchema).default({}),
|
|
80
|
+
models: z.array(ModelSchema).default([]),
|
|
81
|
+
}).strict();
|
|
82
|
+
|
|
83
|
+
/** 给配置页用的字段清单(表单按它生成,不再手写第二份) */
|
|
84
|
+
export const CONFIG_ENUMS = Object.freeze({ PROTOCOLS, AUTH_STYLES, THINKING_MODES, REASONING_EFFORTS, BRANDS, MAX_RETRY_BUDGET_MS });
|
|
85
|
+
|
|
86
|
+
function issueText(issue) {
|
|
87
|
+
return `${issue.path.join('.') || '(根)'}: ${issue.message}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 纯校验 + 归一化。不读文件、不看 profile,所以配置页保存和启动加载用的是同一个判据。
|
|
92
|
+
* @returns {{ upstreams: Record<string, object>, models: object[], errors: {where: string, message: string}[] }}
|
|
93
|
+
* upstreams 值已是 UPSTREAMS 条目形状(label/baseUrl/keyEnv/key/authStyle/protocol/countTokens/imageFormats/external)
|
|
94
|
+
* models 值是归一化后的配置条目(还不是表行;转表行在 model-context.js toExternalRow)
|
|
95
|
+
*/
|
|
96
|
+
export function validateLocalConfig(raw) {
|
|
97
|
+
const errors = [];
|
|
98
|
+
const out = { upstreams: {}, models: [], errors };
|
|
99
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
100
|
+
errors.push({ where: '(根)', message: '配置必须是一个对象 { upstreams, models }' });
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
// 顶层形状:分别校验每个 upstream / model,一个坏了不连坐别的
|
|
104
|
+
const top = z.object({ upstreams: z.record(z.string(), z.unknown()).default({}), models: z.array(z.unknown()).default([]) }).strict().safeParse(raw);
|
|
105
|
+
if (!top.success) {
|
|
106
|
+
for (const i of top.error.issues) errors.push({ where: '(根)', message: issueText(i) });
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
for (const [id, u] of Object.entries(top.data.upstreams)) {
|
|
110
|
+
const where = `upstreams.${id}`;
|
|
111
|
+
if (!ID_RE.test(id)) { errors.push({ where, message: 'upstream id 只能是字母数字 . _ -' }); continue; }
|
|
112
|
+
if (RESERVED_UPSTREAMS.has(id)) { errors.push({ where, message: `'${id}' 是内置上游名,换一个` }); continue; }
|
|
113
|
+
const r = UpstreamSchema.safeParse(u);
|
|
114
|
+
if (!r.success) { for (const i of r.error.issues) errors.push({ where, message: issueText(i) }); continue; }
|
|
115
|
+
const d = r.data;
|
|
116
|
+
const authStyle = d.authStyle || (d.protocol === 'openai-chat' ? 'bearer' : 'x-api-key');
|
|
117
|
+
if (authStyle !== 'none' && !d.key && !d.keyEnv) { errors.push({ where, message: 'key 或 keyEnv 至少填一个(authStyle 不是 none)' }); continue; }
|
|
118
|
+
out.upstreams[id] = Object.freeze({
|
|
119
|
+
label: d.label || id, baseUrl: d.baseUrl.replace(/\/+$/, ''), protocol: d.protocol, authStyle,
|
|
120
|
+
key: d.key || null, keyEnv: d.keyEnv || null, countTokens: d.countTokens,
|
|
121
|
+
...(d.imageFormats ? { imageFormats: Object.freeze(d.imageFormats) } : {}),
|
|
122
|
+
external: true,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const seen = new Set();
|
|
126
|
+
top.data.models.forEach((m, i) => {
|
|
127
|
+
const where = `models[${i}]${m && typeof m === 'object' && m.id ? ` (${m.id})` : ''}`;
|
|
128
|
+
const r = ModelSchema.safeParse(m);
|
|
129
|
+
if (!r.success) { for (const iss of r.error.issues) errors.push({ where, message: issueText(iss) }); return; }
|
|
130
|
+
const d = r.data;
|
|
131
|
+
if (RESERVED_MODELS.has(d.id)) { errors.push({ where, message: `'${d.id}' 是内置模型名,换一个` }); return; }
|
|
132
|
+
if (seen.has(d.id)) { errors.push({ where, message: `id '${d.id}' 重复` }); return; }
|
|
133
|
+
if (!out.upstreams[d.upstream]) { errors.push({ where, message: `upstream '${d.upstream}' 不存在或没通过校验(外部模型只能指向本文件里的 upstream)` }); return; }
|
|
134
|
+
if (d.emptyRetries === 0 && d.retryBudgetMs) { errors.push({ where, message: 'emptyRetries=0 时 retryBudgetMs 没有意义' }); return; }
|
|
135
|
+
seen.add(d.id);
|
|
136
|
+
out.models.push(Object.freeze(d));
|
|
137
|
+
});
|
|
138
|
+
// fastModel 指向:只能是本文件里通过校验的模型(或自己)。在这里查而不是留给 model-context 的断言,
|
|
139
|
+
// 让用户在配置页就看见,而不是重启后日志里一行丢行
|
|
140
|
+
out.models = out.models.filter((d) => {
|
|
141
|
+
if (d.fastModel && !seen.has(d.fastModel)) { errors.push({ where: `models (${d.id})`, message: `fastModel '${d.fastModel}' 不是本文件里的模型` }); return false; }
|
|
142
|
+
return true;
|
|
143
|
+
});
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const configPath = process.env.NODESIGN_MODELS_CONFIG
|
|
148
|
+
? path.resolve(process.env.NODESIGN_MODELS_CONFIG)
|
|
149
|
+
: (profile.isLocal ? path.join(profile.dataRoot, 'config.json') : null);
|
|
150
|
+
|
|
151
|
+
/** 读文件 + 校验。文件不存在 = 空配置(不是错);JSON 坏 = 一条错、当空配置(进程照起,别让一个逗号把整站拉下来) */
|
|
152
|
+
export function loadLocalConfig() {
|
|
153
|
+
const empty = { upstreams: {}, models: [], errors: [], path: configPath, exists: false, raw: null };
|
|
154
|
+
if (!configPath) return empty;
|
|
155
|
+
let text;
|
|
156
|
+
try { text = fs.readFileSync(configPath, 'utf8'); } catch (err) {
|
|
157
|
+
if (err.code === 'ENOENT') return empty;
|
|
158
|
+
return { ...empty, errors: [{ where: '(文件)', message: `读不了 ${configPath}: ${err.message}` }] };
|
|
159
|
+
}
|
|
160
|
+
let raw;
|
|
161
|
+
try { raw = JSON.parse(text); } catch (err) {
|
|
162
|
+
return { ...empty, exists: true, errors: [{ where: '(文件)', message: `${configPath} 不是合法 JSON: ${err.message}` }] };
|
|
163
|
+
}
|
|
164
|
+
const v = validateLocalConfig(raw);
|
|
165
|
+
return { ...v, path: configPath, exists: true, raw };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 配置页保存:先校验(错了也照存——用户在编辑半成品,但返回 errors 让页面标红),原子写 */
|
|
169
|
+
export function saveLocalConfig(raw) {
|
|
170
|
+
if (!configPath) throw new Error('hosted profile 没有本地配置文件');
|
|
171
|
+
const v = validateLocalConfig(raw);
|
|
172
|
+
const tmp = `${configPath}.tmp-${process.pid}`;
|
|
173
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
174
|
+
fs.writeFileSync(tmp, JSON.stringify(raw, null, 2) + '\n', { mode: 0o600 });
|
|
175
|
+
fs.renameSync(tmp, configPath);
|
|
176
|
+
return { ...v, path: configPath, exists: true, raw };
|
|
177
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/runtime/local-env.js — 本地分发版的钥匙与开关(<dataRoot>/.env)读写,白名单制。
|
|
3
|
+
*
|
|
4
|
+
* 配置页「钥匙」那一栏的后端:只暴露 ENV_KEYS 里列的键(搜索 / 生图 / 发布 / Anthropic 直连 / 沙盒开关),
|
|
5
|
+
* 值在 GET 时打码(只报配没配 + 末四位),PUT 时改文件并同步进 process.env(web_search 这类调用时读 env 的
|
|
6
|
+
* 工具立刻生效;能力表由调用方 probeCapabilities({force:true}) 重探)。.env 里不在白名单的行原样保留。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { profile } from './profile.js';
|
|
12
|
+
|
|
13
|
+
export const ENV_KEYS = Object.freeze([
|
|
14
|
+
{ key: 'ANTHROPIC_API_KEY', group: '模型', label: 'API Key', secret: true, hint: '不填也行:本机 claude login 过就用那份登录态。别家服务商的钥匙不填这里,在「自定义接入」里配' },
|
|
15
|
+
{ key: 'ANTHROPIC_BASE_URL', group: '模型', label: '接口地址(可选)', secret: false, hint: '不填 = Anthropic 官方;用 Anthropic 格式的中转站就填它给的地址,Claude 行直接打到那里' },
|
|
16
|
+
{ key: 'NODESIGN_TAVILY_KEY', group: '联网搜索', label: 'Tavily', secret: true, hint: 'web_search 四家任一即可;英文检索优先用它' },
|
|
17
|
+
{ key: 'NODESIGN_EXA_KEY', group: '联网搜索', label: 'Exa', secret: true },
|
|
18
|
+
{ key: 'NODESIGN_BAIDU_QIANFAN_KEY', group: '联网搜索', label: '百度千帆', secret: true, hint: '中文检索自动路由到它' },
|
|
19
|
+
{ key: 'NODESIGN_ZHIPU_KEY', group: '联网搜索', label: '智谱', secret: true },
|
|
20
|
+
// 生图通道:选哪个就只露哪个的配置项(showIf 表驱动,前端按它显隐;optionHints 是选中该项时的说明)
|
|
21
|
+
{ key: 'NODESIGN_IMAGE_PROVIDER', group: '生图', label: '生图通道', secret: false, options: ['codex', 'gateway'], default: 'codex',
|
|
22
|
+
optionHints: { codex: '走本机的 OpenAI codex CLI(用你的 ChatGPT 账号)。装法:npm i -g @openai/codex,然后终端里 codex login 一次。装好后点右上角「重启」重探', gateway: '走一个兼容 OpenAI Images 接口的网关(中转站):填它给的钥匙和地址' } },
|
|
23
|
+
{ key: 'NODESIGN_GATEWAY_KEY', group: '生图', label: '网关 Key', secret: true, showIf: { NODESIGN_IMAGE_PROVIDER: 'gateway' } },
|
|
24
|
+
{ key: 'NODESIGN_GATEWAY_URL', group: '生图', label: '网关 URL', secret: false, showIf: { NODESIGN_IMAGE_PROVIDER: 'gateway' }, hint: '如 https://api.example.com/v1' },
|
|
25
|
+
{ key: 'NODESIGN_CODEX_BIN', group: '生图', label: 'codex 可执行路径(可选)', secret: false, showIf: { NODESIGN_IMAGE_PROVIDER: 'codex' }, hint: '装了但不在 PATH 时填绝对路径;在 PATH 里就留空' },
|
|
26
|
+
{ key: 'CLOUDFLARE_API_TOKEN', group: '发布', label: 'Cloudflare API Token', secret: true, hint: 'publish_site 一键上线;还要装 wrangler' },
|
|
27
|
+
{ key: 'NODESIGN_PUBLISH_DOMAIN', group: '发布', label: '发布域名后缀', secret: false, hint: '如 share.example.com;站点发到 <slug>.<这个>' },
|
|
28
|
+
{ key: 'NODESIGN_CF_ACCOUNT_ID', group: '发布', label: 'Cloudflare Account ID', secret: false },
|
|
29
|
+
{ key: 'NODESIGN_SANDBOX', group: '运行', label: '沙盒(Bash 隔离)', secret: false, options: ['', 'on'], hint: 'Linux bwrap / macOS sandbox-exec;Windows 没有。默认关(本地版靠 CLI 自己的权限模式)' },
|
|
30
|
+
{ key: 'NODESIGN_PERMISSION_MODE', group: '运行', label: '权限模式', secret: false, options: ['', 'auto'], hint: "空 = bypassPermissions(全放);auto = 模型分类器判每次工具调用,多花 15~20 秒/轮" },
|
|
31
|
+
]);
|
|
32
|
+
const ALLOWED = new Set(ENV_KEYS.map((k) => k.key));
|
|
33
|
+
|
|
34
|
+
export const envPath = profile.isLocal ? path.join(profile.dataRoot, '.env') : null;
|
|
35
|
+
|
|
36
|
+
function mask(v) {
|
|
37
|
+
if (!v) return '';
|
|
38
|
+
return v.length <= 8 ? '••••' : `••••${v.slice(-4)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 配置页视图:每个白名单键配没配 + 打码预览(真值不出门) */
|
|
42
|
+
export function envView() {
|
|
43
|
+
return ENV_KEYS.map((k) => {
|
|
44
|
+
const v = process.env[k.key] || '';
|
|
45
|
+
return { ...k, set: !!v, preview: k.secret ? mask(v) : v };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseLine(line) {
|
|
50
|
+
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line);
|
|
51
|
+
return m ? m[1] : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 写 .env:只动白名单键;null/'' = 删行;其它行(注释、别的键)原样。同步 process.env。
|
|
56
|
+
* @param {Record<string, string|null>} values
|
|
57
|
+
* @returns {{ changed: string[] }}
|
|
58
|
+
*/
|
|
59
|
+
export function setEnvValues(values) {
|
|
60
|
+
if (!envPath) throw new Error('hosted profile 不在这里改 .env');
|
|
61
|
+
const changed = [];
|
|
62
|
+
for (const [k, v] of Object.entries(values || {})) {
|
|
63
|
+
if (!ALLOWED.has(k)) throw new Error(`不允许改 ${k}(不在白名单)`);
|
|
64
|
+
if (v != null && typeof v !== 'string') throw new Error(`${k} 的值必须是字符串`);
|
|
65
|
+
if (v && /[\r\n]/.test(v)) throw new Error(`${k} 的值不能含换行`);
|
|
66
|
+
const def = ENV_KEYS.find((d) => d.key === k);
|
|
67
|
+
if (def.options && v && !def.options.includes(v)) throw new Error(`${k} 只能是 ${def.options.filter(Boolean).join(' | ')} 或留空`);
|
|
68
|
+
}
|
|
69
|
+
let lines = [];
|
|
70
|
+
try { lines = fs.readFileSync(envPath, 'utf8').split('\n'); if (lines.at(-1) === '') lines.pop(); } catch (err) { if (err.code !== 'ENOENT') throw err; }
|
|
71
|
+
for (const [k, vRaw] of Object.entries(values || {})) {
|
|
72
|
+
const v = vRaw ? vRaw.trim() : '';
|
|
73
|
+
const idx = lines.findIndex((l) => parseLine(l) === k);
|
|
74
|
+
const rendered = `${k}=${/[\s#"']/.test(v) ? JSON.stringify(v) : v}`;
|
|
75
|
+
if (!v) {
|
|
76
|
+
if (idx !== -1) { lines.splice(idx, 1); changed.push(k); }
|
|
77
|
+
delete process.env[k];
|
|
78
|
+
} else {
|
|
79
|
+
if (idx !== -1) { if (lines[idx] !== rendered) { lines[idx] = rendered; changed.push(k); } } else { lines.push(rendered); changed.push(k); }
|
|
80
|
+
process.env[k] = v;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
fs.mkdirSync(path.dirname(envPath), { recursive: true });
|
|
84
|
+
const tmp = `${envPath}.tmp-${process.pid}`;
|
|
85
|
+
fs.writeFileSync(tmp, lines.join('\n') + (lines.length ? '\n' : ''), { mode: 0o600 });
|
|
86
|
+
fs.renameSync(tmp, envPath);
|
|
87
|
+
return { changed };
|
|
88
|
+
}
|