@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,348 +1,348 @@
1
- /**
2
- * server/auth/users-store.js — 用户与邀请码(2026-07-30 内测多用户)
3
- *
4
- * 复用 engine/runs/store.js 的 better-sqlite3 连接(与 projects/runs 同一个
5
- * nodesign.db)。建表走仓里的既有范式:import 副作用式幂等 DDL。
6
- *
7
- * 密码:node 内置 crypto.scrypt,无新依赖。存储格式
8
- * scrypt$<N>$<saltHex>$<hashHex>
9
- * 参数变更时旧记录仍能按自记录的 N 校验。
10
- *
11
- * 邀请码:admin 生成,限次数/可过期;注册时事务内 used_count+1 防并发超发。
12
- *
13
- * bootstrapAuth()(index.js 启动时调,幂等):
14
- * - users 空 && NODESIGN_AUTH_PASSWORD 存在 → 用该密码建 admin 账号
15
- * (单密码墙 → 多用户的无感迁移:你用老密码 + 用户名 admin 重登即可)
16
- * - projects.owner_id 为 NULL 的存量行 → 回填 admin(历史项目全归你)
17
- */
18
-
19
- import crypto from 'node:crypto';
20
- import db from '../engine/runs/store.js';
21
- import { PLANS, basicDefaultDailyUsd } from './tier.js';
22
- import { platform } from '../runtime/platform.js';
23
-
24
- /**
25
- * 登录墙关闭时的请求者(HTTP / WS / 工具内 owner 反查三处共用同一个对象)。
26
- * 形状对齐 rowToUser:role admin ⇒ tier admin ⇒ 全能力、免额度、免外审;没有 plan /
27
- * moderationLevel ⇒ 档位默认值。id 进 projects.owner_id / runs.user_id 这些列,所以
28
- * 必须稳定不变 —— 改了它,老项目在本地版里会「找不到」。
29
- */
30
- export const LOCAL_OWNER = Object.freeze({ id: '_anon', username: 'anon', role: 'admin', dailyTokenLimit: null, disabled: false });
31
-
32
- /**
33
- * 登录墙是否启用。
34
- * - local profile:钉死关闭(单租户;服务只绑环回,见 runtime/profile.js)。users 表里有没有行都不看 ——
35
- * 开发者用同一个 DB 切过 profile 也不会突然被一堵墙拦住。
36
- * - hosted:有用户就启用;一个用户都没有且没密码 = 关闭(本地开发)。
37
- */
38
- export function authEnabled() {
39
- if (platform.isLocal) return false;
40
- return countUsers() > 0 || (process.env.NODESIGN_AUTH_PASSWORD || '').length > 0;
41
- }
42
-
43
- db.exec(`
44
- CREATE TABLE IF NOT EXISTS users (
45
- id TEXT PRIMARY KEY,
46
- username TEXT NOT NULL UNIQUE,
47
- password_hash TEXT NOT NULL,
48
- role TEXT NOT NULL DEFAULT 'user',
49
- daily_token_limit INTEGER,
50
- disabled INTEGER NOT NULL DEFAULT 0,
51
- invite_code TEXT,
52
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
53
- );
54
- CREATE TABLE IF NOT EXISTS invites (
55
- code TEXT PRIMARY KEY,
56
- created_by TEXT,
57
- max_uses INTEGER NOT NULL DEFAULT 1,
58
- used_count INTEGER NOT NULL DEFAULT 0,
59
- expires_at TEXT,
60
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
61
- );
62
- `);
63
-
64
- // 老 DB 补列(幂等,同 projects/store.js 范式):07-31 限额口径从 token 换成
65
- // 金额,per-user 覆盖也跟着换单位。daily_token_limit 保留不删 —— 它是老口径的
66
- // 存量数据,删了就没法回溯当时给谁开过什么口子。
67
- const userCols = new Set(db.prepare('PRAGMA table_info(users)').all().map(c => c.name));
68
- if (!userCols.has('daily_cost_limit_usd')) {
69
- db.exec('ALTER TABLE users ADD COLUMN daily_cost_limit_usd REAL');
70
- console.log('[users-store] users.daily_cost_limit_usd column added');
71
- }
72
-
73
- // 08-02 试用账号(简历上的通用邀请码):终身额度,烧满不刷新。非空即生效,
74
- // 且**取代**日限而不是叠加 —— 试用要的是完整体验一晚,不是细水长流。
75
- // 注册时从 invites.grant_lifetime_usd 复制过来,之后改码删码不影响已注册的号。
76
- if (!userCols.has('lifetime_cost_limit_usd')) {
77
- db.exec('ALTER TABLE users ADD COLUMN lifetime_cost_limit_usd REAL');
78
- console.log('[users-store] users.lifetime_cost_limit_usd column added');
79
- }
80
- // 08-02 内容外审强度(见 lib/moderation.js):'off' | 'loose' | 'strict'。
81
- // NULL = 跟随该账号的默认档(试用号 strict / 正式号 loose / admin off),
82
- // 站主可 per-user 覆盖。列存的是覆盖值,不是最终值 —— 默认档改了,
83
- // 没设过的号跟着走。
84
- if (!userCols.has('moderation_level')) {
85
- db.exec('ALTER TABLE users ADD COLUMN moderation_level TEXT');
86
- console.log('[users-store] users.moderation_level column added');
87
- }
88
- // 08-20 外审档按模型通路拆成两个旋钮:moderation_level 只管订阅模型(Sonnet/Opus,
89
- // 跑在站主账号上),moderation_level_api 管本地 qwen / 中转站那些走 ingress 的 API 行。
90
- // 站主要的是"给朋友开 qwen 无审查"不必顺带放开 Sonnet、"收紧 Sonnet"不必顺带收紧 qwen。
91
- // 迁移口径:已显式设过档位的号把现值**复制**进 API 旋钮 —— 迁移当天谁的行为都不变
92
- // (否则昨天为 qwen 设了 off 的朋友,今天 qwen 突然变 loose),之后站主按人调。
93
- if (!userCols.has('moderation_level_api')) {
94
- db.exec('ALTER TABLE users ADD COLUMN moderation_level_api TEXT');
95
- const n = db.prepare('UPDATE users SET moderation_level_api = moderation_level WHERE moderation_level IS NOT NULL').run().changes;
96
- console.log(`[users-store] users.moderation_level_api column added (copied ${n} explicit level(s) from moderation_level)`);
97
- }
98
- // 本地产线(roll_film/paint_still)批准制:admin 天生有,其余账号站主点开才有
99
- if (!userCols.has('local_gen')) {
100
- db.exec('ALTER TABLE users ADD COLUMN local_gen INTEGER NOT NULL DEFAULT 0');
101
- console.log('[users-store] users.local_gen column added');
102
- }
103
- // 08-21 经营态转向:订阅 Claude 资格按账号发。新列默认 0(公开注册号只能用免费模型),
104
- // **迁移时把已有用户全部置 1**(老号保留,用户说他手动关);邀请码注册的号拿 1。
105
- if (!userCols.has('allow_subscription')) {
106
- db.exec('ALTER TABLE users ADD COLUMN allow_subscription INTEGER NOT NULL DEFAULT 0');
107
- const n = db.prepare('UPDATE users SET allow_subscription = 1').run().changes;
108
- console.log(`[users-store] users.allow_subscription column added(存量 ${n} 个账号置 1)`);
109
- }
110
- // 08-21 晚 账号档位真相源(auth/tier.js):users.plan ∈ 'pro' | 'basic',admin 由 role 派生。
111
- // 取代 allow_subscription 那个单能力布尔 —— 订阅资格、生图、发布、外审默认档全从档位派生,
112
- // 消费方问能力不问字段。迁移口径一比一复制(allow_subscription=1 → pro,0 → basic),
113
- // 迁移当天谁的行为都不变。allow_subscription 列**退役**:不读不写,留着只是 SQLite 不爱删列。
114
- if (!userCols.has('plan')) {
115
- db.exec("ALTER TABLE users ADD COLUMN plan TEXT NOT NULL DEFAULT 'basic'");
116
- const n = db.prepare("UPDATE users SET plan = 'pro' WHERE allow_subscription = 1").run().changes;
117
- console.log(`[users-store] users.plan column added(${n} 个账号按 allow_subscription 迁成 pro,其余 basic)`);
118
- }
119
-
120
- const inviteCols = new Set(db.prepare('PRAGMA table_info(invites)').all().map(c => c.name));
121
- if (!inviteCols.has('grant_lifetime_usd')) {
122
- db.exec('ALTER TABLE invites ADD COLUMN grant_lifetime_usd REAL');
123
- console.log('[users-store] invites.grant_lifetime_usd column added');
124
- }
125
-
126
- // ── 密码 ──
127
-
128
- const SCRYPT_N = 16384;
129
-
130
- export function hashPassword(password) {
131
- const salt = crypto.randomBytes(16).toString('hex');
132
- const hash = crypto.scryptSync(String(password), salt, 64, { N: SCRYPT_N, r: 8, p: 1 }).toString('hex');
133
- return `scrypt$${SCRYPT_N}$${salt}$${hash}`;
134
- }
135
-
136
- export function verifyPassword(password, stored) {
137
- const m = /^scrypt\$(\d+)\$([0-9a-f]+)\$([0-9a-f]+)$/.exec(stored || '');
138
- if (!m) return false;
139
- const [, nStr, salt, hashHex] = m;
140
- try {
141
- const calc = crypto.scryptSync(String(password), salt, hashHex.length / 2, { N: Number(nStr), r: 8, p: 1 });
142
- return crypto.timingSafeEqual(calc, Buffer.from(hashHex, 'hex'));
143
- } catch {
144
- return false;
145
- }
146
- }
147
-
148
- // ── 用户 ──
149
-
150
- const USERNAME_RE = /^[A-Za-z0-9_一-鿿-]{2,32}$/;
151
-
152
- export function validUsername(name) {
153
- return typeof name === 'string' && USERNAME_RE.test(name);
154
- }
155
-
156
- function newUserId() {
157
- return `u_${Date.now().toString(36)}_${crypto.randomBytes(3).toString('hex')}`;
158
- }
159
-
160
- function rowToUser(row) {
161
- if (!row) return null;
162
- return {
163
- id: row.id,
164
- username: row.username,
165
- role: row.role,
166
- dailyCostLimitUsd: row.daily_cost_limit_usd ?? null,
167
- lifetimeCostLimitUsd: row.lifetime_cost_limit_usd ?? null,
168
- dailyTokenLimit: row.daily_token_limit ?? null, // 老口径存量,只读不用
169
- moderationLevel: row.moderation_level ?? null, // 订阅模型的外审档;null = 跟随默认档
170
- moderationLevelApi: row.moderation_level_api ?? null, // 本地 qwen / 中转站的外审档;null = 跟随默认档
171
- allowLocalGen: !!row.local_gen, // 本地产线逐人批准(叠在档位之上;见 auth/tier.js localGenApproved)
172
- plan: row.plan === 'pro' ? 'pro' : 'basic', // 档位真相源:pro(邀请码)| basic(公开注册);admin 看 role。能力问 auth/tier.js
173
- disabled: !!row.disabled,
174
- inviteCode: row.invite_code || null,
175
- createdAt: row.created_at,
176
- };
177
- }
178
-
179
- // requestUser 每个请求都要查 —— 60s 内存缓存压掉热路径的 SQLite 读。
180
- // disable 用户最迟 60s 生效,内测语境可接受。
181
- const userCache = new Map(); // id → { user, at }
182
- const USER_CACHE_MS = 60_000;
183
-
184
- export function getUserById(id) {
185
- // 登录墙关闭时 owner 是 LOCAL_OWNER,它不在表里。不在这里接住,session-loop 的订阅资格断言 /
186
- // tier-gate / paint_still 这些按 ownerId 反查的地方会拿到 null ⇒ fail-closed ⇒ 本地版什么都跑不了。
187
- // 登录墙开着时 '_anon' 不是合法身份(表里没有这行),照常查表得 null。
188
- if (id === LOCAL_OWNER.id && !authEnabled()) return LOCAL_OWNER;
189
- const hit = userCache.get(id);
190
- if (hit && Date.now() - hit.at < USER_CACHE_MS) return hit.user;
191
- const user = rowToUser(db.prepare('SELECT * FROM users WHERE id = ?').get(id));
192
- userCache.set(id, { user, at: Date.now() });
193
- return user;
194
- }
195
-
196
- export function getUserByUsername(username) {
197
- return rowToUser(db.prepare('SELECT * FROM users WHERE username = ?').get(username));
198
- }
199
-
200
- /** 登录用:要拿 hash 比对,不走 rowToUser(hash 不出模块) */
201
- export function getCredential(username) {
202
- const row = db.prepare('SELECT id, password_hash, disabled FROM users WHERE username = ?').get(username);
203
- return row ? { id: row.id, passwordHash: row.password_hash, disabled: !!row.disabled } : null;
204
- }
205
-
206
- export function createUser({ username, password, role = 'user', inviteCode = null, lifetimeCostLimitUsd = null, dailyCostLimitUsd = null, plan = 'basic' }) {
207
- if (!PLANS.includes(plan)) throw new Error(`createUser: plan 需为 ${PLANS.join('/')},收到 '${plan}'`);
208
- const id = newUserId();
209
- db.prepare(`INSERT INTO users (id, username, password_hash, role, invite_code, lifetime_cost_limit_usd, daily_cost_limit_usd, plan) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
210
- .run(id, username, hashPassword(password), role, inviteCode, lifetimeCostLimitUsd, dailyCostLimitUsd, plan);
211
- return getUserById(id);
212
- }
213
-
214
- export function listUsers() {
215
- return db.prepare('SELECT * FROM users ORDER BY created_at ASC').all().map(rowToUser);
216
- }
217
-
218
- export function updateUser(id, { disabled, dailyTokenLimit, dailyCostLimitUsd, lifetimeCostLimitUsd, role, moderationLevel, moderationLevelApi, localGen, plan } = {}) {
219
- const sets = [];
220
- const args = [];
221
- if (disabled !== undefined) { sets.push('disabled = ?'); args.push(disabled ? 1 : 0); }
222
- if (moderationLevel !== undefined) { sets.push('moderation_level = ?'); args.push(moderationLevel ?? null); }
223
- if (moderationLevelApi !== undefined) { sets.push('moderation_level_api = ?'); args.push(moderationLevelApi ?? null); }
224
- if (localGen !== undefined) { sets.push('local_gen = ?'); args.push(localGen ? 1 : 0); }
225
- if (plan !== undefined) {
226
- if (!PLANS.includes(plan)) throw new Error(`updateUser: plan 需为 ${PLANS.join('/')},收到 '${plan}'`);
227
- sets.push('plan = ?'); args.push(plan);
228
- }
229
- if (dailyCostLimitUsd !== undefined) { sets.push('daily_cost_limit_usd = ?'); args.push(dailyCostLimitUsd ?? null); }
230
- if (lifetimeCostLimitUsd !== undefined) { sets.push('lifetime_cost_limit_usd = ?'); args.push(lifetimeCostLimitUsd ?? null); }
231
- if (dailyTokenLimit !== undefined) { sets.push('daily_token_limit = ?'); args.push(dailyTokenLimit ?? null); }
232
- if (role !== undefined) { sets.push('role = ?'); args.push(role); }
233
- if (!sets.length) return getUserById(id);
234
- db.prepare(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`).run(...args, id);
235
- userCache.delete(id);
236
- return getUserById(id);
237
- }
238
-
239
- export function countUsers() {
240
- return db.prepare('SELECT COUNT(*) AS n FROM users').get().n;
241
- }
242
-
243
- // ── 邀请码 ──
244
-
245
- export function createInvite({ createdBy = null, maxUses = 1, expiresAt = null, grantLifetimeUsd = null } = {}) {
246
- // 可读形态:nd-xxxxxxxx(发群里手输不痛苦;去掉易混字符)
247
- const alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
248
- let code = 'nd-';
249
- for (const b of crypto.randomBytes(8)) code += alphabet[b % alphabet.length];
250
- db.prepare('INSERT INTO invites (code, created_by, max_uses, expires_at, grant_lifetime_usd) VALUES (?, ?, ?, ?, ?)')
251
- .run(code, createdBy, maxUses, expiresAt, grantLifetimeUsd);
252
- return getInvite(code);
253
- }
254
-
255
- export function getInvite(code) {
256
- return db.prepare('SELECT * FROM invites WHERE code = ?').get(code) || null;
257
- }
258
-
259
- /**
260
- * 改邀请码的可用次数(控制台用)。语义是**总次数**不是剩余次数 ——
261
- * 改到 ≤ used_count 等于立即封死这个码(简历码泄漏时的一键止血)。
262
- */
263
- export function updateInvite(code, { maxUses } = {}) {
264
- if (maxUses !== undefined) {
265
- db.prepare('UPDATE invites SET max_uses = ? WHERE code = ?').run(maxUses, code);
266
- }
267
- return getInvite(code);
268
- }
269
-
270
- export function listInvites() {
271
- return db.prepare('SELECT * FROM invites ORDER BY created_at DESC').all();
272
- }
273
-
274
- /** 开放注册开关(.env NODESIGN_OPEN_REGISTRATION=1)。关着时没邀请码照旧拒 */
275
- export function openRegistrationEnabled() {
276
- return /^(1|true|yes)$/i.test(String(process.env.NODESIGN_OPEN_REGISTRATION || ''));
277
- }
278
-
279
- /** 邀请码注册的号默认**每日**额度(美元,08-21 晚用户拍板 $20)。env NODESIGN_INVITE_DEFAULT_DAILY_USD;0 或非法 = 不写(走全局默认日限) */
280
- export function defaultInviteDailyUsd(env = process.env) {
281
- const raw = env.NODESIGN_INVITE_DEFAULT_DAILY_USD;
282
- if (raw === undefined || raw === '') return 20;
283
- const v = Number(raw);
284
- return Number.isFinite(v) && v > 0 ? v : null;
285
- }
286
-
287
- /**
288
- * 注册主流程(08-21 起两条路):
289
- * - 带邀请码:校验 + 消耗 + 建用户,落 **pro 档**(plan='pro')+ 每日 $20(默认)+ 码上的终身额度(写了才有,取代日限)
290
- * - 不带邀请码:开放注册开着才放行,落 **basic 档**(plan='basic':只能用免费模型/搜索,
291
- * 不开生图、不开发布;能力表在 auth/tier.js)
292
- * 单事务(used_count+1 与 INSERT 原子,两人同抢最后一个名额只有一个成)。失败抛带 .code 的 Error。
293
- */
294
- export const registerUser = db.transaction(({ username, password, inviteCode }) => {
295
- if (!validUsername(username)) {
296
- throw Object.assign(new Error('用户名 2-32 位,仅限字母数字下划线连字符和中文'), { code: 'BAD_USERNAME' });
297
- }
298
- if (typeof password !== 'string' || password.length < 8) {
299
- throw Object.assign(new Error('密码至少 8 位'), { code: 'BAD_PASSWORD' });
300
- }
301
- if (getUserByUsername(username)) {
302
- throw Object.assign(new Error('用户名已被使用'), { code: 'USERNAME_TAKEN' });
303
- }
304
- const code = String(inviteCode || '').trim();
305
- if (!code) {
306
- if (!openRegistrationEnabled()) throw Object.assign(new Error('邀请码无效'), { code: 'BAD_INVITE' });
307
- // basic:每人每天 $5 总额度(Go 付费行按表价 + 生图 $0.20/张 同一本账;Ox 免费行不计)
308
- return createUser({ username, password, role: 'user', inviteCode: null, plan: 'basic', dailyCostLimitUsd: basicDefaultDailyUsd() });
309
- }
310
- const inv = getInvite(code);
311
- if (!inv) throw Object.assign(new Error('邀请码无效'), { code: 'BAD_INVITE' });
312
- if (inv.expires_at && new Date(inv.expires_at).getTime() < Date.now()) {
313
- throw Object.assign(new Error('邀请码已过期'), { code: 'INVITE_EXPIRED' });
314
- }
315
- if (inv.used_count >= inv.max_uses) {
316
- throw Object.assign(new Error('邀请码已用完'), { code: 'INVITE_EXHAUSTED' });
317
- }
318
- db.prepare('UPDATE invites SET used_count = used_count + 1 WHERE code = ?').run(inv.code);
319
- return createUser({
320
- username, password, role: 'user', inviteCode: inv.code,
321
- // 花费上限(不是档位):终身额度只在码上写了才有(非空即取代日限);日限默认 $20(08-21 晚起,以前跟全局默认 $50)
322
- lifetimeCostLimitUsd: inv.grant_lifetime_usd ?? null,
323
- dailyCostLimitUsd: defaultInviteDailyUsd(),
324
- plan: 'pro',
325
- });
326
- });
327
-
328
- // ── 启动 bootstrap(index.js 调,幂等)──
329
-
330
- export function bootstrapAuth() {
331
- let admin = db.prepare("SELECT * FROM users WHERE role = 'admin' ORDER BY created_at ASC").get();
332
- if (!admin && countUsers() === 0) {
333
- const pw = process.env.NODESIGN_AUTH_PASSWORD || '';
334
- if (pw) {
335
- const created = createUser({ username: 'admin', password: pw, role: 'admin' });
336
- admin = db.prepare('SELECT * FROM users WHERE id = ?').get(created.id);
337
- console.log('[auth] bootstrap: 用 NODESIGN_AUTH_PASSWORD 创建了 admin 账号(用户名 admin)');
338
- } else {
339
- console.warn('[auth] users 表为空且未配置 NODESIGN_AUTH_PASSWORD —— 登录墙关闭(仅限本地开发)');
340
- }
341
- }
342
- if (admin) {
343
- // 存量项目回填归属(owner_id 列由 projects/store.js 幂等 ALTER 加上)
344
- const r = db.prepare('UPDATE projects SET owner_id = ? WHERE owner_id IS NULL').run(admin.id);
345
- if (r.changes > 0) console.log(`[auth] bootstrap: ${r.changes} 个存量项目归属到 admin`);
346
- }
347
- return admin ? rowToUser(admin) : null;
348
- }
1
+ /**
2
+ * server/auth/users-store.js — 用户与邀请码(2026-07-30 内测多用户)
3
+ *
4
+ * 复用 engine/runs/store.js 的 better-sqlite3 连接(与 projects/runs 同一个
5
+ * nodesign.db)。建表走仓里的既有范式:import 副作用式幂等 DDL。
6
+ *
7
+ * 密码:node 内置 crypto.scrypt,无新依赖。存储格式
8
+ * scrypt$<N>$<saltHex>$<hashHex>
9
+ * 参数变更时旧记录仍能按自记录的 N 校验。
10
+ *
11
+ * 邀请码:admin 生成,限次数/可过期;注册时事务内 used_count+1 防并发超发。
12
+ *
13
+ * bootstrapAuth()(index.js 启动时调,幂等):
14
+ * - users 空 && NODESIGN_AUTH_PASSWORD 存在 → 用该密码建 admin 账号
15
+ * (单密码墙 → 多用户的无感迁移:你用老密码 + 用户名 admin 重登即可)
16
+ * - projects.owner_id 为 NULL 的存量行 → 回填 admin(历史项目全归你)
17
+ */
18
+
19
+ import crypto from 'node:crypto';
20
+ import db from '../engine/runs/store.js';
21
+ import { PLANS, basicDefaultDailyUsd } from './tier.js';
22
+ import { platform } from '../runtime/platform.js';
23
+
24
+ /**
25
+ * 登录墙关闭时的请求者(HTTP / WS / 工具内 owner 反查三处共用同一个对象)。
26
+ * 形状对齐 rowToUser:role admin ⇒ tier admin ⇒ 全能力、免额度、免外审;没有 plan /
27
+ * moderationLevel ⇒ 档位默认值。id 进 projects.owner_id / runs.user_id 这些列,所以
28
+ * 必须稳定不变 —— 改了它,老项目在本地版里会「找不到」。
29
+ */
30
+ export const LOCAL_OWNER = Object.freeze({ id: '_anon', username: 'anon', role: 'admin', dailyTokenLimit: null, disabled: false });
31
+
32
+ /**
33
+ * 登录墙是否启用。
34
+ * - local profile:钉死关闭(单租户;服务只绑环回,见 runtime/profile.js)。users 表里有没有行都不看 ——
35
+ * 开发者用同一个 DB 切过 profile 也不会突然被一堵墙拦住。
36
+ * - hosted:有用户就启用;一个用户都没有且没密码 = 关闭(本地开发)。
37
+ */
38
+ export function authEnabled() {
39
+ if (platform.isLocal) return false;
40
+ return countUsers() > 0 || (process.env.NODESIGN_AUTH_PASSWORD || '').length > 0;
41
+ }
42
+
43
+ db.exec(`
44
+ CREATE TABLE IF NOT EXISTS users (
45
+ id TEXT PRIMARY KEY,
46
+ username TEXT NOT NULL UNIQUE,
47
+ password_hash TEXT NOT NULL,
48
+ role TEXT NOT NULL DEFAULT 'user',
49
+ daily_token_limit INTEGER,
50
+ disabled INTEGER NOT NULL DEFAULT 0,
51
+ invite_code TEXT,
52
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
53
+ );
54
+ CREATE TABLE IF NOT EXISTS invites (
55
+ code TEXT PRIMARY KEY,
56
+ created_by TEXT,
57
+ max_uses INTEGER NOT NULL DEFAULT 1,
58
+ used_count INTEGER NOT NULL DEFAULT 0,
59
+ expires_at TEXT,
60
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
61
+ );
62
+ `);
63
+
64
+ // 老 DB 补列(幂等,同 projects/store.js 范式):07-31 限额口径从 token 换成
65
+ // 金额,per-user 覆盖也跟着换单位。daily_token_limit 保留不删 —— 它是老口径的
66
+ // 存量数据,删了就没法回溯当时给谁开过什么口子。
67
+ const userCols = new Set(db.prepare('PRAGMA table_info(users)').all().map(c => c.name));
68
+ if (!userCols.has('daily_cost_limit_usd')) {
69
+ db.exec('ALTER TABLE users ADD COLUMN daily_cost_limit_usd REAL');
70
+ console.log('[users-store] users.daily_cost_limit_usd column added');
71
+ }
72
+
73
+ // 08-02 试用账号(简历上的通用邀请码):终身额度,烧满不刷新。非空即生效,
74
+ // 且**取代**日限而不是叠加 —— 试用要的是完整体验一晚,不是细水长流。
75
+ // 注册时从 invites.grant_lifetime_usd 复制过来,之后改码删码不影响已注册的号。
76
+ if (!userCols.has('lifetime_cost_limit_usd')) {
77
+ db.exec('ALTER TABLE users ADD COLUMN lifetime_cost_limit_usd REAL');
78
+ console.log('[users-store] users.lifetime_cost_limit_usd column added');
79
+ }
80
+ // 08-02 内容外审强度(见 lib/moderation.js):'off' | 'loose' | 'strict'。
81
+ // NULL = 跟随该账号的默认档(试用号 strict / 正式号 loose / admin off),
82
+ // 站主可 per-user 覆盖。列存的是覆盖值,不是最终值 —— 默认档改了,
83
+ // 没设过的号跟着走。
84
+ if (!userCols.has('moderation_level')) {
85
+ db.exec('ALTER TABLE users ADD COLUMN moderation_level TEXT');
86
+ console.log('[users-store] users.moderation_level column added');
87
+ }
88
+ // 08-20 外审档按模型通路拆成两个旋钮:moderation_level 只管订阅模型(Sonnet/Opus,
89
+ // 跑在站主账号上),moderation_level_api 管本地 qwen / 中转站那些走 ingress 的 API 行。
90
+ // 站主要的是"给朋友开 qwen 无审查"不必顺带放开 Sonnet、"收紧 Sonnet"不必顺带收紧 qwen。
91
+ // 迁移口径:已显式设过档位的号把现值**复制**进 API 旋钮 —— 迁移当天谁的行为都不变
92
+ // (否则昨天为 qwen 设了 off 的朋友,今天 qwen 突然变 loose),之后站主按人调。
93
+ if (!userCols.has('moderation_level_api')) {
94
+ db.exec('ALTER TABLE users ADD COLUMN moderation_level_api TEXT');
95
+ const n = db.prepare('UPDATE users SET moderation_level_api = moderation_level WHERE moderation_level IS NOT NULL').run().changes;
96
+ console.log(`[users-store] users.moderation_level_api column added (copied ${n} explicit level(s) from moderation_level)`);
97
+ }
98
+ // 本地产线(roll_film/paint_still)批准制:admin 天生有,其余账号站主点开才有
99
+ if (!userCols.has('local_gen')) {
100
+ db.exec('ALTER TABLE users ADD COLUMN local_gen INTEGER NOT NULL DEFAULT 0');
101
+ console.log('[users-store] users.local_gen column added');
102
+ }
103
+ // 08-21 经营态转向:订阅 Claude 资格按账号发。新列默认 0(公开注册号只能用免费模型),
104
+ // **迁移时把已有用户全部置 1**(老号保留,用户说他手动关);邀请码注册的号拿 1。
105
+ if (!userCols.has('allow_subscription')) {
106
+ db.exec('ALTER TABLE users ADD COLUMN allow_subscription INTEGER NOT NULL DEFAULT 0');
107
+ const n = db.prepare('UPDATE users SET allow_subscription = 1').run().changes;
108
+ console.log(`[users-store] users.allow_subscription column added(存量 ${n} 个账号置 1)`);
109
+ }
110
+ // 08-21 晚 账号档位真相源(auth/tier.js):users.plan ∈ 'pro' | 'basic',admin 由 role 派生。
111
+ // 取代 allow_subscription 那个单能力布尔 —— 订阅资格、生图、发布、外审默认档全从档位派生,
112
+ // 消费方问能力不问字段。迁移口径一比一复制(allow_subscription=1 → pro,0 → basic),
113
+ // 迁移当天谁的行为都不变。allow_subscription 列**退役**:不读不写,留着只是 SQLite 不爱删列。
114
+ if (!userCols.has('plan')) {
115
+ db.exec("ALTER TABLE users ADD COLUMN plan TEXT NOT NULL DEFAULT 'basic'");
116
+ const n = db.prepare("UPDATE users SET plan = 'pro' WHERE allow_subscription = 1").run().changes;
117
+ console.log(`[users-store] users.plan column added(${n} 个账号按 allow_subscription 迁成 pro,其余 basic)`);
118
+ }
119
+
120
+ const inviteCols = new Set(db.prepare('PRAGMA table_info(invites)').all().map(c => c.name));
121
+ if (!inviteCols.has('grant_lifetime_usd')) {
122
+ db.exec('ALTER TABLE invites ADD COLUMN grant_lifetime_usd REAL');
123
+ console.log('[users-store] invites.grant_lifetime_usd column added');
124
+ }
125
+
126
+ // ── 密码 ──
127
+
128
+ const SCRYPT_N = 16384;
129
+
130
+ export function hashPassword(password) {
131
+ const salt = crypto.randomBytes(16).toString('hex');
132
+ const hash = crypto.scryptSync(String(password), salt, 64, { N: SCRYPT_N, r: 8, p: 1 }).toString('hex');
133
+ return `scrypt$${SCRYPT_N}$${salt}$${hash}`;
134
+ }
135
+
136
+ export function verifyPassword(password, stored) {
137
+ const m = /^scrypt\$(\d+)\$([0-9a-f]+)\$([0-9a-f]+)$/.exec(stored || '');
138
+ if (!m) return false;
139
+ const [, nStr, salt, hashHex] = m;
140
+ try {
141
+ const calc = crypto.scryptSync(String(password), salt, hashHex.length / 2, { N: Number(nStr), r: 8, p: 1 });
142
+ return crypto.timingSafeEqual(calc, Buffer.from(hashHex, 'hex'));
143
+ } catch {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ // ── 用户 ──
149
+
150
+ const USERNAME_RE = /^[A-Za-z0-9_一-鿿-]{2,32}$/;
151
+
152
+ export function validUsername(name) {
153
+ return typeof name === 'string' && USERNAME_RE.test(name);
154
+ }
155
+
156
+ function newUserId() {
157
+ return `u_${Date.now().toString(36)}_${crypto.randomBytes(3).toString('hex')}`;
158
+ }
159
+
160
+ function rowToUser(row) {
161
+ if (!row) return null;
162
+ return {
163
+ id: row.id,
164
+ username: row.username,
165
+ role: row.role,
166
+ dailyCostLimitUsd: row.daily_cost_limit_usd ?? null,
167
+ lifetimeCostLimitUsd: row.lifetime_cost_limit_usd ?? null,
168
+ dailyTokenLimit: row.daily_token_limit ?? null, // 老口径存量,只读不用
169
+ moderationLevel: row.moderation_level ?? null, // 订阅模型的外审档;null = 跟随默认档
170
+ moderationLevelApi: row.moderation_level_api ?? null, // 本地 qwen / 中转站的外审档;null = 跟随默认档
171
+ allowLocalGen: !!row.local_gen, // 本地产线逐人批准(叠在档位之上;见 auth/tier.js localGenApproved)
172
+ plan: row.plan === 'pro' ? 'pro' : 'basic', // 档位真相源:pro(邀请码)| basic(公开注册);admin 看 role。能力问 auth/tier.js
173
+ disabled: !!row.disabled,
174
+ inviteCode: row.invite_code || null,
175
+ createdAt: row.created_at,
176
+ };
177
+ }
178
+
179
+ // requestUser 每个请求都要查 —— 60s 内存缓存压掉热路径的 SQLite 读。
180
+ // disable 用户最迟 60s 生效,内测语境可接受。
181
+ const userCache = new Map(); // id → { user, at }
182
+ const USER_CACHE_MS = 60_000;
183
+
184
+ export function getUserById(id) {
185
+ // 登录墙关闭时 owner 是 LOCAL_OWNER,它不在表里。不在这里接住,session-loop 的订阅资格断言 /
186
+ // tier-gate / paint_still 这些按 ownerId 反查的地方会拿到 null ⇒ fail-closed ⇒ 本地版什么都跑不了。
187
+ // 登录墙开着时 '_anon' 不是合法身份(表里没有这行),照常查表得 null。
188
+ if (id === LOCAL_OWNER.id && !authEnabled()) return LOCAL_OWNER;
189
+ const hit = userCache.get(id);
190
+ if (hit && Date.now() - hit.at < USER_CACHE_MS) return hit.user;
191
+ const user = rowToUser(db.prepare('SELECT * FROM users WHERE id = ?').get(id));
192
+ userCache.set(id, { user, at: Date.now() });
193
+ return user;
194
+ }
195
+
196
+ export function getUserByUsername(username) {
197
+ return rowToUser(db.prepare('SELECT * FROM users WHERE username = ?').get(username));
198
+ }
199
+
200
+ /** 登录用:要拿 hash 比对,不走 rowToUser(hash 不出模块) */
201
+ export function getCredential(username) {
202
+ const row = db.prepare('SELECT id, password_hash, disabled FROM users WHERE username = ?').get(username);
203
+ return row ? { id: row.id, passwordHash: row.password_hash, disabled: !!row.disabled } : null;
204
+ }
205
+
206
+ export function createUser({ username, password, role = 'user', inviteCode = null, lifetimeCostLimitUsd = null, dailyCostLimitUsd = null, plan = 'basic' }) {
207
+ if (!PLANS.includes(plan)) throw new Error(`createUser: plan 需为 ${PLANS.join('/')},收到 '${plan}'`);
208
+ const id = newUserId();
209
+ db.prepare(`INSERT INTO users (id, username, password_hash, role, invite_code, lifetime_cost_limit_usd, daily_cost_limit_usd, plan) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
210
+ .run(id, username, hashPassword(password), role, inviteCode, lifetimeCostLimitUsd, dailyCostLimitUsd, plan);
211
+ return getUserById(id);
212
+ }
213
+
214
+ export function listUsers() {
215
+ return db.prepare('SELECT * FROM users ORDER BY created_at ASC').all().map(rowToUser);
216
+ }
217
+
218
+ export function updateUser(id, { disabled, dailyTokenLimit, dailyCostLimitUsd, lifetimeCostLimitUsd, role, moderationLevel, moderationLevelApi, localGen, plan } = {}) {
219
+ const sets = [];
220
+ const args = [];
221
+ if (disabled !== undefined) { sets.push('disabled = ?'); args.push(disabled ? 1 : 0); }
222
+ if (moderationLevel !== undefined) { sets.push('moderation_level = ?'); args.push(moderationLevel ?? null); }
223
+ if (moderationLevelApi !== undefined) { sets.push('moderation_level_api = ?'); args.push(moderationLevelApi ?? null); }
224
+ if (localGen !== undefined) { sets.push('local_gen = ?'); args.push(localGen ? 1 : 0); }
225
+ if (plan !== undefined) {
226
+ if (!PLANS.includes(plan)) throw new Error(`updateUser: plan 需为 ${PLANS.join('/')},收到 '${plan}'`);
227
+ sets.push('plan = ?'); args.push(plan);
228
+ }
229
+ if (dailyCostLimitUsd !== undefined) { sets.push('daily_cost_limit_usd = ?'); args.push(dailyCostLimitUsd ?? null); }
230
+ if (lifetimeCostLimitUsd !== undefined) { sets.push('lifetime_cost_limit_usd = ?'); args.push(lifetimeCostLimitUsd ?? null); }
231
+ if (dailyTokenLimit !== undefined) { sets.push('daily_token_limit = ?'); args.push(dailyTokenLimit ?? null); }
232
+ if (role !== undefined) { sets.push('role = ?'); args.push(role); }
233
+ if (!sets.length) return getUserById(id);
234
+ db.prepare(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`).run(...args, id);
235
+ userCache.delete(id);
236
+ return getUserById(id);
237
+ }
238
+
239
+ export function countUsers() {
240
+ return db.prepare('SELECT COUNT(*) AS n FROM users').get().n;
241
+ }
242
+
243
+ // ── 邀请码 ──
244
+
245
+ export function createInvite({ createdBy = null, maxUses = 1, expiresAt = null, grantLifetimeUsd = null } = {}) {
246
+ // 可读形态:nd-xxxxxxxx(发群里手输不痛苦;去掉易混字符)
247
+ const alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
248
+ let code = 'nd-';
249
+ for (const b of crypto.randomBytes(8)) code += alphabet[b % alphabet.length];
250
+ db.prepare('INSERT INTO invites (code, created_by, max_uses, expires_at, grant_lifetime_usd) VALUES (?, ?, ?, ?, ?)')
251
+ .run(code, createdBy, maxUses, expiresAt, grantLifetimeUsd);
252
+ return getInvite(code);
253
+ }
254
+
255
+ export function getInvite(code) {
256
+ return db.prepare('SELECT * FROM invites WHERE code = ?').get(code) || null;
257
+ }
258
+
259
+ /**
260
+ * 改邀请码的可用次数(控制台用)。语义是**总次数**不是剩余次数 ——
261
+ * 改到 ≤ used_count 等于立即封死这个码(简历码泄漏时的一键止血)。
262
+ */
263
+ export function updateInvite(code, { maxUses } = {}) {
264
+ if (maxUses !== undefined) {
265
+ db.prepare('UPDATE invites SET max_uses = ? WHERE code = ?').run(maxUses, code);
266
+ }
267
+ return getInvite(code);
268
+ }
269
+
270
+ export function listInvites() {
271
+ return db.prepare('SELECT * FROM invites ORDER BY created_at DESC').all();
272
+ }
273
+
274
+ /** 开放注册开关(.env NODESIGN_OPEN_REGISTRATION=1)。关着时没邀请码照旧拒 */
275
+ export function openRegistrationEnabled() {
276
+ return /^(1|true|yes)$/i.test(String(process.env.NODESIGN_OPEN_REGISTRATION || ''));
277
+ }
278
+
279
+ /** 邀请码注册的号默认**每日**额度(美元,08-21 晚用户拍板 $20)。env NODESIGN_INVITE_DEFAULT_DAILY_USD;0 或非法 = 不写(走全局默认日限) */
280
+ export function defaultInviteDailyUsd(env = process.env) {
281
+ const raw = env.NODESIGN_INVITE_DEFAULT_DAILY_USD;
282
+ if (raw === undefined || raw === '') return 20;
283
+ const v = Number(raw);
284
+ return Number.isFinite(v) && v > 0 ? v : null;
285
+ }
286
+
287
+ /**
288
+ * 注册主流程(08-21 起两条路):
289
+ * - 带邀请码:校验 + 消耗 + 建用户,落 **pro 档**(plan='pro')+ 每日 $20(默认)+ 码上的终身额度(写了才有,取代日限)
290
+ * - 不带邀请码:开放注册开着才放行,落 **basic 档**(plan='basic':只能用免费模型/搜索,
291
+ * 不开生图、不开发布;能力表在 auth/tier.js)
292
+ * 单事务(used_count+1 与 INSERT 原子,两人同抢最后一个名额只有一个成)。失败抛带 .code 的 Error。
293
+ */
294
+ export const registerUser = db.transaction(({ username, password, inviteCode }) => {
295
+ if (!validUsername(username)) {
296
+ throw Object.assign(new Error('用户名 2-32 位,仅限字母数字下划线连字符和中文'), { code: 'BAD_USERNAME' });
297
+ }
298
+ if (typeof password !== 'string' || password.length < 8) {
299
+ throw Object.assign(new Error('密码至少 8 位'), { code: 'BAD_PASSWORD' });
300
+ }
301
+ if (getUserByUsername(username)) {
302
+ throw Object.assign(new Error('用户名已被使用'), { code: 'USERNAME_TAKEN' });
303
+ }
304
+ const code = String(inviteCode || '').trim();
305
+ if (!code) {
306
+ if (!openRegistrationEnabled()) throw Object.assign(new Error('邀请码无效'), { code: 'BAD_INVITE' });
307
+ // basic:每人每天 $5 总额度(Go 付费行按表价 + 生图 $0.20/张 同一本账;Ox 免费行不计)
308
+ return createUser({ username, password, role: 'user', inviteCode: null, plan: 'basic', dailyCostLimitUsd: basicDefaultDailyUsd() });
309
+ }
310
+ const inv = getInvite(code);
311
+ if (!inv) throw Object.assign(new Error('邀请码无效'), { code: 'BAD_INVITE' });
312
+ if (inv.expires_at && new Date(inv.expires_at).getTime() < Date.now()) {
313
+ throw Object.assign(new Error('邀请码已过期'), { code: 'INVITE_EXPIRED' });
314
+ }
315
+ if (inv.used_count >= inv.max_uses) {
316
+ throw Object.assign(new Error('邀请码已用完'), { code: 'INVITE_EXHAUSTED' });
317
+ }
318
+ db.prepare('UPDATE invites SET used_count = used_count + 1 WHERE code = ?').run(inv.code);
319
+ return createUser({
320
+ username, password, role: 'user', inviteCode: inv.code,
321
+ // 花费上限(不是档位):终身额度只在码上写了才有(非空即取代日限);日限默认 $20(08-21 晚起,以前跟全局默认 $50)
322
+ lifetimeCostLimitUsd: inv.grant_lifetime_usd ?? null,
323
+ dailyCostLimitUsd: defaultInviteDailyUsd(),
324
+ plan: 'pro',
325
+ });
326
+ });
327
+
328
+ // ── 启动 bootstrap(index.js 调,幂等)──
329
+
330
+ export function bootstrapAuth() {
331
+ let admin = db.prepare("SELECT * FROM users WHERE role = 'admin' ORDER BY created_at ASC").get();
332
+ if (!admin && countUsers() === 0) {
333
+ const pw = process.env.NODESIGN_AUTH_PASSWORD || '';
334
+ if (pw) {
335
+ const created = createUser({ username: 'admin', password: pw, role: 'admin' });
336
+ admin = db.prepare('SELECT * FROM users WHERE id = ?').get(created.id);
337
+ console.log('[auth] bootstrap: 用 NODESIGN_AUTH_PASSWORD 创建了 admin 账号(用户名 admin)');
338
+ } else {
339
+ console.warn('[auth] users 表为空且未配置 NODESIGN_AUTH_PASSWORD —— 登录墙关闭(仅限本地开发)');
340
+ }
341
+ }
342
+ if (admin) {
343
+ // 存量项目回填归属(owner_id 列由 projects/store.js 幂等 ALTER 加上)
344
+ const r = db.prepare('UPDATE projects SET owner_id = ? WHERE owner_id IS NULL').run(admin.id);
345
+ if (r.changes > 0) console.log(`[auth] bootstrap: ${r.changes} 个存量项目归属到 admin`);
346
+ }
347
+ return admin ? rowToUser(admin) : null;
348
+ }