@musnows/scriverse 0.3.11 → 0.4.0

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 (560) hide show
  1. package/README.en.md +2 -0
  2. package/README.md +2 -0
  3. package/dist/ai.js +292 -19
  4. package/dist/ai.js.map +1 -1
  5. package/dist/app.js +45 -4
  6. package/dist/app.js.map +1 -1
  7. package/dist/database.js +47 -0
  8. package/dist/database.js.map +1 -1
  9. package/dist/public/app.js +826 -173
  10. package/dist/public/character-markdown.js +12 -0
  11. package/dist/public/index.html +57 -26
  12. package/dist/public/model-config.d.ts +1 -0
  13. package/dist/public/model-config.js +12 -3
  14. package/dist/public/module-layout.d.ts +17 -0
  15. package/dist/public/module-layout.js +28 -0
  16. package/dist/public/page-route.js +1 -1
  17. package/dist/public/styles.css +183 -60
  18. package/dist/public/vendor/vditor/dist/css/content-theme/ant-design.css +210 -0
  19. package/dist/public/vendor/vditor/dist/css/content-theme/dark.css +123 -0
  20. package/dist/public/vendor/vditor/dist/css/content-theme/light.css +101 -0
  21. package/dist/public/vendor/vditor/dist/css/content-theme/wechat.css +94 -0
  22. package/dist/public/vendor/vditor/dist/images/emoji/b3log.png +0 -0
  23. package/dist/public/vendor/vditor/dist/images/emoji/chainbook.png +0 -0
  24. package/dist/public/vendor/vditor/dist/images/emoji/doge.png +0 -0
  25. package/dist/public/vendor/vditor/dist/images/emoji/hacpai.png +0 -0
  26. package/dist/public/vendor/vditor/dist/images/emoji/huaji.gif +0 -0
  27. package/dist/public/vendor/vditor/dist/images/emoji/latke.png +0 -0
  28. package/dist/public/vendor/vditor/dist/images/emoji/liandi.png +0 -0
  29. package/dist/public/vendor/vditor/dist/images/emoji/lute.png +0 -0
  30. package/dist/public/vendor/vditor/dist/images/emoji/octocat.png +0 -0
  31. package/dist/public/vendor/vditor/dist/images/emoji/pipe.png +0 -0
  32. package/dist/public/vendor/vditor/dist/images/emoji/siyuan.png +0 -0
  33. package/dist/public/vendor/vditor/dist/images/emoji/solo.png +0 -0
  34. package/dist/public/vendor/vditor/dist/images/emoji/sym.png +0 -0
  35. package/dist/public/vendor/vditor/dist/images/emoji/trollface.png +0 -0
  36. package/dist/public/vendor/vditor/dist/images/emoji/vditor.png +0 -0
  37. package/dist/public/vendor/vditor/dist/images/emoji/wide.png +0 -0
  38. package/dist/public/vendor/vditor/dist/images/emoji/wulian.png +0 -0
  39. package/dist/public/vendor/vditor/dist/images/img-loading.svg +1 -0
  40. package/dist/public/vendor/vditor/dist/images/logo.png +0 -0
  41. package/dist/public/vendor/vditor/dist/index.css +1823 -0
  42. package/dist/public/vendor/vditor/dist/index.d.ts +82 -0
  43. package/dist/public/vendor/vditor/dist/index.js +15995 -0
  44. package/dist/public/vendor/vditor/dist/index.min.js +1 -0
  45. package/dist/public/vendor/vditor/dist/js/abcjs/abcjs_basic.min.js +3 -0
  46. package/dist/public/vendor/vditor/dist/js/echarts/echarts.min.js +45 -0
  47. package/dist/public/vendor/vditor/dist/js/flowchart.js/flowchart.min.js +8 -0
  48. package/dist/public/vendor/vditor/dist/js/graphviz/full.render.js +90 -0
  49. package/dist/public/vendor/vditor/dist/js/graphviz/viz.js +333 -0
  50. package/dist/public/vendor/vditor/dist/js/highlight.js/LICENSE +29 -0
  51. package/dist/public/vendor/vditor/dist/js/highlight.js/highlight.min.js +3835 -0
  52. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/a11y-dark.min.css +7 -0
  53. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/a11y-light.min.css +7 -0
  54. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/agate.min.css +20 -0
  55. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/an-old-hope.min.css +9 -0
  56. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/androidstudio.min.css +1 -0
  57. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/ant-design.min.css +84 -0
  58. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/arduino-light.min.css +1 -0
  59. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/arta.min.css +1 -0
  60. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/ascetic.min.css +1 -0
  61. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/atom-one-dark-reasonable.min.css +1 -0
  62. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/atom-one-dark.min.css +1 -0
  63. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/atom-one-light.min.css +1 -0
  64. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/3024.min.css +7 -0
  65. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/apathy.min.css +7 -0
  66. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/apprentice.min.css +7 -0
  67. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ashes.min.css +7 -0
  68. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-cave-light.min.css +7 -0
  69. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-cave.min.css +7 -0
  70. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-dune-light.min.css +7 -0
  71. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-dune.min.css +7 -0
  72. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-estuary-light.min.css +7 -0
  73. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-estuary.min.css +7 -0
  74. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-forest-light.min.css +7 -0
  75. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-forest.min.css +7 -0
  76. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-heath-light.min.css +7 -0
  77. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-heath.min.css +7 -0
  78. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-lakeside-light.min.css +7 -0
  79. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-lakeside.min.css +7 -0
  80. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-plateau-light.min.css +7 -0
  81. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-plateau.min.css +7 -0
  82. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-savanna-light.min.css +7 -0
  83. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-savanna.min.css +7 -0
  84. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-seaside-light.min.css +7 -0
  85. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-seaside.min.css +7 -0
  86. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-sulphurpool-light.min.css +7 -0
  87. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-sulphurpool.min.css +7 -0
  88. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atlas.min.css +7 -0
  89. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/bespin.min.css +7 -0
  90. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-bathory.min.css +7 -0
  91. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-burzum.min.css +7 -0
  92. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-dark-funeral.min.css +7 -0
  93. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-gorgoroth.min.css +7 -0
  94. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-immortal.min.css +7 -0
  95. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-khold.min.css +7 -0
  96. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-marduk.min.css +7 -0
  97. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-mayhem.min.css +7 -0
  98. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-nile.min.css +7 -0
  99. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-venom.min.css +7 -0
  100. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal.min.css +7 -0
  101. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/brewer.min.css +7 -0
  102. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/bright.min.css +7 -0
  103. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/brogrammer.min.css +7 -0
  104. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/brush-trees-dark.min.css +7 -0
  105. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/brush-trees.min.css +7 -0
  106. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/chalk.min.css +7 -0
  107. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/circus.min.css +7 -0
  108. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/classic-dark.min.css +7 -0
  109. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/classic-light.min.css +7 -0
  110. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/codeschool.min.css +7 -0
  111. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/colors.min.css +7 -0
  112. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/cupcake.min.css +7 -0
  113. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/cupertino.min.css +7 -0
  114. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/danqing.min.css +7 -0
  115. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/darcula.min.css +7 -0
  116. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/dark-violet.min.css +7 -0
  117. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/darkmoss.min.css +7 -0
  118. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/darktooth.min.css +7 -0
  119. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/decaf.min.css +7 -0
  120. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/default-dark.min.css +7 -0
  121. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/default-light.min.css +7 -0
  122. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/dirtysea.min.css +7 -0
  123. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/dracula.min.css +7 -0
  124. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/edge-dark.min.css +7 -0
  125. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/edge-light.min.css +7 -0
  126. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/eighties.min.css +7 -0
  127. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/embers.min.css +7 -0
  128. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/equilibrium-dark.min.css +7 -0
  129. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/equilibrium-gray-dark.min.css +7 -0
  130. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/equilibrium-gray-light.min.css +7 -0
  131. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/equilibrium-light.min.css +7 -0
  132. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/espresso.min.css +7 -0
  133. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/eva-dim.min.css +7 -0
  134. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/eva.min.css +7 -0
  135. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/flat.min.css +7 -0
  136. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/framer.min.css +7 -0
  137. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/fruit-soda.min.css +7 -0
  138. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gigavolt.min.css +7 -0
  139. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/github.min.css +7 -0
  140. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/google-dark.min.css +7 -0
  141. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/google-light.min.css +7 -0
  142. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/grayscale-dark.min.css +7 -0
  143. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/grayscale-light.min.css +7 -0
  144. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/green-screen.min.css +7 -0
  145. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gruvbox-dark-hard.min.css +7 -0
  146. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gruvbox-dark-medium.min.css +7 -0
  147. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gruvbox-dark-pale.min.css +7 -0
  148. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gruvbox-dark-soft.min.css +7 -0
  149. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gruvbox-light-hard.min.css +7 -0
  150. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gruvbox-light-medium.min.css +7 -0
  151. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gruvbox-light-soft.min.css +7 -0
  152. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/hardcore.min.css +7 -0
  153. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/harmonic16-dark.min.css +7 -0
  154. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/harmonic16-light.min.css +7 -0
  155. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/heetch-dark.min.css +7 -0
  156. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/heetch-light.min.css +7 -0
  157. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/helios.min.css +7 -0
  158. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/hopscotch.min.css +7 -0
  159. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/horizon-dark.min.css +7 -0
  160. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/horizon-light.min.css +7 -0
  161. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/humanoid-dark.min.css +7 -0
  162. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/humanoid-light.min.css +7 -0
  163. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ia-dark.min.css +7 -0
  164. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ia-light.min.css +7 -0
  165. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/icy-dark.min.css +7 -0
  166. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ir-black.min.css +7 -0
  167. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/isotope.min.css +7 -0
  168. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/kimber.min.css +7 -0
  169. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/london-tube.min.css +7 -0
  170. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/macintosh.min.css +7 -0
  171. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/marrakesh.min.css +7 -0
  172. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/materia.min.css +7 -0
  173. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/material-darker.min.css +7 -0
  174. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/material-lighter.min.css +7 -0
  175. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/material-palenight.min.css +7 -0
  176. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/material-vivid.min.css +7 -0
  177. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/material.min.css +7 -0
  178. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/mellow-purple.min.css +7 -0
  179. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/mexico-light.min.css +7 -0
  180. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/mocha.min.css +7 -0
  181. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/monokai.min.css +7 -0
  182. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/nebula.min.css +7 -0
  183. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/nord.min.css +7 -0
  184. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/nova.min.css +7 -0
  185. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ocean.min.css +7 -0
  186. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/oceanicnext.min.css +7 -0
  187. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/one-light.min.css +7 -0
  188. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/onedark.min.css +7 -0
  189. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/outrun-dark.min.css +7 -0
  190. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/papercolor-dark.min.css +7 -0
  191. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/papercolor-light.min.css +7 -0
  192. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/paraiso.min.css +7 -0
  193. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/pasque.min.css +7 -0
  194. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/phd.min.css +7 -0
  195. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/pico.min.css +7 -0
  196. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/pop.min.css +7 -0
  197. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/porple.min.css +7 -0
  198. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/qualia.min.css +7 -0
  199. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/railscasts.min.css +7 -0
  200. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/rebecca.min.css +7 -0
  201. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ros-pine-dawn.min.css +7 -0
  202. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ros-pine-moon.min.css +7 -0
  203. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ros-pine.min.css +7 -0
  204. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/sagelight.min.css +7 -0
  205. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/sandcastle.min.css +7 -0
  206. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/seti-ui.min.css +7 -0
  207. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/shapeshifter.min.css +7 -0
  208. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/silk-dark.min.css +7 -0
  209. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/silk-light.min.css +7 -0
  210. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/snazzy.min.css +7 -0
  211. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/solar-flare-light.min.css +7 -0
  212. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/solar-flare.min.css +7 -0
  213. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/solarized-dark.min.css +7 -0
  214. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/solarized-light.min.css +7 -0
  215. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/spacemacs.min.css +7 -0
  216. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/summercamp.min.css +7 -0
  217. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/summerfruit-dark.min.css +7 -0
  218. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/summerfruit-light.min.css +7 -0
  219. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/synth-midnight-terminal-dark.min.css +7 -0
  220. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/synth-midnight-terminal-light.min.css +7 -0
  221. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/tango.min.css +7 -0
  222. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/tender.min.css +7 -0
  223. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/tomorrow-night.min.css +7 -0
  224. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/tomorrow.min.css +7 -0
  225. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/twilight.min.css +7 -0
  226. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/unikitty-dark.min.css +7 -0
  227. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/unikitty-light.min.css +7 -0
  228. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/vulcan.min.css +7 -0
  229. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-10-light.min.css +7 -0
  230. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-10.min.css +7 -0
  231. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-95-light.min.css +7 -0
  232. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-95.min.css +7 -0
  233. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-high-contrast-light.min.css +7 -0
  234. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-high-contrast.min.css +7 -0
  235. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-nt-light.min.css +7 -0
  236. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-nt.min.css +7 -0
  237. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/woodland.min.css +7 -0
  238. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/xcode-dusk.min.css +7 -0
  239. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/zenburn.min.css +7 -0
  240. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/brown-paper.min.css +1 -0
  241. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/brown-papersq.png +0 -0
  242. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/codepen-embed.min.css +1 -0
  243. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/color-brewer.min.css +1 -0
  244. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/dark.min.css +1 -0
  245. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/default.min.css +9 -0
  246. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/devibeans.min.css +7 -0
  247. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/docco.min.css +1 -0
  248. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/far.min.css +1 -0
  249. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/felipec.min.css +7 -0
  250. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/foundation.min.css +1 -0
  251. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/github-dark-dimmed.min.css +9 -0
  252. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/github-dark.min.css +10 -0
  253. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/github.min.css +10 -0
  254. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/gml.min.css +1 -0
  255. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/googlecode.min.css +1 -0
  256. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/gradient-dark.min.css +1 -0
  257. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/gradient-light.min.css +1 -0
  258. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/grayscale.min.css +1 -0
  259. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/hybrid.min.css +1 -0
  260. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/idea.min.css +1 -0
  261. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/intellij-light.min.css +1 -0
  262. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/ir-black.min.css +1 -0
  263. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/isbl-editor-dark.min.css +1 -0
  264. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/isbl-editor-light.min.css +1 -0
  265. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/kimbie-dark.min.css +1 -0
  266. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/kimbie-light.min.css +1 -0
  267. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/lightfair.min.css +1 -0
  268. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/lioshi.min.css +1 -0
  269. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/magula.min.css +1 -0
  270. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/mono-blue.min.css +1 -0
  271. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/monokai-sublime.min.css +1 -0
  272. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/monokai.min.css +1 -0
  273. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/night-owl.min.css +1 -0
  274. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/nnfx-dark.min.css +10 -0
  275. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/nnfx-light.min.css +10 -0
  276. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/nord.min.css +1 -0
  277. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/obsidian.min.css +1 -0
  278. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/panda-syntax-dark.min.css +1 -0
  279. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/panda-syntax-light.min.css +1 -0
  280. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/paraiso-dark.min.css +1 -0
  281. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/paraiso-light.min.css +1 -0
  282. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/pojoaque.jpg +0 -0
  283. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/pojoaque.min.css +1 -0
  284. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/purebasic.min.css +1 -0
  285. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/qtcreator-dark.min.css +1 -0
  286. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/qtcreator-light.min.css +1 -0
  287. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/rainbow.min.css +1 -0
  288. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/routeros.min.css +1 -0
  289. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/school-book.min.css +1 -0
  290. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/shades-of-purple.min.css +1 -0
  291. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/srcery.min.css +1 -0
  292. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/stackoverflow-dark.min.css +13 -0
  293. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/stackoverflow-light.min.css +13 -0
  294. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/sunburst.min.css +1 -0
  295. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/tokyo-night-dark.min.css +8 -0
  296. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/tokyo-night-light.min.css +8 -0
  297. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/tomorrow-night-blue.min.css +1 -0
  298. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/tomorrow-night-bright.min.css +1 -0
  299. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/vs.min.css +1 -0
  300. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/vs2015.min.css +1 -0
  301. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/xcode.min.css +1 -0
  302. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/xt256.min.css +1 -0
  303. package/dist/public/vendor/vditor/dist/js/highlight.js/third-languages.js +135 -0
  304. package/dist/public/vendor/vditor/dist/js/i18n/de_DE.js +86 -0
  305. package/dist/public/vendor/vditor/dist/js/i18n/en_US.js +86 -0
  306. package/dist/public/vendor/vditor/dist/js/i18n/es_ES.js +86 -0
  307. package/dist/public/vendor/vditor/dist/js/i18n/fr_FR.js +86 -0
  308. package/dist/public/vendor/vditor/dist/js/i18n/ja_JP.js +86 -0
  309. package/dist/public/vendor/vditor/dist/js/i18n/ko_KR.js +86 -0
  310. package/dist/public/vendor/vditor/dist/js/i18n/pt_BR.js +86 -0
  311. package/dist/public/vendor/vditor/dist/js/i18n/ru_RU.js +86 -0
  312. package/dist/public/vendor/vditor/dist/js/i18n/sv_SE.js +86 -0
  313. package/dist/public/vendor/vditor/dist/js/i18n/vi_VN.js +86 -0
  314. package/dist/public/vendor/vditor/dist/js/i18n/zh_CN.js +86 -0
  315. package/dist/public/vendor/vditor/dist/js/i18n/zh_TW.js +86 -0
  316. package/dist/public/vendor/vditor/dist/js/icons/ant.js +174 -0
  317. package/dist/public/vendor/vditor/dist/js/icons/material.js +195 -0
  318. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
  319. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
  320. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  321. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  322. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  323. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  324. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  325. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  326. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  327. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  328. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  329. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  330. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  331. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  332. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  333. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
  334. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Bold.woff +0 -0
  335. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  336. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  337. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  338. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  339. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
  340. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Italic.woff +0 -0
  341. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  342. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
  343. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Regular.woff +0 -0
  344. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  345. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  346. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  347. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  348. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
  349. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Math-Italic.woff +0 -0
  350. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  351. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  352. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  353. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  354. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  355. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  356. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  357. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  358. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  359. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  360. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
  361. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Script-Regular.woff +0 -0
  362. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  363. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
  364. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
  365. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  366. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
  367. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
  368. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  369. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
  370. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
  371. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  372. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
  373. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
  374. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  375. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  376. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  377. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  378. package/dist/public/vendor/vditor/dist/js/katex/katex.min.css +1 -0
  379. package/dist/public/vendor/vditor/dist/js/katex/katex.min.js +1 -0
  380. package/dist/public/vendor/vditor/dist/js/katex/mhchem.min.js +1 -0
  381. package/dist/public/vendor/vditor/dist/js/lute/lute.min.js +72 -0
  382. package/dist/public/vendor/vditor/dist/js/markmap/katex.min.css +1 -0
  383. package/dist/public/vendor/vditor/dist/js/markmap/markmap.min.js +8251 -0
  384. package/dist/public/vendor/vditor/dist/js/markmap/prism.css +140 -0
  385. package/dist/public/vendor/vditor/dist/js/mathjax/LICENSE +202 -0
  386. package/dist/public/vendor/vditor/dist/js/mathjax/a11y/assistive-mml.js +1 -0
  387. package/dist/public/vendor/vditor/dist/js/mathjax/a11y/complexity.js +1 -0
  388. package/dist/public/vendor/vditor/dist/js/mathjax/a11y/explorer.js +1 -0
  389. package/dist/public/vendor/vditor/dist/js/mathjax/a11y/semantic-enrich.js +1 -0
  390. package/dist/public/vendor/vditor/dist/js/mathjax/input/asciimath.js +1 -0
  391. package/dist/public/vendor/vditor/dist/js/mathjax/input/mml/entities.js +1 -0
  392. package/dist/public/vendor/vditor/dist/js/mathjax/input/mml.js +1 -0
  393. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/action.js +1 -0
  394. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/all-packages.js +1 -0
  395. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/ams.js +1 -0
  396. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/amscd.js +1 -0
  397. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/autoload.js +1 -0
  398. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/bbox.js +1 -0
  399. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/boldsymbol.js +1 -0
  400. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/braket.js +1 -0
  401. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/bussproofs.js +1 -0
  402. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/cancel.js +1 -0
  403. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/color.js +1 -0
  404. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/colorV2.js +1 -0
  405. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/configMacros.js +1 -0
  406. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/enclose.js +1 -0
  407. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/extpfeil.js +1 -0
  408. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/html.js +1 -0
  409. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/mhchem.js +1 -0
  410. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/newcommand.js +1 -0
  411. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/noerrors.js +1 -0
  412. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/noundefined.js +1 -0
  413. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/physics.js +1 -0
  414. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/require.js +1 -0
  415. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/tagFormat.js +1 -0
  416. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/textmacros.js +1 -0
  417. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/unicode.js +1 -0
  418. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/verb.js +1 -0
  419. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex-base.js +1 -0
  420. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex-full.js +1 -0
  421. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex.js +1 -0
  422. package/dist/public/vendor/vditor/dist/js/mathjax/sre/mathmaps/de.js +104 -0
  423. package/dist/public/vendor/vditor/dist/js/mathjax/sre/mathmaps/en.js +110 -0
  424. package/dist/public/vendor/vditor/dist/js/mathjax/sre/mathmaps/es.js +104 -0
  425. package/dist/public/vendor/vditor/dist/js/mathjax/sre/mathmaps/fr.js +104 -0
  426. package/dist/public/vendor/vditor/dist/js/mathjax/sre/mathmaps/mathmaps_ie.js +518 -0
  427. package/dist/public/vendor/vditor/dist/js/mathjax/sre/mathmaps/nemeth.js +104 -0
  428. package/dist/public/vendor/vditor/dist/js/mathjax/sre/sre-node.js +11 -0
  429. package/dist/public/vendor/vditor/dist/js/mathjax/sre/sre_browser.js +1633 -0
  430. package/dist/public/vendor/vditor/dist/js/mathjax/tex-svg-full.js +1 -0
  431. package/dist/public/vendor/vditor/dist/js/mermaid/mermaid.min.js +2607 -0
  432. package/dist/public/vendor/vditor/dist/js/plantuml/plantuml-encoder.min.js +1 -0
  433. package/dist/public/vendor/vditor/dist/js/smiles-drawer/smiles-drawer.min.js +71 -0
  434. package/dist/public/vendor/vditor/dist/method.d.ts +51 -0
  435. package/dist/public/vendor/vditor/dist/method.js +2673 -0
  436. package/dist/public/vendor/vditor/dist/method.min.js +1 -0
  437. package/dist/public/vendor/vditor/dist/ts/constants.d.ts +45 -0
  438. package/dist/public/vendor/vditor/dist/ts/devtools/index.d.ts +7 -0
  439. package/dist/public/vendor/vditor/dist/ts/export/index.d.ts +4 -0
  440. package/dist/public/vendor/vditor/dist/ts/hint/index.d.ts +14 -0
  441. package/dist/public/vendor/vditor/dist/ts/ir/expandMarker.d.ts +1 -0
  442. package/dist/public/vendor/vditor/dist/ts/ir/highlightToolbarIR.d.ts +1 -0
  443. package/dist/public/vendor/vditor/dist/ts/ir/index.d.ts +13 -0
  444. package/dist/public/vendor/vditor/dist/ts/ir/input.d.ts +1 -0
  445. package/dist/public/vendor/vditor/dist/ts/ir/process.d.ts +8 -0
  446. package/dist/public/vendor/vditor/dist/ts/ir/processKeydown.d.ts +1 -0
  447. package/dist/public/vendor/vditor/dist/ts/markdown/SMILESRender.d.ts +1 -0
  448. package/dist/public/vendor/vditor/dist/ts/markdown/abcRender.d.ts +1 -0
  449. package/dist/public/vendor/vditor/dist/ts/markdown/adapterRender.d.ts +41 -0
  450. package/dist/public/vendor/vditor/dist/ts/markdown/anchorRender.d.ts +1 -0
  451. package/dist/public/vendor/vditor/dist/ts/markdown/chartRender.d.ts +1 -0
  452. package/dist/public/vendor/vditor/dist/ts/markdown/codeRender.d.ts +1 -0
  453. package/dist/public/vendor/vditor/dist/ts/markdown/flowchartRender.d.ts +1 -0
  454. package/dist/public/vendor/vditor/dist/ts/markdown/getHTML.d.ts +1 -0
  455. package/dist/public/vendor/vditor/dist/ts/markdown/getMarkdown.d.ts +1 -0
  456. package/dist/public/vendor/vditor/dist/ts/markdown/graphvizRender.d.ts +1 -0
  457. package/dist/public/vendor/vditor/dist/ts/markdown/highlightRender.d.ts +1 -0
  458. package/dist/public/vendor/vditor/dist/ts/markdown/lazyLoadImageRender.d.ts +6 -0
  459. package/dist/public/vendor/vditor/dist/ts/markdown/markmapRender.d.ts +1 -0
  460. package/dist/public/vendor/vditor/dist/ts/markdown/mathRender.d.ts +9 -0
  461. package/dist/public/vendor/vditor/dist/ts/markdown/mediaRender.d.ts +1 -0
  462. package/dist/public/vendor/vditor/dist/ts/markdown/mermaidRender.d.ts +1 -0
  463. package/dist/public/vendor/vditor/dist/ts/markdown/mindmapRender.d.ts +1 -0
  464. package/dist/public/vendor/vditor/dist/ts/markdown/outlineRender.d.ts +1 -0
  465. package/dist/public/vendor/vditor/dist/ts/markdown/plantumlRender.d.ts +1 -0
  466. package/dist/public/vendor/vditor/dist/ts/markdown/previewRender.d.ts +2 -0
  467. package/dist/public/vendor/vditor/dist/ts/markdown/setLute.d.ts +1 -0
  468. package/dist/public/vendor/vditor/dist/ts/markdown/speechRender.d.ts +6 -0
  469. package/dist/public/vendor/vditor/dist/ts/outline/index.d.ts +7 -0
  470. package/dist/public/vendor/vditor/dist/ts/preview/image.d.ts +1 -0
  471. package/dist/public/vendor/vditor/dist/ts/preview/index.d.ts +10 -0
  472. package/dist/public/vendor/vditor/dist/ts/resize/index.d.ts +6 -0
  473. package/dist/public/vendor/vditor/dist/ts/sv/combineFootnote.d.ts +6 -0
  474. package/dist/public/vendor/vditor/dist/ts/sv/index.d.ts +13 -0
  475. package/dist/public/vendor/vditor/dist/ts/sv/inputEvent.d.ts +1 -0
  476. package/dist/public/vendor/vditor/dist/ts/sv/process.d.ts +11 -0
  477. package/dist/public/vendor/vditor/dist/ts/sv/processKeydown.d.ts +1 -0
  478. package/dist/public/vendor/vditor/dist/ts/tip/index.d.ts +6 -0
  479. package/dist/public/vendor/vditor/dist/ts/toolbar/Both.d.ts +5 -0
  480. package/dist/public/vendor/vditor/dist/ts/toolbar/Br.d.ts +4 -0
  481. package/dist/public/vendor/vditor/dist/ts/toolbar/CodeTheme.d.ts +6 -0
  482. package/dist/public/vendor/vditor/dist/ts/toolbar/ContentTheme.d.ts +6 -0
  483. package/dist/public/vendor/vditor/dist/ts/toolbar/Counter.d.ts +6 -0
  484. package/dist/public/vendor/vditor/dist/ts/toolbar/Custom.d.ts +5 -0
  485. package/dist/public/vendor/vditor/dist/ts/toolbar/Devtools.d.ts +5 -0
  486. package/dist/public/vendor/vditor/dist/ts/toolbar/Divider.d.ts +4 -0
  487. package/dist/public/vendor/vditor/dist/ts/toolbar/EditMode.d.ts +8 -0
  488. package/dist/public/vendor/vditor/dist/ts/toolbar/Emoji.d.ts +7 -0
  489. package/dist/public/vendor/vditor/dist/ts/toolbar/Export.d.ts +6 -0
  490. package/dist/public/vendor/vditor/dist/ts/toolbar/Fullscreen.d.ts +6 -0
  491. package/dist/public/vendor/vditor/dist/ts/toolbar/Headings.d.ts +7 -0
  492. package/dist/public/vendor/vditor/dist/ts/toolbar/Help.d.ts +5 -0
  493. package/dist/public/vendor/vditor/dist/ts/toolbar/Indent.d.ts +5 -0
  494. package/dist/public/vendor/vditor/dist/ts/toolbar/Info.d.ts +5 -0
  495. package/dist/public/vendor/vditor/dist/ts/toolbar/InsertAfter.d.ts +5 -0
  496. package/dist/public/vendor/vditor/dist/ts/toolbar/InsertBefore.d.ts +5 -0
  497. package/dist/public/vendor/vditor/dist/ts/toolbar/MenuItem.d.ts +5 -0
  498. package/dist/public/vendor/vditor/dist/ts/toolbar/Outdent.d.ts +5 -0
  499. package/dist/public/vendor/vditor/dist/ts/toolbar/Outline.d.ts +5 -0
  500. package/dist/public/vendor/vditor/dist/ts/toolbar/Preview.d.ts +6 -0
  501. package/dist/public/vendor/vditor/dist/ts/toolbar/Record.d.ts +6 -0
  502. package/dist/public/vendor/vditor/dist/ts/toolbar/Redo.d.ts +5 -0
  503. package/dist/public/vendor/vditor/dist/ts/toolbar/Undo.d.ts +5 -0
  504. package/dist/public/vendor/vditor/dist/ts/toolbar/Upload.d.ts +6 -0
  505. package/dist/public/vendor/vditor/dist/ts/toolbar/index.d.ts +10 -0
  506. package/dist/public/vendor/vditor/dist/ts/toolbar/setToolbar.d.ts +20 -0
  507. package/dist/public/vendor/vditor/dist/ts/ui/initUI.d.ts +4 -0
  508. package/dist/public/vendor/vditor/dist/ts/ui/setCodeTheme.d.ts +1 -0
  509. package/dist/public/vendor/vditor/dist/ts/ui/setContentTheme.d.ts +1 -0
  510. package/dist/public/vendor/vditor/dist/ts/ui/setPreviewMode.d.ts +1 -0
  511. package/dist/public/vendor/vditor/dist/ts/ui/setTheme.d.ts +1 -0
  512. package/dist/public/vendor/vditor/dist/ts/undo/index.d.ts +19 -0
  513. package/dist/public/vendor/vditor/dist/ts/upload/getElement.d.ts +1 -0
  514. package/dist/public/vendor/vditor/dist/ts/upload/index.d.ts +8 -0
  515. package/dist/public/vendor/vditor/dist/ts/upload/setHeaders.d.ts +1 -0
  516. package/dist/public/vendor/vditor/dist/ts/util/Options.d.ts +8 -0
  517. package/dist/public/vendor/vditor/dist/ts/util/RecordMedia.d.ts +18 -0
  518. package/dist/public/vendor/vditor/dist/ts/util/addScript.d.ts +2 -0
  519. package/dist/public/vendor/vditor/dist/ts/util/addStyle.d.ts +1 -0
  520. package/dist/public/vendor/vditor/dist/ts/util/code160to32.d.ts +1 -0
  521. package/dist/public/vendor/vditor/dist/ts/util/compatibility.d.ts +7 -0
  522. package/dist/public/vendor/vditor/dist/ts/util/editorCommonEvent.d.ts +9 -0
  523. package/dist/public/vendor/vditor/dist/ts/util/fixBrowserBehavior.d.ts +39 -0
  524. package/dist/public/vendor/vditor/dist/ts/util/function.d.ts +3 -0
  525. package/dist/public/vendor/vditor/dist/ts/util/getSelectText.d.ts +1 -0
  526. package/dist/public/vendor/vditor/dist/ts/util/hasClosest.d.ts +9 -0
  527. package/dist/public/vendor/vditor/dist/ts/util/hasClosestByHeadings.d.ts +2 -0
  528. package/dist/public/vendor/vditor/dist/ts/util/highlightToolbar.d.ts +1 -0
  529. package/dist/public/vendor/vditor/dist/ts/util/hotKey.d.ts +1 -0
  530. package/dist/public/vendor/vditor/dist/ts/util/log.d.ts +1 -0
  531. package/dist/public/vendor/vditor/dist/ts/util/merge.d.ts +1 -0
  532. package/dist/public/vendor/vditor/dist/ts/util/processCode.d.ts +2 -0
  533. package/dist/public/vendor/vditor/dist/ts/util/selection.d.ts +14 -0
  534. package/dist/public/vendor/vditor/dist/ts/util/toc.d.ts +5 -0
  535. package/dist/public/vendor/vditor/dist/ts/wysiwyg/afterRenderEvent.d.ts +5 -0
  536. package/dist/public/vendor/vditor/dist/ts/wysiwyg/highlightToolbarWYSIWYG.d.ts +4 -0
  537. package/dist/public/vendor/vditor/dist/ts/wysiwyg/index.d.ts +22 -0
  538. package/dist/public/vendor/vditor/dist/ts/wysiwyg/inlineTag.d.ts +10 -0
  539. package/dist/public/vendor/vditor/dist/ts/wysiwyg/input.d.ts +1 -0
  540. package/dist/public/vendor/vditor/dist/ts/wysiwyg/processKeydown.d.ts +2 -0
  541. package/dist/public/vendor/vditor/dist/ts/wysiwyg/renderDomByMd.d.ts +5 -0
  542. package/dist/public/vendor/vditor/dist/ts/wysiwyg/setHeading.d.ts +2 -0
  543. package/dist/public/vendor/vditor/dist/ts/wysiwyg/showCode.d.ts +1 -0
  544. package/dist/public/vendor/vditor/dist/ts/wysiwyg/toolbarEvent.d.ts +1 -0
  545. package/dist/public/vendor/vditor/dist/types/index.d.ts +921 -0
  546. package/dist/public/work-permissions.d.ts +5 -2
  547. package/dist/public/work-permissions.js +40 -13
  548. package/dist/security.js +11 -0
  549. package/dist/security.js.map +1 -1
  550. package/dist/server-runtime.js +5 -2
  551. package/dist/server-runtime.js.map +1 -1
  552. package/dist/store.js +182 -27
  553. package/dist/store.js.map +1 -1
  554. package/dist/user-auth.js +36 -7
  555. package/dist/user-auth.js.map +1 -1
  556. package/dist/version.js +1 -1
  557. package/dist/version.js.map +1 -1
  558. package/dist/work-permissions.js +29 -6
  559. package/dist/work-permissions.js.map +1 -1
  560. package/package.json +2 -1
@@ -4,7 +4,7 @@ import { renderMarkdown } from "/markdown.js?v=20260722-inline-code";
4
4
  import { buildAiReferenceScope, findAiMention, listAiMentionOptions } from "/ai-mentions.js?v=20260716-chapter-references";
5
5
  import { shouldShowAiQuickActions } from "/ai-conversation.js?v=20260713-quick-actions";
6
6
  import { calculateLineNumberRowHeight, calculateLineNumberRowTop, calculateLineNumberTextOffset, calculateLineNumberTop } from "/line-number-layout.js?v=20260713-row-box-alignment";
7
- import { MODEL_PURPOSE_OPTIONS, modelFormValues, modelOptionLabel, modelPayload } from "/model-config.js?v=20260718-thinking-toggle";
7
+ import { MODEL_PURPOSE_OPTIONS, isKimiModelId, modelFormValues, modelOptionLabel, modelPayload } from "/model-config.js?v=20260723-kimi-temperature";
8
8
  import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
9
9
  import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260713-persisted-output-tokens";
10
10
  import { formatAiMessageTime } from "/ai-message-time.js?v=20260713-cross-day-time";
@@ -14,12 +14,13 @@ import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "
14
14
  import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=20260713-character-editor";
15
15
  import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=20260713-character-history";
16
16
  import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=20260714-all-knowledge-history";
17
- import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260722-entity-editor-page";
17
+ import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260723-knowledge-editor-page";
18
18
  import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
19
19
  import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
20
20
  import { buildRaceForest, eligibleRaceParents, racePathLabel } from "/race-hierarchy.js?v=20260721-race-hierarchy";
21
21
  import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20260721-analysis-descriptions";
22
- import { WORK_PERMISSION_MODULES, canReadUiModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260722-module-permissions";
22
+ import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260723-ai-analysis-permission";
23
+ import { MODULE_LAYOUT_STORAGE_KEY, LEGACY_SETTINGS_LAYOUT_STORAGE_KEY, normalizeModuleLayout, moduleLayoutLabel } from "/module-layout.js?v=20260723-module-layout-toggle";
23
24
 
24
25
  const state = {
25
26
  user: null,
@@ -82,7 +83,7 @@ function analysisTaskStatusLabel(status) {
82
83
  }
83
84
 
84
85
  function canEditWork(work = state.work) {
85
- return WORK_PERMISSION_MODULES.some((item) => canWriteUiModule(work, item.uiModule));
86
+ return WORK_PERMISSION_MODULES.some((item) => canWritePermissionModule(work, item.id));
86
87
  }
87
88
 
88
89
  function canEditProse(work = state.work) {
@@ -92,7 +93,7 @@ function canEditProse(work = state.work) {
92
93
  function canReplaceProse(work = state.work) {
93
94
  return WORK_PERMISSION_MODULES
94
95
  .filter((item) => item.id !== "ai-settings")
95
- .every((item) => canWriteUiModule(work, item.uiModule));
96
+ .every((item) => canWritePermissionModule(work, item.id));
96
97
  }
97
98
 
98
99
  function canManageWork(work = state.work) {
@@ -116,8 +117,8 @@ function applyWorkAccessMode() {
116
117
  const viewOnly = Boolean(state.work) && !canEditWork();
117
118
  const proseReadOnly = Boolean(state.work) && !canEditProse();
118
119
  const proseHidden = Boolean(state.work) && !canReadModule("editor");
119
- const aiHidden = Boolean(state.work) && !canReadModule("tasks");
120
- const aiReadOnly = Boolean(state.work) && !canEditModule("tasks");
120
+ const aiHidden = Boolean(state.work) && !canReadPermissionModule(state.work, "ai-chat");
121
+ const aiReadOnly = Boolean(state.work) && !canWritePermissionModule(state.work, "ai-chat");
121
122
  const moduleReadOnly = Boolean(state.work) && !canEditModule(state.module);
122
123
  $("#app").classList.toggle("view-only-mode", viewOnly);
123
124
  $("#app").classList.toggle("prose-read-only-mode", proseReadOnly);
@@ -125,14 +126,16 @@ function applyWorkAccessMode() {
125
126
  $("#app").classList.toggle("ai-hidden-mode", aiHidden);
126
127
  document.body.classList.toggle("work-viewer-mode", moduleReadOnly);
127
128
  for (const item of WORK_PERMISSION_MODULES) {
129
+ if (!item.uiModule) continue;
128
130
  const button = $(`#module-nav [data-module="${item.uiModule}"]`);
129
131
  if (button) button.classList.toggle("permission-hidden", !canReadModule(item.uiModule));
130
132
  }
131
133
  $("#module-nav [data-work-settings]").classList.toggle("permission-hidden", Boolean(state.work) && !canManageWork());
132
- $("#import-file-button").classList.toggle("permission-hidden", proseReadOnly);
133
- $("#import-file-button").setAttribute("aria-hidden", String(proseReadOnly));
134
+ $("#new-volume-button").classList.toggle("permission-hidden", Boolean(state.work) && proseReadOnly);
135
+ $("#welcome-new-work").classList.toggle("permission-hidden", Boolean(state.work) && proseReadOnly);
136
+ $("#import-file-button").setAttribute("aria-disabled", String(proseReadOnly));
137
+ $("#import-file-button").setAttribute("title", proseReadOnly ? "当前权限不能导入正文" : "导入 TXT / DOCX");
134
138
  $("#import-file").disabled = proseReadOnly;
135
- $("#import-history-button").classList.toggle("permission-hidden", Boolean(state.work) && !canReplaceProse());
136
139
  $(".ai-panel").classList.toggle("permission-hidden", aiHidden);
137
140
  $("#chapter-title").readOnly = proseReadOnly;
138
141
  $("#chapter-content").readOnly = proseReadOnly;
@@ -226,7 +229,7 @@ const shelfOnboardingSteps = [
226
229
  const workspaceOnboardingSteps = [
227
230
  { selector: "#home-button", eyebrow: "作品入口", title: "随时返回书架", description: "点击叙界标志返回书架,切换作品或创建新的故事世界。", placement: "bottom" },
228
231
  { selector: ".file-button", eyebrow: "稿件导入", title: "导入 TXT 或 DOCX", description: "已有稿件可以直接导入,系统会解析分卷与章节结构。", placement: "right" },
229
- { selector: "#new-chapter-button", eyebrow: "正文结构", title: "新建章节", description: "使用分卷和章节组织长篇正文,章节会自动保存并保留版本。", placement: "right" },
232
+ { selector: "[data-new-chapter-volume]", eyebrow: "正文结构", title: "新建章节", description: "使用分卷和章节组织长篇正文,章节会自动保存并保留版本。", placement: "right" },
230
233
  { selector: "#versions-button", eyebrow: "版本安全", title: "查看章节版本", description: "每次保存都会生成可恢复版本,误改内容时可以随时回溯。", placement: "bottom" },
231
234
  { selector: "[data-module=\"characters\"]", eyebrow: "作品知识", title: "维护角色与世界资料", description: "角色、种族、组织、设定和时间线共同构成 AI 可引用的作品知识。", placement: "right" },
232
235
  { selector: "[data-module=\"outlines\"]", eyebrow: "创作规划", title: "跟踪大纲与伏笔", description: "记录剧情目标、冲突、转折和伏笔回收,避免长线遗漏。", placement: "right" },
@@ -408,7 +411,7 @@ function replacePageRoute(route) {
408
411
  function currentPageRoute() {
409
412
  const workId = state.work?.id ?? null;
410
413
  if (!$("#entity-editor-view").classList.contains("hidden") && workId && entityEditorType) {
411
- const entityId = entityEditorType === "setting" ? settingEditorItem?.id : characterEditorItem?.id;
414
+ const entityId = entityEditorType === "setting" ? settingEditorItem?.id : entityEditorType === "character" ? characterEditorItem?.id : knowledgeEditorItem?.id;
412
415
  return { view: "entity-editor", workId, entity: entityEditorType, entityId: entityId ?? null };
413
416
  }
414
417
  if (!$("#settings-hub-view").classList.contains("hidden")) return { view: "settings", workId, ...settingsRouteContext() };
@@ -524,50 +527,76 @@ let entityEditorType = null;
524
527
  let entityEditorDirty = false;
525
528
  let settingEditorItem = null;
526
529
  let characterEditorItem = null;
530
+ let knowledgeEditorItem = null;
531
+ let knowledgeEditorKind = null;
532
+ let knowledgeEditorSections = [];
533
+ let knowledgeSectionEditorIndex = null;
534
+ let knowledgeSectionEditorDirty = false;
527
535
  let characterEditorVersions = [];
528
536
  let characterEditorRelationships = [];
529
537
  let characterEditorRelationshipsLoading = false;
530
538
  let characterEditorSections = [];
531
- let characterSectionPreviewTimer = null;
532
539
  let characterSectionPendingAttachments = [];
540
+ let markdownEditorPendingAttachments = [];
533
541
  let characterSectionEditorDirty = false;
542
+ let settingEditorVditor = null;
543
+ let knowledgeSectionVditor = null;
544
+ let characterSectionVditor = null;
534
545
  let entityHistoryContext = null;
535
546
 
536
547
  function showEntityEditorPage(type) {
537
548
  entityEditorType = type;
538
549
  entityEditorDirty = false;
539
550
  characterSectionEditorDirty = false;
551
+ knowledgeSectionEditorDirty = false;
540
552
  $("#entity-editor-view").classList.remove("hidden");
541
553
  $("#setting-editor-form").classList.toggle("hidden", type !== "setting");
542
554
  $("#character-editor-form").classList.toggle("hidden", type !== "character");
555
+ $("#knowledge-editor-form").classList.toggle("hidden", !["race", "organization"].includes(type));
543
556
  $("#character-section-editor-view").classList.add("hidden");
557
+ $("#knowledge-section-editor-view").classList.add("hidden");
544
558
  $("#app").inert = true;
545
559
  document.body.classList.add("entity-editor-open");
546
560
  replacePageRoute(currentPageRoute());
547
561
  }
548
562
 
549
563
  function markEntityEditorDirty() {
550
- const module = entityEditorType === "setting" ? "settings" : "characters";
564
+ const module = entityEditorType === "setting" ? "settings" : entityEditorType === "character" ? "characters" : entityEditorType === "race" ? "races" : "organizations";
551
565
  if (entityEditorType && canEditModule(module)) entityEditorDirty = true;
552
566
  }
553
567
 
554
- function confirmEntityEditorDiscard(message) {
568
+ async function confirmEntityEditorDiscard(message) {
555
569
  if (!entityEditorDirty) return true;
556
- return window.confirm(message ?? "当前资料有未保存修改,返回列表将丢弃这些修改。是否继续?");
570
+ return confirmToast(message ?? "当前资料有未保存修改,返回列表将丢弃这些修改。是否继续?", {
571
+ title: "放弃未保存修改",
572
+ confirmLabel: "放弃并继续",
573
+ cancelLabel: "继续编辑"
574
+ });
557
575
  }
558
576
 
559
577
  async function closeEntityEditor({ force = false } = {}) {
560
578
  if (!$("#character-section-editor-view").classList.contains("hidden")) return closeCharacterSectionEditor({ force });
561
- if (!force && !confirmEntityEditorDiscard()) return false;
579
+ if (!$("#knowledge-section-editor-view").classList.contains("hidden")) return closeKnowledgeSectionEditor({ force });
580
+ if (!force && !(await confirmEntityEditorDiscard())) return false;
562
581
  await discardPendingCharacterAttachments();
563
- const module = entityEditorType === "setting" ? "settings" : "characters";
582
+ await discardPendingMarkdownAttachments();
583
+ destroyVditorEditor(settingEditorVditor);
584
+ settingEditorVditor = null;
585
+ const module = entityEditorType === "setting" ? "settings" : entityEditorType === "character" ? "characters" : entityEditorType === "race" ? "races" : "organizations";
564
586
  entityEditorType = null;
565
587
  entityEditorDirty = false;
566
588
  settingEditorItem = null;
567
589
  characterEditorItem = null;
590
+ knowledgeEditorItem = null;
591
+ knowledgeEditorKind = null;
592
+ knowledgeEditorSections = [];
593
+ knowledgeSectionEditorIndex = null;
594
+ knowledgeSectionEditorDirty = false;
568
595
  $("#entity-editor-view").classList.add("hidden");
569
596
  $("#setting-editor-form").classList.add("hidden");
570
597
  $("#character-editor-form").classList.add("hidden");
598
+ $("#knowledge-editor-form").classList.add("hidden");
599
+ $("#knowledge-section-editor-view").classList.add("hidden");
571
600
  $("#app").inert = false;
572
601
  document.body.classList.remove("entity-editor-open");
573
602
  await showModule(module);
@@ -1407,6 +1436,7 @@ function applyColorTheme(theme) {
1407
1436
  button.setAttribute("aria-pressed", String(normalized === "dark"));
1408
1437
  const themeColor = document.querySelector('meta[name="theme-color"]');
1409
1438
  if (themeColor) themeColor.content = normalized === "dark" ? "#171714" : "#8b3d2c";
1439
+ [settingEditorVditor, knowledgeSectionVditor, characterSectionVditor].forEach((editor) => editor?.setTheme(normalized === "dark" ? "dark" : "classic"));
1410
1440
  }
1411
1441
 
1412
1442
  function saveColorTheme(theme) {
@@ -1701,6 +1731,47 @@ function toast(message, type = "info") {
1701
1731
  }, 3600);
1702
1732
  }
1703
1733
 
1734
+ function confirmToast(message, { title = "请再次确认", confirmLabel = "确认", cancelLabel = "取消" } = {}) {
1735
+ const region = $("#toast-region");
1736
+ const element = document.createElement("section");
1737
+ element.className = "toast toast-confirmation";
1738
+ element.setAttribute("role", "alertdialog");
1739
+ element.setAttribute("aria-label", title);
1740
+ const heading = document.createElement("strong");
1741
+ heading.textContent = title;
1742
+ const description = document.createElement("p");
1743
+ description.textContent = message;
1744
+ const actions = document.createElement("div");
1745
+ actions.className = "toast-confirmation-actions";
1746
+ const cancel = document.createElement("button");
1747
+ cancel.className = "ghost-button";
1748
+ cancel.type = "button";
1749
+ cancel.textContent = cancelLabel;
1750
+ const confirm = document.createElement("button");
1751
+ confirm.className = "primary-button";
1752
+ confirm.type = "button";
1753
+ confirm.textContent = confirmLabel;
1754
+ actions.append(cancel, confirm);
1755
+ element.append(heading, description, actions);
1756
+ region.append(element);
1757
+ raiseToastRegion();
1758
+ cancel.focus();
1759
+ return new Promise((resolve) => {
1760
+ const finish = (confirmed) => {
1761
+ element.remove();
1762
+ if (!region.childElementCount && typeof region.hidePopover === "function" && region.matches(":popover-open")) region.hidePopover();
1763
+ resolve(confirmed);
1764
+ };
1765
+ cancel.addEventListener("click", () => finish(false), { once: true });
1766
+ confirm.addEventListener("click", () => finish(true), { once: true });
1767
+ element.addEventListener("keydown", (event) => {
1768
+ if (event.key !== "Escape") return;
1769
+ event.preventDefault();
1770
+ finish(false);
1771
+ });
1772
+ });
1773
+ }
1774
+
1704
1775
  document.addEventListener("toggle", (event) => {
1705
1776
  const target = event.target;
1706
1777
  if (target instanceof HTMLDialogElement && target.open && $("#toast-region").childElementCount) {
@@ -1806,23 +1877,34 @@ async function persistChapter({ automatic = false } = {}) {
1806
1877
  }
1807
1878
  }
1808
1879
 
1809
- function confirmDiscardChanges(message = "当前章节有未保存修改,继续将丢弃这些修改。是否继续?") {
1880
+ async function confirmDiscardChanges(message = "当前章节有未保存修改,继续将丢弃这些修改。是否继续?") {
1810
1881
  if (!state.dirty) return true;
1811
- return window.confirm(message);
1882
+ return confirmToast(message, {
1883
+ title: "放弃未保存修改",
1884
+ confirmLabel: "放弃并继续",
1885
+ cancelLabel: "继续编辑"
1886
+ });
1812
1887
  }
1813
1888
 
1814
1889
  function chooseExistingWorkImportMode(file) {
1815
1890
  const dialog = $("#import-mode-dialog");
1891
+ const form = dialog.querySelector("form");
1892
+ const confirm = $("#import-mode-confirm");
1816
1893
  const canOverwrite = canReplaceProse();
1817
1894
  $("#import-mode-file-summary").textContent = `文件:${file.name};当前作品:《${state.work.title}》`;
1818
1895
  $("#import-mode-unsaved-warning").classList.toggle("hidden", !state.dirty);
1896
+ form.reset();
1819
1897
  $("#import-mode-overwrite").disabled = !canOverwrite;
1820
1898
  $("#import-mode-overwrite-permission").classList.toggle("hidden", canOverwrite);
1899
+ confirm.disabled = true;
1900
+ form.onchange = () => { confirm.disabled = !form.querySelector('input[name="importMode"]:checked'); };
1821
1901
  dialog.returnValue = "cancel";
1822
1902
  dialog.showModal();
1903
+ $("#import-mode-dialog-title").focus();
1823
1904
  return new Promise((resolve) => {
1824
1905
  dialog.addEventListener("close", () => {
1825
- resolve(["append", "overwrite"].includes(dialog.returnValue) ? dialog.returnValue : null);
1906
+ const mode = form.querySelector('input[name="importMode"]:checked')?.value;
1907
+ resolve(dialog.returnValue === "confirm" && ["append", "overwrite"].includes(mode) ? mode : null);
1826
1908
  }, { once: true });
1827
1909
  });
1828
1910
  }
@@ -1874,7 +1956,7 @@ async function initializePage() {
1874
1956
  state.module = route.view === "module"
1875
1957
  ? route.module
1876
1958
  : route.view === "entity-editor"
1877
- ? (route.entity === "setting" ? "settings" : "characters")
1959
+ ? ({ setting: "settings", character: "characters", race: "races", organization: "organizations" }[route.entity] ?? "characters")
1878
1960
  : "editor";
1879
1961
  await selectWork(requestedWork.id, route.view === "editor" ? route.chapterId : null);
1880
1962
  }
@@ -1885,14 +1967,16 @@ async function initializePage() {
1885
1967
  }
1886
1968
  if (route.view === "module") return;
1887
1969
  if (route.view === "entity-editor") {
1888
- const records = route.entity === "setting" ? state.settings : state.characters;
1970
+ const records = route.entity === "setting" ? state.settings : route.entity === "character" ? state.characters : route.entity === "race" ? state.races : state.organizations;
1889
1971
  const item = route.entityId ? records.find((record) => record.id === route.entityId) : null;
1890
1972
  if (route.entityId && !item) {
1891
- toast(route.entity === "setting" ? "未找到要编辑的设定" : "未找到要编辑的角色", "error");
1973
+ toast(({ setting: "未找到要编辑的设定", character: "未找到要编辑的角色", race: "未找到要编辑的种族", organization: "未找到要编辑的组织" }[route.entity] ?? "未找到要编辑的档案"), "error");
1892
1974
  return;
1893
1975
  }
1894
1976
  if (route.entity === "setting") openSettingEditor(item);
1895
- else await openCharacterEditor(item);
1977
+ else if (route.entity === "character") await openCharacterEditor(item);
1978
+ else if (route.entity === "race") await openRaceDialog(item);
1979
+ else if (route.entity === "organization") await openOrganizationDialog(item);
1896
1980
  return;
1897
1981
  }
1898
1982
  if (route.view === "welcome") {
@@ -1900,7 +1984,7 @@ async function initializePage() {
1900
1984
  return;
1901
1985
  }
1902
1986
  if (route.view === "settings") {
1903
- showSettingsHub();
1987
+ await showSettingsHub();
1904
1988
  settingsReturnContext = restoredSettingsReturnContext(route);
1905
1989
  renderSettingsHub();
1906
1990
  return;
@@ -2195,10 +2279,10 @@ async function openSearchResult(result) {
2195
2279
  }
2196
2280
  }
2197
2281
 
2198
- function showSettingsHub() {
2282
+ async function showSettingsHub() {
2199
2283
  const alreadyInSettings = !$("#settings-hub-view").classList.contains("hidden") || !$("#platform-ai-view").classList.contains("hidden");
2200
2284
  if (!alreadyInSettings) {
2201
- if (state.dirty && !confirmDiscardChanges("当前章节有未保存修改,进入设置将放弃本地修改。是否继续?")) return false;
2285
+ if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入设置将放弃本地修改。是否继续?"))) return false;
2202
2286
  settingsReturnContext = captureSettingsReturnContext();
2203
2287
  state.dirty = false;
2204
2288
  }
@@ -2235,7 +2319,7 @@ async function returnFromSettings() {
2235
2319
  }
2236
2320
 
2237
2321
  async function showPlatformAi() {
2238
- if (state.dirty && !confirmDiscardChanges("当前章节有未保存修改,进入平台 AI 管理将放弃本地修改。是否继续?")) return false;
2322
+ if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入平台 AI 管理将放弃本地修改。是否继续?"))) return false;
2239
2323
  state.dirty = false;
2240
2324
  updateDocumentTitle();
2241
2325
  $("#app").classList.add("shelf-mode");
@@ -2311,7 +2395,7 @@ function resetWorkScopedUiCaches() {
2311
2395
 
2312
2396
  async function selectWork(workId, preferredChapterId = null) {
2313
2397
  const discarding = state.work?.id !== workId && state.dirty;
2314
- if (discarding && !confirmDiscardChanges()) return false;
2398
+ if (discarding && !(await confirmDiscardChanges())) return false;
2315
2399
  const nextWork = await api(`/api/works/${workId}?page=1&limit=100`);
2316
2400
  if (state.work?.id !== nextWork.id) resetWorkScopedUiCaches();
2317
2401
  if (discarding) setSaveState("就绪");
@@ -2342,11 +2426,15 @@ async function selectWork(workId, preferredChapterId = null) {
2342
2426
  function renderTree() {
2343
2427
  if (!state.work) return;
2344
2428
  const count = state.work.volumes.reduce((total, volume) => total + volume.chapters.length, 0);
2429
+ const proseEditable = canEditProse();
2345
2430
  $("#chapter-count").textContent = `${count} 章`;
2346
2431
  $("#novel-tree").classList.remove("empty-copy");
2347
2432
  $("#novel-tree").innerHTML = state.work.volumes.map((volume) => `
2348
2433
  <div class="volume-node ${state.collapsedVolumeIds.has(volume.id) ? "is-collapsed" : ""}" data-volume-id="${esc(volume.id)}">
2349
- <button class="volume-title" type="button" data-volume-toggle="${esc(volume.id)}" aria-expanded="${state.collapsedVolumeIds.has(volume.id) ? "false" : "true"}" title="左键折叠,右键设置分卷"><span>${esc(volume.title)}</span><span>${volume.chapters.length} 章</span></button>
2434
+ <div class="volume-title">
2435
+ <button class="volume-toggle" type="button" data-volume-toggle="${esc(volume.id)}" aria-expanded="${state.collapsedVolumeIds.has(volume.id) ? "false" : "true"}" title="左键折叠,右键设置分卷"><span>${esc(volume.title)}</span><span>${volume.chapters.length} 章</span></button>
2436
+ ${proseEditable ? `<button class="add-button chapter-add-button" type="button" data-new-chapter-volume="${esc(volume.id)}" aria-label="在“${esc(volume.title)}”中新建章节" title="在“${esc(volume.title)}”中新建章节">+</button>` : ""}
2437
+ </div>
2350
2438
  <div class="volume-chapters">
2351
2439
  ${volume.chapters.map((chapter) => `
2352
2440
  <button class="chapter-node ${state.chapter?.id === chapter.id ? "active" : ""}" type="button" data-chapter-id="${esc(chapter.id)}">
@@ -2367,6 +2455,9 @@ function renderTree() {
2367
2455
  openVolumeDialog(state.work.volumes.find((volume) => volume.id === button.dataset.volumeToggle));
2368
2456
  });
2369
2457
  });
2458
+ $("#novel-tree").querySelectorAll("[data-new-chapter-volume]").forEach((button) => {
2459
+ button.addEventListener("click", () => openChapterDialog(button.dataset.newChapterVolume));
2460
+ });
2370
2461
  $("#novel-tree").querySelectorAll("[data-chapter-id]").forEach((button) => {
2371
2462
  button.addEventListener("click", () => selectChapter(button.dataset.chapterId));
2372
2463
  button.addEventListener("contextmenu", (event) => {
@@ -2399,7 +2490,7 @@ function openChapterTypeMenu(chapterId, clientX, clientY) {
2399
2490
  }
2400
2491
 
2401
2492
  async function selectChapter(chapterId) {
2402
- if (state.chapter?.id !== chapterId && !confirmDiscardChanges("当前章节有未保存修改,仍要切换吗?")) return;
2493
+ if (state.chapter?.id !== chapterId && !(await confirmDiscardChanges("当前章节有未保存修改,仍要切换吗?"))) return;
2403
2494
  cancelChapterAutoSave();
2404
2495
  state.chapter = await api(`/api/chapters/${chapterId}`);
2405
2496
  lastSavedChapterSnapshot = { chapterId: state.chapter.id, title: state.chapter.title, content: state.chapter.content };
@@ -2487,7 +2578,7 @@ async function showModule(module) {
2487
2578
  }
2488
2579
  module = fallback;
2489
2580
  }
2490
- if (module !== "editor" && state.module === "editor" && !confirmDiscardChanges()) return;
2581
+ if (module !== "editor" && state.module === "editor" && !(await confirmDiscardChanges())) return;
2491
2582
  if (module !== "editor" && state.module === "editor" && state.dirty) setSaveState("已放弃修改");
2492
2583
  state.module = module;
2493
2584
  applyWorkAccessMode();
@@ -2616,8 +2707,10 @@ function openEntityMergeDialog({ typeLabel, source, candidates, endpoint, body,
2616
2707
  }
2617
2708
 
2618
2709
  async function deleteManagedEntity({ typeLabel, item, endpoint, refresh, warning = "" }) {
2619
- const detail = warning ? `\n${warning}` : "";
2620
- if (!window.confirm(`确认删除${typeLabel}“${item.name}”吗?${detail}`)) return;
2710
+ const message = warning
2711
+ ? `确认删除${typeLabel}“${item.name}”吗?\n${warning}`
2712
+ : `确认删除${typeLabel}“${item.name}”吗?`;
2713
+ if (!(await confirmToast(message, { title: `删除${typeLabel}`, confirmLabel: "确认删除" }))) return;
2621
2714
  try {
2622
2715
  await api(endpoint(item), { method: "DELETE" });
2623
2716
  await refresh();
@@ -2628,14 +2721,76 @@ async function deleteManagedEntity({ typeLabel, item, endpoint, refresh, warning
2628
2721
  }
2629
2722
  }
2630
2723
 
2724
+ function readModuleLayout() {
2725
+ try {
2726
+ const stored = localStorage.getItem(MODULE_LAYOUT_STORAGE_KEY) ?? localStorage.getItem(LEGACY_SETTINGS_LAYOUT_STORAGE_KEY);
2727
+ return normalizeModuleLayout(stored);
2728
+ } catch {
2729
+ return "cards";
2730
+ }
2731
+ }
2732
+
2733
+ function saveModuleLayout(layout) {
2734
+ const normalized = normalizeModuleLayout(layout);
2735
+ try {
2736
+ localStorage.setItem(MODULE_LAYOUT_STORAGE_KEY, normalized);
2737
+ } catch {
2738
+ /* 浏览器禁用存储时仅保留当前会话选择 */
2739
+ }
2740
+ return normalized;
2741
+ }
2742
+
2743
+ function moduleRowPreview(text, max = 180) {
2744
+ const preview = String(text ?? "").replace(/\s+/g, " ").trim();
2745
+ return preview.length > max ? `${preview.slice(0, max)}…` : preview;
2746
+ }
2747
+
2748
+ function renderModuleLayoutToggle(layout, ariaLabel = "列表样式") {
2749
+ return `<div class="module-layout-toolbar">
2750
+ <div class="module-layout-toggle" role="group" aria-label="${esc(ariaLabel)}">
2751
+ <button type="button" data-module-layout="cards" aria-pressed="${layout === "cards"}">卡片</button>
2752
+ <button type="button" data-module-layout="rows" aria-pressed="${layout === "rows"}">列表</button>
2753
+ </div>
2754
+ <span class="module-layout-hint">当前:${esc(moduleLayoutLabel(layout))}</span>
2755
+ </div>`;
2756
+ }
2757
+
2758
+ function bindModuleLayoutToggle(refresh) {
2759
+ $("#module-content").querySelectorAll("[data-module-layout]").forEach((button) => button.addEventListener("click", async () => {
2760
+ saveModuleLayout(button.dataset.moduleLayout);
2761
+ await refresh();
2762
+ }));
2763
+ }
2764
+
2765
+ function settingRecordActions(item) {
2766
+ return `${item.status === "pending" ? `<button data-setting-status="confirmed" data-setting-id="${esc(item.id)}">确认候选</button><button data-setting-status="deprecated" data-setting-id="${esc(item.id)}">弃用</button>` : ""}<button data-edit-setting="${esc(item.id)}">编辑</button><button data-entity-history="setting" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.title)}">版本历史</button>`;
2767
+ }
2768
+
2769
+ function renderSettingCards(records) {
2770
+ return `<div class="card-grid">${records.map((item) => `
2771
+ <article class="record-card"><small>${esc(item.category)} · ${item.locked ? "已锁定" : esc(item.status)}</small>
2772
+ <h3>${esc(item.title)}</h3><div class="record-markdown-preview message-body">${renderMarkdown(item.content) || '<p class="markdown-editor-empty">暂无正文</p>'}</div>
2773
+ <div class="card-actions">${settingRecordActions(item)}</div></article>`).join("")}</div>`;
2774
+ }
2775
+
2776
+ function renderSettingRows(records) {
2777
+ return `<div class="module-row-list">${records.map((item) => {
2778
+ const preview = moduleRowPreview(item.content);
2779
+ return `
2780
+ <article class="record-card module-row"><small>${esc(item.category)} · ${item.locked ? "已锁定" : esc(item.status)}</small>
2781
+ <h3>${esc(item.title)}</h3><p class="module-row-preview" title="${esc(preview)}">${esc(preview)}</p>
2782
+ <div class="card-actions">${settingRecordActions(item)}</div></article>`;
2783
+ }).join("")}</div>`;
2784
+ }
2785
+
2631
2786
  async function renderSettings() {
2632
2787
  const records = (await apiPage(`/api/works/${state.work.id}/settings`)).items;
2633
2788
  state.settings = records;
2634
- $("#module-content").innerHTML = records.length ? `<div class="card-grid">${records.map((item) => `
2635
- <article class="record-card"><small>${esc(item.category)} · ${item.locked ? "已锁定" : esc(item.status)}</small>
2636
- <h3>${esc(item.title)}</h3><p>${esc(item.content)}</p>
2637
- <div class="card-actions">${item.status === "pending" ? `<button data-setting-status="confirmed" data-setting-id="${esc(item.id)}">确认候选</button><button data-setting-status="deprecated" data-setting-id="${esc(item.id)}">弃用</button>` : ""}<button data-edit-setting="${esc(item.id)}">编辑</button><button data-entity-history="setting" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.title)}">版本历史</button></div></article>`).join("")}</div>`
2789
+ const layout = readModuleLayout();
2790
+ $("#module-content").innerHTML = records.length
2791
+ ? `${renderModuleLayoutToggle(layout, "设定列表样式")}${layout === "rows" ? renderSettingRows(records) : renderSettingCards(records)}`
2638
2792
  : emptyModule("还没有世界观设定", "新建规则、地点、组织、科技或创作约束。AI 提取的候选也会进入这里。");
2793
+ bindModuleLayoutToggle(renderSettings);
2639
2794
  $("#module-content").querySelectorAll("[data-setting-status]").forEach((button) => button.addEventListener("click", async () => {
2640
2795
  await api(`/api/settings/${button.dataset.settingId}`, { method: "PATCH", body: { status: button.dataset.settingStatus, changeNote: button.dataset.settingStatus === "confirmed" ? "确认 AI 设定候选" : "弃用 AI 设定候选" } });
2641
2796
  await renderSettings();
@@ -2651,8 +2806,9 @@ async function renderCharacters() {
2651
2806
  canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
2652
2807
  canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
2653
2808
  ]);
2654
- const auditPanel = canEditModule("tasks") ? `<section class="character-audit-panel"><div><strong>角色身份确认</strong><small>让 AI 查询角色档案并搜索正文,找出可能被误建成两个档案的同一角色。AI 只提交审核建议,不会自动合并。</small></div><button id="create-character-audit-task" class="ghost-button" type="button" ${state.characters.length < 2 ? "disabled" : ""}>AI 角色查重</button></section>` : "";
2655
- $("#module-content").innerHTML = auditPanel + (state.characters.length ? `<div class="card-grid">${state.characters.map((item) => {
2809
+ const layout = readModuleLayout();
2810
+ const characterActions = (item) => `<button data-edit-character="${esc(item.id)}">编辑</button>${canEditModule("characters") && state.characters.length > 1 ? `<button data-merge-character="${esc(item.id)}">合并</button>` : ""}${canEditModule("characters") ? `<button class="danger-button" data-delete-character="${esc(item.id)}">删除</button>` : ""}`;
2811
+ const characterCards = () => `<div class="card-grid">${state.characters.map((item) => {
2656
2812
  const details = normalizeCharacterDetails(item.attributes?.details);
2657
2813
  return `
2658
2814
  <article class="record-card character-card" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}"><small>${item.lockedFields.length ? `锁定 ${item.lockedFields.length} 项` : esc(item.visibility)}</small>
@@ -2663,9 +2819,29 @@ async function renderCharacters() {
2663
2819
  <div class="organization-links"><b>所属组织</b>${(item.organizations ?? []).length ? item.organizations.map((organization) => `<span class="pill organization-pill">${esc(organization.name)}</span>`).join("") : '<span class="organization-empty">未加入组织</span>'}</div>
2664
2820
  ${item.profile?.summary ? `<p class="character-summary">${esc(item.profile.summary)}</p>` : `<p>${esc(Object.entries(item.currentState).map(([key, value]) => `${key}:${value}`).join("\n") || "尚未记录当前状态")}</p>`}
2665
2821
  ${item.profileSectionCount ? `<small class="character-section-count">${item.profileSectionCount} 个设定章节</small>` : ""}
2666
- <div class="card-actions"><button data-edit-character="${esc(item.id)}">编辑</button>${canEditModule("characters") && state.characters.length > 1 ? `<button data-merge-character="${esc(item.id)}">合并</button>` : ""}${canEditModule("characters") ? `<button class="danger-button" data-delete-character="${esc(item.id)}">删除</button>` : ""}</div></article>`;
2667
- }).join("")}</div>`
2822
+ <div class="card-actions">${characterActions(item)}</div></article>`;
2823
+ }).join("")}</div>`;
2824
+ const characterRows = () => `<div class="module-row-list">${state.characters.map((item) => {
2825
+ const preview = moduleRowPreview(item.profile?.summary || item.attributes?.identity || Object.entries(item.currentState).map(([key, value]) => `${key}:${value}`).join(" ") || "尚未记录当前状态");
2826
+ const meta = [
2827
+ item.species ? (racePathLabel(item.race) || item.species) : "",
2828
+ ...(item.aliases ?? []).slice(0, 3),
2829
+ (item.organizations ?? []).length ? (item.organizations ?? []).map((organization) => organization.name).join("、") : ""
2830
+ ].filter(Boolean).join(" · ");
2831
+ const line = meta ? `${meta} · ${preview}` : preview;
2832
+ return `
2833
+ <article class="record-card module-row character-card" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">
2834
+ <small>${item.lockedFields.length ? `锁定 ${item.lockedFields.length} 项` : esc(item.visibility)}</small>
2835
+ <h3>${esc(item.name)}</h3>
2836
+ <p class="module-row-preview" title="${esc(line)}">${esc(line)}</p>
2837
+ <div class="card-actions">${characterActions(item)}</div>
2838
+ </article>`;
2839
+ }).join("")}</div>`;
2840
+ const auditPanel = canEditModule("tasks") ? `<section class="character-audit-panel"><div><strong>角色身份确认</strong><small>让 AI 查询角色档案并搜索正文,找出可能被误建成两个档案的同一角色。AI 只提交审核建议,不会自动合并。</small></div><button id="create-character-audit-task" class="ghost-button" type="button" ${state.characters.length < 2 ? "disabled" : ""}>AI 角色查重</button></section>` : "";
2841
+ $("#module-content").innerHTML = auditPanel + (state.characters.length
2842
+ ? `${renderModuleLayoutToggle(layout, "角色列表样式")}${layout === "rows" ? characterRows() : characterCards()}`
2668
2843
  : emptyModule("还没有角色档案", "创建主要人物,并维护别名、身份、动机和当前状态。"));
2844
+ bindModuleLayoutToggle(renderCharacters);
2669
2845
  $("#create-character-audit-task")?.addEventListener("click", async () => {
2670
2846
  const button = $("#create-character-audit-task");
2671
2847
  button.disabled = true;
@@ -2723,20 +2899,36 @@ async function renderRaces() {
2723
2899
  apiAllPages(`/api/works/${state.work.id}/races`),
2724
2900
  canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
2725
2901
  ]);
2902
+ const layout = readModuleLayout();
2903
+ const raceActions = (item) => `<button data-edit-race="${esc(item.id)}">编辑</button><button data-entity-history="race" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button>${canEditModule("races") && state.races.length > 1 ? `<button data-merge-race="${esc(item.id)}">合并</button>` : ""}${canEditModule("races") ? `<button class="danger-button" data-delete-race="${esc(item.id)}">删除</button>` : ""}`;
2726
2904
  const renderRaceNode = (item) => `<details class="race-tree-node" open data-race-node="${esc(item.id)}">
2727
2905
  <summary><span>${esc(item.name)}</span><small>${item.children.length} 个直接子种族</small></summary>
2728
2906
  <div class="race-tree-branch">
2729
- <article class="record-card race-card"><small>${item.memberIds.length} 位直接角色 · ${item.settings.length} 条自身设定</small>
2907
+ <article class="record-card race-card"><small>${item.memberIds.length} 位直接角色 · ${item.settings.length ? "已填写共同设定" : "暂无共同设定"}</small>
2730
2908
  <div class="race-path" aria-label="种族路径">${esc(racePathLabel(item))}</div>
2731
2909
  <p>${esc(item.description || "尚未填写种族简介")}</p>
2732
- <div class="race-settings">${item.effectiveSettings.length ? item.effectiveSettings.map((setting) => `<span class="pill${setting.inherited ? " inherited" : ""}" title="${esc(setting.inherited ? `继承自 ${setting.sourceRaceName}` : `定义于 ${setting.sourceRaceName}`)}">${esc(setting.value)}<small>${esc(setting.sourceRaceName)}</small></span>`).join("") : '<span class="pill">暂无共同设定</span>'}</div>
2910
+ <div class="race-settings">${item.effectiveSettings.length ? item.effectiveSettings.map((setting) => `<section class="knowledge-markdown-block${setting.inherited ? " inherited" : ""}"><div class="knowledge-markdown-block-heading"><h4>${esc(setting.title || "未命名章节")}</h4><small>${esc(setting.inherited ? `继承自 ${setting.sourceRaceName}` : `定义于 ${setting.sourceRaceName}`)}</small></div><div class="message-body">${renderMarkdown(setting.value) || '<p class="markdown-editor-empty">暂无内容</p>'}</div></section>`).join("") : '<span class="pill">暂无共同设定</span>'}</div>
2733
2911
  <p class="race-members">直接角色:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
2734
- <div class="card-actions"><button data-edit-race="${esc(item.id)}">编辑</button><button data-entity-history="race" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button>${canEditModule("races") && state.races.length > 1 ? `<button data-merge-race="${esc(item.id)}">合并</button>` : ""}${canEditModule("races") ? `<button class="danger-button" data-delete-race="${esc(item.id)}">删除</button>` : ""}</div>
2912
+ <div class="card-actions">${raceActions(item)}</div>
2735
2913
  </article>
2736
2914
  ${item.children.length ? `<div class="race-tree-children">${item.children.map(renderRaceNode).join("")}</div>` : ""}
2737
2915
  </div>
2738
2916
  </details>`;
2739
- $("#module-content").innerHTML = state.races.length ? `<section class="race-tree" aria-label="种族层级">${buildRaceForest(state.races).map(renderRaceNode).join("")}</section>` : emptyModule("还没有种族档案", "先创建种族及共同设定,之后角色编辑器才能选择该种族。");
2917
+ const raceRows = () => `<div class="module-row-list">${state.races.map((item) => {
2918
+ const preview = moduleRowPreview(item.description || "尚未填写种族简介");
2919
+ const meta = `${item.memberIds.length} 位直接角色 · ${item.settings.length ? "已填写共同设定" : "暂无共同设定"}`;
2920
+ return `
2921
+ <article class="record-card module-row race-card">
2922
+ <small>${esc(meta)}</small>
2923
+ <h3>${esc(item.name)}<span class="module-row-path">${esc(racePathLabel(item))}</span></h3>
2924
+ <p class="module-row-preview" title="${esc(preview)}">${esc(preview)}${item.members.length ? ` · ${esc(item.members.map((member) => member.name).join("、"))}` : ""}</p>
2925
+ <div class="card-actions">${raceActions(item)}</div>
2926
+ </article>`;
2927
+ }).join("")}</div>`;
2928
+ $("#module-content").innerHTML = state.races.length
2929
+ ? `${renderModuleLayoutToggle(layout, "种族列表样式")}${layout === "rows" ? raceRows() : `<section class="race-tree" aria-label="种族层级">${buildRaceForest(state.races).map(renderRaceNode).join("")}</section>`}`
2930
+ : emptyModule("还没有种族档案", "先创建种族及共同设定,之后角色编辑器才能选择该种族。");
2931
+ bindModuleLayoutToggle(renderRaces);
2740
2932
  $("#module-content").querySelectorAll("[data-edit-race]").forEach((button) => button.addEventListener("click", () => openRaceDialog(state.races.find((item) => item.id === button.dataset.editRace))));
2741
2933
  $("#module-content").querySelectorAll("[data-merge-race]").forEach((button) => button.addEventListener("click", () => {
2742
2934
  const source = state.races.find((item) => item.id === button.dataset.mergeRace);
@@ -2770,13 +2962,30 @@ async function renderOrganizations() {
2770
2962
  apiAllPages(`/api/works/${state.work.id}/organizations`),
2771
2963
  canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
2772
2964
  ]);
2773
- $("#module-content").innerHTML = state.organizations.length ? `<div class="card-grid organization-grid">${state.organizations.map((item) => `
2774
- <article class="record-card organization-card"><small>${item.memberIds.length} 位成员 · ${item.settings.length} 条设定</small>
2965
+ const layout = readModuleLayout();
2966
+ const organizationActions = (item) => `<button data-edit-organization="${esc(item.id)}">编辑</button><button data-entity-history="organization" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button>${canEditModule("organizations") && state.organizations.length > 1 ? `<button data-merge-organization="${esc(item.id)}">合并</button>` : ""}${canEditModule("organizations") ? `<button class="danger-button" data-delete-organization="${esc(item.id)}">删除</button>` : ""}`;
2967
+ const organizationCards = () => `<div class="card-grid organization-grid">${state.organizations.map((item) => `
2968
+ <article class="record-card organization-card"><small>${item.memberIds.length} 位成员 · ${item.settings.length ? "已填写组织设定" : "暂无组织设定"}</small>
2775
2969
  <h3>${esc(item.name)}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
2776
- <div class="organization-settings">${item.settings.map((setting) => `<span class="pill">${esc(setting)}</span>`).join("") || '<span class="pill">暂无组织设定</span>'}</div>
2970
+ <div class="organization-settings">${item.settingsSections?.length ? item.settingsSections.map((section) => `<article class="knowledge-markdown-block"><div class="knowledge-markdown-block-heading"><h4>${esc(section.title || "未命名章节")}</h4></div><div class="message-body">${renderMarkdown(section.contentMarkdown) || '<p class="markdown-editor-empty">暂无内容</p>'}</div></article>`).join("") : '<span class="pill">暂无组织设定</span>'}</div>
2777
2971
  <p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
2778
- <div class="card-actions"><button data-edit-organization="${esc(item.id)}">编辑</button><button data-entity-history="organization" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button>${canEditModule("organizations") && state.organizations.length > 1 ? `<button data-merge-organization="${esc(item.id)}">合并</button>` : ""}${canEditModule("organizations") ? `<button class="danger-button" data-delete-organization="${esc(item.id)}">删除</button>` : ""}</div>
2779
- </article>`).join("")}</div>` : emptyModule("还没有组织", "创建国家、机构、阵营或团队,并维护组织设定与成员。");
2972
+ <div class="card-actions">${organizationActions(item)}</div>
2973
+ </article>`).join("")}</div>`;
2974
+ const organizationRows = () => `<div class="module-row-list">${state.organizations.map((item) => {
2975
+ const preview = moduleRowPreview(item.description || "尚未填写组织简介");
2976
+ const members = item.members.length ? item.members.map((member) => member.name).join("、") : "暂无绑定角色";
2977
+ return `
2978
+ <article class="record-card module-row organization-card">
2979
+ <small>${item.memberIds.length} 位成员 · ${item.settings.length ? "已填写组织设定" : "暂无组织设定"}</small>
2980
+ <h3>${esc(item.name)}</h3>
2981
+ <p class="module-row-preview" title="${esc(`${preview} · 成员:${members}`)}">${esc(preview)} · ${esc(members)}</p>
2982
+ <div class="card-actions">${organizationActions(item)}</div>
2983
+ </article>`;
2984
+ }).join("")}</div>`;
2985
+ $("#module-content").innerHTML = state.organizations.length
2986
+ ? `${renderModuleLayoutToggle(layout, "组织列表样式")}${layout === "rows" ? organizationRows() : organizationCards()}`
2987
+ : emptyModule("还没有组织", "创建国家、机构、阵营或团队,并维护组织设定与成员。");
2988
+ bindModuleLayoutToggle(renderOrganizations);
2780
2989
  $("#module-content").querySelectorAll("[data-edit-organization]").forEach((button) => button.addEventListener("click", () => openOrganizationDialog(state.organizations.find((item) => item.id === button.dataset.editOrganization))));
2781
2990
  $("#module-content").querySelectorAll("[data-merge-organization]").forEach((button) => button.addEventListener("click", () => {
2782
2991
  const source = state.organizations.find((item) => item.id === button.dataset.mergeOrganization);
@@ -2844,17 +3053,35 @@ async function renderOutlines() {
2844
3053
  apiPage(`/api/works/${state.work.id}/outlines`).then((result) => result.items),
2845
3054
  apiPage(`/api/works/${state.work.id}/foreshadows?status=all${currentChapterId ? `&currentChapterId=${encodeURIComponent(currentChapterId)}` : ""}`).then((result) => result.items)
2846
3055
  ]);
3056
+ const layout = readModuleLayout();
2847
3057
  const unresolved = foreshadows.filter((item) => item.unresolved);
2848
3058
  const overdue = unresolved.filter((item) => item.overdue);
2849
3059
  const navButton = $("#module-nav [data-module=outlines] .nav-label");
2850
3060
  if (navButton) navButton.textContent = unresolved.length ? `大纲与伏笔 · ${unresolved.length}` : "大纲与伏笔";
2851
- const foreshadowHtml = foreshadows.length ? `<div class="card-grid foreshadow-grid">${foreshadows.map((item) => `
3061
+ const foreshadowActions = (item) => `<button data-edit-foreshadow="${esc(item.id)}">编辑伏笔</button><button data-entity-history="foreshadow" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.title)}">版本历史</button>`;
3062
+ const foreshadowCards = () => `<div class="card-grid foreshadow-grid">${foreshadows.map((item) => `
2852
3063
  <article class="record-card foreshadow-card ${item.overdue ? "is-overdue" : ""}">
2853
3064
  <small>${esc(item.importance)} · ${esc(item.status)}${item.overdue ? " · 已逾期" : ""}</small>
2854
3065
  <h3>${esc(item.title)}</h3><p>${esc(item.description || "暂无说明")}</p>
2855
3066
  <div class="foreshadow-links">${item.occurrences.length ? item.occurrences.map((link) => `<span class="pill">${esc({ setup: "埋设", reminder: "提醒", payoff: "回收" }[link.role] ?? link.role)} · ${esc(link.volumeTitle)} / ${esc(link.chapterTitle)}</span>`).join("") : '<span class="pill">尚未关联章节</span>'}</div>
2856
- <div class="card-actions"><button data-edit-foreshadow="${esc(item.id)}">编辑伏笔</button><button data-entity-history="foreshadow" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.title)}">版本历史</button></div>
2857
- </article>`).join("")}</div>` : emptyModule("还没有伏笔", "创建伏笔并关联埋设、提醒与回收章节,未回收项会持续显示。\n");
3067
+ <div class="card-actions">${foreshadowActions(item)}</div>
3068
+ </article>`).join("")}</div>`;
3069
+ const foreshadowRows = () => `<div class="module-row-list">${foreshadows.map((item) => {
3070
+ const preview = moduleRowPreview(item.description || "暂无说明");
3071
+ const links = item.occurrences.length
3072
+ ? item.occurrences.map((link) => `${({ setup: "埋设", reminder: "提醒", payoff: "回收" }[link.role] ?? link.role)} · ${link.volumeTitle} / ${link.chapterTitle}`).join(";")
3073
+ : "尚未关联章节";
3074
+ return `
3075
+ <article class="record-card module-row foreshadow-card ${item.overdue ? "is-overdue" : ""}">
3076
+ <small>${esc(item.importance)} · ${esc(item.status)}${item.overdue ? " · 已逾期" : ""}</small>
3077
+ <h3>${esc(item.title)}</h3>
3078
+ <p class="module-row-preview" title="${esc(`${preview} · ${links}`)}">${esc(preview)} · ${esc(links)}</p>
3079
+ <div class="card-actions">${foreshadowActions(item)}</div>
3080
+ </article>`;
3081
+ }).join("")}</div>`;
3082
+ const foreshadowHtml = foreshadows.length
3083
+ ? `${renderModuleLayoutToggle(layout, "伏笔列表样式")}${layout === "rows" ? foreshadowRows() : foreshadowCards()}`
3084
+ : emptyModule("还没有伏笔", "创建伏笔并关联埋设、提醒与回收章节,未回收项会持续显示。\n");
2858
3085
  const outlineHtml = outlines.length ? `<div class="outline-list">${outlines.map((item) => `
2859
3086
  <article class="outline-row ${item.status === "completed" ? "is-complete" : ""}">
2860
3087
  <div><small>${esc(item.volumeTitle)} · ${esc(item.status)}</small><h3>${esc(item.chapterTitle)}</h3></div>
@@ -2864,6 +3091,7 @@ async function renderOutlines() {
2864
3091
  <div class="outline-actions">${item.unresolvedForeshadowCount ? `<span>${item.unresolvedForeshadowCount} 个未回收伏笔</span>` : ""}<button data-edit-outline="${esc(item.chapterId)}">编辑</button><button data-entity-history="chapter-outline" data-entity-id="${esc(item.chapterId)}" data-entity-title="${esc(item.chapterTitle)}">版本历史</button></div>
2865
3092
  </article>`).join("")}</div>` : emptyModule("还没有章节", "先创建章节,再为每章维护目标、冲突和转折。\n");
2866
3093
  $("#module-content").innerHTML = `<div class="outline-summary"><article><strong>${outlines.length}</strong><span>章节规划</span></article><article><strong>${unresolved.length}</strong><span>未回收伏笔</span></article><article class="${overdue.length ? "danger-text" : ""}"><strong>${overdue.length}</strong><span>已逾期</span></article></div><section class="planning-section"><div class="section-title"><div><span class="eyebrow">伏笔追踪</span><h2>尚未回收与历史伏笔</h2></div></div>${foreshadowHtml}</section><section class="planning-section"><div class="section-title"><div><span class="eyebrow">逐章规划</span><h2>章节目标、冲突与转折</h2></div></div>${outlineHtml}</section>`;
3094
+ bindModuleLayoutToggle(renderOutlines);
2867
3095
  $("#module-content").querySelectorAll("[data-edit-outline]").forEach((button) => button.addEventListener("click", () => openOutlineDialog(outlines.find((item) => item.chapterId === button.dataset.editOutline))));
2868
3096
  $("#module-content").querySelectorAll("[data-edit-foreshadow]").forEach((button) => button.addEventListener("click", () => openForeshadowDialog(foreshadows.find((item) => item.id === button.dataset.editForeshadow))));
2869
3097
  bindEntityHistoryButtons(renderOutlines);
@@ -2926,11 +3154,28 @@ async function renderReviews() {
2926
3154
  : "";
2927
3155
  return `<article class="record-card character-duplicate-review"><small>角色查重 · ${esc(item.severity)} · ${esc(item.status)}</small><h3>${esc(item.title)}</h3><div class="character-duplicate-pair">${sideHtml}</div><p>${esc(item.description)}${item.suggestion ? `\n建议:${esc(item.suggestion)}` : ""}</p>${evidenceHtml ? `<ul class="character-duplicate-evidence">${evidenceHtml}</ul>` : ""}${actions}${item.resolutionNote ? `<p class="review-resolution-note">处理结果:${esc(item.resolutionNote)}</p>` : ""}</article>`;
2928
3156
  };
2929
- $("#module-content").innerHTML = reviews.length ? `<div class="card-grid">${reviews.map((item) => item.itemType === "character-duplicate" ? duplicateCard(item) : `
3157
+ const layout = readModuleLayout();
3158
+ const reviewCard = (item) => item.itemType === "character-duplicate" ? duplicateCard(item) : `
2930
3159
  <article class="record-card"><small>${esc(item.itemType)} · ${esc(item.severity)} · ${esc(item.status)}</small><h3>${esc(item.title)}</h3>
2931
3160
  <p>${esc(item.description)}${item.suggestion ? `\n建议:${esc(item.suggestion)}` : ""}</p>
2932
- ${item.status === "pending" && canResolveReview ? `<div class="card-actions"><button data-review-status="fixed" data-review-id="${esc(item.id)}">标为已修复</button><button data-review-status="ignored" data-review-id="${esc(item.id)}">忽略</button></div>` : ""}</article>`).join("")}</div>`
3161
+ ${item.status === "pending" && canResolveReview ? `<div class="card-actions"><button data-review-status="fixed" data-review-id="${esc(item.id)}">标为已修复</button><button data-review-status="ignored" data-review-id="${esc(item.id)}">忽略</button></div>` : ""}</article>`;
3162
+ const reviewRow = (item) => {
3163
+ if (item.itemType === "character-duplicate") {
3164
+ return `<div class="module-row-span">${duplicateCard(item)}</div>`;
3165
+ }
3166
+ const preview = moduleRowPreview(`${item.description || ""}${item.suggestion ? ` 建议:${item.suggestion}` : ""}`);
3167
+ return `
3168
+ <article class="record-card module-row">
3169
+ <small>${esc(item.itemType)} · ${esc(item.severity)} · ${esc(item.status)}</small>
3170
+ <h3>${esc(item.title)}</h3>
3171
+ <p class="module-row-preview" title="${esc(preview)}">${esc(preview)}</p>
3172
+ <div class="card-actions">${item.status === "pending" && canResolveReview ? `<button data-review-status="fixed" data-review-id="${esc(item.id)}">标为已修复</button><button data-review-status="ignored" data-review-id="${esc(item.id)}">忽略</button>` : ""}</div>
3173
+ </article>`;
3174
+ };
3175
+ $("#module-content").innerHTML = reviews.length
3176
+ ? `${renderModuleLayoutToggle(layout, "审核列表样式")}${layout === "rows" ? `<div class="module-row-list">${reviews.map(reviewRow).join("")}</div>` : `<div class="card-grid">${reviews.map(reviewCard).join("")}</div>`}`
2933
3177
  : emptyModule("没有待审核事项", "候选设定、冲突与低置信度结论会集中显示在这里。");
3178
+ bindModuleLayoutToggle(renderReviews);
2934
3179
  $("#module-content").querySelectorAll("[data-review-id]").forEach((button) => button.addEventListener("click", async () => {
2935
3180
  await api(`/api/reviews/${button.dataset.reviewId}`, { method: "PATCH", body: { status: button.dataset.reviewStatus } });
2936
3181
  await renderReviews();
@@ -2938,7 +3183,10 @@ async function renderReviews() {
2938
3183
  $("#module-content").querySelectorAll("[data-merge-review]").forEach((button) => button.addEventListener("click", async () => {
2939
3184
  const target = characterById.get(button.dataset.mergeTarget);
2940
3185
  const source = characterById.get(button.dataset.mergeSource);
2941
- if (!target || !source || !window.confirm(`确认把“${source.name}”合并到“${target.name}”?来源角色的别名、组织、时间线和关系会迁移到目标角色。`)) return;
3186
+ if (!target || !source || !(await confirmToast(
3187
+ `确认把“${source.name}”合并到“${target.name}”?来源角色的别名、组织、时间线和关系会迁移到目标角色。`,
3188
+ { title: "确认合并角色", confirmLabel: "确认合并" }
3189
+ ))) return;
2942
3190
  button.disabled = true;
2943
3191
  try {
2944
3192
  await api(`/api/reviews/${button.dataset.mergeReview}/character-resolution`, { method: "POST", body: {
@@ -2971,7 +3219,7 @@ async function renderReviews() {
2971
3219
 
2972
3220
  async function renderTasks() {
2973
3221
  const [tasks, settings] = await Promise.all([
2974
- apiPage(`/api/works/${state.work.id}/tasks`).then((result) => result.items),
3222
+ apiPage(`/api/works/${state.work.id}/tasks?view=summary`).then((result) => result.items),
2975
3223
  canReadModule("ai-settings")
2976
3224
  ? api(`/api/works/${state.work.id}/ai-settings`)
2977
3225
  : Promise.resolve({ autoRunEnabled: false, autoRunConcurrency: 2, autoRunBatchLimit: 20 })
@@ -3042,9 +3290,13 @@ async function renderTasks() {
3042
3290
  }
3043
3291
  });
3044
3292
 
3045
- const taskById = new Map(tasks.map((item) => [item.id, item]));
3046
3293
  $("#module-content").querySelectorAll("[data-task-detail]").forEach((button) => button.addEventListener("click", () => {
3047
- openTaskDetailDialog(taskById.get(button.dataset.taskDetail));
3294
+ if (button.disabled) return;
3295
+ button.disabled = true;
3296
+ api(`/api/tasks/${encodeURIComponent(button.dataset.taskDetail)}`)
3297
+ .then((task) => openTaskDetailDialog(task))
3298
+ .catch((error) => toast(error.message, "error"))
3299
+ .finally(() => { button.disabled = false; });
3048
3300
  }));
3049
3301
  $("#module-content").querySelectorAll("[data-run-task]").forEach((button) => button.addEventListener("click", async () => {
3050
3302
  const workId = state.work.id;
@@ -3307,17 +3559,19 @@ let aiContextUsageTimer = null;
3307
3559
  let aiContextUsageRequest = 0;
3308
3560
 
3309
3561
  function currentAiRequestScope() {
3310
- if (!state.work || !state.chapter) return null;
3562
+ if (!state.work) return null;
3311
3563
  const taskType = $("#ai-task").value;
3312
3564
  const scopeType = $("#ai-scope").value;
3313
- const selection = $("#chapter-content").value.slice($("#chapter-content").selectionStart, $("#chapter-content").selectionEnd);
3314
- const volume = state.work.volumes.find((item) => item.id === state.chapter.volumeId);
3565
+ const requiresChapter = taskType === "polish" || taskType === "continue" || scopeType !== "none";
3566
+ if (requiresChapter && !state.chapter) return null;
3567
+ const selection = state.chapter ? $("#chapter-content").value.slice($("#chapter-content").selectionStart, $("#chapter-content").selectionEnd) : "";
3568
+ const volume = state.chapter ? state.work.volumes.find((item) => item.id === state.chapter.volumeId) : null;
3315
3569
  const includeBookSummary = scopeType === "chapter-summary";
3316
- const scope = taskType === "polish" ? { type: "chapter", chapterId: state.chapter.id, selection }
3317
- : scopeType === "none" ? { type: "none", ...(taskType === "continue" ? { chapterId: state.chapter.id } : {}) }
3570
+ const scope = taskType === "polish" ? { type: "chapter", chapterId: state.chapter?.id, selection }
3571
+ : scopeType === "none" ? { type: "none", ...(taskType === "continue" && state.chapter ? { chapterId: state.chapter.id } : {}) }
3318
3572
  : scopeType === "book" ? { type: "book" }
3319
3573
  : scopeType === "volume" ? { type: "volume", volumeId: volume?.id }
3320
- : { type: "chapter", chapterId: state.chapter.id };
3574
+ : { type: "chapter", chapterId: state.chapter?.id };
3321
3575
  Object.assign(scope, buildAiReferenceScope(state.aiReferences));
3322
3576
  if (includeBookSummary) scope.includeBookSummary = true;
3323
3577
  return { taskType, scope, selection };
@@ -3442,6 +3696,7 @@ async function ensureAiReferencesLoaded() {
3442
3696
 
3443
3697
  function field(name, label, type = "text", value = "", options = []) {
3444
3698
  if (type === "textarea") return `<label>${esc(label)}<textarea name="${esc(name)}">${esc(value)}</textarea></label>`;
3699
+ if (type === "markdown") return `<div class="form-field markdown-editor-field" data-vditor-editor-field><div class="vditor-editor-host" data-vditor-editor data-placeholder="${esc(options.placeholder ?? `在这里编辑${label}`)}" aria-label="Markdown 编辑器"></div><textarea class="hidden" name="${esc(name)}" data-vditor-value maxlength="200000" aria-label="Markdown 原文">${esc(value)}</textarea></div>`;
3445
3700
  if (type === "item-list") {
3446
3701
  const values = Array.isArray(value) && value.length ? value : [""];
3447
3702
  return `<div class="form-field item-list-field"><span>${esc(label)}</span><div class="item-list-rows" data-item-list-rows data-name="${esc(name)}" data-label="${esc(label)}">${values.map((item) => `<div class="item-list-row"><input name="${esc(name)}" value="${esc(item)}" aria-label="${esc(label)}"><button type="button" data-item-list-remove aria-label="删除此条">删除</button></div>`).join("")}</div><button class="item-list-add" type="button" data-item-list-add>添加一条</button></div>`;
@@ -3483,6 +3738,52 @@ function field(name, label, type = "text", value = "", options = []) {
3483
3738
  return `<label>${esc(label)}<input name="${esc(name)}" type="${esc(type)}" value="${esc(value)}" ${type === "password" ? 'autocomplete="new-password"' : ""} ${type === "number" ? 'step="any"' : ""}></label>`;
3484
3739
  }
3485
3740
 
3741
+ function knowledgeSectionTitleFromMarkdown(content, index) {
3742
+ return String(content).match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/mu)?.[1]?.trim() || `设定 ${index + 1}`;
3743
+ }
3744
+
3745
+ function normalizeKnowledgeEditorSections(item) {
3746
+ const sections = Array.isArray(item?.settingsSections)
3747
+ ? item.settingsSections.map((section, index) => ({
3748
+ title: String(section.title ?? "").trim() || knowledgeSectionTitleFromMarkdown(section.contentMarkdown ?? "", index),
3749
+ contentMarkdown: String(section.contentMarkdown ?? section.content ?? ""),
3750
+ summary: String(section.summary ?? ""),
3751
+ sortOrder: Number.isFinite(Number(section.sortOrder)) ? Number(section.sortOrder) : index
3752
+ }))
3753
+ : [];
3754
+ if (sections.length > 0) return sections;
3755
+ const legacy = String(item?.settingsMarkdown ?? (Array.isArray(item?.settings) ? item.settings.join("\n\n") : ""));
3756
+ return legacy.trim() ? [{ title: knowledgeSectionTitleFromMarkdown(legacy, 0), contentMarkdown: legacy, summary: "", sortOrder: 0 }] : [];
3757
+ }
3758
+
3759
+ function knowledgeSectionPreviewText(section) {
3760
+ const content = String(section.contentMarkdown ?? "").replace(/!\[[^\]]*\]\([^)]*\)/gu, "图片").replace(/[`*_>#-]/gu, "").replace(/\s+/gu, " ").trim();
3761
+ return content ? `${content.slice(0, 180)}${content.length > 180 ? "…" : ""}` : "暂无正文内容";
3762
+ }
3763
+
3764
+ function renderKnowledgeMarkdownSections() {
3765
+ const host = $("#knowledge-markdown-sections");
3766
+ if (!host) return;
3767
+ const label = knowledgeEditorKind === "race" ? "种族" : "组织";
3768
+ const canEdit = canEditModule(knowledgeEditorKind === "race" ? "races" : "organizations");
3769
+ const sections = Array.isArray(knowledgeEditorSections) ? knowledgeEditorSections : [];
3770
+ host.innerHTML = `<div class="knowledge-markdown-list-toolbar"><div><b>${label} Markdown 设定</b><span>将每条设定单独保存为章节,需要编辑时打开大编辑器。</span></div>${canEdit ? '<button type="button" class="ghost-button" data-knowledge-section-create>新建设定</button>' : ""}</div>${sections.length ? `<div class="knowledge-markdown-list">${sections.map((section, index) => `<article class="knowledge-markdown-section" data-knowledge-section-index="${index}"><header><div><span>设定 ${index + 1}</span><h4>${esc(section.title || `未命名设定 ${index + 1}`)}</h4>${section.summary ? `<p>${esc(section.summary)}</p>` : ""}</div><div>${canEdit ? `<button type="button" data-knowledge-section-edit="${index}">编辑</button><button type="button" data-knowledge-section-delete="${index}">删除</button>` : ""}</div></header><p class="knowledge-section-card-preview">${esc(knowledgeSectionPreviewText(section))}</p></article>`).join("")}</div>` : '<p class="knowledge-markdown-empty">还没有 Markdown 设定,点击“新建设定”开始记录。</p>'}`;
3771
+ host.querySelector("[data-knowledge-section-create]")?.addEventListener("click", () => void openKnowledgeSectionEditor());
3772
+ host.querySelectorAll("[data-knowledge-section-edit]").forEach((button) => button.addEventListener("click", () => void openKnowledgeSectionEditor(Number(button.dataset.knowledgeSectionEdit))));
3773
+ host.querySelectorAll("[data-knowledge-section-delete]").forEach((button) => button.addEventListener("click", async () => {
3774
+ const index = Number(button.dataset.knowledgeSectionDelete);
3775
+ const section = knowledgeEditorSections[index];
3776
+ if (!section || !(await confirmToast(
3777
+ `确定删除“${section.title || `设定 ${index + 1}`}”吗?`,
3778
+ { title: "删除设定", confirmLabel: "确认删除" }
3779
+ ))) return;
3780
+ knowledgeEditorSections.splice(index, 1);
3781
+ knowledgeEditorSections.forEach((item, sortOrder) => { item.sortOrder = sortOrder; });
3782
+ markEntityEditorDirty();
3783
+ renderKnowledgeMarkdownSections();
3784
+ }));
3785
+ }
3786
+
3486
3787
  function bindDynamicListControls(container) {
3487
3788
  container.querySelectorAll("[data-item-list-add]").forEach((button) => button.addEventListener("click", () => {
3488
3789
  const rows = button.previousElementSibling;
@@ -3565,6 +3866,7 @@ function commitRelationshipKeywordInputs(container) {
3565
3866
  }
3566
3867
 
3567
3868
  function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
3869
+ void discardPendingMarkdownAttachments();
3568
3870
  $("#dialog-title").textContent = title;
3569
3871
  $("#dialog-eyebrow").textContent = eyebrow;
3570
3872
  $("#dialog-fields").innerHTML = fields;
@@ -3572,15 +3874,21 @@ function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
3572
3874
  $("#form-dialog").classList.toggle("wide-dialog", Boolean(options.wide));
3573
3875
  bindDynamicListControls($("#dialog-fields"));
3574
3876
  bindRelationshipKeywordControls($("#dialog-fields"));
3877
+ bindVditorEditors($("#dialog-fields"));
3575
3878
  const form = $("#dynamic-form");
3576
3879
  form.onsubmit = async (event) => {
3577
- if (event.submitter?.value === "cancel") return;
3880
+ if (event.submitter?.value === "cancel") {
3881
+ void discardPendingMarkdownAttachments();
3882
+ return;
3883
+ }
3578
3884
  event.preventDefault();
3579
3885
  const submit = $("#dialog-submit");
3580
3886
  submit.disabled = true;
3581
3887
  try {
3582
3888
  commitRelationshipKeywordInputs(form);
3583
3889
  await onSubmit(new FormData(form));
3890
+ const markdown = [...form.querySelectorAll("[data-vditor-value]")].map((textarea) => textarea.value).join("\n\n");
3891
+ await cleanupPendingMarkdownAttachments(markdown);
3584
3892
  $("#form-dialog").close();
3585
3893
  } catch (error) {
3586
3894
  toast(error.message, "error");
@@ -3652,12 +3960,21 @@ function bindWorkCoverControls(work) {
3652
3960
  function openWorkSettingsDialog(work) {
3653
3961
  if (!work) return;
3654
3962
  const canManageAccess = ["admin", "owner"].includes(String(work.accessRole));
3963
+ const isCurrentWork = state.work?.id === work.id;
3964
+ const canOpenImportHistory = isCurrentWork && canReplaceProse(work);
3965
+ const importHistoryAction = isCurrentWork
3966
+ ? canOpenImportHistory ? "查看导入历史" : "需要完整编辑权限"
3967
+ : "打开作品后可用";
3655
3968
  const accessField = `<section class="work-access-field" aria-labelledby="work-access-title">
3656
3969
  <div><strong id="work-access-title">成员权限</strong><small>选择成员后,可为每个作品模块单独设置无权限、只读或可编辑。</small><div class="work-access-options" aria-label="成员权限配置方式"><span>按成员配置</span><span>按模块授权</span><span>读写分离</span></div></div>
3657
3970
  ${canManageAccess ? '<button id="work-access-manage" class="ghost-button" type="button">配置成员权限</button>' : '<small>仅作品创建者或系统管理员可以调整访问权限。</small>'}
3658
3971
  </section>`;
3972
+ const importHistoryField = `<section class="work-access-field" aria-labelledby="import-history-settings-title">
3973
+ <div><strong id="import-history-settings-title">正文导入历史</strong><small>查看导入前快照并恢复被覆盖的分卷、章节标题和正文;大纲、伏笔等章节关联资料不在快照中。</small></div>
3974
+ <button id="import-history-button" class="ghost-button" type="button" aria-controls="import-history-dialog" aria-haspopup="dialog" ${canOpenImportHistory ? "" : "disabled"}>${importHistoryAction}</button>
3975
+ </section>`;
3659
3976
  openDialog("作品信息",
3660
- workCoverFieldHtml(work) + field("title", "作品名称", "text", work.title) + field("author", "作者", "text", work.author) + field("description", "简介", "textarea", work.description) + accessField,
3977
+ workCoverFieldHtml(work) + field("title", "作品名称", "text", work.title) + field("author", "作者", "text", work.author) + field("description", "简介", "textarea", work.description) + accessField + importHistoryField,
3661
3978
  async (form) => {
3662
3979
  await api(`/api/works/${work.id}`, { method: "PATCH", body: { title: form.get("title"), author: form.get("author"), description: form.get("description") } });
3663
3980
  state.works = (await apiPage("/api/works")).items;
@@ -3675,13 +3992,17 @@ function openWorkSettingsDialog(work) {
3675
3992
  toast("作品信息已保存");
3676
3993
  }, "作品设置");
3677
3994
  bindWorkCoverControls(work);
3995
+ $("#import-history-button")?.addEventListener("click", () => {
3996
+ $("#form-dialog").close();
3997
+ void openImportHistory();
3998
+ });
3678
3999
  $("#work-access-manage")?.addEventListener("click", () => {
3679
4000
  $("#form-dialog").close();
3680
4001
  openMembersDialog(work);
3681
4002
  });
3682
4003
  }
3683
4004
 
3684
- async function openChapterDialog() {
4005
+ async function openChapterDialog(volumeId = null) {
3685
4006
  if (!state.work) return openWorkDialog();
3686
4007
  if (!canEditProse()) return toast("当前权限只能编辑设定资料,正文为只读", "error");
3687
4008
  if (!state.work.volumes.length) {
@@ -3689,7 +4010,8 @@ async function openChapterDialog() {
3689
4010
  state.work = await api(`/api/works/${state.work.id}`);
3690
4011
  renderTree();
3691
4012
  }
3692
- openDialog("新建章节", field("title", "章节标题") + field("volumeId", "所属卷", "select", state.work.volumes[0].id, state.work.volumes.map((volume) => [volume.id, volume.title])) + field("chapterType", "章节类型", "select", "正文", chapterTypes.map((value) => [value, value])), async (form) => {
4013
+ const selectedVolumeId = state.work.volumes.some((volume) => volume.id === volumeId) ? volumeId : state.work.volumes[0].id;
4014
+ openDialog("新建章节", field("title", "章节标题") + field("volumeId", "所属卷", "select", selectedVolumeId, state.work.volumes.map((volume) => [volume.id, volume.title])) + field("chapterType", "章节类型", "select", "正文", chapterTypes.map((value) => [value, value])), async (form) => {
3693
4015
  const chapter = await api(`/api/works/${state.work.id}/chapters`, { method: "POST", body: { title: form.get("title"), volumeId: form.get("volumeId"), chapterType: form.get("chapterType"), content: "" } });
3694
4016
  state.work = await api(`/api/works/${state.work.id}`);
3695
4017
  await selectChapter(chapter.id);
@@ -3704,13 +4026,13 @@ function openVolumeDialog(item) {
3704
4026
  field("title", "分卷名称", "text", item?.title) +
3705
4027
  field("kind", "分卷类型", "select", item?.kind ?? "main", kindOptions) +
3706
4028
  field("description", "分卷简介", "textarea", item?.description) +
3707
- field("keywords", "分卷关键词(逐条填写)", "item-list", item?.keywords ?? []),
4029
+ field("keywords", "分卷关键词", "keyword-chips", item?.keywords ?? []),
3708
4030
  async (form) => {
3709
4031
  const body = {
3710
4032
  title: form.get("title"),
3711
4033
  kind: form.get("kind"),
3712
4034
  description: form.get("description"),
3713
- keywords: form.getAll("keywords").map((value) => String(value).trim()).filter(Boolean)
4035
+ keywords: uniqueRelationshipKeywords(form.getAll("keywords").map(String))
3714
4036
  };
3715
4037
  await api(item ? `/api/volumes/${item.id}` : `/api/works/${state.work.id}/volumes`, { method: item ? "PATCH" : "POST", body });
3716
4038
  state.work = await api(`/api/works/${state.work.id}`);
@@ -3720,9 +4042,10 @@ function openVolumeDialog(item) {
3720
4042
  }
3721
4043
 
3722
4044
  function openSettingEditor(item = null) {
4045
+ destroyVditorEditor(settingEditorVditor);
4046
+ settingEditorVditor = null;
3723
4047
  settingEditorItem = item;
3724
4048
  $("#setting-editor-eyebrow").textContent = item ? "人工修正" : "作者事实";
3725
- $("#setting-editor-title").textContent = item ? `编辑“${item.title}”` : "新建设定";
3726
4049
  $("#setting-editor-name").value = item?.title ?? "";
3727
4050
  $("#setting-editor-category").value = item?.category ?? "世界规则";
3728
4051
  $("#setting-editor-locked").checked = Boolean(item?.locked);
@@ -3741,16 +4064,29 @@ function openSettingEditor(item = null) {
3741
4064
  const submit = $("#setting-editor-submit");
3742
4065
  submit.disabled = true;
3743
4066
  try {
4067
+ const title = String(form.get("title") ?? "").trim();
4068
+ if (!title) {
4069
+ toast("请填写设定标题", "error");
4070
+ $("#setting-editor-name").focus();
4071
+ return;
4072
+ }
4073
+ const content = String(form.get("content") ?? "");
4074
+ if (!content.trim()) {
4075
+ toast("请填写设定正文", "error");
4076
+ $("#setting-editor-body").focus();
4077
+ return;
4078
+ }
3744
4079
  const locked = form.get("locked") === "on";
3745
4080
  const body = {
3746
- title: String(form.get("title") ?? "").trim(),
4081
+ title,
3747
4082
  category: String(form.get("category") ?? "世界规则"),
3748
- content: String(form.get("content") ?? ""),
4083
+ content,
3749
4084
  locked,
3750
4085
  status: locked ? "confirmed" : (item?.status ?? "draft"),
3751
4086
  ...(item ? { changeNote: String(form.get("changeNote") ?? "").trim() } : {})
3752
4087
  };
3753
4088
  await api(item ? `/api/settings/${item.id}` : `/api/works/${state.work.id}/settings`, { method: item ? "PATCH" : "POST", body });
4089
+ await cleanupPendingMarkdownAttachments(body.content);
3754
4090
  entityEditorDirty = false;
3755
4091
  await loadAiReferences();
3756
4092
  await closeEntityEditor({ force: true });
@@ -3762,6 +4098,10 @@ function openSettingEditor(item = null) {
3762
4098
  }
3763
4099
  };
3764
4100
  showEntityEditorPage("setting");
4101
+ settingEditorVditor = createVditorEditor($("#setting-editor-markdown"), item?.content ?? "", {
4102
+ onInput: (markdown) => { $("#setting-editor-body").value = markdown; markEntityEditorDirty(); },
4103
+ readOnly: viewOnly
4104
+ });
3765
4105
  $("#setting-editor-name").focus();
3766
4106
  }
3767
4107
 
@@ -3887,22 +4227,165 @@ async function discardPendingCharacterAttachments() {
3887
4227
  }));
3888
4228
  }
3889
4229
 
3890
- function scheduleCharacterSectionPreview() {
3891
- if (characterSectionPreviewTimer !== null) clearTimeout(characterSectionPreviewTimer);
3892
- characterSectionPreviewTimer = window.setTimeout(() => {
3893
- characterSectionPreviewTimer = null;
3894
- const preview = $("#character-section-preview");
3895
- const content = $("#character-section-markdown");
3896
- if (preview && content) preview.innerHTML = renderMarkdown(content.value) || '<p class="character-markdown-empty">预览区域暂无内容。</p>';
3897
- }, 260);
4230
+ async function discardPendingMarkdownAttachments() {
4231
+ const pending = markdownEditorPendingAttachments.splice(0);
4232
+ await Promise.all(pending.map(async (attachmentId) => {
4233
+ try { await api(`/api/attachments/${attachmentId}`, { method: "DELETE" }); } catch { /* 已被引用的附件由正常引用生命周期管理。 */ }
4234
+ }));
3898
4235
  }
3899
4236
 
3900
- async function closeCharacterSectionEditor({ force = false } = {}) {
3901
- if (!force && characterSectionEditorDirty && !window.confirm("当前 Markdown 章节有未保存修改,返回人物档案将丢弃这些修改。是否继续?")) return false;
3902
- if (characterSectionPreviewTimer !== null) {
3903
- clearTimeout(characterSectionPreviewTimer);
3904
- characterSectionPreviewTimer = null;
4237
+ async function cleanupPendingMarkdownAttachments(contentMarkdown) {
4238
+ const referenced = new Set([...String(contentMarkdown).matchAll(/attachment:\/\/([A-Za-z0-9_-]{1,300})/gu)].map((match) => String(match[1])));
4239
+ const pending = markdownEditorPendingAttachments.splice(0);
4240
+ await Promise.all(pending.filter((attachmentId) => !referenced.has(attachmentId)).map((attachmentId) => api(`/api/attachments/${attachmentId}`, { method: "DELETE" }).catch(() => null)));
4241
+ }
4242
+
4243
+ function markdownImageLabel(file, fallback = "图片附件") {
4244
+ return String(file?.name ?? "").replace(/[\[\]\r\n]/gu, "").trim() || fallback;
4245
+ }
4246
+
4247
+ async function uploadMarkdownAttachment(file) {
4248
+ const body = new FormData();
4249
+ body.append("file", file);
4250
+ const attachment = await api(`/api/works/${state.work.id}/attachments`, { method: "POST", body });
4251
+ if (!attachment.deduplicated) markdownEditorPendingAttachments.push(String(attachment.id));
4252
+ return { attachment, imageLabel: markdownImageLabel(file) };
4253
+ }
4254
+
4255
+ function createVditorUploadHandler(uploadAttachment, getEditor) {
4256
+ return async (files) => {
4257
+ const selection = window.getSelection();
4258
+ const range = selection?.rangeCount ? selection.getRangeAt(0).cloneRange() : null;
4259
+ const insertions = [];
4260
+ for (const file of files) {
4261
+ try {
4262
+ const { attachment, imageLabel } = await uploadAttachment(file);
4263
+ insertions.push(`![${imageLabel}](attachment://${attachment.id})`);
4264
+ } catch (error) {
4265
+ toast(error.message, "error");
4266
+ }
4267
+ }
4268
+ const editor = getEditor();
4269
+ if (editor && insertions.length > 0) {
4270
+ if (range && editor.vditor?.element?.contains(range.startContainer)) {
4271
+ selection?.removeAllRanges();
4272
+ selection?.addRange(range);
4273
+ }
4274
+ editor.insertMD(insertions.join("\n\n"));
4275
+ normalizeVditorAttachmentImages(editor);
4276
+ }
4277
+ return null;
4278
+ };
4279
+ }
4280
+
4281
+ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment = uploadMarkdownAttachment, placeholder = "", readOnly = false } = {}) {
4282
+ if (!window.Vditor) {
4283
+ toast("Markdown 编辑器资源加载失败,请刷新页面后重试", "error");
4284
+ return null;
3905
4285
  }
4286
+ ensureVditorIconScript();
4287
+ let editor = null;
4288
+ editor = new window.Vditor(host, {
4289
+ cdn: "/vendor/vditor",
4290
+ lang: "zh_CN",
4291
+ theme: currentColorTheme() === "dark" ? "dark" : "classic",
4292
+ mode: "ir",
4293
+ value: String(value ?? ""),
4294
+ height: "100%",
4295
+ minHeight: 260,
4296
+ placeholder,
4297
+ preview: { transform: transformVditorPreview },
4298
+ cache: { enable: false },
4299
+ toolbar: ["headings", "bold", "italic", "strike", "|", "line", "quote", "list", "ordered-list", "check", "|", "code", "inline-code", "link", "table", "upload", "|", "undo", "redo", "edit-mode", "fullscreen"],
4300
+ upload: {
4301
+ accept: "image/*",
4302
+ max: 10 * 1024 * 1024,
4303
+ multiple: true,
4304
+ handler: createVditorUploadHandler(uploadAttachment, () => editor)
4305
+ },
4306
+ input: (markdown) => {
4307
+ normalizeVditorAttachmentImages(editor);
4308
+ onInput(markdown);
4309
+ },
4310
+ after: () => {
4311
+ normalizeVditorAttachmentImages(editor);
4312
+ if (readOnly) editor?.disabled();
4313
+ }
4314
+ });
4315
+ const attachmentObserver = new MutationObserver(() => normalizeVditorAttachmentImages(editor));
4316
+ attachmentObserver.observe(host, { subtree: true, childList: true, attributes: true, attributeFilter: ["src"] });
4317
+ editor.__attachmentObserver = attachmentObserver;
4318
+ host.__vditor = editor;
4319
+ if (readOnly) editor.disabled();
4320
+ return editor;
4321
+ }
4322
+
4323
+ function transformVditorPreview(html) {
4324
+ return String(html ?? "").replace(/(<img\b[^>]*\bsrc\s*=\s*)(["'])attachment:\/\/([A-Za-z0-9_-]{1,300})\2/giu, (_match, prefix, quote, attachmentId) => `${prefix}${quote}/api/attachments/${encodeURIComponent(attachmentId)}/content${quote}`);
4325
+ }
4326
+
4327
+ function normalizeVditorAttachmentImages(editor) {
4328
+ const root = editor?.vditor?.element;
4329
+ if (!root) return;
4330
+ root.querySelectorAll('img[src^="attachment://"]').forEach((image) => {
4331
+ const target = image.getAttribute("src")?.trim() ?? "";
4332
+ const attachment = target.match(/^attachment:\/\/([A-Za-z0-9_-]{1,300})$/u);
4333
+ if (attachment) image.setAttribute("src", `/api/attachments/${encodeURIComponent(attachment[1])}/content`);
4334
+ });
4335
+ }
4336
+
4337
+ function ensureVditorIconScript() {
4338
+ if (document.getElementById("vditorIconScript")) return;
4339
+ const script = document.createElement("script");
4340
+ script.id = "vditorIconScript";
4341
+ script.src = "/vendor/vditor/dist/js/icons/ant.js?v=3.11.2";
4342
+ document.body.appendChild(script);
4343
+ }
4344
+
4345
+ function destroyVditorEditor(editor) {
4346
+ if (!editor) return;
4347
+ editor.__attachmentObserver?.disconnect();
4348
+ const host = editor.vditor?.element;
4349
+ editor.destroy();
4350
+ if (host) delete host.__vditor;
4351
+ }
4352
+
4353
+ function bindVditorEditors(container) {
4354
+ container.querySelectorAll("[data-vditor-editor]").forEach((host) => {
4355
+ const valueField = host.parentElement?.querySelector("[data-vditor-value]");
4356
+ const editor = createVditorEditor(host, valueField?.value ?? "", {
4357
+ onInput: (markdown) => {
4358
+ if (valueField) valueField.value = markdown;
4359
+ markEntityEditorDirty();
4360
+ },
4361
+ placeholder: "",
4362
+ readOnly: Boolean(valueField?.readOnly)
4363
+ });
4364
+ });
4365
+ }
4366
+
4367
+ function characterSectionImageLabel(file, fallback = "图片附件") {
4368
+ return String(file?.name ?? "").replace(/[\[\]\r\n]/gu, "").trim() || fallback;
4369
+ }
4370
+
4371
+ async function uploadCharacterSectionAttachment(file) {
4372
+ const body = new FormData();
4373
+ body.append("file", file);
4374
+ const attachment = await api(`/api/works/${state.work.id}/attachments`, { method: "POST", body });
4375
+ if (!attachment.deduplicated) characterSectionPendingAttachments.push(String(attachment.id));
4376
+ return {
4377
+ attachment,
4378
+ imageLabel: characterSectionImageLabel(file)
4379
+ };
4380
+ }
4381
+
4382
+ async function closeCharacterSectionEditor({ force = false } = {}) {
4383
+ if (!force && characterSectionEditorDirty && !(await confirmToast(
4384
+ "当前 Markdown 章节有未保存修改,返回人物档案将丢弃这些修改。是否继续?",
4385
+ { title: "放弃未保存修改", confirmLabel: "放弃并继续", cancelLabel: "继续编辑" }
4386
+ ))) return false;
4387
+ destroyVditorEditor(characterSectionVditor);
4388
+ characterSectionVditor = null;
3906
4389
  await discardPendingCharacterAttachments();
3907
4390
  characterSectionEditorDirty = false;
3908
4391
  $("#character-section-editor-view").classList.add("hidden");
@@ -3912,6 +4395,82 @@ async function closeCharacterSectionEditor({ force = false } = {}) {
3912
4395
  return true;
3913
4396
  }
3914
4397
 
4398
+ function knowledgeSectionEditorHtml(section = null) {
4399
+ const label = knowledgeEditorKind === "race" ? "种族" : "组织";
4400
+ return `<div class="character-section-editor-shell knowledge-section-editor-shell">
4401
+ <header class="character-section-editor-header">
4402
+ <div><span class="eyebrow">${label} Markdown 设定</span><h2 id="knowledge-section-editor-title">${section ? `编辑“${esc(section.title)}”` : "新建设定"}</h2></div>
4403
+ <button class="entity-editor-back" type="button" data-knowledge-section-edit-close>返回设定列表</button>
4404
+ </header>
4405
+ <section class="character-markdown-editor" aria-label="${section ? "编辑" : "新建"}${label} Markdown 设定">
4406
+ <div class="character-markdown-editor-meta">
4407
+ <label>设定标题<input id="knowledge-section-title" maxlength="200" value="${esc(section?.title ?? "")}" placeholder="例如:组织章程、种族特征" required></label>
4408
+ </div>
4409
+ <div id="knowledge-section-markdown" class="vditor-editor-host" data-vditor-editor aria-label="Markdown 编辑器"></div>
4410
+ <div class="character-markdown-editor-footer">
4411
+ <div class="character-markdown-editor-actions"><button type="button" data-knowledge-section-edit-cancel>取消</button><button type="button" class="primary-button" data-knowledge-section-edit-save>${section ? "保存设定" : "添加设定"}</button></div>
4412
+ </div>
4413
+ </section>
4414
+ </div>`;
4415
+ }
4416
+
4417
+ async function closeKnowledgeSectionEditor({ force = false } = {}) {
4418
+ if (!force && knowledgeSectionEditorDirty && !(await confirmToast(
4419
+ "当前 Markdown 设定有未保存修改,返回设定列表将丢弃这些修改。是否继续?",
4420
+ { title: "放弃未保存修改", confirmLabel: "放弃并继续", cancelLabel: "继续编辑" }
4421
+ ))) return false;
4422
+ destroyVditorEditor(knowledgeSectionVditor);
4423
+ knowledgeSectionVditor = null;
4424
+ knowledgeSectionEditorDirty = false;
4425
+ knowledgeSectionEditorIndex = null;
4426
+ $("#knowledge-section-editor-view").classList.add("hidden");
4427
+ $("#knowledge-editor-form").classList.remove("hidden");
4428
+ $("#knowledge-section-editor-host").innerHTML = "";
4429
+ renderKnowledgeMarkdownSections();
4430
+ replacePageRoute(currentPageRoute());
4431
+ return true;
4432
+ }
4433
+
4434
+ async function openKnowledgeSectionEditor(index = null) {
4435
+ if (!canEditModule(knowledgeEditorKind === "race" ? "races" : "organizations")) return;
4436
+ destroyVditorEditor(knowledgeSectionVditor);
4437
+ knowledgeSectionVditor = null;
4438
+ const section = Number.isInteger(index) ? knowledgeEditorSections[index] : null;
4439
+ if (Number.isInteger(index) && !section) return;
4440
+ knowledgeSectionEditorIndex = Number.isInteger(index) ? index : null;
4441
+ knowledgeSectionEditorDirty = false;
4442
+ const host = $("#knowledge-section-editor-host");
4443
+ host.innerHTML = knowledgeSectionEditorHtml(section);
4444
+ $("#knowledge-editor-form").classList.add("hidden");
4445
+ $("#knowledge-section-editor-view").classList.remove("hidden");
4446
+ const titleInput = $("#knowledge-section-title");
4447
+ host.querySelectorAll("input, textarea").forEach((control) => control.addEventListener("input", () => { knowledgeSectionEditorDirty = true; }));
4448
+ knowledgeSectionVditor = createVditorEditor($("#knowledge-section-markdown"), section?.contentMarkdown ?? "", {
4449
+ onInput: () => { knowledgeSectionEditorDirty = true; }
4450
+ });
4451
+ host.querySelector("[data-knowledge-section-edit-close]").addEventListener("click", () => void closeKnowledgeSectionEditor());
4452
+ host.querySelector("[data-knowledge-section-edit-cancel]").addEventListener("click", () => void closeKnowledgeSectionEditor());
4453
+ host.querySelector("[data-knowledge-section-edit-save]").addEventListener("click", async (event) => {
4454
+ const button = event.currentTarget;
4455
+ const wasEditing = knowledgeSectionEditorIndex !== null;
4456
+ const title = titleInput.value.trim();
4457
+ if (!title) {
4458
+ toast("请填写设定标题", "error");
4459
+ titleInput.focus();
4460
+ return;
4461
+ }
4462
+ button.disabled = true;
4463
+ const nextSection = { title, contentMarkdown: knowledgeSectionVditor?.getValue() ?? "", summary: String(section?.summary ?? ""), sortOrder: knowledgeSectionEditorIndex ?? knowledgeEditorSections.length };
4464
+ if (knowledgeSectionEditorIndex === null) knowledgeEditorSections.push(nextSection);
4465
+ else knowledgeEditorSections[knowledgeSectionEditorIndex] = nextSection;
4466
+ knowledgeEditorSections.forEach((item, sortOrder) => { item.sortOrder = sortOrder; });
4467
+ markEntityEditorDirty();
4468
+ await closeKnowledgeSectionEditor({ force: true });
4469
+ toast(wasEditing ? "设定已更新" : "设定已添加");
4470
+ });
4471
+ titleInput.focus();
4472
+ }
4473
+
3915
4474
  function characterSectionEditorHtml(section = null) {
3916
4475
  const options = Object.entries(characterSectionTypeLabels).map(([value, label]) => `<option value="${value}" ${section?.sectionType === value ? "selected" : ""}>${esc(label)}</option>`).join("");
3917
4476
  return `<div class="character-section-editor-shell">
@@ -3925,15 +4484,7 @@ function characterSectionEditorHtml(section = null) {
3925
4484
  <label>章节标题<input id="character-section-title" maxlength="200" value="${esc(section?.title ?? "")}" placeholder="例如:背景故事" required></label>
3926
4485
  <label class="character-markdown-summary-field">章节摘要<textarea id="character-section-summary" maxlength="20000" placeholder="用于角色列表和 AI 快速定位,不会替代正文">${esc(section?.summary ?? "")}</textarea></label>
3927
4486
  </div>
3928
- <div class="character-markdown-toolbar">
3929
- <label class="ghost-button" for="character-section-attachment">上传并插入图片</label>
3930
- <input id="character-section-attachment" class="hidden" type="file" accept=".png,.jpg,.jpeg,.webp,.gif,image/png,image/jpeg,image/webp,image/gif">
3931
- <span>图片会优先转换为体积更小的无损 WebP</span>
3932
- </div>
3933
- <div class="character-markdown-compose">
3934
- <label>Markdown 原文<textarea id="character-section-markdown" maxlength="500000" spellcheck="true" placeholder="支持标题、列表、引用、表格、链接和图片">${esc(section?.contentMarkdown ?? "")}</textarea></label>
3935
- <div><span class="character-markdown-preview-label">安全预览</span><article id="character-section-preview" class="character-markdown-document message-body">${renderMarkdown(section?.contentMarkdown ?? "") || '<p class="character-markdown-empty">预览区域暂无内容。</p>'}</article></div>
3936
- </div>
4487
+ <div id="character-section-markdown" class="vditor-editor-host" data-vditor-editor aria-label="Markdown 编辑器"></div>
3937
4488
  <div class="character-markdown-editor-footer">
3938
4489
  <label class="character-markdown-change-note">版本说明<input id="character-section-change-note" maxlength="500" placeholder="可选,例如:补充远古时期经历"></label>
3939
4490
  <div class="character-markdown-editor-actions"><button type="button" data-character-section-edit-cancel>取消</button><button type="button" class="primary-button" data-character-section-edit-save>${section ? "保存章节版本" : "创建章节"}</button></div>
@@ -3944,41 +4495,17 @@ function characterSectionEditorHtml(section = null) {
3944
4495
 
3945
4496
  async function openCharacterSectionEditor(section = null) {
3946
4497
  await discardPendingCharacterAttachments();
4498
+ destroyVditorEditor(characterSectionVditor);
4499
+ characterSectionVditor = null;
3947
4500
  const host = $("#character-section-editor-host");
3948
4501
  host.innerHTML = characterSectionEditorHtml(section);
3949
4502
  characterSectionEditorDirty = false;
3950
4503
  $("#character-editor-form").classList.add("hidden");
3951
4504
  $("#character-section-editor-view").classList.remove("hidden");
3952
- const textarea = $("#character-section-markdown");
3953
4505
  host.querySelectorAll("input, textarea, select").forEach((control) => control.addEventListener("input", () => { characterSectionEditorDirty = true; }));
3954
- textarea.addEventListener("input", scheduleCharacterSectionPreview);
3955
- $("#character-section-attachment").addEventListener("change", async (event) => {
3956
- const file = event.target.files?.[0];
3957
- if (!file) return;
3958
- const input = event.currentTarget;
3959
- input.disabled = true;
3960
- const body = new FormData();
3961
- body.append("file", file);
3962
- try {
3963
- const attachment = await api(`/api/works/${state.work.id}/attachments`, { method: "POST", body });
3964
- if (!attachment.deduplicated) characterSectionPendingAttachments.push(String(attachment.id));
3965
- const imageLabel = file.name.replace(/[\]\r\n]/gu, "").trim() || "图片附件";
3966
- const insertion = `![${imageLabel}](attachment://${attachment.id})`;
3967
- const start = textarea.selectionStart ?? textarea.value.length;
3968
- const end = textarea.selectionEnd ?? start;
3969
- const prefix = start > 0 && !textarea.value.slice(0, start).endsWith("\n") ? "\n\n" : "";
3970
- const suffix = end < textarea.value.length && !textarea.value.slice(end).startsWith("\n") ? "\n\n" : "";
3971
- textarea.setRangeText(`${prefix}${insertion}${suffix}`, start, end, "end");
3972
- textarea.focus();
3973
- characterSectionEditorDirty = true;
3974
- scheduleCharacterSectionPreview();
3975
- toast(attachment.storedMimeType === "image/webp" ? "图片已转换为无损 WebP 并插入" : "图片已插入;转换后未变小,因此保留原格式");
3976
- } catch (error) {
3977
- toast(error.message, "error");
3978
- } finally {
3979
- input.disabled = false;
3980
- input.value = "";
3981
- }
4506
+ characterSectionVditor = createVditorEditor($("#character-section-markdown"), section?.contentMarkdown ?? "", {
4507
+ uploadAttachment: uploadCharacterSectionAttachment,
4508
+ onInput: () => { characterSectionEditorDirty = true; }
3982
4509
  });
3983
4510
  host.querySelector("[data-character-section-edit-close]").addEventListener("click", () => void closeCharacterSectionEditor());
3984
4511
  host.querySelector("[data-character-section-edit-cancel]").addEventListener("click", () => void closeCharacterSectionEditor());
@@ -3991,7 +4518,7 @@ async function openCharacterSectionEditor(section = null) {
3991
4518
  return;
3992
4519
  }
3993
4520
  button.disabled = true;
3994
- const contentMarkdown = textarea.value;
4521
+ const contentMarkdown = characterSectionVditor?.getValue() ?? "";
3995
4522
  try {
3996
4523
  const saved = await api(section ? `/api/character-sections/${section.id}` : `/api/characters/${characterEditorItem.id}/sections`, {
3997
4524
  method: section ? "PATCH" : "POST",
@@ -4049,7 +4576,7 @@ function renderCharacterMarkdownSections() {
4049
4576
  const host = $("#character-markdown-sections");
4050
4577
  if (!host) return;
4051
4578
  if (!characterEditorItem?.id) {
4052
- host.innerHTML = '<div class="character-editor-empty-field"><b>Markdown 档案章节</b><span>创建人物档案后即可添加背景故事、能力、经历和研究记录。</span></div>';
4579
+ host.innerHTML = '<div class="character-editor-empty-field" role="note" aria-label="保存角色提示"><b>请先保存当前角色</b><span>完成基础资料后,请点击页面底部的“创建人物档案”。保存成功后即可新建 Markdown 档案章节。</span></div>';
4053
4580
  return;
4054
4581
  }
4055
4582
  const toolbar = `<div class="character-markdown-list-toolbar"><div><b>Markdown 档案章节</b><span>长篇内容独立保存、渲染、检索和版本管理。</span></div>${canEditModule("characters") ? '<button type="button" class="primary-button" data-character-section-create>新建章节</button>' : ""}</div>`;
@@ -4315,44 +4842,115 @@ async function openCharacterEditor(item = null) {
4315
4842
  }
4316
4843
  }
4317
4844
 
4318
- async function openRaceDialog(item) {
4845
+ function knowledgeEditorSection(key, title, description, content) {
4846
+ return `<section class="character-editor-section${key === "basic" ? "" : " hidden"}" data-knowledge-editor-panel="${esc(key)}" role="tabpanel"><header><div><span class="eyebrow">${esc(title)}</span><h3>${esc(title)}</h3></div>${description ? `<p>${esc(description)}</p>` : ""}</header><div class="character-editor-section-fields">${content}</div></section>`;
4847
+ }
4848
+
4849
+ function activateKnowledgeEditorTab(key) {
4850
+ document.querySelectorAll("[data-knowledge-editor-tab]").forEach((button) => {
4851
+ const active = button.dataset.knowledgeEditorTab === key;
4852
+ button.setAttribute("aria-selected", String(active));
4853
+ button.tabIndex = active ? 0 : -1;
4854
+ });
4855
+ document.querySelectorAll("[data-knowledge-editor-panel]").forEach((panel) => panel.classList.toggle("hidden", panel.dataset.knowledgeEditorPanel !== key));
4856
+ }
4857
+
4858
+ function renderKnowledgeEditorFields(kind, item, memberOptions, parentOptions) {
4859
+ const isRace = kind === "race";
4860
+ const label = isRace ? "种族" : "组织";
4861
+ const title = isRace ? "种族共同设定" : "组织设定";
4862
+ const tabs = isRace
4863
+ ? [["basic", "基础资料", "名称、层级与简介"], ["settings", "共同设定", "Markdown 设定章节"], ["members", "种族成员", "角色归属"]]
4864
+ : [["basic", "基础资料", "名称与简介"], ["settings", "组织设定", "Markdown 设定章节"], ["members", "组织成员", "角色归属"]];
4865
+ $("#knowledge-editor-nav").innerHTML = tabs.map(([key, tabTitle, description], index) => `<button type="button" role="tab" data-knowledge-editor-tab="${key}" aria-selected="${index === 0}" tabindex="${index === 0 ? "0" : "-1"}">${tabTitle}<small>${description}</small></button>`).join("");
4866
+ const basicFields = field("name", `${label}名称`, "text", item?.name, []) + (isRace ? field("parentRaceId", "父种族", "select", item?.parentRaceId ?? "", parentOptions) : "") + field("description", `${label}简介`, "textarea", item?.description, []);
4867
+ const memberField = memberOptions.length
4868
+ ? field("memberIds", isRace ? "属于该种族的角色(可多选)" : "组织成员(可多选)", "chips", item?.memberIds ?? [], memberOptions)
4869
+ : `<div class="character-editor-empty-field"><strong>${isRace ? "种族成员" : "组织成员"}</strong><span>当前还没有可绑定的角色。</span></div>`;
4870
+ $("#knowledge-editor-fields").innerHTML = knowledgeEditorSection("basic", "基础资料", isRace ? "先定义名称、层级和简介,再补充共同设定。" : "先定义组织名称和简介,再补充完整的组织设定。", basicFields)
4871
+ + knowledgeEditorSection("settings", title, "", '<div id="knowledge-markdown-sections" class="knowledge-markdown-sections"></div>')
4872
+ + knowledgeEditorSection("members", isRace ? "种族成员" : "组织成员", "成员关系会同步到角色档案中。", memberField);
4873
+ document.querySelectorAll("[data-knowledge-editor-tab]").forEach((button) => {
4874
+ button.onclick = () => activateKnowledgeEditorTab(button.dataset.knowledgeEditorTab);
4875
+ });
4876
+ renderKnowledgeMarkdownSections();
4877
+ activateKnowledgeEditorTab("basic");
4878
+ }
4879
+
4880
+ async function openKnowledgeEditor(kind, item) {
4881
+ await discardPendingMarkdownAttachments();
4319
4882
  state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
4320
4883
  const memberOptions = state.characters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
4321
- const parentOptions = [["", "无(根种族)"], ...eligibleRaceParents(state.races, item?.id)
4322
- .sort((left, right) => racePathLabel(left).localeCompare(racePathLabel(right), "zh-CN"))
4323
- .map((race) => [race.id, racePathLabel(race)])];
4324
- openDialog(item ? "编辑种族" : "新建种族",
4325
- field("name", "种族名称", "text", item?.name) +
4326
- field("parentRaceId", "父种族", "select", item?.parentRaceId ?? "", parentOptions) +
4327
- field("description", "种族简介", "textarea", item?.description) +
4328
- field("settings", "种族共同设定(逐条填写)", "item-list", item?.settings ?? []) +
4329
- (memberOptions.length ? field("memberIds", "属于该种族的角色(可多选)", "chips", item?.memberIds ?? [], memberOptions) : ""),
4330
- async (form) => {
4331
- const settings = form.getAll("settings").map((value) => String(value).trim()).filter(Boolean);
4332
- const body = { name: form.get("name"), parentRaceId: form.get("parentRaceId") || null, description: form.get("description"), settings };
4333
- if (canReadModule("characters")) body.memberIds = form.getAll("memberIds").map(String);
4334
- await api(item ? `/api/races/${item.id}` : `/api/works/${state.work.id}/races`, { method: item ? "PATCH" : "POST", body });
4335
- await renderRaces();
4884
+ const isRace = kind === "race";
4885
+ const module = isRace ? "races" : "organizations";
4886
+ const label = isRace ? "种族" : "组织";
4887
+ const parentOptions = isRace
4888
+ ? [["", "无(根种族)"], ...eligibleRaceParents(state.races, item?.id)
4889
+ .sort((left, right) => racePathLabel(left).localeCompare(racePathLabel(right), "zh-CN"))
4890
+ .map((race) => [race.id, racePathLabel(race)])]
4891
+ : [];
4892
+ knowledgeEditorItem = item ?? null;
4893
+ knowledgeEditorKind = kind;
4894
+ knowledgeEditorSections = normalizeKnowledgeEditorSections(item);
4895
+ knowledgeSectionEditorIndex = null;
4896
+ knowledgeSectionEditorDirty = false;
4897
+ $("#knowledge-editor-eyebrow").textContent = item ? `${label}档案` : `建立${label}档案`;
4898
+ $("#knowledge-editor-title").textContent = item?.name || `新建${label}`;
4899
+ $("#knowledge-editor-version").textContent = item ? `v${item.versionNo ?? 1}` : "新档案";
4900
+ $("#knowledge-editor-header-note").textContent = isRace ? "层级、共同设定与角色归属" : "简介、组织设定与成员归属";
4901
+ $("#knowledge-editor-footer-note").textContent = `保存${label}档案后返回${isRace ? "种族" : "组织"}列表。`;
4902
+ $("#knowledge-editor-submit").textContent = item ? `保存${label}档案` : `创建${label}档案`;
4903
+ renderKnowledgeEditorFields(kind, item, memberOptions, parentOptions);
4904
+ const viewOnly = !canEditModule(module);
4905
+ if (viewOnly) {
4906
+ $("#knowledge-editor-eyebrow").textContent = `${label}档案`;
4907
+ $("#knowledge-editor-fields").querySelectorAll("input, textarea").forEach((control) => { control.readOnly = true; });
4908
+ $("#knowledge-editor-fields").querySelectorAll("select, input[type='checkbox']").forEach((control) => { control.disabled = true; });
4909
+ }
4910
+ $("#knowledge-editor-submit").classList.toggle("hidden", viewOnly);
4911
+ const form = $("#knowledge-editor-form");
4912
+ form.onsubmit = async (event) => {
4913
+ event.preventDefault();
4914
+ if (!canEditModule(module)) return;
4915
+ const submit = $("#knowledge-editor-submit");
4916
+ submit.disabled = true;
4917
+ try {
4918
+ const data = new FormData(form);
4919
+ const name = String(data.get("name") ?? "").trim();
4920
+ if (!name) throw new Error(`请填写${label}名称`);
4921
+ const settingsSections = knowledgeEditorSections
4922
+ .map((section, index) => ({ title: String(section.title ?? "").trim(), contentMarkdown: String(section.contentMarkdown ?? ""), summary: String(section.summary ?? "").trim(), sortOrder: index }))
4923
+ .filter((section) => section.title || section.contentMarkdown.trim());
4924
+ const untitled = settingsSections.findIndex((section) => !section.title);
4925
+ if (untitled >= 0) throw new Error(`请填写第 ${untitled + 1} 条 Markdown 设定的标题`);
4926
+ const settingsMarkdown = settingsSections.map((section) => section.contentMarkdown).join("\n\n");
4927
+ const body = isRace
4928
+ ? { name, parentRaceId: data.get("parentRaceId") || null, description: data.get("description"), settingsMarkdown, settingsSections }
4929
+ : { name, description: data.get("description"), settingsMarkdown, settingsSections };
4930
+ if (canReadModule("characters")) body.memberIds = data.getAll("memberIds").map(String);
4931
+ const wasEditing = Boolean(knowledgeEditorItem);
4932
+ await api(wasEditing ? `/api/${module}/${knowledgeEditorItem.id}` : `/api/works/${state.work.id}/${module}`, { method: wasEditing ? "PATCH" : "POST", body });
4933
+ await cleanupPendingMarkdownAttachments(settingsMarkdown);
4934
+ entityEditorDirty = false;
4336
4935
  await loadAiReferences();
4337
- }, item ? "种族档案" : "作品内种族");
4936
+ await closeEntityEditor({ force: true });
4937
+ toast(wasEditing ? `${label}档案已保存` : `${label}档案已创建`);
4938
+ } catch (error) {
4939
+ toast(error.message, "error");
4940
+ } finally {
4941
+ submit.disabled = false;
4942
+ }
4943
+ };
4944
+ showEntityEditorPage(kind);
4945
+ $("#knowledge-editor-fields").querySelector("input:not([type='checkbox']), textarea")?.focus();
4946
+ }
4947
+
4948
+ async function openRaceDialog(item) {
4949
+ await openKnowledgeEditor("race", item);
4338
4950
  }
4339
4951
 
4340
4952
  async function openOrganizationDialog(item) {
4341
- state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
4342
- const memberOptions = state.characters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
4343
- openDialog(item ? "编辑组织" : "新建组织",
4344
- field("name", "组织名称", "text", item?.name) +
4345
- field("description", "组织简介", "textarea", item?.description) +
4346
- field("settings", "组织设定(逐条填写)", "item-list", item?.settings ?? []) +
4347
- (memberOptions.length ? field("memberIds", "组织成员(可多选)", "chips", item?.memberIds ?? [], memberOptions) : ""),
4348
- async (form) => {
4349
- const settings = form.getAll("settings").map((value) => String(value).trim()).filter(Boolean);
4350
- const body = { name: form.get("name"), description: form.get("description"), settings };
4351
- if (canReadModule("characters")) body.memberIds = form.getAll("memberIds").map(String);
4352
- await api(item ? `/api/organizations/${item.id}` : `/api/works/${state.work.id}/organizations`, { method: item ? "PATCH" : "POST", body });
4353
- await renderOrganizations();
4354
- await loadAiReferences();
4355
- }, item ? "组织档案" : "世界内组织");
4953
+ await openKnowledgeEditor("organization", item);
4356
4954
  }
4357
4955
 
4358
4956
  function openTimelineTrackDialog(item) {
@@ -4493,16 +5091,29 @@ function openProviderDialog(item) {
4493
5091
 
4494
5092
  function openModelDialog(providerId, item = null) {
4495
5093
  const values = modelFormValues(item);
4496
- openDialog(item ? "编辑模型" : "添加模型", field("displayName", "显示名称", "text", values.displayName) + field("modelId", "模型标识符", "text", values.modelId) + field("purposes", "支持用途(可多选)", "chips", values.purposes, MODEL_PURPOSE_OPTIONS) + field("contextWindow", "模型上下文总量(Token)", "number", values.contextWindow) + field("temperature", "默认温度", "number", values.temperature) + field("maxTokens", "默认 max_tokens", "number", values.maxTokens) + field("thinkingEnabled", "开启 Thinking(供应商需支持 thinking 参数)", "checkbox", values.thinkingEnabled) + field("enabled", "启用模型", "checkbox", values.enabled), async (form) => {
5094
+ const temperatureField = `<div class="form-field model-temperature-field"><label for="model-temperature">默认温度<input id="model-temperature" name="temperature" type="number" value="${esc(values.temperature)}" step="any" aria-describedby="model-temperature-hint"></label><small id="model-temperature-hint" class="model-temperature-hint" hidden>Kimi 模型必须设置温度为 1。</small></div>`;
5095
+ openDialog(item ? "编辑模型" : "添加模型", field("displayName", "显示名称", "text", values.displayName) + field("modelId", "模型标识符", "text", values.modelId) + field("purposes", "支持用途(可多选)", "chips", values.purposes, MODEL_PURPOSE_OPTIONS) + field("contextWindow", "模型上下文总量(Token)", "number", values.contextWindow) + temperatureField + field("maxTokens", "默认 max_tokens", "number", values.maxTokens) + field("thinkingEnabled", "开启 Thinking(供应商需支持 thinking 参数)", "checkbox", values.thinkingEnabled) + field("enabled", "启用模型", "checkbox", values.enabled), async (form) => {
4497
5096
  const body = modelPayload({ displayName: form.get("displayName"), modelId: form.get("modelId"), purposes: form.getAll("purposes"), contextWindow: form.get("contextWindow"), temperature: form.get("temperature"), maxTokens: form.get("maxTokens"), thinkingEnabled: form.get("thinkingEnabled") === "on", enabled: form.get("enabled") === "on" }, item?.preset);
4498
5097
  await api(item ? `/api/models/${item.id}` : `/api/providers/${providerId}/models`, { method: item ? "PATCH" : "POST", body });
4499
5098
  await renderPlatformAiConfig();
4500
5099
  await loadModels();
4501
5100
  }, item ? "模型配置" : "供应商模型");
5101
+ const modelIdInput = $("#dialog-fields input[name='modelId']");
5102
+ const temperatureInput = $("#dialog-fields input[name='temperature']");
5103
+ const temperatureHint = $("#model-temperature-hint");
5104
+ const syncKimiTemperature = () => {
5105
+ const isKimi = isKimiModelId(modelIdInput.value);
5106
+ temperatureHint.hidden = !isKimi;
5107
+ };
5108
+ modelIdInput.addEventListener("input", () => {
5109
+ if (isKimiModelId(modelIdInput.value)) temperatureInput.value = "1";
5110
+ syncKimiTemperature();
5111
+ });
5112
+ syncKimiTemperature();
4502
5113
  }
4503
5114
 
4504
5115
  async function sendAi() {
4505
- if (!state.work || !state.chapter) return toast("请先选择章节", "error");
5116
+ if (!state.work) return toast("请先选择作品", "error");
4506
5117
  try {
4507
5118
  await Promise.all([ensureAiModelsLoaded(), ensureAiConversationsLoaded()]);
4508
5119
  } catch (error) {
@@ -4758,7 +5369,10 @@ async function showVersions() {
4758
5369
  const versions = await api(`/api/chapters/${state.chapter.id}/versions`);
4759
5370
  $("#versions-list").innerHTML = versions.map((version) => `<div class="version-row"><div><b>v${version.versionNo}</b><small>${esc(version.source)} · ${esc(version.actor || "历史数据")}</small></div><p>${esc(version.content.slice(0, 300) || "空白章节")}</p>${canEditProse() ? `<button class="ghost-button" data-restore-version="${version.versionNo}">恢复</button>` : ""}</div>`).join("");
4760
5371
  $("#versions-list").querySelectorAll("[data-restore-version]").forEach((button) => button.addEventListener("click", async () => {
4761
- if (!window.confirm(`将版本 v${button.dataset.restoreVersion} 恢复为一个新的保存版本?`)) return;
5372
+ if (!(await confirmToast(
5373
+ `将版本 v${button.dataset.restoreVersion} 恢复为一个新的保存版本?`,
5374
+ { title: "恢复历史版本", confirmLabel: "确认恢复" }
5375
+ ))) return;
4762
5376
  state.chapter = await api(`/api/chapters/${state.chapter.id}/restore`, { method: "POST", body: { versionNo: Number(button.dataset.restoreVersion) } });
4763
5377
  lastSavedChapterSnapshot = { chapterId: state.chapter.id, title: state.chapter.title, content: state.chapter.content };
4764
5378
  $("#chapter-title").value = state.chapter.title;
@@ -4820,7 +5434,7 @@ function renderImportHistory(versions, nextPage = null) {
4820
5434
  }, 5000);
4821
5435
  return;
4822
5436
  }
4823
- if (!confirmDiscardChanges("当前章节有未保存修改,恢复正文会丢弃这些本地修改。是否继续?")) return;
5437
+ if (!(await confirmDiscardChanges("当前章节有未保存修改,恢复正文会丢弃这些本地修改。是否继续?"))) return;
4824
5438
  button.disabled = true;
4825
5439
  cancelChapterAutoSave();
4826
5440
  const workId = state.work.id;
@@ -4901,11 +5515,13 @@ async function showChapterInsight() {
4901
5515
  panel.innerHTML = `<strong>章节概览${esc(stale)}</strong>${esc(insight.summary || "暂无梗概")}${eventNames.length ? `<br><strong>事件</strong>${esc(eventNames.join(";"))}` : ""}${insight.uncertainties.length ? `<br><strong>待确认</strong>${esc(insight.uncertainties.map((item) => typeof item === "string" ? item : JSON.stringify(item)).join(";"))}` : ""}`;
4902
5516
  }
4903
5517
 
4904
- $("#home-button").addEventListener("click", () => {
4905
- if (!confirmDiscardChanges()) return;
5518
+ $("#home-button").addEventListener("click", async () => {
5519
+ if (!(await confirmDiscardChanges())) return;
4906
5520
  loadWorks().catch((error) => toast(error.message, "error"));
4907
5521
  });
4908
- $("#settings-button").addEventListener("click", showSettingsHub);
5522
+ $("#settings-button").addEventListener("click", () => {
5523
+ void showSettingsHub();
5524
+ });
4909
5525
  $("#account-button").addEventListener("click", () => {
4910
5526
  const expanded = $("#account-menu").classList.toggle("hidden") === false;
4911
5527
  $("#account-button").setAttribute("aria-expanded", String(expanded));
@@ -4986,7 +5602,7 @@ $("#avatar-file").addEventListener("change", async (event) => {
4986
5602
  }
4987
5603
  });
4988
5604
  $("#avatar-remove-button").addEventListener("click", async () => {
4989
- if (!state.user?.avatarUrl || !window.confirm("确定移除当前头像吗?")) return;
5605
+ if (!state.user?.avatarUrl || !(await confirmToast("确定移除当前头像吗?", { title: "移除头像", confirmLabel: "确认移除" }))) return;
4990
5606
  $("#avatar-upload-button").disabled = true;
4991
5607
  $("#avatar-remove-button").disabled = true;
4992
5608
  try {
@@ -5014,14 +5630,35 @@ $("#profile-form").addEventListener("submit", async (event) => {
5014
5630
  $("#password-form").addEventListener("submit", async (event) => {
5015
5631
  event.preventDefault();
5016
5632
  const form = new FormData(event.currentTarget);
5633
+ const newPassword = String(form.get("newPassword") ?? "");
5634
+ const passwordConfirmation = String(form.get("passwordConfirmation") ?? "");
5635
+ if (newPassword !== passwordConfirmation) {
5636
+ const confirmationInput = $("#password-form input[name='passwordConfirmation']");
5637
+ confirmationInput.setCustomValidity("两次输入的密码不一致");
5638
+ confirmationInput.reportValidity();
5639
+ confirmationInput.focus();
5640
+ return;
5641
+ }
5017
5642
  try {
5018
- await api("/api/auth/password", { method: "PATCH", body: { currentPassword: form.get("currentPassword"), newPassword: form.get("newPassword") } });
5643
+ await api("/api/auth/password", { method: "PATCH", body: { currentPassword: form.get("currentPassword"), newPassword, passwordConfirmation } });
5019
5644
  event.currentTarget.reset();
5020
5645
  toast("密码已更新,其他设备的会话已退出");
5021
5646
  } catch (error) { toast(error.message, "error"); }
5022
5647
  });
5648
+ function validatePasswordChangeConfirmation() {
5649
+ const newPasswordInput = $("#password-form input[name='newPassword']");
5650
+ const confirmationInput = $("#password-form input[name='passwordConfirmation']");
5651
+ const matches = !confirmationInput.value || newPasswordInput.value === confirmationInput.value;
5652
+ confirmationInput.setCustomValidity(matches ? "" : "两次输入的密码不一致");
5653
+ return matches;
5654
+ }
5655
+ $("#password-form input[name='newPassword']").addEventListener("input", validatePasswordChangeConfirmation);
5656
+ $("#password-form input[name='passwordConfirmation']").addEventListener("input", validatePasswordChangeConfirmation);
5023
5657
  $("#api-key-reset-button").addEventListener("click", async () => {
5024
- if ($("#api-key-reset-button").textContent.includes("重置") && !window.confirm("重置后,所有使用旧 API Key 的 CLI 会立即退出登录。确定继续吗?")) return;
5658
+ if ($("#api-key-reset-button").textContent.includes("重置") && !(await confirmToast(
5659
+ "重置后,所有使用旧 API Key 的 CLI 会立刻退出登录。确定继续吗?",
5660
+ { title: "重置 API Key", confirmLabel: "确认重置" }
5661
+ ))) return;
5025
5662
  try {
5026
5663
  const result = await api("/api/auth/api-key/reset", { method: "POST", body: {} });
5027
5664
  $("#api-key-status").textContent = `已配置 ${result.prefix}…,尚未使用`;
@@ -5195,24 +5832,26 @@ $("#member-permission-form").addEventListener("submit", async (event) => {
5195
5832
  $("#platform-new-provider").addEventListener("click", () => openProviderDialog());
5196
5833
  $("#shelf-new-work").addEventListener("click", openWorkDialog);
5197
5834
  $("#welcome-new-work").addEventListener("click", () => state.work ? openChapterDialog() : openWorkDialog());
5198
- $("#new-chapter-button").addEventListener("click", openChapterDialog);
5199
5835
  $("#save-button").addEventListener("click", saveChapter);
5200
5836
  $("#tidy-blank-lines-button").addEventListener("click", tidyChapterBlankLines);
5201
5837
  $("#new-volume-button").addEventListener("click", () => openVolumeDialog());
5202
5838
  $("#insight-button").addEventListener("click", () => showChapterInsight().catch((error) => toast(error.message, "error")));
5203
5839
  $("#versions-button").addEventListener("click", showVersions);
5204
5840
  $("#versions-close").addEventListener("click", () => $("#versions-dialog").close());
5205
- $("#import-history-button").addEventListener("click", () => openImportHistory());
5206
5841
  $("#import-history-close").addEventListener("click", () => $("#import-history-dialog").close());
5207
5842
  $("#entity-history-close").addEventListener("click", () => $("#entity-history-dialog").close());
5208
5843
  $("#ai-tool-call-close").addEventListener("click", () => $("#ai-tool-call-dialog").close());
5209
5844
  $("#setting-editor-back").addEventListener("click", () => { void closeEntityEditor(); });
5210
5845
  $("#character-editor-close").addEventListener("click", () => { void closeEntityEditor(); });
5211
5846
  $("#character-editor-cancel").addEventListener("click", () => { void closeEntityEditor(); });
5847
+ $("#knowledge-editor-close").addEventListener("click", () => { void closeEntityEditor(); });
5848
+ $("#knowledge-editor-cancel").addEventListener("click", () => { void closeEntityEditor(); });
5212
5849
  $("#setting-editor-form").addEventListener("input", markEntityEditorDirty);
5213
5850
  $("#setting-editor-form").addEventListener("change", markEntityEditorDirty);
5214
5851
  $("#character-editor-form").addEventListener("input", markEntityEditorDirty);
5215
5852
  $("#character-editor-form").addEventListener("change", markEntityEditorDirty);
5853
+ $("#knowledge-editor-form").addEventListener("input", markEntityEditorDirty);
5854
+ $("#knowledge-editor-form").addEventListener("change", markEntityEditorDirty);
5216
5855
  $("#character-editor-fields").addEventListener("click", (event) => {
5217
5856
  if (event.target.closest("[data-item-list-add], [data-structured-list-add], [data-item-list-remove], [data-structured-list-remove]")) markEntityEditorDirty();
5218
5857
  });
@@ -5339,6 +5978,13 @@ $("#ai-mention-menu").addEventListener("click", (event) => {
5339
5978
  const button = event.target.closest("[data-ai-reference-id]");
5340
5979
  if (button) selectAiMention(button);
5341
5980
  });
5981
+ $("#import-file-button").addEventListener("click", (event) => {
5982
+ if (!state.work || canEditProse()) return;
5983
+ event.preventDefault();
5984
+ event.stopPropagation();
5985
+ $("#import-file").value = "";
5986
+ toast("当前权限只能编辑设定资料,不能导入正文", "error");
5987
+ });
5342
5988
  $("#import-file").addEventListener("change", async (event) => {
5343
5989
  const file = event.target.files[0];
5344
5990
  if (!state.work || !file) return;
@@ -5352,6 +5998,13 @@ $("#import-file").addEventListener("change", async (event) => {
5352
5998
  event.target.value = "";
5353
5999
  return;
5354
6000
  }
6001
+ if (mode === "overwrite" && !(await confirmToast(
6002
+ `当前选项:覆盖正文。确认后将使用“${file.name}”替换《${state.work.title}》的现有正文目录。`,
6003
+ { title: "覆盖正文二次确认", confirmLabel: "确认覆盖" }
6004
+ ))) {
6005
+ event.target.value = "";
6006
+ return;
6007
+ }
5355
6008
  cancelChapterAutoSave();
5356
6009
  const body = new FormData();
5357
6010
  body.append("file", file);