@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,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* services/rembg-launcher.js — Node 侧管 rembg-service.py 子进程生命周期
|
|
3
|
+
*
|
|
4
|
+
* server/index.js 启动时调 startRembgService(),shutdown 时调 stopRembgService()。
|
|
5
|
+
* service 自己常驻一个 python 进程把 onnxruntime session 缓存在内存里——
|
|
6
|
+
* 比 per-call cold subprocess spawn 省 ~20-40s/call。
|
|
7
|
+
*
|
|
8
|
+
* Service 死了不影响主流程(fallback 到老的 spawn-bridge 路径,详见 helpers/rembg.js)。
|
|
9
|
+
*
|
|
10
|
+
* ── RSS 看门狗(2026-07-31 事故后加)──────────────────────────────
|
|
11
|
+
* onnxruntime + pymatting 的内存高水位**不会自己降**:抠 8 张 1.5-2MP 的图能让
|
|
12
|
+
* 这个进程从 345MB 涨到 1.2GB 并停在那儿。这台机器 3.9G 内存 + 2G swap,塞着
|
|
13
|
+
* Cursor server(约 970MB)、SillyTavern、Claude SDK 子进程之后余量本来就薄,
|
|
14
|
+
* 高水位不降就一路把系统推进内存抖动:内核忙于回收/换页,单核 CPU 被钉死 99%,
|
|
15
|
+
* 机器活着但 SSH 和 HTTP 全都握不上手,只能人工关机。2026-07-31 04:24 就是这么死的。
|
|
16
|
+
*
|
|
17
|
+
* 所以:定期看 RSS,超阈值且**在途请求为 0** 时把 service 回收重开。它是无状态的,
|
|
18
|
+
* 代价只是下次调用重新 load 一次模型(~20-40s,且预热是异步的不挡请求)。
|
|
19
|
+
* service 侧还会在每次抠图后 gc + malloc_trim 主动还内存,看门狗是兜底不是主力。
|
|
20
|
+
*
|
|
21
|
+
* Env override:
|
|
22
|
+
* NODESIGN_REMBG_PYTHON venv python 解释器路径
|
|
23
|
+
* NODESIGN_REMBG_SERVICE service 脚本路径
|
|
24
|
+
* NODESIGN_REMBG_SOCKET Unix socket 路径
|
|
25
|
+
* NODESIGN_REMBG_PRELOAD 逗号分隔预加载模型列表(默认 isnet-general-use)
|
|
26
|
+
* NODESIGN_REMBG_MAX_RSS_MB RSS 回收阈值(默认 900;0 = 关掉看门狗)
|
|
27
|
+
* NODESIGN_REMBG_WATCH_MS 看门狗巡检间隔(默认 60000)
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { spawn, execSync } from 'node:child_process';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import http from 'node:http';
|
|
34
|
+
import { promises as fs, readFileSync, writeFileSync } from 'node:fs';
|
|
35
|
+
import { REMBG_VENV_PYTHON, REMBG_SETUP_HINT } from '../engine/mcp/tools/helpers/rembg.js';
|
|
36
|
+
|
|
37
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
// services/ 在 server/services/,server root 上溯 1 层
|
|
39
|
+
const SERVER_ROOT = path.resolve(__dirname, '..');
|
|
40
|
+
|
|
41
|
+
const DEFAULT_PYTHON = REMBG_VENV_PYTHON;
|
|
42
|
+
const DEFAULT_SERVICE = path.join(__dirname, 'rembg-service.py');
|
|
43
|
+
// 默认只预热 isnet-general-use(fast 档)。birefnet-general-lite (balanced) /
|
|
44
|
+
// birefnet-general (best) 都带 alpha matting,pymatting 单线程 CPU 重,预热占
|
|
45
|
+
// 200-400MB 内存但实际很少用——按需 lazy load 即可。
|
|
46
|
+
// 改回多模型预热:env NODESIGN_REMBG_PRELOAD=isnet-general-use,birefnet-general-lite
|
|
47
|
+
const DEFAULT_PRELOAD = 'isnet-general-use';
|
|
48
|
+
|
|
49
|
+
const DEFAULT_SOCKET = '/tmp/nodesign-rembg.sock';
|
|
50
|
+
const MAX_RSS_MB = Number(process.env.NODESIGN_REMBG_MAX_RSS_MB ?? 900);
|
|
51
|
+
const WATCH_MS = Number(process.env.NODESIGN_REMBG_WATCH_MS ?? 60_000);
|
|
52
|
+
|
|
53
|
+
let serviceProc = null;
|
|
54
|
+
let started = false;
|
|
55
|
+
let watchTimer = null;
|
|
56
|
+
/** 看门狗主动回收时置位:让 exit handler 知道该重开,而不是当成异常退出 */
|
|
57
|
+
let recycling = false;
|
|
58
|
+
|
|
59
|
+
/** 问 service 的 /health(走 Unix socket)。拿不到就返 null,看门狗这轮跳过 */
|
|
60
|
+
function probeHealth(timeoutMs = 3000) {
|
|
61
|
+
const socketPath = process.env.NODESIGN_REMBG_SOCKET || DEFAULT_SOCKET;
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
const req = http.request({ socketPath, path: '/health', method: 'GET', timeout: timeoutMs }, (res) => {
|
|
64
|
+
let body = '';
|
|
65
|
+
res.setEncoding('utf8');
|
|
66
|
+
res.on('data', (c) => { body += c; });
|
|
67
|
+
res.on('end', () => {
|
|
68
|
+
try { resolve(JSON.parse(body)); } catch { resolve(null); }
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
req.on('error', () => resolve(null));
|
|
72
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
73
|
+
req.end();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 一轮巡检。超阈值且在途为 0 → 回收。
|
|
79
|
+
*
|
|
80
|
+
* 在途不为 0 时**不回收**:宁可这一轮继续超着,也不能把用户正在等的抠图打断。
|
|
81
|
+
* 下一轮还会再看,抠图是秒级的,不会一直忙。
|
|
82
|
+
*/
|
|
83
|
+
async function watchdogTick() {
|
|
84
|
+
if (!serviceProc || recycling) return;
|
|
85
|
+
const health = await probeHealth();
|
|
86
|
+
if (!health || typeof health.rss_mb !== 'number') return;
|
|
87
|
+
if (health.rss_mb < MAX_RSS_MB) return;
|
|
88
|
+
if (health.inflight > 0) {
|
|
89
|
+
console.log(`[rembg-service] RSS ${health.rss_mb}MB 超阈值 ${MAX_RSS_MB}MB,但有 ${health.inflight} 个在途请求,本轮不回收`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
console.log(`[rembg-service] RSS ${health.rss_mb}MB ≥ ${MAX_RSS_MB}MB 且空闲 → 回收重开(内存高水位不会自己降,见文件头注释)`);
|
|
93
|
+
recycling = true;
|
|
94
|
+
try {
|
|
95
|
+
serviceProc.kill('SIGTERM');
|
|
96
|
+
} catch { /* 已经死了,exit handler 会接手 */ }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 把 service 进程钉成"内存不够时第一个该死的人"。
|
|
101
|
+
*
|
|
102
|
+
* 为什么需要:内存告急时,内核 OOM killer 和 earlyoom 都是按 oom_score 挑人,
|
|
103
|
+
* 而这台机器上前几名的分数挤在一起(实测 rembg 712 / Cursor 708 / claude 扩展
|
|
104
|
+
* 705 / nodesign 服务端 692,满分 1000)。差 4 分意味着 RSS 稍一波动排序就翻,
|
|
105
|
+
* 于是本该背锅的抠图服务躲过去,反而杀掉 nodesign 或者你的 IDE 连接。
|
|
106
|
+
*
|
|
107
|
+
* oom_score_adj 加 500 分把它顶到榜首且拉开差距,让"谁会被杀"变成确定的事:
|
|
108
|
+
* 抠图服务无状态,被杀了 helpers/rembg.js 会 fallback 到 per-call cold spawn,
|
|
109
|
+
* 用户最多觉得慢一次;杀 nodesign 会丢掉正在跑的 agent 回合。
|
|
110
|
+
*
|
|
111
|
+
* 非 root 只能调高不能调低,加分这个方向一定能成功。
|
|
112
|
+
*/
|
|
113
|
+
function pinOomScore(pid) {
|
|
114
|
+
const adj = Number(process.env.NODESIGN_REMBG_OOM_ADJ ?? 500);
|
|
115
|
+
if (!Number.isFinite(adj) || adj === 0) return;
|
|
116
|
+
try {
|
|
117
|
+
writeFileSync(`/proc/${pid}/oom_score_adj`, String(adj));
|
|
118
|
+
console.log(`[rembg-service] oom_score_adj=${adj}(内存告急时优先被杀,它是无状态的)`);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// 非 Linux / 权限不足:不是致命问题,earlyoom 的 --prefer 仍在兜着
|
|
121
|
+
console.warn(`[rembg-service] oom_score_adj 设置失败(不影响功能): ${err.message}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function startWatchdog() {
|
|
126
|
+
if (watchTimer || !(MAX_RSS_MB > 0) || !(WATCH_MS > 0)) return;
|
|
127
|
+
watchTimer = setInterval(() => { watchdogTick().catch(() => {}); }, WATCH_MS);
|
|
128
|
+
watchTimer.unref(); // 不因为它拖着不让进程退出
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 杀掉所有 stale rembg-service.py 进程(不属于本 launcher 的)。
|
|
133
|
+
* 防止 node --watch reload / pm2 restart / 上次 SIGKILL 没清干净时
|
|
134
|
+
* 多个 service 实例累积——每个加载一份模型 = 重复 200-400MB 内存浪费 +
|
|
135
|
+
* 同一个 socket 文件 bind 冲突。
|
|
136
|
+
*
|
|
137
|
+
* 用 pgrep -f 找匹配 rembg-service.py 的进程,全 kill。同步执行——
|
|
138
|
+
* 启动时一次性清干净,spawn 新 service 之前。
|
|
139
|
+
*/
|
|
140
|
+
function killStaleServices() {
|
|
141
|
+
const mySocket = process.env.NODESIGN_REMBG_SOCKET || DEFAULT_SOCKET;
|
|
142
|
+
try {
|
|
143
|
+
// pgrep -f 匹配完整 cmdline,-d ' ' 用空格分隔多 PID。
|
|
144
|
+
// pattern "python.*rembg-service\\.py" 收紧只匹配 python 解释器执行
|
|
145
|
+
// rembg-service.py 的进程(避免误杀含字符串的 node test / 编辑器等)。
|
|
146
|
+
const out = execSync('pgrep -ifd " " "python.*rembg-service\\.py"', {
|
|
147
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
148
|
+
}).toString().trim();
|
|
149
|
+
if (!out) return;
|
|
150
|
+
// 排除自己当前进程(防自杀的边角)
|
|
151
|
+
const myPid = process.pid;
|
|
152
|
+
let pids = out.split(/\s+/).filter(Boolean).filter((p) => Number(p) !== myPid);
|
|
153
|
+
// 只杀**绑同一个 socket** 的实例。socket 路径在 env 里不在 argv 里,所以
|
|
154
|
+
// 读 /proc/<pid>/environ 判断。
|
|
155
|
+
//
|
|
156
|
+
// 为什么必须过滤:不加这层,任何第二个 nodesign 实例(隔离 e2e、本地
|
|
157
|
+
// 另开一份、看门狗自测)一启动就会把生产实例的抠图服务杀掉,而生产侧
|
|
158
|
+
// 对"意外退出"是不自动重启的,于是抠图静默降级成 per-call cold spawn,
|
|
159
|
+
// 没人会发现。2026-07-31 我自己踩了一次。
|
|
160
|
+
// 读不到 environ 的(权限/进程刚没)当作不是自己的,宁可漏杀不可错杀。
|
|
161
|
+
pids = pids.filter((p) => {
|
|
162
|
+
try {
|
|
163
|
+
const env = readFileSync(`/proc/${p}/environ`, 'utf8');
|
|
164
|
+
return env.split('\0').includes(`NODESIGN_REMBG_SOCKET=${mySocket}`)
|
|
165
|
+
// 老进程可能没显式设这个 env(走的是 service 侧默认值)
|
|
166
|
+
|| (mySocket === DEFAULT_SOCKET && !env.includes('NODESIGN_REMBG_SOCKET='));
|
|
167
|
+
} catch { return false; }
|
|
168
|
+
});
|
|
169
|
+
if (pids.length === 0) return;
|
|
170
|
+
console.log(`[rembg-service] killing ${pids.length} stale service process(es): ${pids.join(', ')}`);
|
|
171
|
+
for (const pid of pids) {
|
|
172
|
+
try { process.kill(Number(pid), 'SIGTERM'); } catch { /* already dead */ }
|
|
173
|
+
}
|
|
174
|
+
// 给 1s 让 SIGTERM 走 atexit 清 socket;之后 SIGKILL 兜底
|
|
175
|
+
const deadline = Date.now() + 1000;
|
|
176
|
+
while (Date.now() < deadline) {
|
|
177
|
+
try {
|
|
178
|
+
execSync('pgrep -if "python.*rembg-service\\.py"', { stdio: 'ignore' });
|
|
179
|
+
// 还活着,继续 wait
|
|
180
|
+
} catch {
|
|
181
|
+
// pgrep exit 1 = 没找到 = 全死了
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// SIGTERM 1s 还没死 → SIGKILL
|
|
186
|
+
for (const pid of pids) {
|
|
187
|
+
try { process.kill(Number(pid), 'SIGKILL'); } catch { /* ignore */ }
|
|
188
|
+
}
|
|
189
|
+
} catch (err) {
|
|
190
|
+
// pgrep exit 1 = 没匹配进程,正常路径,不报错
|
|
191
|
+
if (err.status !== 1) {
|
|
192
|
+
console.warn(`[rembg-service] killStaleServices error: ${err.message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 启动 rembg-service。幂等:重复调用 noop。
|
|
199
|
+
* 返回 boolean —— 是否启动成功(venv 不存在 / spawn 错时返 false,server 继续)。
|
|
200
|
+
*/
|
|
201
|
+
export async function startRembgService() {
|
|
202
|
+
if (started) return Boolean(serviceProc);
|
|
203
|
+
started = true;
|
|
204
|
+
|
|
205
|
+
const py = process.env.NODESIGN_REMBG_PYTHON || DEFAULT_PYTHON;
|
|
206
|
+
const script = process.env.NODESIGN_REMBG_SERVICE || DEFAULT_SERVICE;
|
|
207
|
+
const preload = process.env.NODESIGN_REMBG_PRELOAD || DEFAULT_PRELOAD;
|
|
208
|
+
|
|
209
|
+
// 先杀掉所有 stale rembg-service 进程(node --watch reload / SIGKILL 没清等
|
|
210
|
+
// 路径会让多个 service 累积,每个吃 200-400MB 内存)
|
|
211
|
+
killStaleServices();
|
|
212
|
+
|
|
213
|
+
// venv 不存在直接 noop(首次部署 / dev 没装 rembg 的环境)
|
|
214
|
+
try {
|
|
215
|
+
await fs.access(py);
|
|
216
|
+
await fs.access(script);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.warn(
|
|
219
|
+
`[rembg-service] not started: ${err.code === 'ENOENT' ? 'venv or script missing' : err.message}.`
|
|
220
|
+
+ ` Setup: ${REMBG_SETUP_HINT}.`
|
|
221
|
+
+ ' remove_background tool will fall back to per-call spawn (slower).',
|
|
222
|
+
);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const ok = spawnService(py, script, preload);
|
|
227
|
+
if (ok) startWatchdog();
|
|
228
|
+
return ok;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 真正 spawn 一次。startRembgService 首次调,看门狗回收后也调。
|
|
233
|
+
*
|
|
234
|
+
* 退出处理分两种:
|
|
235
|
+
* 看门狗主动回收(recycling=true)→ 立刻重开,这是预期行为
|
|
236
|
+
* 意外退出(崩了 / 被 earlyoom 或 OOM killer 杀了)→ **不自动重启**,
|
|
237
|
+
* 保持原有约定:单方面重启子进程会掩盖问题。抠图会 fallback 到 per-call
|
|
238
|
+
* cold spawn(慢但能用),日志里那行 exited 就是要让人看见的信号。
|
|
239
|
+
*/
|
|
240
|
+
function spawnService(py, script, preload) {
|
|
241
|
+
try {
|
|
242
|
+
serviceProc = spawn(py, [script], {
|
|
243
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
244
|
+
env: {
|
|
245
|
+
...process.env,
|
|
246
|
+
NODESIGN_REMBG_PRELOAD: preload,
|
|
247
|
+
// 显式写进子进程 env:killStaleServices 靠读 /proc/<pid>/environ
|
|
248
|
+
// 判断某个 service 是不是"自己这一份",不写的话认不出来
|
|
249
|
+
NODESIGN_REMBG_SOCKET: process.env.NODESIGN_REMBG_SOCKET || DEFAULT_SOCKET,
|
|
250
|
+
},
|
|
251
|
+
// detached:false → child 跟父 share 进程组,父收 SIGINT 时 shell 也会
|
|
252
|
+
// 转给 child(双保险);显式 SIGTERM 仍走 stopRembgService()
|
|
253
|
+
detached: false,
|
|
254
|
+
});
|
|
255
|
+
} catch (err) {
|
|
256
|
+
console.warn(`[rembg-service] spawn failed: ${err.message}`);
|
|
257
|
+
serviceProc = null;
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log(`[rembg-service] spawned PID ${serviceProc.pid}, preload=[${preload}]`);
|
|
262
|
+
pinOomScore(serviceProc.pid);
|
|
263
|
+
|
|
264
|
+
serviceProc.on('exit', (code, signal) => {
|
|
265
|
+
console.log(`[rembg-service] exited code=${code} signal=${signal}`);
|
|
266
|
+
serviceProc = null;
|
|
267
|
+
if (recycling) {
|
|
268
|
+
recycling = false;
|
|
269
|
+
// 隔一拍再起:给 atexit 清 socket 的时间,否则新进程 bind 会撞上旧 socket 文件
|
|
270
|
+
setTimeout(() => {
|
|
271
|
+
if (started) {
|
|
272
|
+
console.log('[rembg-service] 回收后重开');
|
|
273
|
+
spawnService(py, script, preload);
|
|
274
|
+
}
|
|
275
|
+
}, 500).unref();
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
serviceProc.on('error', (err) => {
|
|
280
|
+
console.warn(`[rembg-service] process error: ${err.message}`);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 优雅关闭 rembg-service。SIGTERM 让 service 走 atexit 清 socket。
|
|
288
|
+
*/
|
|
289
|
+
export function stopRembgService() {
|
|
290
|
+
started = false; // 让 exit handler 里的重开分支不再触发
|
|
291
|
+
recycling = false;
|
|
292
|
+
if (watchTimer) { clearInterval(watchTimer); watchTimer = null; }
|
|
293
|
+
if (!serviceProc) return;
|
|
294
|
+
console.log(`[rembg-service] stopping PID ${serviceProc.pid}`);
|
|
295
|
+
try {
|
|
296
|
+
serviceProc.kill('SIGTERM');
|
|
297
|
+
} catch { /* ignore — already dead */ }
|
|
298
|
+
// 兜底 3s 后 SIGKILL
|
|
299
|
+
const killer = setTimeout(() => {
|
|
300
|
+
if (serviceProc) {
|
|
301
|
+
try { serviceProc.kill('SIGKILL'); } catch { /* ignore */ }
|
|
302
|
+
}
|
|
303
|
+
}, 3000);
|
|
304
|
+
killer.unref();
|
|
305
|
+
serviceProc = null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 给外部探活用(helpers/rembg.js 不强依赖;自己也会 isAvailable check service health)
|
|
310
|
+
*/
|
|
311
|
+
export function isRembgServiceRunning() {
|
|
312
|
+
return serviceProc !== null && !serviceProc.killed;
|
|
313
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""rembg-service — long-running FastAPI service over Unix socket.
|
|
3
|
+
|
|
4
|
+
server/index.js spawn 这个进程在自己启动时;自己 shutdown 时 SIGTERM 它。
|
|
5
|
+
所有 onnxruntime session 在内存里 warm 缓存,避免 per-call cold spawn 税
|
|
6
|
+
(每次省 ~20-40s)。
|
|
7
|
+
|
|
8
|
+
启动顺序(2026-05-11 重构):
|
|
9
|
+
1. bind Unix socket(立刻 ready,~1s)
|
|
10
|
+
2. uvicorn startup event 起后台 _preload(models) task 异步预热
|
|
11
|
+
3. /remove 请求路径:未预热则同步 load(per-model asyncio.Lock 防并发重 load)
|
|
12
|
+
避免老版"先 preload → 再 bind socket"路径在 birefnet+CoreML 卡死时
|
|
13
|
+
socket 永不创建的死锁。
|
|
14
|
+
|
|
15
|
+
Endpoints:
|
|
16
|
+
GET /health
|
|
17
|
+
返回 {"ok": true, "loaded_models": [...], "preload_done": bool}(也用作 isAvailable 探活)
|
|
18
|
+
|
|
19
|
+
POST /remove
|
|
20
|
+
Headers:
|
|
21
|
+
X-Model: <rembg model name> default 'isnet-general-use'(安全档)
|
|
22
|
+
X-Alpha-Matting: 0|1 default 0
|
|
23
|
+
Body: raw image bytes (PNG/JPEG/WEBP/...)
|
|
24
|
+
Returns: RGBA PNG bytes (200) or JSON error (4xx/5xx)
|
|
25
|
+
|
|
26
|
+
Env:
|
|
27
|
+
NODESIGN_REMBG_SOCKET Unix socket path (default /tmp/nodesign-rembg.sock)
|
|
28
|
+
NODESIGN_REMBG_PRELOAD 逗号分隔的 model 列表,启动时异步预加载
|
|
29
|
+
例:'isnet-general-use,birefnet-general-lite'
|
|
30
|
+
NODESIGN_REMBG_PROVIDERS 逗号分隔 onnxruntime providers 覆盖默认。
|
|
31
|
+
未设时:darwin 默认 'CPUExecutionProvider' 单一
|
|
32
|
+
项绕过 CoreML+birefnet 卡死 bug(onnxruntime
|
|
33
|
+
1.19.2 验证),其它平台传 None 让 ort 自选
|
|
34
|
+
(Linux CUDA EP / Windows DML 等)。
|
|
35
|
+
NODESIGN_REMBG_AM_MAX_DIM alpha matting 的分辨率上限(长边 px,默认 1024)。
|
|
36
|
+
见下方 _remove_with_am_cap 的长注释。0 = 不限制。
|
|
37
|
+
"""
|
|
38
|
+
import asyncio
|
|
39
|
+
import atexit
|
|
40
|
+
import ctypes
|
|
41
|
+
import ctypes.util
|
|
42
|
+
import gc
|
|
43
|
+
import io
|
|
44
|
+
import os
|
|
45
|
+
import sys
|
|
46
|
+
import warnings
|
|
47
|
+
|
|
48
|
+
# urllib3 v2 + LibreSSL(Apple python)只是警告,silence 防污染 stderr
|
|
49
|
+
warnings.filterwarnings('ignore')
|
|
50
|
+
|
|
51
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
52
|
+
from fastapi.responses import Response
|
|
53
|
+
from PIL import Image
|
|
54
|
+
import onnxruntime as ort
|
|
55
|
+
from rembg import remove, new_session
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _make_malloc_trim():
|
|
59
|
+
"""glibc 的 malloc_trim(0):把 free 出来但还挂在堆上的内存还给内核。
|
|
60
|
+
|
|
61
|
+
只对 glibc 有效(这台是 Debian,有)。musl / macOS 拿不到符号就返回一个空
|
|
62
|
+
函数,调用处不用关心平台差异。
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)
|
|
66
|
+
trim = libc.malloc_trim
|
|
67
|
+
trim.argtypes = [ctypes.c_size_t]
|
|
68
|
+
trim.restype = ctypes.c_int
|
|
69
|
+
return lambda: trim(0)
|
|
70
|
+
except Exception:
|
|
71
|
+
return lambda: None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
_malloc_trim = _make_malloc_trim()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _resolve_providers():
|
|
78
|
+
"""决定传给 rembg new_session 的 providers 列表。
|
|
79
|
+
|
|
80
|
+
darwin(Apple Silicon Mac)必须排除 CoreMLExecutionProvider:
|
|
81
|
+
onnxruntime 1.19.2 在 birefnet-general-lite 上 CoreML graph compile
|
|
82
|
+
卡死(>110s 未返回;CPU-only providers 2.5s 完成)。
|
|
83
|
+
|
|
84
|
+
Linux / Windows 走 None = ort 自选,保留 CUDA EP / DML 等加速。
|
|
85
|
+
Env NODESIGN_REMBG_PROVIDERS 覆盖一切(escape hatch + ort 升级后复测用)。
|
|
86
|
+
|
|
87
|
+
将来 onnxruntime 升级时重测:
|
|
88
|
+
.venv-rembg/bin/python3 -c "from rembg import new_session; \\
|
|
89
|
+
import time; t=time.time(); new_session('birefnet-general-lite'); \\
|
|
90
|
+
print(time.time()-t)"
|
|
91
|
+
< 30s 返回即可考虑放开 darwin 默认。
|
|
92
|
+
"""
|
|
93
|
+
env = os.environ.get('NODESIGN_REMBG_PROVIDERS', '').strip()
|
|
94
|
+
if env:
|
|
95
|
+
return [p.strip() for p in env.split(',') if p.strip()]
|
|
96
|
+
if sys.platform == 'darwin':
|
|
97
|
+
return ['CPUExecutionProvider']
|
|
98
|
+
return None # ort 自选
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
PROVIDERS = _resolve_providers()
|
|
102
|
+
|
|
103
|
+
app = FastAPI()
|
|
104
|
+
|
|
105
|
+
# 模型 session 缓存:model name → onnxruntime session
|
|
106
|
+
sessions = {}
|
|
107
|
+
# per-model asyncio.Lock 防并发同一 model 重复 new_session(preload + 第一个
|
|
108
|
+
# /remove 撞车场景)。dict 默认 lazy 建 Lock。
|
|
109
|
+
_session_locks: dict = {}
|
|
110
|
+
# preload 任务完成与否(仅 /health 报告用)
|
|
111
|
+
_preload_done = False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _get_lock(model_name: str) -> asyncio.Lock:
|
|
115
|
+
if model_name not in _session_locks:
|
|
116
|
+
_session_locks[model_name] = asyncio.Lock()
|
|
117
|
+
return _session_locks[model_name]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _mem_tuned_options():
|
|
121
|
+
"""省内存优先的 onnxruntime SessionOptions。
|
|
122
|
+
|
|
123
|
+
起因:birefnet-general-lite 只有 214MB 权重,但一次 1.57MP 推理的峰值 RSS
|
|
124
|
+
到 2.4GB,在 3.9G 无 swap 的机器上直接被 OOM killer 杀掉(2026-07-29 两次、
|
|
125
|
+
07-31 复现一次,dmesg 有记录)。杀的是这个常驻进程,连带 fast 档一起死,
|
|
126
|
+
所以整档被 env 禁掉了。
|
|
127
|
+
|
|
128
|
+
钱花在哪:ort 默认开 CPU arena 分配器,它按需成倍扩张且**不还给系统**,
|
|
129
|
+
对 birefnet 这种 transformer backbone(大量临时激活)峰值会翻好几倍。
|
|
130
|
+
enable_cpu_mem_arena=False —— 用完即还,峰值大幅下降,代价是分配器调用
|
|
131
|
+
多一点(单核上这点开销远小于被 OOM 杀掉)
|
|
132
|
+
enable_mem_pattern=False —— 内存复用模式表本身也要预分配一大块
|
|
133
|
+
intra/inter_op=1 —— 这台机器就 1 核,多线程只是每条线程再占一份
|
|
134
|
+
临时缓冲,一点也不快
|
|
135
|
+
|
|
136
|
+
isnet 那档本来就只占 997MB,加上这些也不会变慢到哪去,统一走同一份配置。
|
|
137
|
+
"""
|
|
138
|
+
opts = ort.SessionOptions()
|
|
139
|
+
opts.enable_cpu_mem_arena = False
|
|
140
|
+
opts.enable_mem_pattern = False
|
|
141
|
+
opts.intra_op_num_threads = int(os.environ.get('NODESIGN_REMBG_THREADS', '1'))
|
|
142
|
+
opts.inter_op_num_threads = 1
|
|
143
|
+
return opts
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _load_session_sync(model_name: str):
|
|
147
|
+
"""同步 load——锁内调用。onnxruntime new_session 是阻塞 CPU 工作,
|
|
148
|
+
用 asyncio.to_thread 在外层包裹。"""
|
|
149
|
+
if model_name not in sessions:
|
|
150
|
+
sessions[model_name] = new_session(
|
|
151
|
+
model_name, providers=PROVIDERS, sess_opts=_mem_tuned_options(),
|
|
152
|
+
)
|
|
153
|
+
return sessions[model_name]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def ensure_session(model_name: str):
|
|
157
|
+
"""异步获取 session:已 load 直接返;未 load 锁内同步 load 然后返。"""
|
|
158
|
+
if model_name in sessions:
|
|
159
|
+
return sessions[model_name]
|
|
160
|
+
async with _get_lock(model_name):
|
|
161
|
+
# double-check:另一 task 可能已经 load 完
|
|
162
|
+
if model_name in sessions:
|
|
163
|
+
return sessions[model_name]
|
|
164
|
+
return await asyncio.to_thread(_load_session_sync, model_name)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def _preload(models):
|
|
168
|
+
"""启动后台预热——串行 load 防同时撑爆内存。失败单 model 不影响整体。"""
|
|
169
|
+
global _preload_done
|
|
170
|
+
for m in models:
|
|
171
|
+
print(f'[rembg-service] preloading {m}...', file=sys.stderr, flush=True)
|
|
172
|
+
try:
|
|
173
|
+
await ensure_session(m)
|
|
174
|
+
print(f'[rembg-service] ✓ {m} ready', file=sys.stderr, flush=True)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
print(f'[rembg-service] ✗ {m} failed: {type(e).__name__}: {e}',
|
|
177
|
+
file=sys.stderr, flush=True)
|
|
178
|
+
_preload_done = True
|
|
179
|
+
print(f'[rembg-service] preload done, loaded={sorted(sessions.keys())}',
|
|
180
|
+
file=sys.stderr, flush=True)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@app.on_event('startup')
|
|
184
|
+
async def _on_startup():
|
|
185
|
+
preload = os.environ.get('NODESIGN_REMBG_PRELOAD', '').strip()
|
|
186
|
+
if not preload:
|
|
187
|
+
return
|
|
188
|
+
models = [s.strip() for s in preload.split(',') if s.strip()]
|
|
189
|
+
if models:
|
|
190
|
+
asyncio.create_task(_preload(models))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# 在途请求数。launcher 的 RSS 看门狗只在这个数为 0 时才敢回收本进程,
|
|
194
|
+
# 否则会把用户正在等的那次抠图打断。
|
|
195
|
+
_inflight = 0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _self_rss_mb():
|
|
199
|
+
"""本进程 RSS(MB)。读 /proc 而不是引 psutil:少一个依赖,Linux 上够用。"""
|
|
200
|
+
try:
|
|
201
|
+
with open('/proc/self/status') as f:
|
|
202
|
+
for line in f:
|
|
203
|
+
if line.startswith('VmRSS:'):
|
|
204
|
+
return round(int(line.split()[1]) / 1024)
|
|
205
|
+
except OSError:
|
|
206
|
+
pass
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@app.get('/health')
|
|
211
|
+
def health():
|
|
212
|
+
return {
|
|
213
|
+
'ok': True,
|
|
214
|
+
'loaded_models': sorted(sessions.keys()),
|
|
215
|
+
'preload_done': _preload_done,
|
|
216
|
+
# 下面两个给 launcher 的看门狗用(见 rembg-launcher.js)
|
|
217
|
+
'inflight': _inflight,
|
|
218
|
+
'rss_mb': _self_rss_mb(),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
AM_MAX_DIM = int(os.environ.get('NODESIGN_REMBG_AM_MAX_DIM', '1024'))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _remove_with_am_cap(image_bytes, session, alpha_matting):
|
|
226
|
+
"""抠图。开 alpha matting 时把 AM 那一步限制在 AM_MAX_DIM 之内。
|
|
227
|
+
|
|
228
|
+
alpha matting 走 pymatting,它对 W×H 的图建一个 (W·H)×(W·H) 的稀疏拉普拉斯
|
|
229
|
+
矩阵再解方程组,内存和时间都随**像素数**走。这里的生图动辄 1.6MP 到 6.5MP,
|
|
230
|
+
不限制的话解算既慢又吃内存,原来 "fast 加 AM" 试过一次没通就是卡在这。
|
|
231
|
+
|
|
232
|
+
限制之后:AM 在 ≤1024 长边上算(≤1MP),算出来的 alpha 再 LANCZOS 放大回原
|
|
233
|
+
尺寸,跟**原分辨率的 RGB** 合成。输出仍是全分辨率,只有边缘的柔和过渡是在低
|
|
234
|
+
分辨率上解出来再插值的。代价是边缘比全分辨率 AM 略"雾"一点;收益是这一档在
|
|
235
|
+
这台机器上第一次变得可用。
|
|
236
|
+
|
|
237
|
+
⚠️ 这个上限**不是** birefnet 档 OOM 的解药,别把两件事记混。2026-07-31 实测:
|
|
238
|
+
birefnet-general-lite 在 alpha_matting=0 的情况下,一张 1.57MP 的图峰值 RSS
|
|
239
|
+
就到 2435MB,照样被 OOM killer 杀掉(可用内存 2234MB)。撑爆的是模型推理本身,
|
|
240
|
+
跟 AM 无关。ort 的 arena/mem_pattern 调优只把 fast 从 997MB 压到 892MB,对
|
|
241
|
+
birefnet 那 2.4GB 基本没动。birefnet 档要能用,得先给这台机器加 swap。
|
|
242
|
+
|
|
243
|
+
不开 AM 时原样走 rembg,一个像素都不动。
|
|
244
|
+
"""
|
|
245
|
+
if not alpha_matting:
|
|
246
|
+
return remove(image_bytes, session=session, alpha_matting=False)
|
|
247
|
+
|
|
248
|
+
src = Image.open(io.BytesIO(image_bytes))
|
|
249
|
+
src.load()
|
|
250
|
+
w, h = src.size
|
|
251
|
+
long_edge = max(w, h)
|
|
252
|
+
|
|
253
|
+
if AM_MAX_DIM <= 0 or long_edge <= AM_MAX_DIM:
|
|
254
|
+
return remove(image_bytes, session=session, alpha_matting=True)
|
|
255
|
+
|
|
256
|
+
scale = AM_MAX_DIM / long_edge
|
|
257
|
+
small = src.convert('RGB').resize(
|
|
258
|
+
(max(1, round(w * scale)), max(1, round(h * scale))), Image.LANCZOS,
|
|
259
|
+
)
|
|
260
|
+
buf = io.BytesIO()
|
|
261
|
+
small.save(buf, format='PNG')
|
|
262
|
+
cut_small = remove(buf.getvalue(), session=session, alpha_matting=True)
|
|
263
|
+
|
|
264
|
+
alpha = Image.open(io.BytesIO(cut_small)).convert('RGBA').getchannel('A')
|
|
265
|
+
alpha = alpha.resize((w, h), Image.LANCZOS)
|
|
266
|
+
|
|
267
|
+
out = src.convert('RGB')
|
|
268
|
+
out.putalpha(alpha)
|
|
269
|
+
result = io.BytesIO()
|
|
270
|
+
out.save(result, format='PNG')
|
|
271
|
+
return result.getvalue()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@app.post('/remove')
|
|
275
|
+
async def remove_endpoint(request: Request):
|
|
276
|
+
global _inflight
|
|
277
|
+
# 缺省换成 isnet(2026-08-02):原来的 birefnet-general-lite 是个陷阱 ——
|
|
278
|
+
# 不带 X-Model 的直连调用会加载 2.4GB 峰值的模型,在这台机器上直接 OOM
|
|
279
|
+
# 连坐整个 service(当天真踩过)。工具层永远显式传 model,缺省只服务于
|
|
280
|
+
# 手工调试,就该是安全档。
|
|
281
|
+
model = request.headers.get('x-model', 'isnet-general-use')
|
|
282
|
+
alpha_matting = request.headers.get('x-alpha-matting', '0') == '1'
|
|
283
|
+
image_bytes = await request.body()
|
|
284
|
+
if not image_bytes:
|
|
285
|
+
raise HTTPException(400, detail='empty body')
|
|
286
|
+
_inflight += 1
|
|
287
|
+
try:
|
|
288
|
+
session = await ensure_session(model)
|
|
289
|
+
# rembg remove 是 CPU 密集——丢线程池防阻塞 event loop
|
|
290
|
+
rgba = await asyncio.to_thread(
|
|
291
|
+
_remove_with_am_cap, image_bytes, session, alpha_matting,
|
|
292
|
+
)
|
|
293
|
+
return Response(content=rgba, media_type='image/png')
|
|
294
|
+
except Exception as e:
|
|
295
|
+
print(f'[rembg-service] /remove error: {type(e).__name__}: {e}',
|
|
296
|
+
file=sys.stderr, flush=True)
|
|
297
|
+
raise HTTPException(500, detail=f'{type(e).__name__}: {e}')
|
|
298
|
+
finally:
|
|
299
|
+
_inflight -= 1
|
|
300
|
+
# 主动还内存:pymatting 的稀疏矩阵和 PIL 的全分辨率图都是大块 numpy /
|
|
301
|
+
# buffer,Python 的分配器不一定马上还给系统。gc 一次 + malloc_trim 把
|
|
302
|
+
# 空闲堆顶还回去,能明显压低 RSS 高水位(2026-07-31 事故就是高水位不降
|
|
303
|
+
# 一路把机器推进内存抖动)。拿不到 malloc_trim 的平台静默跳过。
|
|
304
|
+
gc.collect()
|
|
305
|
+
_malloc_trim()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def cleanup_socket(socket_path: str):
|
|
309
|
+
"""退出时删 socket file,方便下次启动重新 bind。"""
|
|
310
|
+
try:
|
|
311
|
+
if os.path.exists(socket_path):
|
|
312
|
+
os.unlink(socket_path)
|
|
313
|
+
print(f'[rembg-service] cleaned up {socket_path}', file=sys.stderr, flush=True)
|
|
314
|
+
except OSError:
|
|
315
|
+
pass
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def main():
|
|
319
|
+
socket_path = os.environ.get('NODESIGN_REMBG_SOCKET', '/tmp/nodesign-rembg.sock')
|
|
320
|
+
# bind 前清掉旧 socket(上次没干净退出会留残骸;SIGKILL 不走 atexit)
|
|
321
|
+
cleanup_socket(socket_path)
|
|
322
|
+
atexit.register(cleanup_socket, socket_path)
|
|
323
|
+
|
|
324
|
+
providers_label = ','.join(PROVIDERS) if PROVIDERS else '<ort-default>'
|
|
325
|
+
print(f'[rembg-service] providers={providers_label}', file=sys.stderr, flush=True)
|
|
326
|
+
print(f'[rembg-service] listening on {socket_path}', file=sys.stderr, flush=True)
|
|
327
|
+
|
|
328
|
+
import uvicorn
|
|
329
|
+
# log_level=warning 静音 uvicorn access logs(每次请求一行很吵)
|
|
330
|
+
uvicorn.run(app, uds=socket_path, log_level='warning', access_log=False)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
if __name__ == '__main__':
|
|
334
|
+
main()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# server/shared/
|
|
2
|
+
|
|
3
|
+
从小合([dev/server/bot/](../../../dev/server/bot/))抠出来的**真正通用、跟工单业务无关**的基建文件。
|
|
4
|
+
|
|
5
|
+
## 文件清单
|
|
6
|
+
|
|
7
|
+
| 文件 | 来源 | 改动 |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `anthropic-client.js` | `dev/server/bot/anthropic-client.js` | env 名 `MINIMAX_*` → `KIMI_*`;baseURL 默认值切 Moonshot;timeout 600s(长任务) |
|
|
10
|
+
| `agent-loop.js` | `dev/server/bot/agent-loop.js` | 仅改 env 名 `BOT_TOOL_TIMEOUT_MS` → `ENGINE_TOOL_TIMEOUT_MS`,默认 30s → 60s;其余照搬 |
|
|
11
|
+
| `concurrency.js` | `dev/server/bot/concurrency.js` | 注释从"飞书 user open_id"泛化为"key";env 名加 `ENGINE_` 前缀;`enqueueMessage` 改 `enqueueTask` |
|
|
12
|
+
| `time.js` | `dev/server/utils/time.js` | 零改动 |
|
|
13
|
+
|
|
14
|
+
## 不该往这里加东西的判断
|
|
15
|
+
|
|
16
|
+
- 业务(工单 / 钩子 / 飞书卡片)→ 不该来
|
|
17
|
+
- 跟 dev/ 共演化的核心抽象 → 暂时直接复制;如果开始反复同步两边,就提到 monorepo `shared/` 包
|
|
18
|
+
|
|
19
|
+
## 待验证(动工前必做)
|
|
20
|
+
|
|
21
|
+
- [ ] `cache_control: { type: 'ephemeral' }` 在 Kimi Anthropic 兼容端点是否生效
|
|
22
|
+
- [ ] `anthropic-beta: interleaved-thinking-2025-05-14` header 是否被 Kimi 接受
|
|
23
|
+
- [ ] 流式 `tool_use` + `input_json_delta` 行为一致性
|
|
24
|
+
|
|
25
|
+
写一个 `server/_probe-kimi.js`(30 行 JS)跑一遍。`npm run probe:kimi`。
|