@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,366 @@
1
+ /**
2
+ * server/api/pending-changes.js — 用户直接编辑 + 评论 buffer(C4)
3
+ *
4
+ * 用户在 canvas 上做的"直接改文本"和"写评论"在前端 push 到这里。下次发 chat
5
+ * 消息时,turn.js 在 composeUserMessage 里 prepend 一个 system 提示告诉 agent
6
+ * "有 N 处变更",agent 主动调 mcp__nodesign__get_pending_changes 拉详情。
7
+ *
8
+ * 路径(2026-08-13 起**项目级**,会话级留作 alias):
9
+ * POST /api/projects/:pid/pending-changes append item
10
+ * GET /api/projects/:pid/pending-changes 返 { items, count }
11
+ * DELETE /api/projects/:pid/pending-changes 全清(也接 ?ids=)
12
+ * (/:pid/sessions/:sid/... 同 handler 双挂载 —— 老前端和 jsonl 里的
13
+ * 历史引用还打得通;sid 在扁平化后本来就只是路由上的仪式,
14
+ * sessionRoot === 工作区根,这份 buffer 从来就是每项目一份)
15
+ *
16
+ * 文件:<工作区根>/pending-changes.json
17
+ * { items: [{ id, kind, anchor, aiContext?, diff?, text?, linkedToEditId?, move?, styleDelta?, reactMount?, ts }] }
18
+ *
19
+ * 2026-05-12 起新增 kind:
20
+ * - 'pending-move' 用户在 canvas 上拖动元素(前端虚拟改 DOM,agent run 时落源码)
21
+ * 带 move: { container: anchor, before: anchor|null }
22
+ * - 'pending-style' 用户拖动 / nudge 改样式(位移、对齐等)
23
+ * 带 styleDelta: { left?, top?, marginLeft?, ... }
24
+ * 可选 constraint: { x, y } (Figma 风格 anchor — 决定父 resize 时跟哪边)
25
+ * - 'pending-duplicate' 用户 alt-drag 复制元素
26
+ * 带 move: { container, before }
27
+ * - 'pending-delete' 用户按 Del 删除元素
28
+ * 全部可选带 reactMount: true 表示要改 JSX 源码不是 HTML。
29
+ */
30
+
31
+ import express from 'express';
32
+ import { promises as fs } from 'fs';
33
+ import path from 'path';
34
+ import { randomUUID } from 'crypto';
35
+ import { mutex } from 'async-mutex-lite';
36
+ import { validateProjectId, getProject } from '../projects/store.js';
37
+ import { guardProject } from './_guard.js';
38
+ import {
39
+ ensureSessionWorkspace, ensureProjectWorkspace, validateSessionId, getSharedDir,
40
+ } from '../projects/workspace.js';
41
+ import { renderRegionShot, saveRegionShot, PADDING, OUT_LONG_EDGE } from '../lib/region-shot.js';
42
+ import { regionShotFromPage } from '../lib/docx-pages.js';
43
+
44
+ const router = express.Router();
45
+
46
+ const FILE_NAME = 'pending-changes.json';
47
+ const MAX_ITEMS = 200;
48
+
49
+ function guard(req, res) {
50
+ // sid 只在走老 alias 时存在 —— 有就校验形状,没有就是项目级路由
51
+ if (req.params.sid !== undefined) {
52
+ try {
53
+ validateSessionId(req.params.sid);
54
+ } catch (err) {
55
+ res.status(400).json({ error: err.message || 'invalid pid/sid' });
56
+ return null;
57
+ }
58
+ }
59
+ // pid 校验 + 存在性 + 归属(2026-07-30 多用户)统一走 guardProject
60
+ return guardProject(req, res);
61
+ }
62
+
63
+ /** 两条挂载共用:alias 带 sid 走原路,项目级直接 ensure 工作区 */
64
+ function rootOf(req) {
65
+ return req.params.sid !== undefined
66
+ ? ensureSessionWorkspace(req.params.pid, req.params.sid)
67
+ : ensureProjectWorkspace(req.params.pid);
68
+ }
69
+
70
+ async function readBuf(sessionRoot) {
71
+ const file = path.join(sessionRoot, FILE_NAME);
72
+ try {
73
+ const raw = await fs.readFile(file, 'utf8');
74
+ const parsed = JSON.parse(raw);
75
+ if (parsed && Array.isArray(parsed.items)) return { items: parsed.items, file };
76
+ } catch (err) {
77
+ if (err.code !== 'ENOENT') throw err;
78
+ }
79
+ return { items: [], file };
80
+ }
81
+
82
+ async function writeBuf(file, items) {
83
+ await fs.writeFile(file, JSON.stringify({ items }, null, 2), 'utf8');
84
+ }
85
+
86
+ router.get(['/:pid/pending-changes', '/:pid/sessions/:sid/pending-changes'], async (req, res, next) => {
87
+ try {
88
+ if (!guard(req, res)) return;
89
+ const sessionRoot = await rootOf(req);
90
+ const { items } = await readBuf(sessionRoot);
91
+ res.json({ items, count: items.length });
92
+ } catch (err) { next(err); }
93
+ });
94
+
95
+ /**
96
+ * 这次编辑是不是"把内容清空了"。`<br>`、`&nbsp;`、空白都算空 ——
97
+ * contenteditable 清空后浏览器常留一个 `<br>` 占位。
98
+ */
99
+ function isBecameEmpty(diff) {
100
+ if (!diff || typeof diff.newText !== 'string') return false;
101
+ if (typeof diff.oldText === 'string' && diff.oldText.replace(/<[^>]*>|&nbsp;|\s+/g, '') === '') return false;
102
+ return diff.newText.replace(/<br\s*\/?>|&nbsp;|<[^>]*>|\s+/gi, '') === '';
103
+ }
104
+
105
+ router.post(['/:pid/pending-changes', '/:pid/sessions/:sid/pending-changes'], async (req, res, next) => {
106
+ try {
107
+ if (!guard(req, res)) return;
108
+ const body = req.body || {};
109
+ const { kind, anchor } = body;
110
+ // applied-*(2026-07-29):站点窗拖拽已直接落盘的 FYI 记录,agent 不再应用
111
+ const VALID_KINDS = ['edit', 'comment', 'pending-move', 'pending-style', 'pending-duplicate', 'pending-delete',
112
+ 'applied-move', 'applied-style', 'applied-duplicate'];
113
+ if (!VALID_KINDS.includes(kind)) {
114
+ return res.status(400).json({ error: `kind must be one of ${VALID_KINDS.join(' / ')}` });
115
+ }
116
+ if (!anchor || typeof anchor !== 'object') {
117
+ return res.status(400).json({ error: 'anchor object required' });
118
+ }
119
+ if (kind === 'edit') {
120
+ const { diff } = body;
121
+ if (!diff || typeof diff !== 'object'
122
+ || typeof diff.oldText !== 'string' || typeof diff.newText !== 'string') {
123
+ return res.status(400).json({ error: 'edit kind requires diff: { oldText, newText }' });
124
+ }
125
+ } else if (kind === 'comment') {
126
+ const { text } = body;
127
+ if (typeof text !== 'string' || !text.trim()) {
128
+ return res.status(400).json({ error: 'comment kind requires non-empty text' });
129
+ }
130
+ // linkedToEditId (optional) —— 把评论挂到某条 pending edit 上,作为该次操作的补充指令
131
+ if (body.linkedToEditId !== undefined && typeof body.linkedToEditId !== 'string') {
132
+ return res.status(400).json({ error: 'linkedToEditId must be string if present' });
133
+ }
134
+ } else if (kind === 'pending-move' || kind === 'pending-duplicate') {
135
+ const { move } = body;
136
+ if (!move || typeof move !== 'object' || !move.container || typeof move.container !== 'object') {
137
+ return res.status(400).json({
138
+ error: `${kind} requires move: { container: anchor, before: anchor|null }`,
139
+ });
140
+ }
141
+ } else if (kind === 'pending-style') {
142
+ const { styleDelta } = body;
143
+ if (!styleDelta || typeof styleDelta !== 'object') {
144
+ return res.status(400).json({ error: 'pending-style requires styleDelta object' });
145
+ }
146
+ }
147
+ // pending-delete 只需 anchor
148
+
149
+ const sessionRoot = await rootOf(req);
150
+
151
+ // 接受 body.id(前端 newId('cmt') 等)以让前后端 id 统一——agent 调
152
+ // clear_pending_changes 时 event 带 clearedIds,前端 comments state 用同一
153
+ // id filter 出橙色 overlay。无传入则后端 randomUUID 兜底。
154
+ const itemId = (typeof body.id === 'string' && body.id.trim()) ? body.id.trim() : randomUUID();
155
+ const item = {
156
+ id: itemId,
157
+ kind,
158
+ anchor,
159
+ // 这条改动是对**哪个文件**做的(2026-07-28)。一个任务可以有多份 deck,
160
+ // 站点更是天然多页 —— 不记路径的话 agent 只能把所有改动都套到它当前打开的
161
+ // 那一份上,改错了还看不出来(anchor 在别的文件里根本找不到,静默 no-op)。
162
+ ...(typeof body.path === 'string' && body.path ? { path: body.path } : {}),
163
+ ...(body.aiContext ? { aiContext: body.aiContext } : {}),
164
+ ...(body.reactMount === true ? { reactMount: true } : {}),
165
+ ...(kind === 'edit' ? {
166
+ diff: body.diff,
167
+ // 用户把一段文字**清空**了(2026-08-18)。源码里留下的是个空壳
168
+ // `<p class="foot-line"></p>` —— 它仍然有 margin/line-height,在页面上
169
+ // 撑出一道空隙,用户看到的是"字没了但地方还空着"。以前 agent 只能从
170
+ // `diff.newText === ''` 自己推断,而且第一次基本都漏,等用户下次截图
171
+ // 才发现(问题库 iss_msc46ion_ydr8:同一个坑一个会话踩两次)。
172
+ // 只打标记不自动删元素:用户也可能是清空重打,误删不可逆。
173
+ ...(isBecameEmpty(body.diff) ? { becameEmpty: true } : {}),
174
+ } : {}),
175
+ ...(kind === 'comment' ? {
176
+ text: body.text,
177
+ ...(body.linkedToEditId ? { linkedToEditId: body.linkedToEditId } : {}),
178
+ } : {}),
179
+ ...((kind === 'pending-move' || kind === 'pending-duplicate'
180
+ || kind === 'applied-move' || kind === 'applied-duplicate') ? { move: body.move } : {}),
181
+ ...((kind === 'pending-style' || kind === 'applied-style') ? {
182
+ styleDelta: body.styleDelta,
183
+ ...(body.constraint && typeof body.constraint === 'object'
184
+ ? { constraint: body.constraint } // { x: 'left'|'right'|'center'|'stretch', y: 'top'|'bottom'|'center'|'stretch' }
185
+ : {}),
186
+ } : {}),
187
+ // applied-*:站点窗已落盘的 FYI 记录特有标记(applied=事实已写盘;
188
+ // serializedFrom=落盘走了运行时序列化兜底,文件可能带脚本运行时产物)
189
+ ...(body.applied === true ? { applied: true } : {}),
190
+ ...(typeof body.serializedFrom === 'string' && body.serializedFrom
191
+ ? { serializedFrom: body.serializedFrom } : {}),
192
+ ts: new Date().toISOString(),
193
+ };
194
+
195
+ // 串行 read-modify-write 防多点击 race(用户连点 5 次评论 → 5 个 POST 几乎同时
196
+ // 到达,原版 readBuf→push→writeBuf 无锁,后写者覆盖前写者 → 用户感"评论凭空消失"。
197
+ // per-sessionRoot mutex 串行所有 pending-changes 写。
198
+ const trimmedCount = await mutex(`pending:${sessionRoot}`, async () => {
199
+ const { items, file } = await readBuf(sessionRoot);
200
+ items.push(item);
201
+ const trimmed = items.length > MAX_ITEMS ? items.slice(items.length - MAX_ITEMS) : items;
202
+ await writeBuf(file, trimmed);
203
+ return trimmed.length;
204
+ });
205
+
206
+ res.json({ ok: true, item, count: trimmedCount });
207
+ } catch (err) { next(err); }
208
+ });
209
+
210
+ /**
211
+ * 圈选评论(2026-08-07)—— 用户在预览上框一块,连同框里涉及的元素、一张
212
+ * 该区域的截图、以及一句话交给 agent。
213
+ *
214
+ * 单独一条路由而不是复用上面那个 POST,因为它要做两件那边不做的事:
215
+ * 跑一次 chromium 把区域截下来落盘,以及**不带 anchor**(圈的是一块地方
216
+ * 不是一个元素,硬塞一个 anchor 只会让 agent 以为用户点的是某一个)。
217
+ *
218
+ * 截图失败不挡下单:元素清单和那句话本身就够 agent 干活了,为了一张图把
219
+ * 用户刚圈完的东西整个丢掉是最差的选择。
220
+ */
221
+ router.post(['/:pid/region-comment', '/:pid/sessions/:sid/region-comment'], async (req, res, next) => {
222
+ try {
223
+ if (!guard(req, res)) return;
224
+ const body = req.body || {};
225
+ const { region, viewport } = body;
226
+ const relPath = typeof body.path === 'string' ? body.path.replace(/\\/g, '/') : '';
227
+ const text = typeof body.text === 'string' ? body.text.trim() : '';
228
+
229
+ const num = (v) => typeof v === 'number' && Number.isFinite(v);
230
+ if (!region || !num(region.x) || !num(region.y) || !(region.w > 0) || !(region.h > 0)) {
231
+ return res.status(400).json({ error: 'region: { x, y, w, h } required' });
232
+ }
233
+ if (!viewport || !(viewport.width > 0) || !(viewport.height > 0)) {
234
+ return res.status(400).json({ error: 'viewport: { width, height } required' });
235
+ }
236
+ // 页面路径相对项目工作区根。扁平化之前这里硬要求 `tasks/<任务>/<文件>`
237
+ // 三段起步,现在根上的 `index.html` 只有一段,那条判据会把它全拒掉。
238
+ const parts = relPath.split('/');
239
+ if (!parts.length || parts.includes('..') || parts.some(p => !p || p.startsWith('.'))) {
240
+ return res.status(400).json({ error: 'invalid page path' });
241
+ }
242
+ const sharedDir = path.resolve(getSharedDir(req.params.pid));
243
+ const absPath = path.resolve(sharedDir, ...parts);
244
+ if (!absPath.startsWith(sharedDir + path.sep)) {
245
+ return res.status(400).json({ error: 'path escapes the workspace' });
246
+ }
247
+ try { await fs.access(absPath); } catch {
248
+ return res.status(404).json({ error: `page not found: ${relPath}` });
249
+ }
250
+
251
+ const elements = Array.isArray(body.elements) ? body.elements.slice(0, 12) : [];
252
+ const itemId = (typeof body.id === 'string' && body.id.trim()) ? body.id.trim() : randomUUID();
253
+ const sessionRoot = await rootOf(req);
254
+
255
+ let shot = null;
256
+ let shotError = null;
257
+ let shotNote = null; // 截图成功但通道退化(file:// 回退)时的提醒
258
+ let docxPage = null; // docx 圈选:圈的是第几页(页图坐标系没有"整页文档"这回事)
259
+ try {
260
+ if (/\.docx$/i.test(relPath)) {
261
+ // docx 没有 DOM 也进不了 chromium —— "用户所见"就是 LO 渲的页图,
262
+ // 直接从页图缓存裁(region 坐标基准 = 页图原始像素,前端换算好再送)
263
+ const r = await regionShotFromPage(absPath, body.docxPage, region,
264
+ { pad: PADDING, longEdge: OUT_LONG_EDGE });
265
+ shot = await saveRegionShot(sessionRoot, itemId, r.buf);
266
+ docxPage = r.page;
267
+ } else {
268
+ const { buffer, degraded } = await renderRegionShot({
269
+ absPath,
270
+ projectId: req.params.pid,
271
+ workspaceRoot: sharedDir,
272
+ region: { x: region.x, y: region.y, w: region.w, h: region.h },
273
+ viewport: { width: Math.round(viewport.width), height: Math.round(viewport.height) },
274
+ });
275
+ shot = await saveRegionShot(sessionRoot, itemId, buffer);
276
+ // 没走成 http 的话这张图不能当"用户所见"用,得让 agent 知道
277
+ if (degraded) shotNote = degraded;
278
+ }
279
+ } catch (err) {
280
+ shotError = err?.message || String(err);
281
+ console.warn('[region-comment] 截图失败,只带元素清单下单:', shotError);
282
+ }
283
+
284
+ const item = {
285
+ id: itemId,
286
+ kind: 'region-comment',
287
+ path: relPath,
288
+ text,
289
+ region: { x: Math.round(region.x), y: Math.round(region.y), w: Math.round(region.w), h: Math.round(region.h) },
290
+ ...(docxPage ? { docxPage } : {}),
291
+ ...(body.container && typeof body.container === 'object' ? { container: body.container } : {}),
292
+ viewport: { width: Math.round(viewport.width), height: Math.round(viewport.height) },
293
+ elements,
294
+ ...(shot ? { shot } : {}),
295
+ ...(shotError ? { shotError } : {}),
296
+ ...(shotNote ? { shotNote } : {}),
297
+ ts: new Date().toISOString(),
298
+ };
299
+
300
+ const count = await mutex(`pending:${sessionRoot}`, async () => {
301
+ const { items, file } = await readBuf(sessionRoot);
302
+ items.push(item);
303
+ const trimmed = items.length > MAX_ITEMS ? items.slice(items.length - MAX_ITEMS) : items;
304
+ await writeBuf(file, trimmed);
305
+ return trimmed.length;
306
+ });
307
+
308
+ res.json({ ok: true, item, count });
309
+ } catch (err) { next(err); }
310
+ });
311
+
312
+ router.delete(['/:pid/pending-changes', '/:pid/sessions/:sid/pending-changes'], async (req, res, next) => {
313
+ try {
314
+ if (!guard(req, res)) return;
315
+ const sessionRoot = await rootOf(req);
316
+
317
+ // 同 POST:mutex 串行避免 DELETE / POST 并发互踩
318
+ const idsParam = req.query?.ids;
319
+ const idsSet = idsParam
320
+ ? new Set(String(idsParam).split(',').map(s => s.trim()).filter(Boolean))
321
+ : null;
322
+ const result = await mutex(`pending:${sessionRoot}`, async () => {
323
+ const { items, file } = await readBuf(sessionRoot);
324
+ const filtered = idsSet ? items.filter(it => !idsSet.has(it.id)) : [];
325
+ await writeBuf(file, filtered);
326
+ return { removed: items.length - filtered.length, count: filtered.length };
327
+ });
328
+
329
+ res.json({ ok: true, ...result });
330
+ } catch (err) { next(err); }
331
+ });
332
+
333
+ export default router;
334
+
335
+ /**
336
+ * Helper for turn.js / agent loop — 同步检查 buffer 是否非空(不加路由)。
337
+ * @returns {Promise<{ count, summary }>}
338
+ * summary 形如 "用户在过去时段做了 3 处变更(2 编辑 + 1 评论)"
339
+ */
340
+ export async function readPendingSummary(sessionRoot) {
341
+ try {
342
+ const { items } = await readBuf(sessionRoot);
343
+ if (items.length === 0) return { count: 0, summary: '' };
344
+ const edits = items.filter(it => it.kind === 'edit').length;
345
+ const comments = items.filter(it => it.kind === 'comment').length;
346
+ const moves = items.filter(it => it.kind === 'pending-move').length;
347
+ const duplicates = items.filter(it => it.kind === 'pending-duplicate').length;
348
+ const styles = items.filter(it => it.kind === 'pending-style').length;
349
+ const deletes = items.filter(it => it.kind === 'pending-delete').length;
350
+ const regions = items.filter(it => it.kind === 'region-comment').length;
351
+ const parts = [];
352
+ if (edits > 0) parts.push(`${edits} 编辑`);
353
+ if (comments > 0) parts.push(`${comments} 评论`);
354
+ if (regions > 0) parts.push(`${regions} 圈选`);
355
+ if (moves > 0) parts.push(`${moves} 拖动`);
356
+ if (duplicates > 0) parts.push(`${duplicates} 复制`);
357
+ if (styles > 0) parts.push(`${styles} 样式`);
358
+ if (deletes > 0) parts.push(`${deletes} 删除`);
359
+ return {
360
+ count: items.length,
361
+ summary: `用户在过去时段做了 ${items.length} 处变更(${parts.join(' + ')})`,
362
+ };
363
+ } catch {
364
+ return { count: 0, summary: '' };
365
+ }
366
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * server/api/plugins.js — Plugin 上传 / 列表 / 卸载(用户级 + project 级 两套 router)
3
+ *
4
+ * 用户级(跨 project 全局,挂在 /api/plugins):
5
+ * GET /api/plugins 列用户级 plugin
6
+ * POST /api/plugins/install 上传 zip → ~/.nodesign/plugins/
7
+ * DELETE /api/plugins/:name 卸载用户级 plugin
8
+ *
9
+ * Project 级(仅当前 project,挂在 /api/projects 跟 assets/turn 同前缀):
10
+ * GET /api/projects/:pid/plugins 列 project 级 plugin
11
+ * POST /api/projects/:pid/plugins/install 上传 zip → <pid>/shared/.claude/plugins/
12
+ * DELETE /api/projects/:pid/plugins/:name 卸载 project 级 plugin
13
+ *
14
+ * 上传流程(两轨同套):
15
+ * 1. multer 收 multipart `file` 字段到 memory buffer
16
+ * 2. validatePluginZip(buffer) — hard-fail 立即 400 + 详细 errors[]
17
+ * 3. 解压到 staging 临时目录(同一 root 下 .staging/<tmpId>/)
18
+ * 4. 目标 plugin name 已存在:没 `?force=true` 返 409 + 当前装的 manifest;
19
+ * 有 force:先 rm -rf 旧目录
20
+ * 5. atomic rename staging → 目标
21
+ * 6. 返 manifest + skills + warnings
22
+ *
23
+ * 安全:validator 已拦 path traversal / 保留 name / 大小 / 格式;extractPluginZip
24
+ * 二次防御解压路径不出 staging dir;DELETE 校验 name 合规 + 不撞保留前缀。
25
+ *
26
+ * 复用:multer memoryStorage pattern from server/api/assets.js:13-26
27
+ */
28
+
29
+ import express from 'express';
30
+ import multer from 'multer';
31
+
32
+ import { validateProjectId, getProject } from '../projects/store.js';
33
+ import { guardProject } from './_guard.js';
34
+ import {
35
+ getUserPluginsRoot,
36
+ getProjectPluginsRoot,
37
+ listInstalledPluginsDetailed,
38
+ } from '../engine/agent/plugin-loader.js';
39
+ import { LIMITS } from '../lib/plugin-validator.js';
40
+ import {
41
+ installPluginToRoot,
42
+ uninstallFromRoot,
43
+ } from '../lib/plugin-install.js';
44
+
45
+ // 允许的上传 mime / 后缀(双轨:单 .md / zip)。multer fileFilter 拦其他类型。
46
+ const ALLOWED_MIME = new Set([
47
+ 'application/zip',
48
+ 'application/x-zip-compressed',
49
+ 'application/octet-stream', // 部分 OS 上 .zip 拿不到准确 mime;按 extension 兜底校验
50
+ 'text/markdown',
51
+ 'text/x-markdown',
52
+ 'text/plain', // 部分 OS 上 .md 报 text/plain
53
+ ]);
54
+ const ALLOWED_EXT_RE = /\.(md|zip)$/i;
55
+
56
+ const upload = multer({
57
+ storage: multer.memoryStorage(),
58
+ limits: { fileSize: LIMITS.ZIP_MAX_BYTES },
59
+ // fileFilter 不 throw error(避免被 express generic error handler 截到返 500),
60
+ // 把 validation 错误存到 req 上 + reject 文件;handler 检查 req.fileValidationError 返 400
61
+ fileFilter: (req, file, cb) => {
62
+ const mimeOk = ALLOWED_MIME.has(file.mimetype);
63
+ const extOk = ALLOWED_EXT_RE.test(file.originalname || '');
64
+ if (mimeOk || extOk) return cb(null, true);
65
+ req.fileValidationError = `不支持的文件类型 (mime=${file.mimetype} name=${file.originalname}) — 仅接受 .md 或 .zip`;
66
+ cb(null, false);
67
+ },
68
+ });
69
+
70
+ /** 统一处理 multer 阶段 reject:fileValidationError 优先于 no-file */
71
+ function rejectInvalidFile(req, res) {
72
+ if (req.fileValidationError) {
73
+ res.status(400).json({ error: req.fileValidationError });
74
+ return true;
75
+ }
76
+ if (!req.file) {
77
+ res.status(400).json({ error: 'no file (field name: file)' });
78
+ return true;
79
+ }
80
+ return false;
81
+ }
82
+
83
+ // ── Router 1:用户级(挂 /api/plugins) ──
84
+ //
85
+ // 2026-07-30 起每个用户一个根(~/.nodesign/plugins/<userId>/)。之前是全站一个
86
+ // 共享目录:任何登录用户装的 plugin 会加载进所有人的 agent 会话,也能删别人的。
87
+ // 这里的 req.user 由 authGuard 挂(登录墙关闭时是 `_anon`),拿不到就 401 —— 没有
88
+ // 身份就没有"用户级"可言,不能退回共享根。
89
+
90
+ export const userPluginsRouter = express.Router();
91
+
92
+ /** 取当前请求者的 plugin 根;没有合法身份时直接回 401 并返 null */
93
+ function userRootOf(req, res) {
94
+ const root = getUserPluginsRoot(req.user?.id);
95
+ if (!root) {
96
+ res.status(401).json({ error: 'unauthorized' });
97
+ return null;
98
+ }
99
+ return root;
100
+ }
101
+
102
+ userPluginsRouter.get('/', async (req, res, next) => {
103
+ try {
104
+ const root = userRootOf(req, res);
105
+ if (!root) return;
106
+ const plugins = await listInstalledPluginsDetailed(root);
107
+ res.json({ plugins });
108
+ } catch (err) { next(err); }
109
+ });
110
+
111
+ userPluginsRouter.post('/install', upload.single('file'), async (req, res, next) => {
112
+ try {
113
+ const root = userRootOf(req, res);
114
+ if (!root) return;
115
+ if (rejectInvalidFile(req, res)) return;
116
+ const force = req.query.force === 'true';
117
+ const result = await installPluginToRoot(req.file.buffer, root, { force });
118
+ res.status(result.status).json(result.body);
119
+ } catch (err) { next(err); }
120
+ });
121
+
122
+ userPluginsRouter.delete('/:name', async (req, res, next) => {
123
+ try {
124
+ const root = userRootOf(req, res);
125
+ if (!root) return;
126
+ const result = await uninstallFromRoot(root, req.params.name);
127
+ res.status(result.status).json(result.body);
128
+ } catch (err) { next(err); }
129
+ });
130
+
131
+ // ── Router 2:project 级(挂 /api/projects,跟 assets/turn 同前缀) ──
132
+
133
+ export const projectPluginsRouter = express.Router();
134
+
135
+ projectPluginsRouter.get('/:pid/plugins', async (req, res, next) => {
136
+ try {
137
+ if (!guardProject(req, res)) return;
138
+ const root = getProjectPluginsRoot(req.params.pid);
139
+ const plugins = await listInstalledPluginsDetailed(root);
140
+ res.json({ plugins });
141
+ } catch (err) { next(err); }
142
+ });
143
+
144
+ projectPluginsRouter.post('/:pid/plugins/install', upload.single('file'), async (req, res, next) => {
145
+ try {
146
+ if (!guardProject(req, res)) return;
147
+ if (rejectInvalidFile(req, res)) return;
148
+ const force = req.query.force === 'true';
149
+ const root = getProjectPluginsRoot(req.params.pid);
150
+ const result = await installPluginToRoot(req.file.buffer, root, { force });
151
+ res.status(result.status).json(result.body);
152
+ } catch (err) { next(err); }
153
+ });
154
+
155
+ projectPluginsRouter.delete('/:pid/plugins/:name', async (req, res, next) => {
156
+ try {
157
+ if (!guardProject(req, res)) return;
158
+ const root = getProjectPluginsRoot(req.params.pid);
159
+ const result = await uninstallFromRoot(root, req.params.name);
160
+ res.status(result.status).json(result.body);
161
+ } catch (err) { next(err); }
162
+ });
163
+
164
+ export default { userPluginsRouter, projectPluginsRouter };