@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,589 @@
1
+ /**
2
+ * server/api/sessions.js — Session CRUD(H3:session-scoped workspace)
3
+ *
4
+ * GET /api/projects/:pid/sessions 列项目所有 session(自实现)
5
+ * GET /api/projects/:pid/sessions/:sid SDK getSessionMessages
6
+ * POST /api/projects/:pid/sessions/:sid/fork SDK forkSession + 复制产物
7
+ * PATCH /api/projects/:pid/sessions/:sid SDK rename + tag
8
+ * DELETE /api/projects/:pid/sessions/:sid SDK deleteSession + 删 session 目录
9
+ *
10
+ * H3 改造:每个 session 独立工作目录 sessions/<sid>/,CLAUDE_CONFIG_DIR
11
+ * per-session(sessions/<sid>/.claude/)。SDK listSessions 按 cwd encoded path
12
+ * 索引 jsonl,跨 session 列要自己 readdir sessions/ 后 per-sid getSessionInfo。
13
+ */
14
+
15
+ import express from 'express';
16
+ import path from 'path';
17
+ import { promises as fs } from 'fs';
18
+ import {
19
+ getSessionInfo,
20
+ getSessionMessages,
21
+ forkSession,
22
+ renameSession,
23
+ tagSession,
24
+ deleteSession,
25
+ query,
26
+ } from '@anthropic-ai/claude-agent-sdk';
27
+ import { validateProjectId, getProject, setActiveSession } from '../projects/store.js';
28
+ import { guardProject, modelUserFor } from './_guard.js';
29
+ import { closeQuerySession, hasActiveQuerySession, getQuerySession } from '../engine/runs/active-runs.js';
30
+ import {
31
+ getProjectWorkspace,
32
+ getWorkspaceRoot,
33
+ getSessionWorkspace,
34
+ ensureSessionWorkspace,
35
+ forkSessionWorkspace,
36
+ removeSessionWorkspace,
37
+ validateSessionId,
38
+ getSessionMetaDir,
39
+ encodeCwdForSDK,
40
+ } from '../projects/workspace.js';
41
+ import { withConfigDir } from '../lib/sdk-session.js';
42
+ import { patchBoard } from '../projects/board-store.js';
43
+ import { platform } from '../runtime/platform.js';
44
+ import { AsyncQueue } from '../lib/async-queue.js';
45
+ import { getProjectBus } from '../ws/broker.js';
46
+ import { getLastContextUsage } from '../engine/runs/live-turn.js';
47
+ import { Events } from '../engine/agent/events.js';
48
+ import { resolveSessionModel, applySessionModel } from '../engine/agent/session-model.js';
49
+ import { selectableModelsFor, allowedModelsFor, isModelLockedFor, defaultModelFor, crossLaneSwitchReason } from '../engine/agent/model-context.js';
50
+
51
+ /**
52
+ * 进行中的 rewind 操作 sid 集合 —— 供 turn.js startNewRunSession 守卫使用,
53
+ * 防止同 sid 临时 rewind query 跟 normal turn query 同时启动撞 jsonl。
54
+ */
55
+ export const pendingRewinds = new Set();
56
+
57
+ const router = express.Router();
58
+
59
+ // SDK session API 需要 CLAUDE_CONFIG_DIR 指向 JSONL 实际存储的全局目录
60
+ // 来自 runtime/platform.js(跨平台决策单一来源)
61
+ const GLOBAL_CLAUDE_CONFIG_DIR = platform.claudeConfigDir;
62
+
63
+ const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
64
+
65
+ /**
66
+ * 列指定 project 的所有 session(按 lastModified 倒序)。共享给:
67
+ * 1. GET /api/projects/:pid/sessions(这文件下面的路由)
68
+ * 2. GET /api/sessions/recent(recent.js 跨项目聚合)
69
+ *
70
+ * 后端实现:readdir sessions/ → 对每个 sid 调 SDK getSessionInfo
71
+ * (per-session CLAUDE_CONFIG_DIR)→ filter null → sort by lastModified。
72
+ *
73
+ * @param {string} pid
74
+ * @returns {Promise<object[]>} sessions 数组(每条至少含 sessionId / lastModified;
75
+ * SDK 还会补 customTitle / summary / firstPrompt / tag 等字段)
76
+ */
77
+ export async function listSessionsForProject(pid) {
78
+ // 会话的落脚点在 2026-08-08 扁平化时搬了家:`<项目>/sessions/<sid>/`(每会话
79
+ // 一个沙盒)→ `<工作区>/.nd/<sid>/`(只剩私档,cwd 是工作区本身)。
80
+ //
81
+ // ⚠️ 这里漏改过一次,后果是**迁移之后会话列表永远为空** —— 界面上历史对话
82
+ // 全部消失(数据没丢,`.nd/` 和转录都在,只是没人去列)。所以两处都读:
83
+ // 迁移过的看 `.nd/`,没迁移的看老的 `sessions/`。
84
+ const workspaceRoot = getWorkspaceRoot(pid);
85
+ const readSids = async (dir) => {
86
+ try {
87
+ return (await fs.readdir(dir, { withFileTypes: true }))
88
+ .filter(e => e.isDirectory() && SESSION_ID_RE.test(e.name)).map(e => e.name);
89
+ } catch (err) {
90
+ if (err.code === 'ENOENT') return [];
91
+ throw err;
92
+ }
93
+ };
94
+ const sids = [...new Set([
95
+ ...await readSids(path.join(workspaceRoot, '.nd')),
96
+ ...await readSids(path.join(getProjectWorkspace(pid), 'sessions')),
97
+ ])];
98
+ const results = await Promise.all(sids.map(async (sid) => {
99
+ // 转录按 **cwd** 编码定位,而 cwd 现在就是工作区(getSessionWorkspace 也返回它)
100
+ const sessionRoot = workspaceRoot;
101
+ try {
102
+ const info = await withConfigDir(GLOBAL_CLAUDE_CONFIG_DIR, () =>
103
+ getSessionInfo(sid, { dir: sessionRoot }),
104
+ );
105
+ return info || null;
106
+ } catch (err) {
107
+ console.warn(`[sessions list] ${sid.slice(0, 8)} info failed:`, err.message);
108
+ return null;
109
+ }
110
+ }));
111
+ return results
112
+ .filter(Boolean)
113
+ .sort((a, b) => (b.lastModified || 0) - (a.lastModified || 0));
114
+ }
115
+
116
+ // ── List:自实现(readdir sessions/ + per-sid getSessionInfo)──
117
+ router.get('/:pid/sessions', async (req, res, next) => {
118
+ try {
119
+ const project = guardProject(req, res);
120
+ if (!project) return;
121
+
122
+ const all = await listSessionsForProject(req.params.pid);
123
+
124
+ const limit = req.query.limit ? Math.min(Number(req.query.limit), 200) : all.length;
125
+ const offset = req.query.offset ? Number(req.query.offset) : 0;
126
+ const sessions = all.slice(offset, offset + limit);
127
+
128
+ res.json({ sessions });
129
+ } catch (err) { next(err); }
130
+ });
131
+
132
+ // ── Read:单 session messages ──
133
+ router.get('/:pid/sessions/:sid', async (req, res, next) => {
134
+ try {
135
+ validateSessionId(req.params.sid);
136
+ const project = guardProject(req, res);
137
+ if (!project) return;
138
+
139
+ const sessionRoot = getSessionWorkspace(req.params.pid, req.params.sid);
140
+
141
+ const includeSystemMessages = req.query.includeSystem === '1';
142
+
143
+ const messages = await withConfigDir(GLOBAL_CLAUDE_CONFIG_DIR, () =>
144
+ getSessionMessages(req.params.sid, {
145
+ dir: sessionRoot,
146
+ includeSystemMessages,
147
+ }),
148
+ );
149
+ res.json({ messages });
150
+ } catch (err) { next(err); }
151
+ });
152
+
153
+ /**
154
+ * ── 上下文用量(按需查询)──
155
+ *
156
+ * run.context_usage 是 turn 内推的,turn 一结束前端就只剩一个空值。可用户想看
157
+ * "现在装了多少、要不要压缩"恰恰是在两轮之间。composer 的 [+] 菜单展开时打这条。
158
+ *
159
+ * 两个来源,优先级从高到低:
160
+ * 1. query 还活着 → 直接向 SDK 现问(streamInput 模式下 query 在 turn 之间不死),
161
+ * 这是权威值
162
+ * 2. query 已经没了 → 内存里记着的最后一次事件,标 live:false 让前端说明白
163
+ * 都没有 → 204,前端显示"还没开始对话"。
164
+ */
165
+ router.get('/:pid/sessions/:sid/context-usage', async (req, res, next) => {
166
+ try {
167
+ validateSessionId(req.params.sid);
168
+ const project = guardProject(req, res);
169
+ if (!project) return;
170
+
171
+ const sid = req.params.sid;
172
+ const qs = getQuerySession(sid);
173
+ if (typeof qs?.query?.getContextUsage === 'function') {
174
+ try {
175
+ const usage = await qs.query.getContextUsage();
176
+ // appModel 决定分母(真实容量 vs SDK 的 compact 触发线),跟 turn 内推的
177
+ // 事件走同一个构造函数,前端拿到的两份数据形状一致。
178
+ // 模型从 session-config 现读 —— 原来这里问的是 querySession.ctx?.appModel,
179
+ // 而那个 ctx 字段从注册起就是 null 且无人填写,那一支永远走不到,分母只能
180
+ // 掉回 SDK 的 compact 触发线,同一个会话两次读数对不上。
181
+ const { model: appModel } = await resolveSessionModel(getSessionMetaDir(req.params.pid, sid));
182
+ if (usage) return res.json({ ...Events.contextUsage(usage, appModel), live: true });
183
+ } catch (err) {
184
+ // SDK 拒答不算错(query 正在收尾等)—— 掉到记忆值上,别把菜单打成红的
185
+ console.warn(`[sessions] getContextUsage failed sid=${sid.slice(0, 8)}: ${err.message}`);
186
+ }
187
+ }
188
+
189
+ const remembered = getLastContextUsage(sid);
190
+ if (remembered) return res.json({ ...remembered, live: false });
191
+ return res.status(204).end();
192
+ } catch (err) { next(err); }
193
+ });
194
+
195
+ /**
196
+ * ── 会话模型 ──
197
+ *
198
+ * GET → { model, override, default, options }
199
+ * model = 这个会话实际会跑的(session-config 的覆盖,没有就是全局默认)
200
+ * override = 用户在这个会话里选过的;null 表示「跟随默认」
201
+ * options = 可选清单,来自 model-context.js 那两张映射表旁边 ——
202
+ * 前端不再自己硬编码 id,写错一个字只会静默降级没人报错
203
+ * PUT → body { model: string | null },null = 清掉覆盖回到默认
204
+ *
205
+ * 为什么单独开一条而不复用 PATCH /config:改模型不只是写字段,还得让**已经跑着的
206
+ * query 认账**(空闲时关掉,下条消息以新模型 resume)。这两步分开过一次,结果是
207
+ * 配置说一套、进程跑另一套。
208
+ */
209
+ router.get('/:pid/sessions/:sid/model', async (req, res, next) => {
210
+ try {
211
+ validateSessionId(req.params.sid);
212
+ const project = guardProject(req, res);
213
+ if (!project) return;
214
+ const { model, override, fallback } = await resolveSessionModel(
215
+ getSessionMetaDir(req.params.pid, req.params.sid),
216
+ );
217
+ // default 按**项目 owner** 算(08-21;_guard.modelUserFor):公开注册号的默认不是环境变量里的订阅行;
218
+ // 没覆盖时按钮上显示的就是它。admin 代看 basic 项目时清单也按 owner(订阅行 locked),跟 turn.js 一致
219
+ const modelUser = modelUserFor(req, project);
220
+ const userDefault = defaultModelFor(modelUser) || fallback;
221
+ res.json({ model: override || userDefault, override, default: userDefault, options: selectableModelsFor(modelUser) });
222
+ } catch (err) { next(err); }
223
+ });
224
+
225
+ router.put('/:pid/sessions/:sid/model', async (req, res, next) => {
226
+ try {
227
+ validateSessionId(req.params.sid);
228
+ const project = guardProject(req, res);
229
+ if (!project) return;
230
+
231
+ const raw = req.body?.model;
232
+ if (raw !== null && typeof raw !== 'string') {
233
+ return res.status(400).json({ error: 'model must be a string or null' });
234
+ }
235
+ // 只收清单里的 id:随手传个拼错的 model 进去,SDK 会自己 fallback、真实容量
236
+ // 查不到,两处都不报错,事后只能从"怎么变慢了"倒推
237
+ const modelUser = modelUserFor(req, project); // 资格按项目 owner 算(_guard.js)
238
+ if (raw !== null && isModelLockedFor(modelUser, raw)) {
239
+ return res.status(403).json({ error: '这个模型仅限 Pro 档,暂未对外开放', code: 'MODEL_LOCKED', model: raw });
240
+ }
241
+ if (typeof raw === 'string' && !allowedModelsFor(modelUser).some((m) => m.id === raw)) {
242
+ return res.status(400).json({ error: `unknown model: ${raw}`, code: 'UNKNOWN_MODEL' });
243
+ }
244
+
245
+ await ensureSessionWorkspace(req.params.pid, req.params.sid);
246
+ const metaDir = getSessionMetaDir(req.params.pid, req.params.sid);
247
+ const result = await applySessionModel(req.params.sid, metaDir, raw, 'picker');
248
+ const { fallback, model: currentModel } = await resolveSessionModel(metaDir);
249
+ if (raw !== null) {
250
+ const why = crossLaneSwitchReason(currentModel, raw);
251
+ if (why) return res.status(409).json({ error: why, code: 'LANE_SWITCH' });
252
+ }
253
+ res.json({
254
+ model: result.model,
255
+ override: result.override,
256
+ default: fallback,
257
+ changed: result.changed,
258
+ restarted: result.restarted,
259
+ options: selectableModelsFor(req.user),
260
+ });
261
+ } catch (err) { next(err); }
262
+ });
263
+
264
+ // ── Fork:SDK fork + 复制产物 + mv jsonl 到新 session 子目录 ──
265
+ router.post('/:pid/sessions/:sid/fork', async (req, res, next) => {
266
+ try {
267
+ validateSessionId(req.params.sid);
268
+ const project = guardProject(req, res);
269
+ if (!project) return;
270
+
271
+ const srcSid = req.params.sid;
272
+ const srcSessionRoot = getSessionWorkspace(req.params.pid, srcSid);
273
+ const { upToMessageId, title } = req.body || {};
274
+
275
+ // 1. SDK fork —— 在 GLOBAL_CLAUDE_CONFIG_DIR 下生成新 sid 的 jsonl
276
+ const result = await withConfigDir(GLOBAL_CLAUDE_CONFIG_DIR, () =>
277
+ forkSession(srcSid, { dir: srcSessionRoot, upToMessageId, title }),
278
+ );
279
+ const newSid = result.sessionId;
280
+ validateSessionId(newSid);
281
+
282
+ // 2. 备好新会话的私档目录(不再复制任何产物 —— 分叉的是对话,不是工作区)
283
+ await forkSessionWorkspace(req.params.pid, srcSid, newSid);
284
+
285
+ // 3. jsonl 归位。
286
+ //
287
+ // 扁平化之后同一个项目的所有会话共用一个 cwd,encoded 目录因此**完全相同**,
288
+ // SDK fork 出来的 newSid.jsonl 一落地就已经在对的位置了。这一整段搬运
289
+ // (含"换个 encoded 目录再找一遍"的兜底)只对旧数据还有意义:那时每个
290
+ // 会话一个 cwd,fork 出来的 jsonl 落在**源会话**的目录里,不搬就找不到。
291
+ const srcEncoded = encodeCwdForSDK(srcSessionRoot);
292
+ const newSessionRoot = getSessionWorkspace(req.params.pid, newSid);
293
+ const newEncoded = encodeCwdForSDK(newSessionRoot);
294
+ if (srcEncoded !== newEncoded) {
295
+ const srcJsonl = path.join(GLOBAL_CLAUDE_CONFIG_DIR, 'projects', srcEncoded, `${newSid}.jsonl`);
296
+ const newJsonlDir = path.join(GLOBAL_CLAUDE_CONFIG_DIR, 'projects', newEncoded);
297
+ const newJsonl = path.join(newJsonlDir, `${newSid}.jsonl`);
298
+ await fs.mkdir(newJsonlDir, { recursive: true });
299
+ try {
300
+ await fs.rename(srcJsonl, newJsonl);
301
+ } catch (err) {
302
+ console.warn(`[fork] rename ${srcJsonl} → ${newJsonl} failed (${err.code}); searching alt encoded dir`);
303
+ const altParent = path.join(GLOBAL_CLAUDE_CONFIG_DIR, 'projects');
304
+ const pidPrefix = encodeCwdForSDK(getProjectWorkspace(req.params.pid));
305
+ const altSubs = (await fs.readdir(altParent).catch(() => []))
306
+ .filter(sub => sub.startsWith(pidPrefix));
307
+ for (const sub of altSubs) {
308
+ const candidate = path.join(altParent, sub, `${newSid}.jsonl`);
309
+ try {
310
+ await fs.access(candidate);
311
+ await fs.rename(candidate, newJsonl);
312
+ break;
313
+ } catch { /* continue */ }
314
+ }
315
+ }
316
+ }
317
+
318
+ res.json({ sessionId: newSid });
319
+ } catch (err) { next(err); }
320
+ });
321
+
322
+ // ── PATCH:rename / tag ──
323
+ router.patch('/:pid/sessions/:sid', async (req, res, next) => {
324
+ try {
325
+ validateSessionId(req.params.sid);
326
+ const project = guardProject(req, res);
327
+ if (!project) return;
328
+
329
+ const sessionRoot = getSessionWorkspace(req.params.pid, req.params.sid);
330
+ const { title, tag } = req.body || {};
331
+
332
+ if (typeof title === 'string') {
333
+ if (title.length > 200) return res.status(400).json({ error: 'title too long (max 200)' });
334
+ await withConfigDir(GLOBAL_CLAUDE_CONFIG_DIR, () =>
335
+ renameSession(req.params.sid, title, { dir: sessionRoot }),
336
+ );
337
+ }
338
+ if ('tag' in (req.body || {})) {
339
+ if (tag !== null && typeof tag !== 'string') {
340
+ return res.status(400).json({ error: 'tag must be string or null' });
341
+ }
342
+ if (typeof tag === 'string' && tag.length > 50) {
343
+ return res.status(400).json({ error: 'tag too long (max 50)' });
344
+ }
345
+ await withConfigDir(GLOBAL_CLAUDE_CONFIG_DIR, () =>
346
+ tagSession(req.params.sid, tag, { dir: sessionRoot }),
347
+ );
348
+ }
349
+ res.json({ ok: true });
350
+ } catch (err) { next(err); }
351
+ });
352
+
353
+ // ── Close:终结活跃 query session(streamInput 模式)──
354
+ // POST /api/projects/:pid/sessions/:sid/close
355
+ // 关掉 inputQueue → runSession for-await-of 自然退出 → query 进程死。
356
+ // 下次 turn 该 sid 起新 runSession(resume 旧 jsonl)。
357
+ // 200 { ok: true, wasActive }
358
+ router.post('/:pid/sessions/:sid/close', async (req, res, next) => {
359
+ try {
360
+ validateSessionId(req.params.sid);
361
+ const project = guardProject(req, res);
362
+ if (!project) return;
363
+ const wasActive = hasActiveQuerySession(req.params.sid);
364
+ if (wasActive) closeQuerySession(req.params.sid, 'user_close');
365
+ res.json({ ok: true, wasActive });
366
+ } catch (err) { next(err); }
367
+ });
368
+
369
+ // ── DELETE:SDK 删 jsonl + rm session 目录(产物 / git) ──
370
+ router.delete('/:pid/sessions/:sid', async (req, res, next) => {
371
+ try {
372
+ validateSessionId(req.params.sid);
373
+ const project = guardProject(req, res);
374
+ if (!project) return;
375
+
376
+ const sessionRoot = getSessionWorkspace(req.params.pid, req.params.sid);
377
+
378
+ // 1. SDK delete jsonl(从全局 CLAUDE_CONFIG_DIR 删除)
379
+ try {
380
+ await withConfigDir(GLOBAL_CLAUDE_CONFIG_DIR, () =>
381
+ deleteSession(req.params.sid, { dir: sessionRoot }),
382
+ );
383
+ } catch (err) {
384
+ // 如果 jsonl 已经不存在或 SDK 找不到,silent skip — 后面 rm 整个目录兜底
385
+ console.warn(`[delete session] SDK delete failed (${err.message}); proceeding to rm dir`);
386
+ }
387
+
388
+ // 2. 删这条会话的私档(`.nd/<sid>/`)。
389
+ //
390
+ // ⚠️ 这里以前是 `rm -rf sessions/<sid>/`(产物 + git + 软链),后面还跟着
391
+ // 一步"连带删掉绑定的任务文件夹"。**删对话现在绝不能碰产物** —— 产物属于
392
+ // 项目,同一个项目里换条对话继续做是常态。
393
+ await removeSessionWorkspace(req.params.pid, req.params.sid);
394
+
395
+ // 3. 清 active_session_id 如果指向被删的。**要广播** —— 指针是会话真相源
396
+ // (2026-08-13 收敛),别的标签页不知道指针被清就会继续往死会话里发。
397
+ // 为什么这条事件不带 sessionId 字段:见 events.js projectActiveSession 注释。
398
+ if (project.activeSessionId === req.params.sid) {
399
+ try {
400
+ setActiveSession(req.params.pid, null);
401
+ getProjectBus(req.params.pid).publish(Events.projectActiveSession(null));
402
+ } catch { /* ignore */ }
403
+ }
404
+
405
+ res.status(204).end();
406
+ } catch (err) { next(err); }
407
+ });
408
+
409
+
410
+ // ── POST /:pid/sessions/:sid/rewind ──
411
+ // SDK Query.rewindFiles(userMessageId) 控制方法 —— 把 session 内文件回滚到该
412
+ // user message 之前的状态(后续 Edit/Write 全撤销)。SDK file checkpoint 写在
413
+ // session jsonl(type='file-history-snapshot'),跨进程持久化天然搞定。
414
+ //
415
+ // 两条路径:
416
+ // 1. active query 在跑 → 直接用现有 query.rewindFiles(最快,无 spawn 成本)
417
+ // 2. session 已 close(历史 session)→ 起临时 query (resume + drain) → rewindFiles → close
418
+ //
419
+ // 历史 session 也能 undo —— 之前返 410 是应用层偷懒,SDK 完全支持 resume + rewindFiles。
420
+ //
421
+ // body: { userMessageId }
422
+ // 200 { canRewind, filesChanged?, insertions?, deletions? }
423
+ // 404 { code: 'JSONL_MISSING' } jsonl 不存在(session 删了 / 部分创建)
424
+ // 409 { code: 'REWIND_BUSY' } 同 sid 已有 rewind 进行中
425
+ // 500 { code: 'REWIND_FAILED' } 临时 query 启动 / rewindFiles 失败
426
+ router.post('/:pid/sessions/:sid/rewind', async (req, res, next) => {
427
+ try {
428
+ validateSessionId(req.params.sid);
429
+ const project = guardProject(req, res);
430
+ if (!project) return;
431
+
432
+ const { userMessageId } = req.body || {};
433
+ if (!userMessageId || typeof userMessageId !== 'string') {
434
+ return res.status(400).json({ error: 'userMessageId required' });
435
+ }
436
+
437
+ const { pid, sid } = req.params;
438
+
439
+ // 路径 1:active query 在跑 —— 直接用现有 query
440
+ const rec = getQuerySession(sid);
441
+ if (rec?.query && !rec.abortController.signal.aborted) {
442
+ const result = await rec.query.rewindFiles(userMessageId);
443
+ // 对话层同步回滚(2026-08-08「做完整」):rewindFiles 只回文件。显示与模型
444
+ // 记忆读的都是这份 jsonl —— 关掉活口 query(下条消息从截断后的 jsonl resume,
445
+ // 记忆才真的回退),等 SDK flush 后把 jsonl 截到该 user 消息之前。
446
+ try { closeQuerySession(sid, 'rewind_truncate'); } catch { /* */ }
447
+ await new Promise((r) => setTimeout(r, 800));
448
+ const removed = await truncateJsonlAtMessage(getSessionWorkspace(pid, sid), sid, userMessageId);
449
+ const payload = { ...result, conversationTruncated: removed != null, removedEntries: removed ?? 0 };
450
+ emitRewindFiles(pid, sid, payload);
451
+ return res.json(payload);
452
+ }
453
+
454
+ // race guard:active session 存在但 query handle 未 attach(session 启动中
455
+ // 的窄 race window — registerQuerySession 已 set Map 但 attachSessionQuery
456
+ // 还没赋值 query 字段)→ 拒 409 让用户重试。如果直接 fallthrough 进路径 2
457
+ // 起临时 query,两个 SDK binary 会同时 attach 同一 jsonl 文件 → 错乱不可恢复。
458
+ if (hasActiveQuerySession(sid) && rec && !rec.abortController.signal.aborted) {
459
+ return res.status(409).json({
460
+ error: 'session is starting (query handle not yet attached), retry shortly',
461
+ code: 'SESSION_STARTING',
462
+ });
463
+ }
464
+
465
+ // 路径 2:起临时 query resume → rewindFiles → close
466
+ if (pendingRewinds.has(sid)) {
467
+ return res.status(409).json({ error: 'rewind in progress', code: 'REWIND_BUSY' });
468
+ }
469
+ const sessionRoot = getSessionWorkspace(pid, sid);
470
+ if (!await jsonlExistsForSession(sessionRoot, sid)) {
471
+ return res.status(404).json({ error: 'session jsonl not found', code: 'JSONL_MISSING' });
472
+ }
473
+
474
+ pendingRewinds.add(sid);
475
+ const inputQueue = new AsyncQueue();
476
+ let tempQuery = null;
477
+ let drain = null;
478
+ try {
479
+ tempQuery = query({
480
+ prompt: inputQueue,
481
+ options: {
482
+ resume: sid,
483
+ enableFileCheckpointing: true,
484
+ cwd: sessionRoot,
485
+ // 关键:跟 runSession 一致传 CLAUDE_CONFIG_DIR,否则 SDK 找不到 jsonl
486
+ env: { ...process.env, CLAUDE_CONFIG_DIR: GLOBAL_CLAUDE_CONFIG_DIR },
487
+ persistSession: true,
488
+ // 不传 hooks / mcpServers / agents / canUseTool —— 临时 query 不跑 turn
489
+ },
490
+ });
491
+ // fire-and-forget consume —— SDK control method 走 bidirectional protocol,
492
+ // stream 不消费会卡死 control RPC。drain 跑在后台,close 后自然结束。
493
+ drain = (async () => {
494
+ try { for await (const _ of tempQuery) { /* discard */ } }
495
+ catch { /* expected on close */ }
496
+ })();
497
+ // 15s timeout(SDK boot + jsonl load + control RPC ~3-5s 正常 → 3× margin)
498
+ const result = await Promise.race([
499
+ tempQuery.rewindFiles(userMessageId),
500
+ new Promise((_, rj) => setTimeout(() => rj(new Error('rewind timeout')), 15000)),
501
+ ]);
502
+ // 对话层回滚:先收干净临时 query(文件句柄/尾部 flush),再截断 jsonl
503
+ try { tempQuery.close(); } catch { /* */ }
504
+ try { inputQueue.close(); } catch { /* */ }
505
+ if (drain) { try { await drain; } catch { /* */ } }
506
+ tempQuery = null; drain = null;
507
+ await new Promise((r) => setTimeout(r, 300));
508
+ const removed = await truncateJsonlAtMessage(sessionRoot, sid, userMessageId);
509
+ const payload = { ...result, conversationTruncated: removed != null, removedEntries: removed ?? 0 };
510
+ emitRewindFiles(pid, sid, payload);
511
+ res.json(payload);
512
+ } catch (err) {
513
+ console.warn(`[sessions.rewind] temp query failed (sid=${sid.slice(0, 8)}): ${err.message}`);
514
+ res.status(500).json({ error: err.message, code: 'REWIND_FAILED' });
515
+ } finally {
516
+ try { tempQuery?.close(); } catch { /* ignore */ }
517
+ try { inputQueue.close(); } catch { /* ignore */ }
518
+ if (drain) { try { await drain; } catch { /* ignore */ } }
519
+ pendingRewinds.delete(sid);
520
+ }
521
+ } catch (err) { next(err); }
522
+ });
523
+
524
+ /**
525
+ * 检查 SDK jsonl 是否存在 —— 历史 session 在 ~/.claude/projects/<encoded-cwd>/<sid>.jsonl。
526
+ * encodeCwdForSDK + GLOBAL_CLAUDE_CONFIG_DIR 都已在文件顶部定义。
527
+ */
528
+ /**
529
+ * 对话层回滚(2026-08-08):把 jsonl 截断到 userMessageId 那条之前(含它与其后全部)。
530
+ * jsonl 是追加式日志,截到 prefix = 它历史上真实存在过的状态,resume 天然自洽;
531
+ * 之后的 file-history-snapshot 属于被撤销的编辑,一并丢弃是正确语义。
532
+ * 原子写(tmp+rename)。找不到该 uuid 返 null(fail-soft:文件回滚仍算成功)。
533
+ */
534
+ async function truncateJsonlAtMessage(sessionRoot, sid, userMessageId) {
535
+ const jsonlPath = path.join(GLOBAL_CLAUDE_CONFIG_DIR, 'projects', encodeCwdForSDK(sessionRoot), `${sid}.jsonl`);
536
+ try {
537
+ const raw = await fs.readFile(jsonlPath, 'utf8');
538
+ const lines = raw.split('\n');
539
+ let cut = -1;
540
+ for (let i = 0; i < lines.length; i++) {
541
+ if (!lines[i] || !lines[i].includes(userMessageId)) continue;
542
+ try { if (JSON.parse(lines[i]).uuid === userMessageId) { cut = i; break; } } catch { /* 非 JSON 行跳过 */ }
543
+ }
544
+ if (cut < 0) {
545
+ console.warn(`[sessions.rewind] uuid ${userMessageId.slice(0, 8)} 不在 jsonl 里,跳过对话截断`);
546
+ return null;
547
+ }
548
+ const kept = lines.slice(0, cut).join('\n');
549
+ const tmp = `${jsonlPath}.tmp-rewind`;
550
+ await fs.writeFile(tmp, kept ? `${kept}\n` : '');
551
+ await fs.rename(tmp, jsonlPath);
552
+ return lines.filter(Boolean).length - lines.slice(0, cut).filter(Boolean).length;
553
+ } catch (err) {
554
+ console.warn(`[sessions.rewind] jsonl 截断失败(不影响文件回滚):${err.message}`);
555
+ return null;
556
+ }
557
+ }
558
+
559
+ async function jsonlExistsForSession(sessionRoot, sid) {
560
+ const encoded = encodeCwdForSDK(sessionRoot);
561
+ const jsonlPath = path.join(GLOBAL_CLAUDE_CONFIG_DIR, 'projects', encoded, `${sid}.jsonl`);
562
+ try {
563
+ await fs.access(jsonlPath);
564
+ return true;
565
+ } catch {
566
+ return false;
567
+ }
568
+ }
569
+
570
+ /**
571
+ * rewindFiles 成功后 emit run.file_changed 事件让前端 iframe 自动 reload。
572
+ * 复用现有 event 类型 —— ProjectWorkspace.jsx 已 case 它(仅 .html 后缀 bump reloadToken),
573
+ * 0 前端事件代码改动。
574
+ */
575
+ function emitRewindFiles(pid, sid, result) {
576
+ if (!result?.canRewind || !Array.isArray(result.filesChanged) || !result.filesChanged.length) return;
577
+ const bus = getProjectBus(pid);
578
+ for (const filePath of result.filesChanged) {
579
+ bus.publish({
580
+ type: 'run.file_changed',
581
+ filePath,
582
+ event: 'change',
583
+ sessionId: sid,
584
+ ts: new Date().toISOString(),
585
+ });
586
+ }
587
+ }
588
+
589
+ export default router;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * server/api/skills.js — Skill 列表(plugin-aware)
3
+ *
4
+ * GET /api/skills 列内置 + 用户级 plugin(不区分 project)
5
+ * GET /api/skills?projectId=xxx 上面 + 加 project 级 plugin 内的 skills(拍平到 projectLocal)
6
+ *
7
+ * 2026-05-18 schema 改造(plugin convention):
8
+ * {
9
+ * plugins: [
10
+ * { name, version, description, scope: 'builtin' | 'user', path, skills: [...] },
11
+ * ...
12
+ * ],
13
+ * projectLocal: [...] // project 级 plugin 内的 skills 拍平,scope: 'project'
14
+ * }
15
+ *
16
+ * 数据源(plugin-loader.js 已经做了 scan + manifest 解析):
17
+ * - builtin: server/engine/plugins/nodesign/
18
+ * - user: <userHome>/.nodesign/plugins/*\/
19
+ * - project: <pid>/shared/.claude/plugins/*\/
20
+ *
21
+ * P0 只读不写;plugin 安装/卸载在 /api/plugins 和 /api/projects/:pid/plugins。
22
+ */
23
+
24
+ import express from 'express';
25
+
26
+ import { validateProjectId, getProject } from '../projects/store.js';
27
+ import {
28
+ getUserPluginsRoot,
29
+ getBuiltinPluginsRoot,
30
+ getProjectPluginsRoot,
31
+ listInstalledPluginsDetailed,
32
+ } from '../engine/agent/plugin-loader.js';
33
+
34
+ const router = express.Router();
35
+
36
+ router.get('/', async (req, res, next) => {
37
+ try {
38
+ // 用户级只列**自己的**(2026-07-30 分用户根);没身份就当没有
39
+ const userRoot = getUserPluginsRoot(req.user?.id);
40
+ const [builtin, user] = await Promise.all([
41
+ listInstalledPluginsDetailed(getBuiltinPluginsRoot()),
42
+ userRoot ? listInstalledPluginsDetailed(userRoot) : Promise.resolve([]),
43
+ ]);
44
+
45
+ const plugins = [
46
+ ...builtin.map(p => ({ ...p, scope: 'builtin' })),
47
+ ...user.map(p => ({ ...p, scope: 'user' })),
48
+ ];
49
+
50
+ let projectLocal = [];
51
+ const pid = req.query.projectId;
52
+ if (pid && typeof pid === 'string') {
53
+ try {
54
+ validateProjectId(pid);
55
+ const proj = getProject(pid);
56
+ // 多用户(2026-07-30):project 级 skill 只给项目归属人/admin 看
57
+ if (proj && (req.user?.role === 'admin' || proj.ownerId === req.user?.id)) {
58
+ const projectPlugins = await listInstalledPluginsDetailed(getProjectPluginsRoot(pid));
59
+ // 拍平 project 级 skills(保留 plugin name 追溯)
60
+ for (const p of projectPlugins) {
61
+ for (const s of p.skills) {
62
+ projectLocal.push({
63
+ ...s,
64
+ scope: 'project',
65
+ pluginName: p.name,
66
+ });
67
+ }
68
+ }
69
+ }
70
+ } catch { /* invalid pid → 静默忽略 */ }
71
+ }
72
+
73
+ res.json({ plugins, projectLocal });
74
+ } catch (err) { next(err); }
75
+ });
76
+
77
+ export default router;