@xiaobuyu/nodesign 0.0.1 → 0.0.3

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 (358) hide show
  1. package/README.md +156 -106
  2. package/bin/nodesign.js +0 -0
  3. package/package.json +1 -1
  4. package/server/api/README.md +45 -45
  5. package/server/api/_guard.js +75 -75
  6. package/server/api/admin.js +187 -187
  7. package/server/api/assets/docx-page.js +103 -103
  8. package/server/api/assets/helpers.js +42 -22
  9. package/server/api/assets/notes.js +194 -144
  10. package/server/api/assets.js +891 -896
  11. package/server/api/board.js +134 -57
  12. package/server/api/browse.js +138 -138
  13. package/server/api/canvas.js +444 -444
  14. package/server/api/chatai.js +324 -324
  15. package/server/api/exports/build-standalone.js +980 -980
  16. package/server/api/exports/cards.js +118 -118
  17. package/server/api/exports/docx-pdf.js +46 -46
  18. package/server/api/exports/export-page.js +142 -142
  19. package/server/api/exports/handoff.js +147 -147
  20. package/server/api/exports.js +690 -690
  21. package/server/api/instruction.js +75 -75
  22. package/server/api/me.js +116 -116
  23. package/server/api/memory.js +185 -185
  24. package/server/api/pending-changes.js +366 -366
  25. package/server/api/plugins.js +164 -164
  26. package/server/api/projects.js +220 -220
  27. package/server/api/publish.js +64 -64
  28. package/server/api/recent.js +73 -73
  29. package/server/api/sessions.js +589 -589
  30. package/server/api/skills.js +77 -77
  31. package/server/api/standalone-fit.js +168 -168
  32. package/server/api/turn-compose.js +180 -180
  33. package/server/api/turn-inflight.js +44 -44
  34. package/server/auth/README.md +17 -17
  35. package/server/auth/middleware.js +138 -138
  36. package/server/auth/origin-guard.js +71 -71
  37. package/server/auth/session.js +108 -108
  38. package/server/auth/tier.js +113 -113
  39. package/server/auth/users-store.js +348 -348
  40. package/server/edit/README.md +29 -29
  41. package/server/engine/README.md +113 -113
  42. package/server/engine/agent/agent-shared.js +550 -550
  43. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
  44. package/server/engine/agent/auto-mode-rules.js +78 -78
  45. package/server/engine/agent/context.js +418 -418
  46. package/server/engine/agent/events.js +465 -464
  47. package/server/engine/agent/hooks/canvas-validate.js +199 -199
  48. package/server/engine/agent/hooks/failure.js +120 -120
  49. package/server/engine/agent/hooks/file-events.js +75 -75
  50. package/server/engine/agent/hooks/lifecycle.js +233 -233
  51. package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
  52. package/server/engine/agent/hooks/post-guidance.js +206 -206
  53. package/server/engine/agent/hooks/post-subagent-report.js +104 -104
  54. package/server/engine/agent/hooks/post-trim.js +58 -58
  55. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
  56. package/server/engine/agent/hooks/pre-defaults.js +84 -84
  57. package/server/engine/agent/hooks/pre-injectors.js +277 -277
  58. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
  59. package/server/engine/agent/hooks/pre-starter-files.js +61 -61
  60. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
  61. package/server/engine/agent/hooks/site-validate.js +94 -94
  62. package/server/engine/agent/hooks/tool-prompts.js +31 -31
  63. package/server/engine/agent/hooks/turn-state-memory.js +56 -56
  64. package/server/engine/agent/hooks/user-prompt-submit.js +258 -215
  65. package/server/engine/agent/hooks.js +354 -354
  66. package/server/engine/agent/init-contract.js +69 -69
  67. package/server/engine/agent/isolation.js +159 -159
  68. package/server/engine/agent/model-table.js +292 -292
  69. package/server/engine/agent/plugin-loader.js +266 -266
  70. package/server/engine/agent/prompts/nodesign-prelude.md +388 -336
  71. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
  72. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
  73. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
  74. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
  75. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
  76. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
  77. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
  78. package/server/engine/agent/prompts/tools/site-reference.md +106 -106
  79. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
  80. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
  81. package/server/engine/agent/session-loop.js +1035 -1024
  82. package/server/engine/agent/session-model.js +164 -164
  83. package/server/engine/agent/skill.js +204 -204
  84. package/server/engine/agent/task-events.js +78 -78
  85. package/server/engine/agents/ds-extractor.md +132 -132
  86. package/server/engine/agents/explorer.md +190 -190
  87. package/server/engine/agents/index.js +230 -230
  88. package/server/engine/agents/schemas/design-system.json +156 -156
  89. package/server/engine/agents/schemas/tweak-schema.json +113 -113
  90. package/server/engine/agents/tweak-proposer.md +127 -127
  91. package/server/engine/agents/vision-checker.md +254 -254
  92. package/server/engine/browse/capture.js +422 -422
  93. package/server/engine/browse/card.js +137 -137
  94. package/server/engine/browse/handover.js +59 -59
  95. package/server/engine/browse/page-digest.js +119 -119
  96. package/server/engine/browse/refs.js +196 -196
  97. package/server/engine/browse/registry.js +307 -307
  98. package/server/engine/browse/screencast.js +170 -170
  99. package/server/engine/browse/state.js +121 -121
  100. package/server/engine/chatai/chat-log.js +79 -79
  101. package/server/engine/chatai/index.js +193 -193
  102. package/server/engine/chatai/openai-compat.js +152 -152
  103. package/server/engine/chatai/orchestrate.js +309 -309
  104. package/server/engine/chatai/perform.js +40 -40
  105. package/server/engine/chatai/summarize.js +79 -79
  106. package/server/engine/mcp/capability-gate.js +52 -52
  107. package/server/engine/mcp/index.js +351 -339
  108. package/server/engine/mcp/param-sanitizer.js +142 -142
  109. package/server/engine/mcp/tools/arrange-on-board.js +124 -124
  110. package/server/engine/mcp/tools/artifact-session.js +278 -278
  111. package/server/engine/mcp/tools/browse-computer.js +421 -421
  112. package/server/engine/mcp/tools/browse-find-batch.js +183 -183
  113. package/server/engine/mcp/tools/browse-screenshot.js +157 -157
  114. package/server/engine/mcp/tools/browse.js +480 -480
  115. package/server/engine/mcp/tools/build-docx.js +101 -101
  116. package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
  117. package/server/engine/mcp/tools/create-on-board.js +152 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/edit-sketch.js +167 -0
  120. package/server/engine/mcp/tools/explain-style.js +237 -237
  121. package/server/engine/mcp/tools/export-handoff.js +133 -133
  122. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  123. package/server/engine/mcp/tools/generate-image.js +842 -842
  124. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  125. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  126. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  127. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  128. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  129. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  130. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  131. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  132. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  133. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  134. package/server/engine/mcp/tools/highlight.js +68 -68
  135. package/server/engine/mcp/tools/list-pages.js +222 -222
  136. package/server/engine/mcp/tools/look-at-board.js +124 -0
  137. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  138. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  139. package/server/engine/mcp/tools/organize-board.js +64 -64
  140. package/server/engine/mcp/tools/paint-still.js +434 -434
  141. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  142. package/server/engine/mcp/tools/preview-deck.js +130 -130
  143. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  144. package/server/engine/mcp/tools/publish-site.js +102 -102
  145. package/server/engine/mcp/tools/query-elements.js +195 -195
  146. package/server/engine/mcp/tools/read-board.js +190 -103
  147. package/server/engine/mcp/tools/read-document.js +93 -93
  148. package/server/engine/mcp/tools/read-page.js +264 -264
  149. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  150. package/server/engine/mcp/tools/read-user-view.js +59 -0
  151. package/server/engine/mcp/tools/record-decision.js +140 -140
  152. package/server/engine/mcp/tools/relate-on-board.js +112 -110
  153. package/server/engine/mcp/tools/report-issue.js +115 -115
  154. package/server/engine/mcp/tools/roll-film.js +266 -266
  155. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  156. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  157. package/server/engine/mcp/tools/screenshot.js +583 -583
  158. package/server/engine/mcp/tools/sketch-on-board.js +337 -0
  159. package/server/engine/mcp/tools/tier-gate.js +96 -96
  160. package/server/engine/mcp/tools/trace-motion.js +215 -215
  161. package/server/engine/mcp/tools/web-search.js +548 -548
  162. package/server/engine/mcp/tools/write-on-board.js +130 -0
  163. package/server/engine/motion/inventory.js +349 -349
  164. package/server/engine/perception/session.js +235 -235
  165. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +199 -192
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  174. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +169 -162
  175. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  176. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  177. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +235 -228
  178. 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 -78
  179. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  180. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +330 -323
  181. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  182. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  183. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  185. package/server/engine/runs/active-runs.js +698 -698
  186. package/server/engine/runs/board-tasklist.js +130 -0
  187. package/server/engine/runs/live-turn.js +252 -252
  188. package/server/engine/runs/store.js +364 -364
  189. package/server/engine/runs/turn-relay.js +217 -217
  190. package/server/engine/runtime/workspace.js +164 -164
  191. package/server/index.js +3 -0
  192. package/server/lib/artifact-file-path.js +80 -80
  193. package/server/lib/artifact-target.js +284 -284
  194. package/server/lib/asset-refs.js +239 -239
  195. package/server/lib/async-queue.js +104 -104
  196. package/server/lib/auto-relations.js +130 -130
  197. package/server/lib/binding-types.js +72 -60
  198. package/server/lib/board-graph-export.js +191 -0
  199. package/server/lib/board-groups.js +167 -0
  200. package/server/lib/board-hero.js +76 -0
  201. package/server/lib/board-kind-sizes.js +85 -62
  202. package/server/lib/board-relations.js +133 -133
  203. package/server/lib/browse-proxy.js +180 -180
  204. package/server/lib/canvas-id.js +36 -36
  205. package/server/lib/chalk.js +121 -0
  206. package/server/lib/cover.js +227 -227
  207. package/server/lib/danbooru-tags.js +291 -291
  208. package/server/lib/doc-extract.js +156 -156
  209. package/server/lib/docx/build-from-source.js +260 -260
  210. package/server/lib/docx/build.js +490 -490
  211. package/server/lib/docx/dump-styles.js +287 -287
  212. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  213. package/server/lib/docx/merge-runs.js +129 -129
  214. package/server/lib/docx/numbering.js +218 -218
  215. package/server/lib/docx/order.js +152 -152
  216. package/server/lib/docx/rawzip.js +172 -172
  217. package/server/lib/docx/render.js +92 -92
  218. package/server/lib/docx/text-lint.js +225 -225
  219. package/server/lib/docx/tokens.js +335 -335
  220. package/server/lib/docx/units.js +29 -29
  221. package/server/lib/docx/xml.js +291 -291
  222. package/server/lib/docx-pages.js +187 -187
  223. package/server/lib/export-collect.js +289 -289
  224. package/server/lib/export-package.js +210 -210
  225. package/server/lib/html-srcset.js +145 -145
  226. package/server/lib/image-variant.js +460 -460
  227. package/server/lib/ingress/forward-openai-chat.js +305 -305
  228. package/server/lib/ingress/openai-chat.js +491 -491
  229. package/server/lib/ingress/session-notice.js +66 -66
  230. package/server/lib/ingress/session-routes.js +68 -68
  231. package/server/lib/ingress/slot-probe.js +130 -130
  232. package/server/lib/ingress/upstream-billing.js +52 -52
  233. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  234. package/server/lib/ingress/upstream-truncation.js +48 -48
  235. package/server/lib/issues-store.js +182 -182
  236. package/server/lib/kinds/deck.js +72 -72
  237. package/server/lib/kinds/docx.js +283 -283
  238. package/server/lib/kinds/file-kinds.js +113 -113
  239. package/server/lib/kinds/index.js +202 -202
  240. package/server/lib/kinds/site.js +161 -161
  241. package/server/lib/model-ingress.js +543 -543
  242. package/server/lib/moderation.js +255 -255
  243. package/server/lib/notice-store.js +103 -103
  244. package/server/lib/plugin-install.js +104 -104
  245. package/server/lib/plugin-validator.js +721 -721
  246. package/server/lib/publish-store.js +115 -115
  247. package/server/lib/quick-summary.js +52 -52
  248. package/server/lib/quota.js +270 -270
  249. package/server/lib/rate-window.js +29 -29
  250. package/server/lib/reference-assets.js +92 -92
  251. package/server/lib/region-shot.js +135 -135
  252. package/server/lib/safe-path.js +73 -73
  253. package/server/lib/sdk-session.js +33 -33
  254. package/server/lib/showcase-store.js +92 -92
  255. package/server/lib/site-publish.js +465 -465
  256. package/server/lib/sketch-layout.js +277 -0
  257. package/server/lib/ssrf-guard.js +432 -432
  258. package/server/lib/task-scan.js +158 -158
  259. package/server/lib/tavern-json.js +154 -154
  260. package/server/lib/video-variant.js +237 -237
  261. package/server/lib/workspace-path.js +33 -33
  262. package/server/ops/fix-sdk-musl.mjs +40 -40
  263. package/server/ops/install-macos-fonts.sh +114 -114
  264. package/server/ops/macos-fonts.conf +336 -336
  265. package/server/ops/sandbox-shim/bwrap +57 -57
  266. package/server/projects/assets-summary.js +119 -119
  267. package/server/projects/auto-name.js +53 -53
  268. package/server/projects/board-limits.js +12 -0
  269. package/server/projects/board-sanitize.js +252 -0
  270. package/server/projects/board-store.js +507 -640
  271. package/server/projects/move-entry.js +103 -103
  272. package/server/projects/store.js +270 -270
  273. package/server/projects/ui-config.js +60 -54
  274. package/server/projects/viewpoint-store.js +78 -0
  275. package/server/projects/workspace-templates.js +53 -53
  276. package/server/projects/workspace.js +1025 -1025
  277. package/server/runtime/local-config.js +177 -177
  278. package/server/runtime/profile.js +72 -72
  279. package/server/services/rembg-service.py +334 -334
  280. package/server/shared/README.md +25 -25
  281. package/server/shared/deck.js +54 -54
  282. package/server/shared/time.js +49 -49
  283. package/server/style-pipeline/README.md +53 -53
  284. package/server/ws/broker.js +39 -36
  285. package/server/ws/browse-channel.js +177 -177
  286. package/server/ws/index.js +386 -386
  287. package/web/README.md +89 -89
  288. package/web/dist/assets/BrowserWindow-P_vIdh-G.js +11 -0
  289. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-BpLv5082.js} +3 -3
  290. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-DSAU1-tD.js} +4 -4
  291. package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
  292. package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
  293. package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
  294. package/web/dist/assets/arc-D_khH_xN.js +1 -0
  295. package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
  296. package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
  297. package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
  298. package/web/dist/assets/channel-DGkccMO1.js +1 -0
  299. package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
  300. package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
  301. package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
  302. package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
  303. package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
  304. package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
  305. package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
  306. package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
  307. package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
  308. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
  309. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
  310. package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
  311. package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
  312. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
  313. package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
  314. package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
  315. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  316. package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
  317. package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
  318. package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
  319. package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
  320. package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
  321. package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
  322. package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
  323. package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
  324. package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
  325. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
  326. package/web/dist/assets/index-GAamh1p4.js +2152 -0
  327. package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
  328. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  329. package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
  330. package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
  331. package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
  332. package/web/dist/assets/layout-CnXiF_NB.js +1 -0
  333. package/web/dist/assets/linear-BMW0u6G6.js +1 -0
  334. package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
  335. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  336. package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
  337. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
  338. package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
  339. package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
  340. package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
  341. package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
  342. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-KAfChbGV.js} +1 -1
  343. package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
  344. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
  345. package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
  346. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
  347. package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
  348. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
  349. package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
  350. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
  351. package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
  352. package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
  353. package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
  354. package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
  355. package/web/dist/index.html +17 -17
  356. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
  357. package/web/dist/assets/SiteWindow-D6eS-2gM.js +0 -18
  358. package/web/dist/assets/index-Cbl3ATvp.js +0 -2125
@@ -1,366 +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
- }
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
+ }