@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,291 +1,291 @@
1
- /**
2
- * server/lib/danbooru-tags.js — danbooru 标签收录量校验 + 查询(2026-08-18 起)
3
- *
4
- * ## 为什么需要
5
- *
6
- * danbooru 系模型(noobai / noobai-eps / pony)吃的是 danbooru 标签。标签写错
7
- * **不会报错,只会静默失效** —— 模型收到的等于空气。一个 agent 在一轮长时间
8
- * 配图里因此烧掉至少五轮出图,每次都是"图不对 → 用户反馈 → 去查标签 → 发现
9
- * 根本没有这个词"。
10
- *
11
- * 最典型的一次:连续三张图里"妆花了"完全不生效,用户反复说"她太干净了",
12
- * 而 `running_makeup` / `smeared_eyeliner` / `dried_tears` / `ruined_makeup`
13
- * 四个词**全是 0 收录**。真正有货的是 tears(29万) / crying_with_eyes_open(5.4万)
14
- * / smeared_lipstick / dirty_face。
15
- *
16
- * ⭐ **判据是 post_count 不是"标签存不存在"**:`running_makeup` 在 danbooru 上
17
- * 确实有这个条目,`post_count` 是 0 —— 存在但没有一张图用过它,训练里等于不存在。
18
- * 只查"有没有这个条目"会漏掉整类问题(实测过)。
19
- *
20
- * ## 两个入口,一份逻辑(2026-08-20)
21
- *
22
- * - `lintTags`:paint_still **画完之后**的体检 —— 收录量 + 「这段像句子不像标签」
23
- * + 「带下划线」两项本地判定(不联网也能说)。
24
- * - `lookupTags`:`lookup_tags` 工具,**画之前**查 —— 同一套收录量判定,外加
25
- * 弱词自动给候选替换(danbooru 通配搜索按收录量排序)、自由词搜索、wiki 释义。
26
- * 目的是把"先理解用户意思 → 查真实标签 → 再画"的纪律变得比 curl 便宜。
27
- *
28
- * ## 纪律
29
- *
30
- * - **fail-open**:查不到、超时、断网一律不挡出图。这是一个提示,不是闸门。
31
- * - **只提示不改写**:绝不替 agent 改 prompt —— 「逐字传递」是既有纪律,
32
- * 而且替换词的选择是创作判断,不是查表能定的。
33
- */
34
-
35
- const API = 'https://danbooru.donmai.us';
36
- const BATCH = 100;
37
- const LOW = 1000;
38
- const UA = { 'User-Agent': 'nodesign-tag-lint' };
39
-
40
- /** 进程内缓存。标签收录量是慢变量,一个进程生命周期内不用重查。 */
41
- const cache = new Map(); // name → post_count(-1 = 查过但 danbooru 上没有这个条目)
42
-
43
- // 句子判定用的功能词。⚠️ 刻意不收 of/with/on/in/own:danbooru 真标签里满是
44
- // `hand_on_own_chest` / `playing_with_own_hair` / `cup_of_tea`,收了就是误伤。
45
- const STOPWORDS = new Set(['a', 'an', 'the', 'and', 'is', 'are', 'was', 'were',
46
- 'who', 'which', 'she', 'he', 'they', 'her', 'his', 'their', 'being', 'very']);
47
-
48
- /** 把 prompt 切成逗号片段(剥 <lora>、BREAK),保留原文便于回显。 */
49
- function splitFragments(text) {
50
- if (!text || typeof text !== 'string') return [];
51
- return String(text)
52
- .replace(/<[^>]*>/g, ' ')
53
- .replace(/\bBREAK\b/gi, ',')
54
- .split(',')
55
- .map(s => s.trim())
56
- .filter(Boolean);
57
- }
58
-
59
- /** 片段是否像自然语言句子而不是标签(模型基本不认这种输入)。 */
60
- export function isSentenceLike(fragment) {
61
- const bare = fragment.replace(/[()[\]{}]/g, '').replace(/:\s*[\d.]+\s*$/, '').trim();
62
- if (!bare) return false;
63
- if (/[぀-ヿ㐀-鿿]/.test(bare)) return true; // 中日文
64
- const words = bare.toLowerCase().split(/[\s_]+/).filter(Boolean);
65
- if (words.length >= 6) return true;
66
- return words.length >= 3 && words.some(w => STOPWORDS.has(w));
67
- }
68
-
69
- /**
70
- * 本地判定(不联网):哪些片段像句子、哪些带下划线。
71
- * @returns {{sentences:string[], underscored:string[]}}
72
- */
73
- export function classifyFragments(text) {
74
- const sentences = [], underscored = [];
75
- for (const f of splitFragments(text)) {
76
- if (isSentenceLike(f)) sentences.push(f);
77
- else if (f.includes('_')) underscored.push(f);
78
- }
79
- return { sentences, underscored };
80
- }
81
-
82
- function balanced(s) {
83
- let d = 0;
84
- for (const ch of s) { if (ch === '(') d++; else if (ch === ')') { d--; if (d < 0) return false; } }
85
- return d === 0;
86
- }
87
-
88
- /**
89
- * 标签名归一成 danbooru 的 name:剥反斜杠转义、`(tag:1.2)` 权重、纯强调用的外层括号、
90
- * `artist:` 一类前缀;空格转下划线。⚠️ 名字自带的括号要留:`lucy \(cyberpunk\)` → `lucy_(cyberpunk)`。
91
- */
92
- export function normalizeTag(raw) {
93
- let s = String(raw).replace(/\\/g, '').trim();
94
- s = s.replace(/:\s*[\d.]+\s*(\)*)\s*$/, '$1'); // (tag:1.2) → (tag)
95
- while (/^[([{][\s\S]*[)\]}]$/.test(s)) { // 外层强调括号
96
- const inner = s.slice(1, -1).trim();
97
- if (!balanced(inner)) break;
98
- s = inner;
99
- }
100
- return s.replace(/[[\]{}]/g, '')
101
- .toLowerCase()
102
- .replace(/^(artist|character|copyright|meta|general):/, '')
103
- .trim().replace(/\s+/g, '_');
104
- }
105
-
106
- /**
107
- * 从一段 danbooru 风格 prompt 里抠出标签名(像句子的片段跳过 —— 它们另有去处)。
108
- */
109
- export function extractTags(text) {
110
- return splitFragments(text)
111
- .filter(f => !isSentenceLike(f))
112
- .map(normalizeTag)
113
- .filter(t => t
114
- && t.length >= 3 && t.length <= 60
115
- && /^[a-z0-9_\-'.()!?&+]+$/.test(t) // 中文/自然语言句子不是 danbooru 标签,跳过
116
- && !/^\d+$/.test(t));
117
- }
118
-
119
- async function getJson(url, timeoutMs) {
120
- const ac = new AbortController();
121
- const timer = setTimeout(() => ac.abort(), timeoutMs);
122
- try {
123
- const res = await fetch(url, { signal: ac.signal, headers: UA });
124
- if (!res.ok) return null;
125
- return await res.json();
126
- } catch {
127
- return null;
128
- } finally {
129
- clearTimeout(timer);
130
- }
131
- }
132
-
133
- async function fetchCounts(names, timeoutMs) {
134
- const out = new Map();
135
- for (let i = 0; i < names.length; i += BATCH) {
136
- const chunk = names.slice(i, i + BATCH);
137
- const rows = await getJson(`${API}/tags.json?search%5Bname_comma%5D=${encodeURIComponent(chunk.join(','))}&limit=${BATCH * 2}`, timeoutMs);
138
- if (!rows) return null; // fail-open
139
- for (const row of rows) if (row?.name) out.set(row.name, Number(row.post_count) || 0);
140
- for (const n of chunk) if (!out.has(n)) out.set(n, -1); // danbooru 上没这条目
141
- }
142
- return out;
143
- }
144
-
145
- async function countsFor(names, timeoutMs) {
146
- const need = names.filter(n => !cache.has(n));
147
- if (need.length) {
148
- const fetched = await fetchCounts(need, timeoutMs);
149
- if (!fetched) return false;
150
- for (const [k, v] of fetched) cache.set(k, v);
151
- }
152
- return true;
153
- }
154
-
155
- function bucket(names) {
156
- const zero = [], low = [], missing = [], ok = [];
157
- for (const n of names) {
158
- const c = cache.get(n);
159
- if (c == null) continue;
160
- if (c === -1) missing.push(n);
161
- else if (c === 0) zero.push(n);
162
- else if (c < LOW) low.push([n, c]);
163
- else ok.push([n, c]);
164
- }
165
- return { zero, low, missing, ok };
166
- }
167
-
168
- /**
169
- * @param {string[]} texts prompt / negative 原文
170
- * @returns {Promise<{zero:string[], low:Array<[string,number]>, missing:string[], checked:number,
171
- * sentences:string[], underscored:string[], offline:boolean}|null>}
172
- * null = 既查不了也没什么本地可说的(调用方什么都不要说)
173
- */
174
- export async function lintTags(texts, { timeoutMs = 3000 } = {}) {
175
- const local = { sentences: [], underscored: [] };
176
- for (const t of texts) {
177
- const c = classifyFragments(t);
178
- local.sentences.push(...c.sentences);
179
- local.underscored.push(...c.underscored);
180
- }
181
- local.sentences = [...new Set(local.sentences)];
182
- local.underscored = [...new Set(local.underscored)];
183
-
184
- const names = [...new Set(texts.flatMap(extractTags))];
185
- const online = names.length ? await countsFor(names, timeoutMs) : true;
186
- const b = online ? bucket(names) : { zero: [], low: [], missing: [] };
187
- const r = { ...b, ok: undefined, checked: online ? names.length : 0, ...local, offline: !online };
188
- delete r.ok;
189
- if (!r.zero.length && !r.low.length && !r.missing.length && !r.sentences.length && !r.underscored.length) return null;
190
- return r;
191
- }
192
-
193
- /** 把 lint 结果写成给 agent 看的一段话。没什么可说时返回 null。 */
194
- export function formatTagLint(r) {
195
- if (!r) return null;
196
- const parts = [];
197
- if (r.sentences?.length) {
198
- parts.push('像句子不像标签(模型基本不认这种输入,还摊薄其它标签的权重 —— 拆成标签):'
199
- + r.sentences.map(s => `"${s}"`).join(' · '));
200
- }
201
- if (r.underscored?.length) parts.push(`带下划线(手册要求写成空格):${r.underscored.join(', ')}`);
202
- if (r.missing?.length) parts.push(`不存在的标签:${r.missing.join(', ')}`);
203
- if (r.zero?.length) parts.push(`0 收录(存在但训练里等于没有):${r.zero.join(', ')}`);
204
- if (r.low?.length) {
205
- parts.push(`低收录(<${LOW},弱但不是禁区:主流需求换掉,用户要的本来就偏门就照用、配近义词加固):`
206
- + r.low.map(([n, c]) => `${n}(${c})`).join(', '));
207
- }
208
- if (!parts.length) return null;
209
- const head = r.offline ? '⚠️ 标签体检(danbooru 查不到,只做了本地判定):' : `⚠️ 标签体检(查了 ${r.checked} 个):`;
210
- return `${head}\n ${parts.join('\n ')}\n`
211
- + ' 这些词模型收不到,等于没写 —— 用 lookup_tags 换成有收录量的词再滚一次,别按这批图判断方向。';
212
- }
213
-
214
- // ── 画之前查:lookup_tags ──
215
-
216
- /** 从一个弱标签里挑最多两个值得拿去通配搜索的词(最长的非功能词优先)。 */
217
- function searchWordsOf(tag) {
218
- const words = tag.split(/[_\s\-()]+/).filter(w => w.length >= 3 && !STOPWORDS.has(w)
219
- && !['own', 'with', 'and', 'from', 'the', 'out', 'off'].includes(w));
220
- return [...new Set(words)].sort((a, b) => b.length - a.length).slice(0, 2);
221
- }
222
-
223
- /** 弱标签 → 候选:每个关键词各搜一次,合并按收录量排,太弱的(<100)不当候选。 */
224
- async function candidatesFor(tag, timeoutMs) {
225
- const seen = new Map();
226
- for (const w of searchWordsOf(tag)) {
227
- const hits = await wildcard(w, timeoutMs, 6);
228
- for (const [n, c] of hits || []) if (n !== tag && c >= 100) seen.set(n, c);
229
- }
230
- return [...seen].sort((a, b) => b[1] - a[1]).slice(0, 8);
231
- }
232
-
233
- async function wildcard(term, timeoutMs, limit = 8) {
234
- const q = `*${normalizeTag(term).replace(/^\*+|\*+$/g, '')}*`;
235
- const rows = await getJson(`${API}/tags.json?search%5Bname_matches%5D=${encodeURIComponent(q)}`
236
- + `&search%5Border%5D=count&search%5Bhide_empty%5D=true&limit=${limit}`, timeoutMs);
237
- if (!rows) return null;
238
- return rows.map(r => [r.name, Number(r.post_count) || 0]);
239
- }
240
-
241
- async function wikiOf(tag, timeoutMs) {
242
- const rows = await getJson(`${API}/wiki_pages.json?search%5Btitle%5D=${encodeURIComponent(normalizeTag(tag))}&only=title,body`, timeoutMs);
243
- const body = rows?.[0]?.body;
244
- if (!body) return null;
245
- return body.replace(/\[\[([^\]|]+)(\|[^\]]*)?\]\]/g, '$1').replace(/\s+/g, ' ').trim().slice(0, 220);
246
- }
247
-
248
- /**
249
- * @param {{tags?:string[], search?:string[], explain?:string[]}} q
250
- * @returns {Promise<{offline:boolean, tags:{ok:Array, low:Array, zero:string[], missing:string[]},
251
- * suggestions:Array<[string, Array<[string,number]>]>, searches:Array<[string, Array<[string,number]>|null]>,
252
- * wiki:Array<[string,string|null]>}>}
253
- */
254
- export async function lookupTags({ tags = [], search = [], explain = [] } = {}, { timeoutMs = 5000 } = {}) {
255
- const names = [...new Set(tags.map(normalizeTag).filter(Boolean))].slice(0, 60);
256
- const online = names.length ? await countsFor(names, timeoutMs) : true;
257
- const b = online ? bucket(names) : { ok: [], low: [], zero: [], missing: [] };
258
-
259
- const suggestions = [];
260
- if (online) {
261
- const weak = [...b.missing, ...b.zero, ...b.low.map(([n]) => n)].slice(0, 8);
262
- for (const w of weak) {
263
- const hits = await candidatesFor(w, timeoutMs);
264
- if (hits.length) suggestions.push([w, hits]);
265
- }
266
- }
267
- const searches = [];
268
- for (const s of [...new Set(search)].slice(0, 10)) searches.push([s, await wildcard(s, timeoutMs, 10)]);
269
- const wiki = [];
270
- for (const t of [...new Set(explain)].slice(0, 8)) wiki.push([t, await wikiOf(t, timeoutMs)]);
271
- return { offline: !online, tags: b, suggestions, searches, wiki };
272
- }
273
-
274
- /** lookup_tags 的文本输出。 */
275
- export function formatLookup(r) {
276
- const fmt = (arr) => arr.map(([n, c]) => `${n}(${c})`).join(', ');
277
- const out = [];
278
- if (r.offline) out.push('⚠️ danbooru 暂时查不到(超时/断网),收录量这部分空着;别因此停工,照手册词表写。');
279
- const t = r.tags;
280
- if (t.ok?.length) out.push(`✅ 可用(≥${LOW}):${fmt(t.ok)}`);
281
- if (t.low?.length) out.push(`⚠️ 低收录(<${LOW}):${fmt(t.low)} —— 弱但不是禁区:主流需求换成候选,用户要的本来就偏门就照用、配近义词加固`);
282
- if (t.zero?.length) out.push(`✗ 0 收录(等于不存在):${t.zero.join(', ')}`);
283
- if (t.missing?.length) out.push(`✗ 不存在:${t.missing.join(', ')}`);
284
- for (const [w, hits] of r.suggestions) out.push(` ↳ ${w} 的候选(按收录量):${fmt(hits) || '(没搜到相近的)'}`);
285
- for (const [s, hits] of r.searches) {
286
- out.push(`🔍 *${s}*:${hits == null ? '(查不到)' : hits.length ? fmt(hits) : '(没有匹配的标签)'}`);
287
- }
288
- for (const [tag, body] of r.wiki) out.push(`📖 ${tag}:${body || '(没有 wiki)'}`);
289
- if (!out.length) out.push('(什么都没查 —— tags / search / explain 至少给一个)');
290
- return out.join('\n');
291
- }
1
+ /**
2
+ * server/lib/danbooru-tags.js — danbooru 标签收录量校验 + 查询(2026-08-18 起)
3
+ *
4
+ * ## 为什么需要
5
+ *
6
+ * danbooru 系模型(noobai / noobai-eps / pony)吃的是 danbooru 标签。标签写错
7
+ * **不会报错,只会静默失效** —— 模型收到的等于空气。一个 agent 在一轮长时间
8
+ * 配图里因此烧掉至少五轮出图,每次都是"图不对 → 用户反馈 → 去查标签 → 发现
9
+ * 根本没有这个词"。
10
+ *
11
+ * 最典型的一次:连续三张图里"妆花了"完全不生效,用户反复说"她太干净了",
12
+ * 而 `running_makeup` / `smeared_eyeliner` / `dried_tears` / `ruined_makeup`
13
+ * 四个词**全是 0 收录**。真正有货的是 tears(29万) / crying_with_eyes_open(5.4万)
14
+ * / smeared_lipstick / dirty_face。
15
+ *
16
+ * ⭐ **判据是 post_count 不是"标签存不存在"**:`running_makeup` 在 danbooru 上
17
+ * 确实有这个条目,`post_count` 是 0 —— 存在但没有一张图用过它,训练里等于不存在。
18
+ * 只查"有没有这个条目"会漏掉整类问题(实测过)。
19
+ *
20
+ * ## 两个入口,一份逻辑(2026-08-20)
21
+ *
22
+ * - `lintTags`:paint_still **画完之后**的体检 —— 收录量 + 「这段像句子不像标签」
23
+ * + 「带下划线」两项本地判定(不联网也能说)。
24
+ * - `lookupTags`:`lookup_tags` 工具,**画之前**查 —— 同一套收录量判定,外加
25
+ * 弱词自动给候选替换(danbooru 通配搜索按收录量排序)、自由词搜索、wiki 释义。
26
+ * 目的是把"先理解用户意思 → 查真实标签 → 再画"的纪律变得比 curl 便宜。
27
+ *
28
+ * ## 纪律
29
+ *
30
+ * - **fail-open**:查不到、超时、断网一律不挡出图。这是一个提示,不是闸门。
31
+ * - **只提示不改写**:绝不替 agent 改 prompt —— 「逐字传递」是既有纪律,
32
+ * 而且替换词的选择是创作判断,不是查表能定的。
33
+ */
34
+
35
+ const API = 'https://danbooru.donmai.us';
36
+ const BATCH = 100;
37
+ const LOW = 1000;
38
+ const UA = { 'User-Agent': 'nodesign-tag-lint' };
39
+
40
+ /** 进程内缓存。标签收录量是慢变量,一个进程生命周期内不用重查。 */
41
+ const cache = new Map(); // name → post_count(-1 = 查过但 danbooru 上没有这个条目)
42
+
43
+ // 句子判定用的功能词。⚠️ 刻意不收 of/with/on/in/own:danbooru 真标签里满是
44
+ // `hand_on_own_chest` / `playing_with_own_hair` / `cup_of_tea`,收了就是误伤。
45
+ const STOPWORDS = new Set(['a', 'an', 'the', 'and', 'is', 'are', 'was', 'were',
46
+ 'who', 'which', 'she', 'he', 'they', 'her', 'his', 'their', 'being', 'very']);
47
+
48
+ /** 把 prompt 切成逗号片段(剥 <lora>、BREAK),保留原文便于回显。 */
49
+ function splitFragments(text) {
50
+ if (!text || typeof text !== 'string') return [];
51
+ return String(text)
52
+ .replace(/<[^>]*>/g, ' ')
53
+ .replace(/\bBREAK\b/gi, ',')
54
+ .split(',')
55
+ .map(s => s.trim())
56
+ .filter(Boolean);
57
+ }
58
+
59
+ /** 片段是否像自然语言句子而不是标签(模型基本不认这种输入)。 */
60
+ export function isSentenceLike(fragment) {
61
+ const bare = fragment.replace(/[()[\]{}]/g, '').replace(/:\s*[\d.]+\s*$/, '').trim();
62
+ if (!bare) return false;
63
+ if (/[぀-ヿ㐀-鿿]/.test(bare)) return true; // 中日文
64
+ const words = bare.toLowerCase().split(/[\s_]+/).filter(Boolean);
65
+ if (words.length >= 6) return true;
66
+ return words.length >= 3 && words.some(w => STOPWORDS.has(w));
67
+ }
68
+
69
+ /**
70
+ * 本地判定(不联网):哪些片段像句子、哪些带下划线。
71
+ * @returns {{sentences:string[], underscored:string[]}}
72
+ */
73
+ export function classifyFragments(text) {
74
+ const sentences = [], underscored = [];
75
+ for (const f of splitFragments(text)) {
76
+ if (isSentenceLike(f)) sentences.push(f);
77
+ else if (f.includes('_')) underscored.push(f);
78
+ }
79
+ return { sentences, underscored };
80
+ }
81
+
82
+ function balanced(s) {
83
+ let d = 0;
84
+ for (const ch of s) { if (ch === '(') d++; else if (ch === ')') { d--; if (d < 0) return false; } }
85
+ return d === 0;
86
+ }
87
+
88
+ /**
89
+ * 标签名归一成 danbooru 的 name:剥反斜杠转义、`(tag:1.2)` 权重、纯强调用的外层括号、
90
+ * `artist:` 一类前缀;空格转下划线。⚠️ 名字自带的括号要留:`lucy \(cyberpunk\)` → `lucy_(cyberpunk)`。
91
+ */
92
+ export function normalizeTag(raw) {
93
+ let s = String(raw).replace(/\\/g, '').trim();
94
+ s = s.replace(/:\s*[\d.]+\s*(\)*)\s*$/, '$1'); // (tag:1.2) → (tag)
95
+ while (/^[([{][\s\S]*[)\]}]$/.test(s)) { // 外层强调括号
96
+ const inner = s.slice(1, -1).trim();
97
+ if (!balanced(inner)) break;
98
+ s = inner;
99
+ }
100
+ return s.replace(/[[\]{}]/g, '')
101
+ .toLowerCase()
102
+ .replace(/^(artist|character|copyright|meta|general):/, '')
103
+ .trim().replace(/\s+/g, '_');
104
+ }
105
+
106
+ /**
107
+ * 从一段 danbooru 风格 prompt 里抠出标签名(像句子的片段跳过 —— 它们另有去处)。
108
+ */
109
+ export function extractTags(text) {
110
+ return splitFragments(text)
111
+ .filter(f => !isSentenceLike(f))
112
+ .map(normalizeTag)
113
+ .filter(t => t
114
+ && t.length >= 3 && t.length <= 60
115
+ && /^[a-z0-9_\-'.()!?&+]+$/.test(t) // 中文/自然语言句子不是 danbooru 标签,跳过
116
+ && !/^\d+$/.test(t));
117
+ }
118
+
119
+ async function getJson(url, timeoutMs) {
120
+ const ac = new AbortController();
121
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
122
+ try {
123
+ const res = await fetch(url, { signal: ac.signal, headers: UA });
124
+ if (!res.ok) return null;
125
+ return await res.json();
126
+ } catch {
127
+ return null;
128
+ } finally {
129
+ clearTimeout(timer);
130
+ }
131
+ }
132
+
133
+ async function fetchCounts(names, timeoutMs) {
134
+ const out = new Map();
135
+ for (let i = 0; i < names.length; i += BATCH) {
136
+ const chunk = names.slice(i, i + BATCH);
137
+ const rows = await getJson(`${API}/tags.json?search%5Bname_comma%5D=${encodeURIComponent(chunk.join(','))}&limit=${BATCH * 2}`, timeoutMs);
138
+ if (!rows) return null; // fail-open
139
+ for (const row of rows) if (row?.name) out.set(row.name, Number(row.post_count) || 0);
140
+ for (const n of chunk) if (!out.has(n)) out.set(n, -1); // danbooru 上没这条目
141
+ }
142
+ return out;
143
+ }
144
+
145
+ async function countsFor(names, timeoutMs) {
146
+ const need = names.filter(n => !cache.has(n));
147
+ if (need.length) {
148
+ const fetched = await fetchCounts(need, timeoutMs);
149
+ if (!fetched) return false;
150
+ for (const [k, v] of fetched) cache.set(k, v);
151
+ }
152
+ return true;
153
+ }
154
+
155
+ function bucket(names) {
156
+ const zero = [], low = [], missing = [], ok = [];
157
+ for (const n of names) {
158
+ const c = cache.get(n);
159
+ if (c == null) continue;
160
+ if (c === -1) missing.push(n);
161
+ else if (c === 0) zero.push(n);
162
+ else if (c < LOW) low.push([n, c]);
163
+ else ok.push([n, c]);
164
+ }
165
+ return { zero, low, missing, ok };
166
+ }
167
+
168
+ /**
169
+ * @param {string[]} texts prompt / negative 原文
170
+ * @returns {Promise<{zero:string[], low:Array<[string,number]>, missing:string[], checked:number,
171
+ * sentences:string[], underscored:string[], offline:boolean}|null>}
172
+ * null = 既查不了也没什么本地可说的(调用方什么都不要说)
173
+ */
174
+ export async function lintTags(texts, { timeoutMs = 3000 } = {}) {
175
+ const local = { sentences: [], underscored: [] };
176
+ for (const t of texts) {
177
+ const c = classifyFragments(t);
178
+ local.sentences.push(...c.sentences);
179
+ local.underscored.push(...c.underscored);
180
+ }
181
+ local.sentences = [...new Set(local.sentences)];
182
+ local.underscored = [...new Set(local.underscored)];
183
+
184
+ const names = [...new Set(texts.flatMap(extractTags))];
185
+ const online = names.length ? await countsFor(names, timeoutMs) : true;
186
+ const b = online ? bucket(names) : { zero: [], low: [], missing: [] };
187
+ const r = { ...b, ok: undefined, checked: online ? names.length : 0, ...local, offline: !online };
188
+ delete r.ok;
189
+ if (!r.zero.length && !r.low.length && !r.missing.length && !r.sentences.length && !r.underscored.length) return null;
190
+ return r;
191
+ }
192
+
193
+ /** 把 lint 结果写成给 agent 看的一段话。没什么可说时返回 null。 */
194
+ export function formatTagLint(r) {
195
+ if (!r) return null;
196
+ const parts = [];
197
+ if (r.sentences?.length) {
198
+ parts.push('像句子不像标签(模型基本不认这种输入,还摊薄其它标签的权重 —— 拆成标签):'
199
+ + r.sentences.map(s => `"${s}"`).join(' · '));
200
+ }
201
+ if (r.underscored?.length) parts.push(`带下划线(手册要求写成空格):${r.underscored.join(', ')}`);
202
+ if (r.missing?.length) parts.push(`不存在的标签:${r.missing.join(', ')}`);
203
+ if (r.zero?.length) parts.push(`0 收录(存在但训练里等于没有):${r.zero.join(', ')}`);
204
+ if (r.low?.length) {
205
+ parts.push(`低收录(<${LOW},弱但不是禁区:主流需求换掉,用户要的本来就偏门就照用、配近义词加固):`
206
+ + r.low.map(([n, c]) => `${n}(${c})`).join(', '));
207
+ }
208
+ if (!parts.length) return null;
209
+ const head = r.offline ? '⚠️ 标签体检(danbooru 查不到,只做了本地判定):' : `⚠️ 标签体检(查了 ${r.checked} 个):`;
210
+ return `${head}\n ${parts.join('\n ')}\n`
211
+ + ' 这些词模型收不到,等于没写 —— 用 lookup_tags 换成有收录量的词再滚一次,别按这批图判断方向。';
212
+ }
213
+
214
+ // ── 画之前查:lookup_tags ──
215
+
216
+ /** 从一个弱标签里挑最多两个值得拿去通配搜索的词(最长的非功能词优先)。 */
217
+ function searchWordsOf(tag) {
218
+ const words = tag.split(/[_\s\-()]+/).filter(w => w.length >= 3 && !STOPWORDS.has(w)
219
+ && !['own', 'with', 'and', 'from', 'the', 'out', 'off'].includes(w));
220
+ return [...new Set(words)].sort((a, b) => b.length - a.length).slice(0, 2);
221
+ }
222
+
223
+ /** 弱标签 → 候选:每个关键词各搜一次,合并按收录量排,太弱的(<100)不当候选。 */
224
+ async function candidatesFor(tag, timeoutMs) {
225
+ const seen = new Map();
226
+ for (const w of searchWordsOf(tag)) {
227
+ const hits = await wildcard(w, timeoutMs, 6);
228
+ for (const [n, c] of hits || []) if (n !== tag && c >= 100) seen.set(n, c);
229
+ }
230
+ return [...seen].sort((a, b) => b[1] - a[1]).slice(0, 8);
231
+ }
232
+
233
+ async function wildcard(term, timeoutMs, limit = 8) {
234
+ const q = `*${normalizeTag(term).replace(/^\*+|\*+$/g, '')}*`;
235
+ const rows = await getJson(`${API}/tags.json?search%5Bname_matches%5D=${encodeURIComponent(q)}`
236
+ + `&search%5Border%5D=count&search%5Bhide_empty%5D=true&limit=${limit}`, timeoutMs);
237
+ if (!rows) return null;
238
+ return rows.map(r => [r.name, Number(r.post_count) || 0]);
239
+ }
240
+
241
+ async function wikiOf(tag, timeoutMs) {
242
+ const rows = await getJson(`${API}/wiki_pages.json?search%5Btitle%5D=${encodeURIComponent(normalizeTag(tag))}&only=title,body`, timeoutMs);
243
+ const body = rows?.[0]?.body;
244
+ if (!body) return null;
245
+ return body.replace(/\[\[([^\]|]+)(\|[^\]]*)?\]\]/g, '$1').replace(/\s+/g, ' ').trim().slice(0, 220);
246
+ }
247
+
248
+ /**
249
+ * @param {{tags?:string[], search?:string[], explain?:string[]}} q
250
+ * @returns {Promise<{offline:boolean, tags:{ok:Array, low:Array, zero:string[], missing:string[]},
251
+ * suggestions:Array<[string, Array<[string,number]>]>, searches:Array<[string, Array<[string,number]>|null]>,
252
+ * wiki:Array<[string,string|null]>}>}
253
+ */
254
+ export async function lookupTags({ tags = [], search = [], explain = [] } = {}, { timeoutMs = 5000 } = {}) {
255
+ const names = [...new Set(tags.map(normalizeTag).filter(Boolean))].slice(0, 60);
256
+ const online = names.length ? await countsFor(names, timeoutMs) : true;
257
+ const b = online ? bucket(names) : { ok: [], low: [], zero: [], missing: [] };
258
+
259
+ const suggestions = [];
260
+ if (online) {
261
+ const weak = [...b.missing, ...b.zero, ...b.low.map(([n]) => n)].slice(0, 8);
262
+ for (const w of weak) {
263
+ const hits = await candidatesFor(w, timeoutMs);
264
+ if (hits.length) suggestions.push([w, hits]);
265
+ }
266
+ }
267
+ const searches = [];
268
+ for (const s of [...new Set(search)].slice(0, 10)) searches.push([s, await wildcard(s, timeoutMs, 10)]);
269
+ const wiki = [];
270
+ for (const t of [...new Set(explain)].slice(0, 8)) wiki.push([t, await wikiOf(t, timeoutMs)]);
271
+ return { offline: !online, tags: b, suggestions, searches, wiki };
272
+ }
273
+
274
+ /** lookup_tags 的文本输出。 */
275
+ export function formatLookup(r) {
276
+ const fmt = (arr) => arr.map(([n, c]) => `${n}(${c})`).join(', ');
277
+ const out = [];
278
+ if (r.offline) out.push('⚠️ danbooru 暂时查不到(超时/断网),收录量这部分空着;别因此停工,照手册词表写。');
279
+ const t = r.tags;
280
+ if (t.ok?.length) out.push(`✅ 可用(≥${LOW}):${fmt(t.ok)}`);
281
+ if (t.low?.length) out.push(`⚠️ 低收录(<${LOW}):${fmt(t.low)} —— 弱但不是禁区:主流需求换成候选,用户要的本来就偏门就照用、配近义词加固`);
282
+ if (t.zero?.length) out.push(`✗ 0 收录(等于不存在):${t.zero.join(', ')}`);
283
+ if (t.missing?.length) out.push(`✗ 不存在:${t.missing.join(', ')}`);
284
+ for (const [w, hits] of r.suggestions) out.push(` ↳ ${w} 的候选(按收录量):${fmt(hits) || '(没搜到相近的)'}`);
285
+ for (const [s, hits] of r.searches) {
286
+ out.push(`🔍 *${s}*:${hits == null ? '(查不到)' : hits.length ? fmt(hits) : '(没有匹配的标签)'}`);
287
+ }
288
+ for (const [tag, body] of r.wiki) out.push(`📖 ${tag}:${body || '(没有 wiki)'}`);
289
+ if (!out.length) out.push('(什么都没查 —— tags / search / explain 至少给一个)');
290
+ return out.join('\n');
291
+ }