@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.
Files changed (372) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +140 -0
  3. package/bin/nodesign.js +176 -0
  4. package/package.json +104 -0
  5. package/server/api/README.md +45 -0
  6. package/server/api/_guard.js +75 -0
  7. package/server/api/admin.js +187 -0
  8. package/server/api/assets/docx-page.js +103 -0
  9. package/server/api/assets/helpers.js +22 -0
  10. package/server/api/assets/notes.js +144 -0
  11. package/server/api/assets.js +896 -0
  12. package/server/api/board.js +57 -0
  13. package/server/api/browse.js +138 -0
  14. package/server/api/canvas.js +444 -0
  15. package/server/api/chatai.js +324 -0
  16. package/server/api/exports/build-standalone.js +980 -0
  17. package/server/api/exports/cards.js +118 -0
  18. package/server/api/exports/docx-pdf.js +46 -0
  19. package/server/api/exports/export-page.js +142 -0
  20. package/server/api/exports/handoff.js +147 -0
  21. package/server/api/exports.js +690 -0
  22. package/server/api/instruction.js +75 -0
  23. package/server/api/local.js +115 -0
  24. package/server/api/me.js +116 -0
  25. package/server/api/memory.js +185 -0
  26. package/server/api/pending-changes.js +366 -0
  27. package/server/api/plugins.js +164 -0
  28. package/server/api/projects.js +220 -0
  29. package/server/api/publish.js +64 -0
  30. package/server/api/recent.js +73 -0
  31. package/server/api/sessions.js +589 -0
  32. package/server/api/skills.js +77 -0
  33. package/server/api/standalone-fit.js +168 -0
  34. package/server/api/turn-compose.js +180 -0
  35. package/server/api/turn-inflight.js +44 -0
  36. package/server/api/turn.js +669 -0
  37. package/server/auth/README.md +17 -0
  38. package/server/auth/middleware.js +138 -0
  39. package/server/auth/origin-guard.js +71 -0
  40. package/server/auth/session.js +108 -0
  41. package/server/auth/tier.js +113 -0
  42. package/server/auth/users-store.js +348 -0
  43. package/server/edit/README.md +29 -0
  44. package/server/engine/README.md +113 -0
  45. package/server/engine/agent/agent-shared.js +550 -0
  46. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -0
  47. package/server/engine/agent/auto-mode-rules.js +78 -0
  48. package/server/engine/agent/context.js +418 -0
  49. package/server/engine/agent/events.js +464 -0
  50. package/server/engine/agent/hooks/canvas-validate.js +199 -0
  51. package/server/engine/agent/hooks/failure.js +120 -0
  52. package/server/engine/agent/hooks/file-events.js +75 -0
  53. package/server/engine/agent/hooks/lifecycle.js +233 -0
  54. package/server/engine/agent/hooks/post-canvas-focus.js +80 -0
  55. package/server/engine/agent/hooks/post-guidance.js +206 -0
  56. package/server/engine/agent/hooks/post-subagent-report.js +104 -0
  57. package/server/engine/agent/hooks/post-trim.js +58 -0
  58. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -0
  59. package/server/engine/agent/hooks/pre-defaults.js +84 -0
  60. package/server/engine/agent/hooks/pre-injectors.js +277 -0
  61. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -0
  62. package/server/engine/agent/hooks/pre-starter-files.js +61 -0
  63. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -0
  64. package/server/engine/agent/hooks/site-validate.js +94 -0
  65. package/server/engine/agent/hooks/tool-prompts.js +31 -0
  66. package/server/engine/agent/hooks/turn-state-memory.js +56 -0
  67. package/server/engine/agent/hooks/user-prompt-submit.js +215 -0
  68. package/server/engine/agent/hooks.js +354 -0
  69. package/server/engine/agent/init-contract.js +69 -0
  70. package/server/engine/agent/isolation.js +159 -0
  71. package/server/engine/agent/model-context.js +359 -0
  72. package/server/engine/agent/model-table.js +292 -0
  73. package/server/engine/agent/plugin-loader.js +266 -0
  74. package/server/engine/agent/prompts/nodesign-prelude.md +336 -0
  75. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -0
  76. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -0
  77. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -0
  78. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -0
  79. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -0
  80. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -0
  81. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -0
  82. package/server/engine/agent/prompts/tools/site-reference.md +106 -0
  83. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -0
  84. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -0
  85. package/server/engine/agent/session-loop.js +1024 -0
  86. package/server/engine/agent/session-model.js +164 -0
  87. package/server/engine/agent/skill.js +204 -0
  88. package/server/engine/agent/system-prompts.js +86 -0
  89. package/server/engine/agent/task-events.js +78 -0
  90. package/server/engine/agents/ds-extractor.md +132 -0
  91. package/server/engine/agents/explorer.md +190 -0
  92. package/server/engine/agents/index.js +230 -0
  93. package/server/engine/agents/schemas/design-system.json +156 -0
  94. package/server/engine/agents/schemas/tweak-schema.json +113 -0
  95. package/server/engine/agents/tweak-proposer.md +127 -0
  96. package/server/engine/agents/vision-checker.md +254 -0
  97. package/server/engine/browse/capture.js +422 -0
  98. package/server/engine/browse/card.js +137 -0
  99. package/server/engine/browse/handover.js +59 -0
  100. package/server/engine/browse/page-digest.js +119 -0
  101. package/server/engine/browse/refs.js +196 -0
  102. package/server/engine/browse/registry.js +307 -0
  103. package/server/engine/browse/screencast.js +170 -0
  104. package/server/engine/browse/state.js +121 -0
  105. package/server/engine/chatai/chat-log.js +79 -0
  106. package/server/engine/chatai/index.js +193 -0
  107. package/server/engine/chatai/openai-compat.js +152 -0
  108. package/server/engine/chatai/orchestrate.js +309 -0
  109. package/server/engine/chatai/perform.js +40 -0
  110. package/server/engine/chatai/summarize.js +79 -0
  111. package/server/engine/mcp/capability-gate.js +52 -0
  112. package/server/engine/mcp/index.js +339 -0
  113. package/server/engine/mcp/param-sanitizer.js +142 -0
  114. package/server/engine/mcp/tools/arrange-on-board.js +124 -0
  115. package/server/engine/mcp/tools/artifact-session.js +278 -0
  116. package/server/engine/mcp/tools/browse-computer.js +421 -0
  117. package/server/engine/mcp/tools/browse-find-batch.js +183 -0
  118. package/server/engine/mcp/tools/browse-screenshot.js +157 -0
  119. package/server/engine/mcp/tools/browse.js +480 -0
  120. package/server/engine/mcp/tools/build-docx.js +101 -0
  121. package/server/engine/mcp/tools/clear-pending-changes.js +99 -0
  122. package/server/engine/mcp/tools/create-on-board.js +155 -0
  123. package/server/engine/mcp/tools/crystallize-skill.js +168 -0
  124. package/server/engine/mcp/tools/deliver-files.js +0 -0
  125. package/server/engine/mcp/tools/explain-style.js +237 -0
  126. package/server/engine/mcp/tools/export-handoff.js +133 -0
  127. package/server/engine/mcp/tools/expose-tweaks.js +149 -0
  128. package/server/engine/mcp/tools/generate-image.js +842 -0
  129. package/server/engine/mcp/tools/get-computed-styles.js +164 -0
  130. package/server/engine/mcp/tools/get-pending-changes.js +205 -0
  131. package/server/engine/mcp/tools/h3box-ssh.js +84 -0
  132. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -0
  133. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -0
  134. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -0
  135. package/server/engine/mcp/tools/helpers/perception-page.js +215 -0
  136. package/server/engine/mcp/tools/helpers/reference-download.js +103 -0
  137. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -0
  138. package/server/engine/mcp/tools/helpers/rembg.js +273 -0
  139. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -0
  140. package/server/engine/mcp/tools/highlight.js +68 -0
  141. package/server/engine/mcp/tools/list-pages.js +222 -0
  142. package/server/engine/mcp/tools/lookup-tags.js +59 -0
  143. package/server/engine/mcp/tools/navigate-to-page.js +60 -0
  144. package/server/engine/mcp/tools/organize-board.js +64 -0
  145. package/server/engine/mcp/tools/paint-still.js +434 -0
  146. package/server/engine/mcp/tools/pin-to-board.js +130 -0
  147. package/server/engine/mcp/tools/preview-deck.js +130 -0
  148. package/server/engine/mcp/tools/profile-scroll.js +305 -0
  149. package/server/engine/mcp/tools/publish-site.js +102 -0
  150. package/server/engine/mcp/tools/query-elements.js +195 -0
  151. package/server/engine/mcp/tools/read-board.js +103 -0
  152. package/server/engine/mcp/tools/read-document.js +93 -0
  153. package/server/engine/mcp/tools/read-page.js +264 -0
  154. package/server/engine/mcp/tools/read-tavern-json.js +143 -0
  155. package/server/engine/mcp/tools/record-decision.js +140 -0
  156. package/server/engine/mcp/tools/relate-on-board.js +110 -0
  157. package/server/engine/mcp/tools/remove-background.js +391 -0
  158. package/server/engine/mcp/tools/report-issue.js +115 -0
  159. package/server/engine/mcp/tools/roll-film.js +266 -0
  160. package/server/engine/mcp/tools/screenshot-docx.js +138 -0
  161. package/server/engine/mcp/tools/screenshot-url.js +195 -0
  162. package/server/engine/mcp/tools/screenshot.js +583 -0
  163. package/server/engine/mcp/tools/tier-gate.js +96 -0
  164. package/server/engine/mcp/tools/trace-motion.js +215 -0
  165. package/server/engine/mcp/tools/web-search.js +548 -0
  166. package/server/engine/motion/inventory.js +349 -0
  167. package/server/engine/perception/session.js +235 -0
  168. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -0
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -0
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -0
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -0
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -0
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -0
  174. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -0
  175. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -0
  176. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -0
  177. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -0
  178. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -0
  179. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -0
  180. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -0
  181. 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
  182. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -0
  183. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -0
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -0
  185. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -0
  186. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -0
  187. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -0
  188. package/server/engine/runs/active-runs.js +698 -0
  189. package/server/engine/runs/live-turn.js +252 -0
  190. package/server/engine/runs/store.js +364 -0
  191. package/server/engine/runs/turn-relay.js +217 -0
  192. package/server/engine/runtime/workspace.js +164 -0
  193. package/server/index.js +235 -0
  194. package/server/lib/artifact-file-path.js +80 -0
  195. package/server/lib/artifact-target.js +284 -0
  196. package/server/lib/asset-refs.js +239 -0
  197. package/server/lib/async-queue.js +104 -0
  198. package/server/lib/auto-relations.js +130 -0
  199. package/server/lib/binding-types.js +60 -0
  200. package/server/lib/board-kind-sizes.js +62 -0
  201. package/server/lib/board-relations.js +133 -0
  202. package/server/lib/browse-proxy.js +180 -0
  203. package/server/lib/canvas-id.js +36 -0
  204. package/server/lib/cover.js +227 -0
  205. package/server/lib/danbooru-tags.js +291 -0
  206. package/server/lib/doc-extract.js +156 -0
  207. package/server/lib/docx/build-from-source.js +260 -0
  208. package/server/lib/docx/build.js +490 -0
  209. package/server/lib/docx/dump-styles.js +287 -0
  210. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -0
  211. package/server/lib/docx/merge-runs.js +129 -0
  212. package/server/lib/docx/numbering.js +218 -0
  213. package/server/lib/docx/order.js +152 -0
  214. package/server/lib/docx/rawzip.js +172 -0
  215. package/server/lib/docx/render.js +92 -0
  216. package/server/lib/docx/text-lint.js +225 -0
  217. package/server/lib/docx/tokens.js +335 -0
  218. package/server/lib/docx/units.js +29 -0
  219. package/server/lib/docx/xml.js +291 -0
  220. package/server/lib/docx-pages.js +187 -0
  221. package/server/lib/export-collect.js +289 -0
  222. package/server/lib/export-package.js +210 -0
  223. package/server/lib/html-srcset.js +145 -0
  224. package/server/lib/image-variant.js +460 -0
  225. package/server/lib/ingress/forward-openai-chat.js +305 -0
  226. package/server/lib/ingress/openai-chat.js +491 -0
  227. package/server/lib/ingress/session-notice.js +66 -0
  228. package/server/lib/ingress/session-routes.js +68 -0
  229. package/server/lib/ingress/slot-probe.js +130 -0
  230. package/server/lib/ingress/upstream-billing.js +52 -0
  231. package/server/lib/ingress/upstream-fail-streak.js +72 -0
  232. package/server/lib/ingress/upstream-truncation.js +48 -0
  233. package/server/lib/issues-store.js +182 -0
  234. package/server/lib/kinds/deck.js +72 -0
  235. package/server/lib/kinds/docx.js +283 -0
  236. package/server/lib/kinds/file-kinds.js +113 -0
  237. package/server/lib/kinds/index.js +202 -0
  238. package/server/lib/kinds/site.js +161 -0
  239. package/server/lib/model-ingress.js +543 -0
  240. package/server/lib/moderation.js +255 -0
  241. package/server/lib/notice-store.js +103 -0
  242. package/server/lib/plugin-install.js +104 -0
  243. package/server/lib/plugin-validator.js +721 -0
  244. package/server/lib/publish-store.js +115 -0
  245. package/server/lib/quick-summary.js +52 -0
  246. package/server/lib/quota.js +270 -0
  247. package/server/lib/rate-window.js +29 -0
  248. package/server/lib/reference-assets.js +92 -0
  249. package/server/lib/region-shot.js +135 -0
  250. package/server/lib/safe-path.js +73 -0
  251. package/server/lib/sdk-session.js +33 -0
  252. package/server/lib/showcase-store.js +92 -0
  253. package/server/lib/site-publish.js +465 -0
  254. package/server/lib/ssrf-guard.js +432 -0
  255. package/server/lib/task-scan.js +158 -0
  256. package/server/lib/tavern-json.js +154 -0
  257. package/server/lib/video-variant.js +237 -0
  258. package/server/lib/workspace-path.js +33 -0
  259. package/server/ops/fix-sdk-musl.mjs +40 -0
  260. package/server/ops/install-macos-fonts.sh +114 -0
  261. package/server/ops/macos-fonts.conf +336 -0
  262. package/server/ops/sandbox-shim/bwrap +57 -0
  263. package/server/projects/assets-summary.js +119 -0
  264. package/server/projects/auto-name.js +53 -0
  265. package/server/projects/board-store.js +640 -0
  266. package/server/projects/move-entry.js +103 -0
  267. package/server/projects/store.js +270 -0
  268. package/server/projects/ui-config.js +54 -0
  269. package/server/projects/workspace-templates.js +53 -0
  270. package/server/projects/workspace.js +1025 -0
  271. package/server/runtime/capabilities.js +153 -0
  272. package/server/runtime/local-config.js +177 -0
  273. package/server/runtime/local-env.js +88 -0
  274. package/server/runtime/platform.js +342 -0
  275. package/server/runtime/profile.js +72 -0
  276. package/server/services/rembg-launcher.js +313 -0
  277. package/server/services/rembg-service.py +334 -0
  278. package/server/shared/README.md +25 -0
  279. package/server/shared/deck.js +54 -0
  280. package/server/shared/time.js +49 -0
  281. package/server/style-pipeline/README.md +53 -0
  282. package/server/ws/broker.js +36 -0
  283. package/server/ws/browse-channel.js +177 -0
  284. package/server/ws/index.js +386 -0
  285. package/web/README.md +89 -0
  286. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +11 -0
  287. package/web/dist/assets/DeckWindow-4G0xX7P3.js +16 -0
  288. package/web/dist/assets/DocxWindow-BXQsoPkh.js +16 -0
  289. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  290. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  291. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  292. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  293. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  294. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  295. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  296. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  297. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  298. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  299. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  300. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  301. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  302. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  303. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  304. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  305. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  306. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  307. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  308. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  309. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  310. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  311. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  312. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  313. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  314. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  315. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  316. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  317. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  318. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  319. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  320. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  321. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  322. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  323. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  324. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  325. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  326. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  327. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  328. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  329. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  330. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  331. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  332. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  333. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  334. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  335. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  336. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  337. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  338. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  339. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  340. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  341. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  342. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  343. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  344. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  345. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  346. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  347. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  348. package/web/dist/assets/SiteWindow-D6eS-2gM.js +18 -0
  349. package/web/dist/assets/again-D8tfgKKO.webp +0 -0
  350. package/web/dist/assets/caveat-nd-CP6HlsNg.woff2 +0 -0
  351. package/web/dist/assets/clock-xJhvfYpS.webp +0 -0
  352. package/web/dist/assets/film-cut-BBKMwH7q.webp +0 -0
  353. package/web/dist/assets/film-still-BZ_DL9Tt.webp +0 -0
  354. package/web/dist/assets/index-C0KUf6Wd.css +1 -0
  355. package/web/dist/assets/index-Cbl3ATvp.js +2125 -0
  356. package/web/dist/assets/ink-desk-BRbxeDsR.webp +0 -0
  357. package/web/dist/assets/ink-night-Bz4BT3XH.webp +0 -0
  358. package/web/dist/assets/ink-portrait-C0DM4V8Q.webp +0 -0
  359. package/web/dist/assets/longcang-regular-C0uvTuKa.woff2 +0 -0
  360. package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
  361. package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
  362. package/web/dist/assets/pending-edit-apply-DTjo2geW.js +31 -0
  363. package/web/dist/assets/question-8H4HSqxh.webp +0 -0
  364. package/web/dist/assets/reject-BaPJqElj.webp +0 -0
  365. package/web/dist/assets/rotate-cw-LMT3x1Zj.js +11 -0
  366. package/web/dist/assets/rp-portrait-CbrGpew2.webp +0 -0
  367. package/web/dist/assets/rp-street-C4jVKjxx.webp +0 -0
  368. package/web/dist/assets/square-dashed-mouse-pointer-CBoAkfhi.js +11 -0
  369. package/web/dist/assets/tangle-DIxHVNuT.webp +0 -0
  370. package/web/dist/assets/thisone-BylZKglV.webp +0 -0
  371. package/web/dist/assets/thumb-DO1wfK1I.webp +0 -0
  372. package/web/dist/index.html +18 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * server/engine/mcp/tools/helpers/perception-page.js
3
+ * 感知工具的**唯一**页面加载口(2026-08-18)
4
+ *
5
+ * ## 为什么要有这一层
6
+ *
7
+ * 在这之前四个感知工具各自 `page.goto('file://' + abs)`,而用户预览走的是
8
+ * `/api/projects/:pid/artifact-file/*`(http)。**两条通道不同源**,于是:
9
+ *
10
+ * - `location.protocol` 守卫(导出 zip 用 file:// 打开时关掉客户端路由那种)
11
+ * 在自检时触发,agent 看到的是退化行为
12
+ * - `fetch`/XHR 被 CORS 拒 —— 任何加载 JSON 的站点在自检里是死的
13
+ * - **localStorage 的 origin 是字符串 "null"**,全部任务共用一个桶;而用户
14
+ * 预览下它们各自独立。mock-app 产线「写了刷新还在」那条硬定义正好踩在
15
+ * 这上面 —— agent 验的东西和用户看的东西不是一回事
16
+ * - Service Worker / 动态 import 同理
17
+ *
18
+ * 一个 agent 为此花了 4 次截图 + 一次把 `location.protocol` 写进 DOM 再截图,
19
+ * 才反推出来自己被 file:// 打开了(问题库 iss_msxk2oci_0v0v)。
20
+ *
21
+ * ## 凭什么能走 http
22
+ *
23
+ * MCP 工具的 handler 跟 express server 跑在**同一个进程**里,而 token 是无状态
24
+ * HMAC:`mintToken(ownerId)` 当场铸一个、塞进 chromium 的 cookie jar 即可。
25
+ * 拿到的权限跟项目所有者在浏览器里一模一样——不是新开一道后门,是走用户那道门。
26
+ * (web/scripts/shot-live.mjs 早就是这么干的,这里只是把它收进产品代码。)
27
+ *
28
+ * ⚠️ 铸出来的 token 只进本机 chromium 的 cookie jar,不写日志、不进返回文本。
29
+ * ⚠️ 前提是**调用方跟 HTTP server 同进程**(共用 NODESIGN_AUTH_SECRET)。独立脚本
30
+ * 里直接调这个 helper 会铸出对不上号的 token,服务端回 401 —— 那是环境问题不是
31
+ * 产品 bug,跑验证脚本记得 `node --env-file=.env`。
32
+ */
33
+
34
+ import path from 'node:path';
35
+ import { getProject } from '../../../../projects/store.js';
36
+ import { COOKIE_NAME, mintToken, authEnabled } from '../../../../auth/session.js';
37
+ import { getUserById } from '../../../../auth/users-store.js';
38
+
39
+ // ── 渲染层保真(2026-08-07 立,2026-08-18 从 screenshot.js 挪来收成一份)──
40
+ // Chromium 的强制暗色(Auto Dark)会在 paint 层反转颜色,而页面自己的 JS
41
+ // 一点都测不到 —— 2026-08-05 那次「位图深色 / computed style 浅色」事故就是它。
42
+ // ⚠️ 这组参数以前只有 screenshot / screenshot-url 两条路带,query_elements /
43
+ // get_computed_styles / list_pages 是裸奔的 —— 同一个 DOM 两条通道两种渲染,
44
+ // 正是「判据本身要先验一遍」那类坑。现在收成一份,谁开 chromium 都带上。
45
+ export const FIDELITY_LAUNCH_ARGS = [
46
+ '--disable-features=WebContentsForceDark', // 自动暗色:paint 层反转,页面自己测不到
47
+ '--force-color-profile=srgb', // 色彩配置固定 srgb,排除 ICC 差异
48
+ ];
49
+
50
+ /** 服务端自己的监听口。跟 server/index.js 同一个默认值,改端口两处一起改。 */
51
+ const PORT = Number(process.env.PORT || 4001);
52
+
53
+ /** 127.0.0.1 而不是 localhost:避免 ::1/127.0.0.1 双栈解析下 cookie 域对不上 */
54
+ export const PERCEPTION_ORIGIN = `http://127.0.0.1:${PORT}`;
55
+ /**
56
+ * 站点检查的真实设备宽(08-21 收成一份:以前 screenshot / explain_style / profile_scroll
57
+ * 各抄一份 {desktop:1440,…})。站点没有"比例",版面是宽度算出来的,所以档位给
58
+ * 真实设备宽、直接当 viewport、不缩放。
59
+ */
60
+ export const SITE_DEVICE_W = { desktop: 1440, tablet: 834, mobile: 390 };
61
+
62
+ /**
63
+ * 开一个「保真」chromium。凡是要把页面变成给人或给模型看的像素的地方都走这里 ——
64
+ * 参数散在九个调用点上时,有三个是漏的(2026-08-18 逐处对账才发现,当天收敛完)。
65
+ *
66
+ * ⚠️ **有两处故意不走这里**,别顺手也"收敛"掉:
67
+ * `screenshot-url.js` 和 `engine/browse/registry.js`。它们打的是**外部 URL**,
68
+ * 必须挂 `--proxy-server` 走出网闸(lib/browse-proxy.js);而这个函数开的浏览器
69
+ * 专门打 `127.0.0.1:4001` 上我们自己的产物 —— 那正是出网闸要拦的地址。
70
+ * 两条通道的**安全前提相反**,共用一个 launch 只会让其中一条失守或不通。
71
+ */
72
+ export async function launchPerceptionBrowser() {
73
+ const { chromium } = await import('playwright');
74
+ return chromium.launch({ headless: true, args: FIDELITY_LAUNCH_ARGS });
75
+ }
76
+
77
+ /**
78
+ * 工作区相对路径 → 用户预览用的同一个 URL。
79
+ * 逐段 encodeURIComponent —— 任务名和文件名大量是中文,整体编码会把 `/` 也吃掉。
80
+ */
81
+ export function artifactFileUrl(projectId, relPath, { raw = true } = {}) {
82
+ const sub = String(relPath || '').split('/').filter(Boolean).map(encodeURIComponent).join('/');
83
+ // ⭐ `?nd=raw` —— 不走显示改写层,拿原始 HTML + 原图。
84
+ //
85
+ // artifact-file 平时是**显示通道**:给 html 注 srcset/sizes/loading=lazy、给图发
86
+ // 降尺寸的 webp/avif 派生图。对用户预览这些都对(省 90% 流量)。但 agent 的眼睛
87
+ // 走同一条路由之后,它看到的就不是**交付物**了 —— 发布出去的站点是原始 HTML
88
+ // (site-publish 拷原文,没有这层注入)。
89
+ //
90
+ // ⛔ 实测差别(2026-08-18 审查攻出来的):一张 1800px 宽、没有 CSS 宽约束的图,
91
+ // 注入 `sizes="100vw"` 之后在 agent 眼里渲染成 1440px、`overflow:false`;
92
+ // 而访客那边是 1800px、`overflow:true` —— **显示改写层把横向溢出藏起来了**,
93
+ // 而「忘写 max-width:100%」正是最常见的那类真 bug。顺带图也不是原图
94
+ // (naturalWidth 1440 vs 1800),逐像素/锐度类判断跟着失真。
95
+ //
96
+ // ⚠️ 代价:感知页加载原图更慢更吃 CPU(1 vCPU 上是真代价)。取舍是**眼睛的
97
+ // 正确性优先于自检速度** —— 一个看错的自检比一个慢的自检坏得多。
98
+ // (http 同源那半边不受影响:fetch/localStorage/SW 照旧跟用户预览一致,实测过。)
99
+ return `${PERCEPTION_ORIGIN}/api/projects/${projectId}/artifact-file/${sub}${raw ? '?nd=raw' : ''}`;
100
+ }
101
+
102
+ /**
103
+ * 退化提示 → 一行可以直接拼进返回文本的话。
104
+ *
105
+ * `openArtifactPage` 的契约是「note 非空 = 没能走成 http,调用方必须把它写进
106
+ * 返回文本」。⛔ 实测只有 screenshot 一个调用方照做了,另外五个工具连 `.note`
107
+ * 两个字都没出现过(grep 计数 0)—— 也就是 07-29 那个「file:// 静默回退」的
108
+ * bug 被重新种了一遍:`profile_scroll` 的描述铁口直断"走 http 同源",一旦回退,
109
+ * 它按 Resource Timing 报「images 0 files 0KB」,那正好是它主诊断的反面,
110
+ * 而唯一能解释这件事的 note 被丢掉了。
111
+ *
112
+ * 所以把措辞收成一份,调用点无脑拼。配套有个 lint 钉住"import 了就必须用"。
113
+ */
114
+ export function degradedNote(opened) {
115
+ if (!opened?.note) return null;
116
+ return `⚠️ 这一页没能按用户预览的方式打开(${opened.note})。`
117
+ + 'fetch/XHR/localStorage 的行为跟用户那边不一样,凡是依赖它们的结论都不作数。';
118
+ }
119
+
120
+ /**
121
+ * 打开一个产物页面,返回 { page, context, url, note }。
122
+ *
123
+ * - `note` 非空表示**没能走成 http**(退回了 file://),调用方应把它写进 caption:
124
+ * 悄悄退回去就是把这个 bug 重新种一遍。
125
+ * - HTTP 状态 >= 400 直接抛错,不截一张 JSON 错误页给 agent 当产物看。
126
+ *
127
+ * @param {import('playwright').Browser} browser
128
+ * @param {object} opts
129
+ * @param {string} opts.projectId
130
+ * @param {string} opts.workspaceRoot 项目工作区根(= artifact-file 的可服务根)
131
+ * @param {string} opts.absPath 要打开的文件绝对路径
132
+ * @param {object} [opts.viewport]
133
+ * @param {number} [opts.deviceScaleFactor]
134
+ * @param {number} [opts.timeout]
135
+ * @param {string} [opts.waitUntil]
136
+ */
137
+ export async function openArtifactPage(browser, {
138
+ projectId, workspaceRoot, absPath,
139
+ viewport, deviceScaleFactor = 1, timeout = 15000, waitUntil = 'networkidle',
140
+ }) {
141
+ const contextOpts = { colorScheme: 'light', deviceScaleFactor };
142
+ if (viewport) contextOpts.viewport = viewport;
143
+ // ⭐ `?nd=raw` 只挂在**主文档**的 URL 上,页面里 `<img src="assets/x.png">` 发出去的
144
+ // 请求不带任何 query —— 于是主文档拿到了原样 HTML,图片却照旧走派生图。实测:
145
+ // 一张 1.36MB 的 PNG 发出去是 **35.7KB 的 webp(2.6%)**。两个后果都真实:
146
+ // ① 没有 CSS 宽约束的图按**固有尺寸**排版,派生图尺寸不同 → 版面跟访客不一样;
147
+ // ② `profile_scroll` 的"图片总字节"少报了几十倍,而它的头号建议就是按体积开药。
148
+ // 每个请求都带一个头最省:query 要改写 HTML(那就不叫 raw 了)。
149
+ //
150
+ // ⚠️ 但**不能用 context 级 extraHTTPHeaders**(第一版就是,agent 上报逮到):
151
+ // 那会把这个自定义头带给页面请求的**所有主机**,而任何自定义头都会把跨域字体
152
+ // 请求从「简单请求」升级成要 CORS 预检 —— fonts.gstatic.com 不认这个头,预检
153
+ // 挂掉,Google Fonts 整站全灭,感知截图里字体全部回退。头只对感知源有意义
154
+ //(assets.js 读它决定给不给原图),所以用 route 只加在同源请求上。
155
+ const context = await browser.newContext(contextOpts);
156
+ await context.route(`${PERCEPTION_ORIGIN}/**`, (route) => route.continue({
157
+ headers: { ...route.request().headers(), 'X-ND-Raw': '1' },
158
+ }));
159
+
160
+ let url = null;
161
+ let note = null;
162
+
163
+ const rel = projectId && workspaceRoot
164
+ ? path.relative(workspaceRoot, absPath)
165
+ : null;
166
+ const insideWorkspace = rel != null && rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
167
+
168
+ if (insideWorkspace) {
169
+ url = artifactFileUrl(projectId, rel.split(path.sep).join('/'));
170
+ if (authEnabled()) {
171
+ // 项目所有者的身份 —— guardProject 只认 owner 或 admin
172
+ const ownerId = getProject(projectId)?.ownerId;
173
+ // ⚠️ 光有 ownerId 不够:`mintToken` 只把 id 烤进签名,而 `requestUser` 会
174
+ // **重新查库**并拒掉 disabled / 已删除的用户。于是账号一被停用,六个感知工具
175
+ // 全部报「artifact-file returned HTTP 401」—— 一句完全指不到根因的话
176
+ // (真因是账号状态,跟文件、路径、权限配置都无关)。这里提前认出来,
177
+ // 退到 file:// 并把真原因写进 note(note 会被所有调用方拼进返回文本,
178
+ // 有 lint 钉着)。
179
+ const owner = ownerId ? getUserById(ownerId) : null;
180
+ if (ownerId && owner && !owner.disabled) {
181
+ await context.addCookies([{
182
+ name: COOKIE_NAME, value: mintToken(ownerId), url: PERCEPTION_ORIGIN,
183
+ }]);
184
+ } else {
185
+ const why = !ownerId ? 'project owner unknown'
186
+ : (!owner ? `owner account ${ownerId} no longer exists`
187
+ : `owner account ${owner.username} is disabled — an admin has to re-enable it; `
188
+ + 'this is an account-state problem, not a file or path problem');
189
+ note = `${why} — loaded via file:// (fetch/XHR/localStorage behave differently from the user preview)`;
190
+ url = null;
191
+ }
192
+ }
193
+ } else {
194
+ // 产物落在工作区外(不该发生,但寻址层出岔子时别整个瞎掉)
195
+ note = 'file is outside the project workspace — loaded via file:// (fetch/XHR/localStorage behave differently from the user preview)';
196
+ }
197
+
198
+ const page = await context.newPage();
199
+ if (!url) {
200
+ return { page, context, url: `file://${absPath}`, note, viaHttp: false, goto: () => page.goto(`file://${absPath}`, { waitUntil, timeout }) };
201
+ }
202
+
203
+ return {
204
+ page, context, url, note, viaHttp: true,
205
+ goto: async () => {
206
+ const resp = await page.goto(url, { waitUntil, timeout });
207
+ // 401/403/404 会渲成一张 JSON 错误页 —— 截它等于给 agent 一张假产物
208
+ const status = resp?.status();
209
+ if (status && status >= 400) {
210
+ throw new Error(`artifact-file returned HTTP ${status} for ${decodeURIComponent(url.replace(PERCEPTION_ORIGIN, ''))}`);
211
+ }
212
+ return resp;
213
+ },
214
+ };
215
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * server/engine/mcp/tools/helpers/reference-download.js
3
+ * 把搜到的参考图下载进 `assets/references/`(2026-08-18 从 web-search.js 拆出)
4
+ *
5
+ * 拆的直接原因是行数棘轮,但拆对了:这跟"搜索"是两件事 —— 一件是问 provider 要结果,
6
+ * 一件是把图落进工作区。`browser_capture` 也往同一个目录写,两边的落点约定
7
+ * (`assets/references/`、`.meta/<主干>.json` 出处 sidecar)应该看得见彼此。
8
+ */
9
+
10
+ import path from 'node:path';
11
+ import fs from 'node:fs/promises';
12
+ import crypto from 'node:crypto';
13
+
14
+ // ── reference image download ──
15
+
16
+ const DL_TIMEOUT_MS = 10_000;
17
+ const DL_MAX_BYTES = 5 * 1024 * 1024; // 5MB(Anthropic image content block 单图上限)
18
+ const DL_MIN_BYTES = 5 * 1024; // 5KB(< 这个多半是 logo / 损坏)
19
+
20
+ const MIME_TO_EXT = {
21
+ 'image/jpeg': '.jpg',
22
+ 'image/jpg': '.jpg',
23
+ 'image/png': '.png',
24
+ 'image/webp': '.webp',
25
+ 'image/gif': '.gif',
26
+ };
27
+
28
+ function shortHash(s) {
29
+ return crypto.createHash('sha256').update(s).digest('hex').slice(0, 10);
30
+ }
31
+
32
+ /**
33
+ * 下载一张图到 <root>/assets/references/ref-<hash>-<role>.<ext>。
34
+ * 已存在 → 直接返回(按 hash 去重)。
35
+ *
36
+ * @returns {Promise<{ relPath: string, absPath: string, sizeBytes: number, mimeType: string } | null>}
37
+ * 失败/校验不通过 → null(caller 跳过这一张)
38
+ */
39
+ async function downloadOneImage(url, refsDir) {
40
+ const controller = new AbortController();
41
+ const timer = setTimeout(() => controller.abort(), DL_TIMEOUT_MS);
42
+ try {
43
+ const hash = shortHash(url);
44
+ const res = await fetch(url, { signal: controller.signal, redirect: 'follow' });
45
+ if (!res.ok) return null;
46
+ const mime = (res.headers.get('content-type') || '').toLowerCase().split(';')[0].trim();
47
+ const ext = MIME_TO_EXT[mime];
48
+ if (!ext) return null;
49
+
50
+ const buf = Buffer.from(await res.arrayBuffer());
51
+ if (buf.length < DL_MIN_BYTES || buf.length > DL_MAX_BYTES) return null;
52
+
53
+ const fileName = `ref-${hash}${ext}`;
54
+ const absPath = path.join(refsDir, fileName);
55
+ // 去重:同 url → 同 hash → 同文件名 → 已存在就跳写
56
+ let exists = false;
57
+ try { await fs.access(absPath); exists = true; } catch { /* not exists */ }
58
+ if (!exists) await fs.writeFile(absPath, buf);
59
+
60
+ return {
61
+ relPath: path.posix.join('assets', 'references', fileName),
62
+ absPath,
63
+ sizeBytes: buf.length,
64
+ mimeType: mime,
65
+ base64: buf.toString('base64'), // 直接传给 CallToolResult image block
66
+ };
67
+ } catch {
68
+ return null;
69
+ } finally {
70
+ clearTimeout(timer);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * 并发下载 top-N 图,filter 掉失败/超大/过小。
76
+ *
77
+ * @returns {Promise<Array<{url, description, title, relPath, absPath, sizeBytes, mimeType}>>}
78
+ */
79
+ export async function downloadReferenceImages(images, { workspaceRoot, sharedRoot, stopAfter = Infinity }) {
80
+ const baseRoot = sharedRoot || workspaceRoot;
81
+ if (!baseRoot) return [];
82
+ const refsDir = path.join(baseRoot, 'assets', 'references');
83
+ await fs.mkdir(refsDir, { recursive: true });
84
+
85
+ // ⚠️ **够了就停**(2026-08-18)。原来是把候选全并发下完再 slice,于是多下的那些
86
+ // 留在磁盘上、agent 不知道它们存在、却照样占空间和进导出包。候选多给是为了容错
87
+ // (有些 URL 会 404),不是为了多存。
88
+ // 代价:从全并发变成小批并发,慢一点;换来的是"盘上有什么 = 报了什么"。
89
+ const out = [];
90
+ const BATCH = 3;
91
+ for (let i = 0; i < images.length && out.length < stopAfter; i += BATCH) {
92
+ const settled = await Promise.allSettled(
93
+ images.slice(i, i + BATCH).map(async (img) => {
94
+ const dl = await downloadOneImage(img.url, refsDir);
95
+ return dl ? { ...img, ...dl } : null;
96
+ }),
97
+ );
98
+ for (const r of settled) {
99
+ if (r.status === 'fulfilled' && r.value && out.length < stopAfter) out.push(r.value);
100
+ }
101
+ }
102
+ return out;
103
+ }
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env python3
2
+ """rembg-bridge — read source PNG/JPEG from stdin, write transparent RGBA PNG to stdout.
3
+
4
+ Args:
5
+ --model NAME rembg session model name. Default: birefnet-general-lite.
6
+ Options: u2net / isnet-general-use / birefnet-general-lite /
7
+ birefnet-general / silueta etc. (see rembg docs)
8
+ --alpha-matting Enable rembg trimap alpha matting post-process. Drastically
9
+ reduces edge halo / 伪影 at cost of ~1-2× inference time.
10
+
11
+ 调用方(Node 端 helpers/rembg.js)spawn 这个脚本,stdin 喂 NB2 生图字节,
12
+ stdout 拿 rembg 抠完背景的 PNG。stderr 走 logging(urllib3 LibreSSL 警告等
13
+ 非致命噪声 silence 掉,真错让 caller 看到非零 exit code)。
14
+
15
+ 依赖:server/.venv-rembg/ 装了 rembg + onnxruntime + 模型缓存到 ~/.u2net/
16
+ (首次 cold load 自动下载,birefnet-general-lite ~214MB / birefnet-general ~880MB)。
17
+
18
+ 跑法:
19
+ printf '<png-bytes>' | <server>/.venv-rembg/bin/python3 rembg-bridge.py \\
20
+ --model birefnet-general-lite --alpha-matting > out.png
21
+ """
22
+ import argparse
23
+ import os
24
+ import sys
25
+ import warnings
26
+
27
+ # urllib3 v2 + LibreSSL(macOS 自带 Python 用 LibreSSL 不是 OpenSSL)只是警告,
28
+ # 抠图不受影响。silence 防污染 stderr 让 caller 误判失败。
29
+ warnings.filterwarnings('ignore')
30
+
31
+ from rembg import remove, new_session
32
+
33
+
34
+ def _resolve_providers():
35
+ """决定 onnxruntime providers,与 rembg-service.py 一致:
36
+ darwin 默认 CPU only(绕 CoreML+birefnet 卡死),其它平台 ort 自选;
37
+ env NODESIGN_REMBG_PROVIDERS 覆盖。"""
38
+ env = os.environ.get('NODESIGN_REMBG_PROVIDERS', '').strip()
39
+ if env:
40
+ return [p.strip() for p in env.split(',') if p.strip()]
41
+ if sys.platform == 'darwin':
42
+ return ['CPUExecutionProvider']
43
+ return None
44
+
45
+
46
+ def main() -> int:
47
+ parser = argparse.ArgumentParser(description='rembg bridge: stdin → stdout')
48
+ parser.add_argument(
49
+ '--model',
50
+ default='birefnet-general-lite',
51
+ help='rembg session model name (default: birefnet-general-lite)',
52
+ )
53
+ parser.add_argument(
54
+ '--alpha-matting',
55
+ action='store_true',
56
+ help='enable rembg trimap alpha matting (cleaner edges, ~1-2× slower)',
57
+ )
58
+ args = parser.parse_args()
59
+
60
+ try:
61
+ input_data = sys.stdin.buffer.read()
62
+ if not input_data:
63
+ print('rembg-bridge: empty stdin', file=sys.stderr)
64
+ return 2
65
+
66
+ session = new_session(args.model, providers=_resolve_providers())
67
+ output_data = remove(
68
+ input_data,
69
+ session=session,
70
+ alpha_matting=args.alpha_matting,
71
+ )
72
+ sys.stdout.buffer.write(output_data)
73
+ return 0
74
+ except Exception as e:
75
+ print(f'rembg-bridge error: {type(e).__name__}: {e}', file=sys.stderr)
76
+ return 1
77
+
78
+
79
+ if __name__ == '__main__':
80
+ sys.exit(main())
@@ -0,0 +1,273 @@
1
+ /**
2
+ * helpers/rembg.js — Node 侧抠图调用包装。两条路径:
3
+ *
4
+ * 1. **优先 Unix socket service**(warm,~5-15s/张)—— server 启动时
5
+ * services/rembg-launcher.js 已 spawn rembg-service.py 常驻进程,
6
+ * onnxruntime session 在内存。每次请求走 HTTP-over-Unix-socket。
7
+ *
8
+ * 2. **降级到 spawn-bridge**(cold,~30-180s/张)—— service down / 不可用时
9
+ * fallback 到 per-call spawn .venv-rembg/bin/python3 跑 rembg-bridge.py。
10
+ * 首次部署 / dev 没装 service 时这条路径仍能跑通。
11
+ *
12
+ * Fail-soft:两条路径都失败时 return null,调用方降级(不抛)。
13
+ *
14
+ * Env override:
15
+ * NODESIGN_REMBG_SOCKET Unix socket 路径(默认 /tmp/nodesign-rembg.sock)
16
+ * NODESIGN_REMBG_PYTHON venv python 路径(默认 server/.venv-rembg/bin/python3)
17
+ * NODESIGN_REMBG_HELPER spawn-bridge 脚本路径
18
+ * NODESIGN_REMBG_TIMEOUT fallback 路径 subprocess 超时 ms
19
+ */
20
+
21
+ import { spawn } from 'node:child_process';
22
+ import http from 'node:http';
23
+ import { fileURLToPath } from 'node:url';
24
+ import path from 'node:path';
25
+ import { promises as fs } from 'node:fs';
26
+
27
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
28
+ // helpers/ 在 server/engine/mcp/tools/helpers/,server root 上溯 4 层
29
+ const SERVER_ROOT = path.resolve(__dirname, '../../../../');
30
+
31
+ // venv 里 python 的位置按平台走(Windows 是 Scripts\python.exe,不是 bin/python3);
32
+ // 安装提示同源,launcher / capabilities / remove_background 三处都引这里,别各写一遍
33
+ const IS_WIN = process.platform === 'win32';
34
+ export const REMBG_VENV_PYTHON = IS_WIN
35
+ ? path.join(SERVER_ROOT, '.venv-rembg', 'Scripts', 'python.exe')
36
+ : path.join(SERVER_ROOT, '.venv-rembg', 'bin', 'python3');
37
+ export const REMBG_SETUP_HINT = IS_WIN
38
+ ? 'cd server && python -m venv .venv-rembg && .venv-rembg\\Scripts\\python.exe -m pip install rembg onnxruntime'
39
+ : 'cd server && python3 -m venv .venv-rembg && .venv-rembg/bin/python3 -m pip install rembg onnxruntime';
40
+ const DEFAULT_PYTHON = REMBG_VENV_PYTHON;
41
+ const DEFAULT_HELPER = path.join(__dirname, 'rembg-bridge.py');
42
+ const DEFAULT_SOCKET = '/tmp/nodesign-rembg.sock';
43
+ // fallback spawn 的 timeout——首次冷启 + 模型 load 留余量
44
+ const DEFAULT_FALLBACK_TIMEOUT_MS = 60_000;
45
+
46
+ function resolvePython() { return process.env.NODESIGN_REMBG_PYTHON || DEFAULT_PYTHON; }
47
+ function resolveHelper() { return process.env.NODESIGN_REMBG_HELPER || DEFAULT_HELPER; }
48
+ function resolveSocket() { return process.env.NODESIGN_REMBG_SOCKET || DEFAULT_SOCKET; }
49
+
50
+ // service health check 缓存(避免每次 request 都探一遍 /health)
51
+ let serviceHealthCache = { ok: false, checkedAt: 0 };
52
+ const HEALTH_CACHE_MS = 5_000;
53
+
54
+ /**
55
+ * 探活 service:HTTP GET /health on Unix socket。带 5s cache 避免高频探测。
56
+ */
57
+ async function isServiceHealthy() {
58
+ const now = Date.now();
59
+ if (now - serviceHealthCache.checkedAt < HEALTH_CACHE_MS) {
60
+ return serviceHealthCache.ok;
61
+ }
62
+ const socketPath = resolveSocket();
63
+ // 先 stat 文件存在再 connect,省一次 ECONNREFUSED 的 noise
64
+ try {
65
+ await fs.access(socketPath);
66
+ } catch {
67
+ serviceHealthCache = { ok: false, checkedAt: now };
68
+ return false;
69
+ }
70
+ const ok = await new Promise((resolve) => {
71
+ const req = http.request({
72
+ socketPath,
73
+ path: '/health',
74
+ method: 'GET',
75
+ timeout: 2000,
76
+ }, (res) => {
77
+ res.resume(); // drain
78
+ resolve(res.statusCode === 200);
79
+ });
80
+ req.on('error', () => resolve(false));
81
+ req.on('timeout', () => { req.destroy(); resolve(false); });
82
+ req.end();
83
+ });
84
+ serviceHealthCache = { ok, checkedAt: now };
85
+ return ok;
86
+ }
87
+
88
+ /**
89
+ * 通过 service Unix socket 调 /remove。
90
+ * @returns {Promise<Buffer | null>}
91
+ */
92
+ async function removeViaService(inputBuf, opts) {
93
+ const socketPath = resolveSocket();
94
+ const model = opts.model || 'birefnet-general-lite';
95
+ const alphaMatting = opts.alphaMatting !== false;
96
+ const timeoutMs = opts.timeoutMs || 180_000;
97
+
98
+ return new Promise((resolve) => {
99
+ const req = http.request({
100
+ socketPath,
101
+ path: '/remove',
102
+ method: 'POST',
103
+ timeout: timeoutMs,
104
+ headers: {
105
+ 'content-type': 'application/octet-stream',
106
+ 'content-length': inputBuf.length,
107
+ 'x-model': model,
108
+ 'x-alpha-matting': alphaMatting ? '1' : '0',
109
+ },
110
+ }, (res) => {
111
+ const chunks = [];
112
+ res.on('data', (c) => chunks.push(c));
113
+ res.on('end', () => {
114
+ if (res.statusCode === 200) {
115
+ resolve(Buffer.concat(chunks));
116
+ } else {
117
+ const text = Buffer.concat(chunks).toString().slice(0, 300);
118
+ console.warn(`[rembg] service status ${res.statusCode}: ${text}`);
119
+ resolve(null);
120
+ }
121
+ });
122
+ res.on('error', () => resolve(null));
123
+ });
124
+ req.on('error', (err) => {
125
+ console.warn(`[rembg] service request error: ${err.message}`);
126
+ // 任何 socket 错都让 cache 立刻失效,下次会重新探活
127
+ serviceHealthCache = { ok: false, checkedAt: 0 };
128
+ resolve(null);
129
+ });
130
+ req.on('timeout', () => {
131
+ console.warn(`[rembg] service timeout after ${timeoutMs}ms`);
132
+ req.destroy();
133
+ resolve(null);
134
+ });
135
+ req.write(inputBuf);
136
+ req.end();
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Fallback:spawn rembg-bridge.py per-call(cold path)。
142
+ * @returns {Promise<Buffer | null>}
143
+ */
144
+ async function removeViaSpawn(inputBuf, opts) {
145
+ const py = resolvePython();
146
+ const helper = resolveHelper();
147
+ const model = opts.model || 'birefnet-general-lite';
148
+ const alphaMatting = opts.alphaMatting !== false;
149
+ const timeoutMs = opts.timeoutMs
150
+ || Number(process.env.NODESIGN_REMBG_TIMEOUT)
151
+ || DEFAULT_FALLBACK_TIMEOUT_MS;
152
+
153
+ const args = [helper, '--model', model];
154
+ if (alphaMatting) args.push('--alpha-matting');
155
+
156
+ return new Promise((resolve) => {
157
+ let proc;
158
+ try {
159
+ proc = spawn(py, args, { stdio: ['pipe', 'pipe', 'pipe'] });
160
+ } catch (err) {
161
+ console.warn('[rembg] spawn failed:', err.message);
162
+ resolve(null);
163
+ return;
164
+ }
165
+
166
+ const stdoutChunks = [];
167
+ let stderrText = '';
168
+ let settled = false;
169
+
170
+ const finish = (result) => {
171
+ if (settled) return;
172
+ settled = true;
173
+ try { proc.kill(); } catch { /* already dead */ }
174
+ resolve(result);
175
+ };
176
+
177
+ const timer = setTimeout(() => {
178
+ console.warn(`[rembg] spawn timeout after ${timeoutMs}ms`);
179
+ finish(null);
180
+ }, timeoutMs);
181
+
182
+ proc.stdout.on('data', (chunk) => stdoutChunks.push(chunk));
183
+ proc.stderr.on('data', (chunk) => { stderrText += chunk.toString(); });
184
+
185
+ proc.on('error', (err) => {
186
+ clearTimeout(timer);
187
+ console.warn('[rembg] spawn process error:', err.message);
188
+ finish(null);
189
+ });
190
+
191
+ proc.on('close', (code) => {
192
+ clearTimeout(timer);
193
+ if (code !== 0) {
194
+ console.warn(
195
+ `[rembg] spawn exit ${code} (model=${model}, alphaMatting=${alphaMatting}): ${stderrText.slice(0, 300).trim() || '(no stderr)'}`,
196
+ );
197
+ finish(null);
198
+ return;
199
+ }
200
+ const out = Buffer.concat(stdoutChunks);
201
+ if (out.length === 0) {
202
+ console.warn(`[rembg] spawn empty stdout despite exit 0 (model=${model})`);
203
+ finish(null);
204
+ return;
205
+ }
206
+ finish(out);
207
+ });
208
+
209
+ try {
210
+ proc.stdin.write(inputBuf);
211
+ proc.stdin.end();
212
+ } catch (err) {
213
+ console.warn('[rembg] spawn stdin write failed:', err.message);
214
+ clearTimeout(timer);
215
+ finish(null);
216
+ }
217
+ });
218
+ }
219
+
220
+ /**
221
+ * 检查 rembg 整体可用——service 在线 OR fallback (venv + bridge) 文件齐。
222
+ * 不实际推理,只探活 + stat 文件。
223
+ *
224
+ * @returns {Promise<{ available: boolean, mode?: 'service'|'spawn', reason?: string }>}
225
+ */
226
+ export async function isAvailable() {
227
+ if (await isServiceHealthy()) {
228
+ return { available: true, mode: 'service' };
229
+ }
230
+ const py = resolvePython();
231
+ const helper = resolveHelper();
232
+ try {
233
+ await fs.access(py);
234
+ } catch {
235
+ return { available: false, reason: `service down + python not found: ${py}` };
236
+ }
237
+ try {
238
+ await fs.access(helper);
239
+ } catch {
240
+ return { available: false, reason: `service down + bridge script not found: ${helper}` };
241
+ }
242
+ return { available: true, mode: 'spawn' };
243
+ }
244
+
245
+ /**
246
+ * 抠掉背景 → 返 transparent RGBA PNG Buffer。
247
+ *
248
+ * 路径选择:先试 service(warm,~5-15s)→ 失败降级 spawn(cold,30-180s)。
249
+ *
250
+ * @param {Buffer} inputBuf
251
+ * @param {object} [opts]
252
+ * @param {string} [opts.model='birefnet-general-lite']
253
+ * @param {boolean} [opts.alphaMatting=true]
254
+ * @param {number} [opts.timeoutMs] - service 路径默认 180s,spawn 路径默认 60s
255
+ * @returns {Promise<Buffer | null>}
256
+ */
257
+ export async function removeBackground(inputBuf, opts = {}) {
258
+ if (!Buffer.isBuffer(inputBuf) || inputBuf.length === 0) {
259
+ console.warn('[rembg] empty input buffer');
260
+ return null;
261
+ }
262
+
263
+ // 路径 1:service warm
264
+ if (await isServiceHealthy()) {
265
+ const out = await removeViaService(inputBuf, opts);
266
+ if (out) return out;
267
+ console.warn('[rembg] service path failed, falling back to spawn');
268
+ // service request 失败 → 降级到 spawn 不要直接返 null
269
+ }
270
+
271
+ // 路径 2:spawn cold fallback
272
+ return removeViaSpawn(inputBuf, opts);
273
+ }