@musnows/scriverse 0.3.10 → 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 +50 -6
  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 +994 -194
  10. package/dist/public/character-markdown.js +12 -0
  11. package/dist/public/index.html +69 -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 +191 -59
  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 +252 -43
  553. package/dist/store.js.map +1 -1
  554. package/dist/user-auth.js +40 -8
  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 +30 -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,13 +83,19 @@ 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) {
89
90
  return canWriteUiModule(work, "editor");
90
91
  }
91
92
 
93
+ function canReplaceProse(work = state.work) {
94
+ return WORK_PERMISSION_MODULES
95
+ .filter((item) => item.id !== "ai-settings")
96
+ .every((item) => canWritePermissionModule(work, item.id));
97
+ }
98
+
92
99
  function canManageWork(work = state.work) {
93
100
  return ["admin", "owner"].includes(String(work?.accessRole));
94
101
  }
@@ -110,8 +117,8 @@ function applyWorkAccessMode() {
110
117
  const viewOnly = Boolean(state.work) && !canEditWork();
111
118
  const proseReadOnly = Boolean(state.work) && !canEditProse();
112
119
  const proseHidden = Boolean(state.work) && !canReadModule("editor");
113
- const aiHidden = Boolean(state.work) && !canReadModule("tasks");
114
- 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");
115
122
  const moduleReadOnly = Boolean(state.work) && !canEditModule(state.module);
116
123
  $("#app").classList.toggle("view-only-mode", viewOnly);
117
124
  $("#app").classList.toggle("prose-read-only-mode", proseReadOnly);
@@ -119,10 +126,16 @@ function applyWorkAccessMode() {
119
126
  $("#app").classList.toggle("ai-hidden-mode", aiHidden);
120
127
  document.body.classList.toggle("work-viewer-mode", moduleReadOnly);
121
128
  for (const item of WORK_PERMISSION_MODULES) {
129
+ if (!item.uiModule) continue;
122
130
  const button = $(`#module-nav [data-module="${item.uiModule}"]`);
123
131
  if (button) button.classList.toggle("permission-hidden", !canReadModule(item.uiModule));
124
132
  }
125
133
  $("#module-nav [data-work-settings]").classList.toggle("permission-hidden", Boolean(state.work) && !canManageWork());
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");
138
+ $("#import-file").disabled = proseReadOnly;
126
139
  $(".ai-panel").classList.toggle("permission-hidden", aiHidden);
127
140
  $("#chapter-title").readOnly = proseReadOnly;
128
141
  $("#chapter-content").readOnly = proseReadOnly;
@@ -198,6 +211,10 @@ let aiReferencesLoadPromise = null;
198
211
  let aiReferencesLoadWorkId = null;
199
212
  let aiConversationsLoadPromise = null;
200
213
  let aiConversationsLoadWorkId = null;
214
+ let workScopedUiGeneration = 0;
215
+ let importHistoryRecords = [];
216
+ let importHistoryNextPage = null;
217
+ let importHistoryRequestId = 0;
201
218
 
202
219
  const shelfOnboardingSteps = [
203
220
  { selector: "#home-button", eyebrow: "作品入口", title: "这里是你的创作书架", description: "点击左上角的叙界标志,可以随时回到书架,在不同作品之间切换。", placement: "bottom" },
@@ -212,7 +229,7 @@ const shelfOnboardingSteps = [
212
229
  const workspaceOnboardingSteps = [
213
230
  { selector: "#home-button", eyebrow: "作品入口", title: "随时返回书架", description: "点击叙界标志返回书架,切换作品或创建新的故事世界。", placement: "bottom" },
214
231
  { selector: ".file-button", eyebrow: "稿件导入", title: "导入 TXT 或 DOCX", description: "已有稿件可以直接导入,系统会解析分卷与章节结构。", placement: "right" },
215
- { selector: "#new-chapter-button", eyebrow: "正文结构", title: "新建章节", description: "使用分卷和章节组织长篇正文,章节会自动保存并保留版本。", placement: "right" },
232
+ { selector: "[data-new-chapter-volume]", eyebrow: "正文结构", title: "新建章节", description: "使用分卷和章节组织长篇正文,章节会自动保存并保留版本。", placement: "right" },
216
233
  { selector: "#versions-button", eyebrow: "版本安全", title: "查看章节版本", description: "每次保存都会生成可恢复版本,误改内容时可以随时回溯。", placement: "bottom" },
217
234
  { selector: "[data-module=\"characters\"]", eyebrow: "作品知识", title: "维护角色与世界资料", description: "角色、种族、组织、设定和时间线共同构成 AI 可引用的作品知识。", placement: "right" },
218
235
  { selector: "[data-module=\"outlines\"]", eyebrow: "创作规划", title: "跟踪大纲与伏笔", description: "记录剧情目标、冲突、转折和伏笔回收,避免长线遗漏。", placement: "right" },
@@ -394,7 +411,7 @@ function replacePageRoute(route) {
394
411
  function currentPageRoute() {
395
412
  const workId = state.work?.id ?? null;
396
413
  if (!$("#entity-editor-view").classList.contains("hidden") && workId && entityEditorType) {
397
- const entityId = entityEditorType === "setting" ? settingEditorItem?.id : characterEditorItem?.id;
414
+ const entityId = entityEditorType === "setting" ? settingEditorItem?.id : entityEditorType === "character" ? characterEditorItem?.id : knowledgeEditorItem?.id;
398
415
  return { view: "entity-editor", workId, entity: entityEditorType, entityId: entityId ?? null };
399
416
  }
400
417
  if (!$("#settings-hub-view").classList.contains("hidden")) return { view: "settings", workId, ...settingsRouteContext() };
@@ -510,50 +527,76 @@ let entityEditorType = null;
510
527
  let entityEditorDirty = false;
511
528
  let settingEditorItem = null;
512
529
  let characterEditorItem = null;
530
+ let knowledgeEditorItem = null;
531
+ let knowledgeEditorKind = null;
532
+ let knowledgeEditorSections = [];
533
+ let knowledgeSectionEditorIndex = null;
534
+ let knowledgeSectionEditorDirty = false;
513
535
  let characterEditorVersions = [];
514
536
  let characterEditorRelationships = [];
515
537
  let characterEditorRelationshipsLoading = false;
516
538
  let characterEditorSections = [];
517
- let characterSectionPreviewTimer = null;
518
539
  let characterSectionPendingAttachments = [];
540
+ let markdownEditorPendingAttachments = [];
519
541
  let characterSectionEditorDirty = false;
542
+ let settingEditorVditor = null;
543
+ let knowledgeSectionVditor = null;
544
+ let characterSectionVditor = null;
520
545
  let entityHistoryContext = null;
521
546
 
522
547
  function showEntityEditorPage(type) {
523
548
  entityEditorType = type;
524
549
  entityEditorDirty = false;
525
550
  characterSectionEditorDirty = false;
551
+ knowledgeSectionEditorDirty = false;
526
552
  $("#entity-editor-view").classList.remove("hidden");
527
553
  $("#setting-editor-form").classList.toggle("hidden", type !== "setting");
528
554
  $("#character-editor-form").classList.toggle("hidden", type !== "character");
555
+ $("#knowledge-editor-form").classList.toggle("hidden", !["race", "organization"].includes(type));
529
556
  $("#character-section-editor-view").classList.add("hidden");
557
+ $("#knowledge-section-editor-view").classList.add("hidden");
530
558
  $("#app").inert = true;
531
559
  document.body.classList.add("entity-editor-open");
532
560
  replacePageRoute(currentPageRoute());
533
561
  }
534
562
 
535
563
  function markEntityEditorDirty() {
536
- const module = entityEditorType === "setting" ? "settings" : "characters";
564
+ const module = entityEditorType === "setting" ? "settings" : entityEditorType === "character" ? "characters" : entityEditorType === "race" ? "races" : "organizations";
537
565
  if (entityEditorType && canEditModule(module)) entityEditorDirty = true;
538
566
  }
539
567
 
540
- function confirmEntityEditorDiscard(message) {
568
+ async function confirmEntityEditorDiscard(message) {
541
569
  if (!entityEditorDirty) return true;
542
- return window.confirm(message ?? "当前资料有未保存修改,返回列表将丢弃这些修改。是否继续?");
570
+ return confirmToast(message ?? "当前资料有未保存修改,返回列表将丢弃这些修改。是否继续?", {
571
+ title: "放弃未保存修改",
572
+ confirmLabel: "放弃并继续",
573
+ cancelLabel: "继续编辑"
574
+ });
543
575
  }
544
576
 
545
577
  async function closeEntityEditor({ force = false } = {}) {
546
578
  if (!$("#character-section-editor-view").classList.contains("hidden")) return closeCharacterSectionEditor({ force });
547
- if (!force && !confirmEntityEditorDiscard()) return false;
579
+ if (!$("#knowledge-section-editor-view").classList.contains("hidden")) return closeKnowledgeSectionEditor({ force });
580
+ if (!force && !(await confirmEntityEditorDiscard())) return false;
548
581
  await discardPendingCharacterAttachments();
549
- 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";
550
586
  entityEditorType = null;
551
587
  entityEditorDirty = false;
552
588
  settingEditorItem = null;
553
589
  characterEditorItem = null;
590
+ knowledgeEditorItem = null;
591
+ knowledgeEditorKind = null;
592
+ knowledgeEditorSections = [];
593
+ knowledgeSectionEditorIndex = null;
594
+ knowledgeSectionEditorDirty = false;
554
595
  $("#entity-editor-view").classList.add("hidden");
555
596
  $("#setting-editor-form").classList.add("hidden");
556
597
  $("#character-editor-form").classList.add("hidden");
598
+ $("#knowledge-editor-form").classList.add("hidden");
599
+ $("#knowledge-section-editor-view").classList.add("hidden");
557
600
  $("#app").inert = false;
558
601
  document.body.classList.remove("entity-editor-open");
559
602
  await showModule(module);
@@ -1106,8 +1149,9 @@ function renderAiConversationHistory() {
1106
1149
  async function loadAiConversations(openLatest = true) {
1107
1150
  const workId = state.work?.id;
1108
1151
  if (!workId) return;
1152
+ const generation = workScopedUiGeneration;
1109
1153
  const conversations = (await apiPage(`/api/works/${workId}/ai-conversations`)).items;
1110
- if (state.work?.id !== workId) return;
1154
+ if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
1111
1155
  state.aiConversations = conversations;
1112
1156
  loadedAiConversationsWorkId = workId;
1113
1157
  renderAiConversationHistory();
@@ -1392,6 +1436,7 @@ function applyColorTheme(theme) {
1392
1436
  button.setAttribute("aria-pressed", String(normalized === "dark"));
1393
1437
  const themeColor = document.querySelector('meta[name="theme-color"]');
1394
1438
  if (themeColor) themeColor.content = normalized === "dark" ? "#171714" : "#8b3d2c";
1439
+ [settingEditorVditor, knowledgeSectionVditor, characterSectionVditor].forEach((editor) => editor?.setTheme(normalized === "dark" ? "dark" : "classic"));
1395
1440
  }
1396
1441
 
1397
1442
  function saveColorTheme(theme) {
@@ -1686,6 +1731,47 @@ function toast(message, type = "info") {
1686
1731
  }, 3600);
1687
1732
  }
1688
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
+
1689
1775
  document.addEventListener("toggle", (event) => {
1690
1776
  const target = event.target;
1691
1777
  if (target instanceof HTMLDialogElement && target.open && $("#toast-region").childElementCount) {
@@ -1791,20 +1877,34 @@ async function persistChapter({ automatic = false } = {}) {
1791
1877
  }
1792
1878
  }
1793
1879
 
1794
- function confirmDiscardChanges(message = "当前章节有未保存修改,继续将丢弃这些修改。是否继续?") {
1880
+ async function confirmDiscardChanges(message = "当前章节有未保存修改,继续将丢弃这些修改。是否继续?") {
1795
1881
  if (!state.dirty) return true;
1796
- return window.confirm(message);
1882
+ return confirmToast(message, {
1883
+ title: "放弃未保存修改",
1884
+ confirmLabel: "放弃并继续",
1885
+ cancelLabel: "继续编辑"
1886
+ });
1797
1887
  }
1798
1888
 
1799
1889
  function chooseExistingWorkImportMode(file) {
1800
1890
  const dialog = $("#import-mode-dialog");
1891
+ const form = dialog.querySelector("form");
1892
+ const confirm = $("#import-mode-confirm");
1893
+ const canOverwrite = canReplaceProse();
1801
1894
  $("#import-mode-file-summary").textContent = `文件:${file.name};当前作品:《${state.work.title}》`;
1802
1895
  $("#import-mode-unsaved-warning").classList.toggle("hidden", !state.dirty);
1896
+ form.reset();
1897
+ $("#import-mode-overwrite").disabled = !canOverwrite;
1898
+ $("#import-mode-overwrite-permission").classList.toggle("hidden", canOverwrite);
1899
+ confirm.disabled = true;
1900
+ form.onchange = () => { confirm.disabled = !form.querySelector('input[name="importMode"]:checked'); };
1803
1901
  dialog.returnValue = "cancel";
1804
1902
  dialog.showModal();
1903
+ $("#import-mode-dialog-title").focus();
1805
1904
  return new Promise((resolve) => {
1806
1905
  dialog.addEventListener("close", () => {
1807
- 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);
1808
1908
  }, { once: true });
1809
1909
  });
1810
1910
  }
@@ -1856,7 +1956,7 @@ async function initializePage() {
1856
1956
  state.module = route.view === "module"
1857
1957
  ? route.module
1858
1958
  : route.view === "entity-editor"
1859
- ? (route.entity === "setting" ? "settings" : "characters")
1959
+ ? ({ setting: "settings", character: "characters", race: "races", organization: "organizations" }[route.entity] ?? "characters")
1860
1960
  : "editor";
1861
1961
  await selectWork(requestedWork.id, route.view === "editor" ? route.chapterId : null);
1862
1962
  }
@@ -1867,14 +1967,16 @@ async function initializePage() {
1867
1967
  }
1868
1968
  if (route.view === "module") return;
1869
1969
  if (route.view === "entity-editor") {
1870
- 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;
1871
1971
  const item = route.entityId ? records.find((record) => record.id === route.entityId) : null;
1872
1972
  if (route.entityId && !item) {
1873
- toast(route.entity === "setting" ? "未找到要编辑的设定" : "未找到要编辑的角色", "error");
1973
+ toast(({ setting: "未找到要编辑的设定", character: "未找到要编辑的角色", race: "未找到要编辑的种族", organization: "未找到要编辑的组织" }[route.entity] ?? "未找到要编辑的档案"), "error");
1874
1974
  return;
1875
1975
  }
1876
1976
  if (route.entity === "setting") openSettingEditor(item);
1877
- 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);
1878
1980
  return;
1879
1981
  }
1880
1982
  if (route.view === "welcome") {
@@ -1882,7 +1984,7 @@ async function initializePage() {
1882
1984
  return;
1883
1985
  }
1884
1986
  if (route.view === "settings") {
1885
- showSettingsHub();
1987
+ await showSettingsHub();
1886
1988
  settingsReturnContext = restoredSettingsReturnContext(route);
1887
1989
  renderSettingsHub();
1888
1990
  return;
@@ -2177,10 +2279,10 @@ async function openSearchResult(result) {
2177
2279
  }
2178
2280
  }
2179
2281
 
2180
- function showSettingsHub() {
2282
+ async function showSettingsHub() {
2181
2283
  const alreadyInSettings = !$("#settings-hub-view").classList.contains("hidden") || !$("#platform-ai-view").classList.contains("hidden");
2182
2284
  if (!alreadyInSettings) {
2183
- if (state.dirty && !confirmDiscardChanges("当前章节有未保存修改,进入设置将放弃本地修改。是否继续?")) return false;
2285
+ if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入设置将放弃本地修改。是否继续?"))) return false;
2184
2286
  settingsReturnContext = captureSettingsReturnContext();
2185
2287
  state.dirty = false;
2186
2288
  }
@@ -2217,7 +2319,7 @@ async function returnFromSettings() {
2217
2319
  }
2218
2320
 
2219
2321
  async function showPlatformAi() {
2220
- if (state.dirty && !confirmDiscardChanges("当前章节有未保存修改,进入平台 AI 管理将放弃本地修改。是否继续?")) return false;
2322
+ if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入平台 AI 管理将放弃本地修改。是否继续?"))) return false;
2221
2323
  state.dirty = false;
2222
2324
  updateDocumentTitle();
2223
2325
  $("#app").classList.add("shelf-mode");
@@ -2257,34 +2359,45 @@ function renderShelf() {
2257
2359
  $("#book-add-card").addEventListener("click", openWorkDialog);
2258
2360
  }
2259
2361
 
2362
+ function resetWorkScopedUiCaches() {
2363
+ workScopedUiGeneration += 1;
2364
+ loadedAiModelsWorkId = null;
2365
+ loadedAiReferencesWorkId = null;
2366
+ loadedAiConversationsWorkId = null;
2367
+ aiModelsLoadPromise = null;
2368
+ aiModelsLoadWorkId = null;
2369
+ aiReferencesLoadPromise = null;
2370
+ aiReferencesLoadWorkId = null;
2371
+ aiConversationsLoadPromise = null;
2372
+ aiConversationsLoadWorkId = null;
2373
+ state.models = [];
2374
+ state.characters = [];
2375
+ state.settings = [];
2376
+ state.collapsedVolumeIds.clear();
2377
+ lastSavedChapterSnapshot = null;
2378
+ if (aiContextUsageTimer !== null) clearTimeout(aiContextUsageTimer);
2379
+ aiContextUsageTimer = null;
2380
+ aiContextUsageRequest += 1;
2381
+ state.aiCitations = [];
2382
+ state.aiReferences = [];
2383
+ state.aiPromptSent = false;
2384
+ state.aiConversationId = null;
2385
+ state.aiConversations = [];
2386
+ renderAiCitations();
2387
+ renderAiReferences();
2388
+ renderAiQuickActions();
2389
+ resetAiFeed();
2390
+ $("#ai-conversation-title").textContent = "新对话";
2391
+ $("#ai-model").innerHTML = '<option value="">使用创作助手时加载模型</option>';
2392
+ setAiContextMeter(null);
2393
+ renderAiConversationHistory();
2394
+ }
2395
+
2260
2396
  async function selectWork(workId, preferredChapterId = null) {
2261
2397
  const discarding = state.work?.id !== workId && state.dirty;
2262
- if (discarding && !confirmDiscardChanges()) return false;
2398
+ if (discarding && !(await confirmDiscardChanges())) return false;
2263
2399
  const nextWork = await api(`/api/works/${workId}?page=1&limit=100`);
2264
- if (state.work?.id !== nextWork.id) {
2265
- loadedAiModelsWorkId = null;
2266
- loadedAiReferencesWorkId = null;
2267
- loadedAiConversationsWorkId = null;
2268
- state.models = [];
2269
- state.characters = [];
2270
- state.settings = [];
2271
- if (aiContextUsageTimer !== null) clearTimeout(aiContextUsageTimer);
2272
- aiContextUsageTimer = null;
2273
- aiContextUsageRequest += 1;
2274
- state.aiCitations = [];
2275
- state.aiReferences = [];
2276
- state.aiPromptSent = false;
2277
- state.aiConversationId = null;
2278
- state.aiConversations = [];
2279
- renderAiCitations();
2280
- renderAiReferences();
2281
- renderAiQuickActions();
2282
- resetAiFeed();
2283
- $("#ai-conversation-title").textContent = "新对话";
2284
- $("#ai-model").innerHTML = '<option value="">使用创作助手时加载模型</option>';
2285
- setAiContextMeter(null);
2286
- renderAiConversationHistory();
2287
- }
2400
+ if (state.work?.id !== nextWork.id) resetWorkScopedUiCaches();
2288
2401
  if (discarding) setSaveState("就绪");
2289
2402
  $("#app").classList.remove("shelf-mode");
2290
2403
  $("#shelf-view").classList.add("hidden");
@@ -2313,11 +2426,15 @@ async function selectWork(workId, preferredChapterId = null) {
2313
2426
  function renderTree() {
2314
2427
  if (!state.work) return;
2315
2428
  const count = state.work.volumes.reduce((total, volume) => total + volume.chapters.length, 0);
2429
+ const proseEditable = canEditProse();
2316
2430
  $("#chapter-count").textContent = `${count} 章`;
2317
2431
  $("#novel-tree").classList.remove("empty-copy");
2318
2432
  $("#novel-tree").innerHTML = state.work.volumes.map((volume) => `
2319
2433
  <div class="volume-node ${state.collapsedVolumeIds.has(volume.id) ? "is-collapsed" : ""}" data-volume-id="${esc(volume.id)}">
2320
- <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>
2321
2438
  <div class="volume-chapters">
2322
2439
  ${volume.chapters.map((chapter) => `
2323
2440
  <button class="chapter-node ${state.chapter?.id === chapter.id ? "active" : ""}" type="button" data-chapter-id="${esc(chapter.id)}">
@@ -2338,6 +2455,9 @@ function renderTree() {
2338
2455
  openVolumeDialog(state.work.volumes.find((volume) => volume.id === button.dataset.volumeToggle));
2339
2456
  });
2340
2457
  });
2458
+ $("#novel-tree").querySelectorAll("[data-new-chapter-volume]").forEach((button) => {
2459
+ button.addEventListener("click", () => openChapterDialog(button.dataset.newChapterVolume));
2460
+ });
2341
2461
  $("#novel-tree").querySelectorAll("[data-chapter-id]").forEach((button) => {
2342
2462
  button.addEventListener("click", () => selectChapter(button.dataset.chapterId));
2343
2463
  button.addEventListener("contextmenu", (event) => {
@@ -2370,7 +2490,7 @@ function openChapterTypeMenu(chapterId, clientX, clientY) {
2370
2490
  }
2371
2491
 
2372
2492
  async function selectChapter(chapterId) {
2373
- if (state.chapter?.id !== chapterId && !confirmDiscardChanges("当前章节有未保存修改,仍要切换吗?")) return;
2493
+ if (state.chapter?.id !== chapterId && !(await confirmDiscardChanges("当前章节有未保存修改,仍要切换吗?"))) return;
2374
2494
  cancelChapterAutoSave();
2375
2495
  state.chapter = await api(`/api/chapters/${chapterId}`);
2376
2496
  lastSavedChapterSnapshot = { chapterId: state.chapter.id, title: state.chapter.title, content: state.chapter.content };
@@ -2458,7 +2578,7 @@ async function showModule(module) {
2458
2578
  }
2459
2579
  module = fallback;
2460
2580
  }
2461
- if (module !== "editor" && state.module === "editor" && !confirmDiscardChanges()) return;
2581
+ if (module !== "editor" && state.module === "editor" && !(await confirmDiscardChanges())) return;
2462
2582
  if (module !== "editor" && state.module === "editor" && state.dirty) setSaveState("已放弃修改");
2463
2583
  state.module = module;
2464
2584
  applyWorkAccessMode();
@@ -2587,8 +2707,10 @@ function openEntityMergeDialog({ typeLabel, source, candidates, endpoint, body,
2587
2707
  }
2588
2708
 
2589
2709
  async function deleteManagedEntity({ typeLabel, item, endpoint, refresh, warning = "" }) {
2590
- const detail = warning ? `\n${warning}` : "";
2591
- 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;
2592
2714
  try {
2593
2715
  await api(endpoint(item), { method: "DELETE" });
2594
2716
  await refresh();
@@ -2599,14 +2721,76 @@ async function deleteManagedEntity({ typeLabel, item, endpoint, refresh, warning
2599
2721
  }
2600
2722
  }
2601
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
+
2602
2786
  async function renderSettings() {
2603
2787
  const records = (await apiPage(`/api/works/${state.work.id}/settings`)).items;
2604
2788
  state.settings = records;
2605
- $("#module-content").innerHTML = records.length ? `<div class="card-grid">${records.map((item) => `
2606
- <article class="record-card"><small>${esc(item.category)} · ${item.locked ? "已锁定" : esc(item.status)}</small>
2607
- <h3>${esc(item.title)}</h3><p>${esc(item.content)}</p>
2608
- <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)}`
2609
2792
  : emptyModule("还没有世界观设定", "新建规则、地点、组织、科技或创作约束。AI 提取的候选也会进入这里。");
2793
+ bindModuleLayoutToggle(renderSettings);
2610
2794
  $("#module-content").querySelectorAll("[data-setting-status]").forEach((button) => button.addEventListener("click", async () => {
2611
2795
  await api(`/api/settings/${button.dataset.settingId}`, { method: "PATCH", body: { status: button.dataset.settingStatus, changeNote: button.dataset.settingStatus === "confirmed" ? "确认 AI 设定候选" : "弃用 AI 设定候选" } });
2612
2796
  await renderSettings();
@@ -2622,8 +2806,9 @@ async function renderCharacters() {
2622
2806
  canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
2623
2807
  canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
2624
2808
  ]);
2625
- 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>` : "";
2626
- $("#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) => {
2627
2812
  const details = normalizeCharacterDetails(item.attributes?.details);
2628
2813
  return `
2629
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>
@@ -2634,9 +2819,29 @@ async function renderCharacters() {
2634
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>
2635
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>`}
2636
2821
  ${item.profileSectionCount ? `<small class="character-section-count">${item.profileSectionCount} 个设定章节</small>` : ""}
2637
- <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>`;
2638
- }).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()}`
2639
2843
  : emptyModule("还没有角色档案", "创建主要人物,并维护别名、身份、动机和当前状态。"));
2844
+ bindModuleLayoutToggle(renderCharacters);
2640
2845
  $("#create-character-audit-task")?.addEventListener("click", async () => {
2641
2846
  const button = $("#create-character-audit-task");
2642
2847
  button.disabled = true;
@@ -2694,20 +2899,36 @@ async function renderRaces() {
2694
2899
  apiAllPages(`/api/works/${state.work.id}/races`),
2695
2900
  canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
2696
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>` : ""}`;
2697
2904
  const renderRaceNode = (item) => `<details class="race-tree-node" open data-race-node="${esc(item.id)}">
2698
2905
  <summary><span>${esc(item.name)}</span><small>${item.children.length} 个直接子种族</small></summary>
2699
2906
  <div class="race-tree-branch">
2700
- <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>
2701
2908
  <div class="race-path" aria-label="种族路径">${esc(racePathLabel(item))}</div>
2702
2909
  <p>${esc(item.description || "尚未填写种族简介")}</p>
2703
- <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>
2704
2911
  <p class="race-members">直接角色:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
2705
- <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>
2706
2913
  </article>
2707
2914
  ${item.children.length ? `<div class="race-tree-children">${item.children.map(renderRaceNode).join("")}</div>` : ""}
2708
2915
  </div>
2709
2916
  </details>`;
2710
- $("#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);
2711
2932
  $("#module-content").querySelectorAll("[data-edit-race]").forEach((button) => button.addEventListener("click", () => openRaceDialog(state.races.find((item) => item.id === button.dataset.editRace))));
2712
2933
  $("#module-content").querySelectorAll("[data-merge-race]").forEach((button) => button.addEventListener("click", () => {
2713
2934
  const source = state.races.find((item) => item.id === button.dataset.mergeRace);
@@ -2741,13 +2962,30 @@ async function renderOrganizations() {
2741
2962
  apiAllPages(`/api/works/${state.work.id}/organizations`),
2742
2963
  canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
2743
2964
  ]);
2744
- $("#module-content").innerHTML = state.organizations.length ? `<div class="card-grid organization-grid">${state.organizations.map((item) => `
2745
- <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>
2746
2969
  <h3>${esc(item.name)}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
2747
- <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>
2748
2971
  <p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
2749
- <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>
2750
- </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);
2751
2989
  $("#module-content").querySelectorAll("[data-edit-organization]").forEach((button) => button.addEventListener("click", () => openOrganizationDialog(state.organizations.find((item) => item.id === button.dataset.editOrganization))));
2752
2990
  $("#module-content").querySelectorAll("[data-merge-organization]").forEach((button) => button.addEventListener("click", () => {
2753
2991
  const source = state.organizations.find((item) => item.id === button.dataset.mergeOrganization);
@@ -2815,17 +3053,35 @@ async function renderOutlines() {
2815
3053
  apiPage(`/api/works/${state.work.id}/outlines`).then((result) => result.items),
2816
3054
  apiPage(`/api/works/${state.work.id}/foreshadows?status=all${currentChapterId ? `&currentChapterId=${encodeURIComponent(currentChapterId)}` : ""}`).then((result) => result.items)
2817
3055
  ]);
3056
+ const layout = readModuleLayout();
2818
3057
  const unresolved = foreshadows.filter((item) => item.unresolved);
2819
3058
  const overdue = unresolved.filter((item) => item.overdue);
2820
3059
  const navButton = $("#module-nav [data-module=outlines] .nav-label");
2821
3060
  if (navButton) navButton.textContent = unresolved.length ? `大纲与伏笔 · ${unresolved.length}` : "大纲与伏笔";
2822
- 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) => `
2823
3063
  <article class="record-card foreshadow-card ${item.overdue ? "is-overdue" : ""}">
2824
3064
  <small>${esc(item.importance)} · ${esc(item.status)}${item.overdue ? " · 已逾期" : ""}</small>
2825
3065
  <h3>${esc(item.title)}</h3><p>${esc(item.description || "暂无说明")}</p>
2826
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>
2827
- <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>
2828
- </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");
2829
3085
  const outlineHtml = outlines.length ? `<div class="outline-list">${outlines.map((item) => `
2830
3086
  <article class="outline-row ${item.status === "completed" ? "is-complete" : ""}">
2831
3087
  <div><small>${esc(item.volumeTitle)} · ${esc(item.status)}</small><h3>${esc(item.chapterTitle)}</h3></div>
@@ -2835,6 +3091,7 @@ async function renderOutlines() {
2835
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>
2836
3092
  </article>`).join("")}</div>` : emptyModule("还没有章节", "先创建章节,再为每章维护目标、冲突和转折。\n");
2837
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);
2838
3095
  $("#module-content").querySelectorAll("[data-edit-outline]").forEach((button) => button.addEventListener("click", () => openOutlineDialog(outlines.find((item) => item.chapterId === button.dataset.editOutline))));
2839
3096
  $("#module-content").querySelectorAll("[data-edit-foreshadow]").forEach((button) => button.addEventListener("click", () => openForeshadowDialog(foreshadows.find((item) => item.id === button.dataset.editForeshadow))));
2840
3097
  bindEntityHistoryButtons(renderOutlines);
@@ -2897,11 +3154,28 @@ async function renderReviews() {
2897
3154
  : "";
2898
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>`;
2899
3156
  };
2900
- $("#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) : `
2901
3159
  <article class="record-card"><small>${esc(item.itemType)} · ${esc(item.severity)} · ${esc(item.status)}</small><h3>${esc(item.title)}</h3>
2902
3160
  <p>${esc(item.description)}${item.suggestion ? `\n建议:${esc(item.suggestion)}` : ""}</p>
2903
- ${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>`}`
2904
3177
  : emptyModule("没有待审核事项", "候选设定、冲突与低置信度结论会集中显示在这里。");
3178
+ bindModuleLayoutToggle(renderReviews);
2905
3179
  $("#module-content").querySelectorAll("[data-review-id]").forEach((button) => button.addEventListener("click", async () => {
2906
3180
  await api(`/api/reviews/${button.dataset.reviewId}`, { method: "PATCH", body: { status: button.dataset.reviewStatus } });
2907
3181
  await renderReviews();
@@ -2909,7 +3183,10 @@ async function renderReviews() {
2909
3183
  $("#module-content").querySelectorAll("[data-merge-review]").forEach((button) => button.addEventListener("click", async () => {
2910
3184
  const target = characterById.get(button.dataset.mergeTarget);
2911
3185
  const source = characterById.get(button.dataset.mergeSource);
2912
- 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;
2913
3190
  button.disabled = true;
2914
3191
  try {
2915
3192
  await api(`/api/reviews/${button.dataset.mergeReview}/character-resolution`, { method: "POST", body: {
@@ -2942,7 +3219,7 @@ async function renderReviews() {
2942
3219
 
2943
3220
  async function renderTasks() {
2944
3221
  const [tasks, settings] = await Promise.all([
2945
- 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),
2946
3223
  canReadModule("ai-settings")
2947
3224
  ? api(`/api/works/${state.work.id}/ai-settings`)
2948
3225
  : Promise.resolve({ autoRunEnabled: false, autoRunConcurrency: 2, autoRunBatchLimit: 20 })
@@ -3013,9 +3290,13 @@ async function renderTasks() {
3013
3290
  }
3014
3291
  });
3015
3292
 
3016
- const taskById = new Map(tasks.map((item) => [item.id, item]));
3017
3293
  $("#module-content").querySelectorAll("[data-task-detail]").forEach((button) => button.addEventListener("click", () => {
3018
- 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; });
3019
3300
  }));
3020
3301
  $("#module-content").querySelectorAll("[data-run-task]").forEach((button) => button.addEventListener("click", async () => {
3021
3302
  const workId = state.work.id;
@@ -3241,8 +3522,9 @@ async function renderBookAiSettings() {
3241
3522
  async function loadModels() {
3242
3523
  const workId = state.work?.id;
3243
3524
  if (!workId) return;
3525
+ const generation = workScopedUiGeneration;
3244
3526
  const models = await api(`/api/works/${workId}/models`);
3245
- if (state.work?.id !== workId) return;
3527
+ if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
3246
3528
  state.models = models;
3247
3529
  loadedAiModelsWorkId = workId;
3248
3530
  const select = $("#ai-model");
@@ -3277,17 +3559,19 @@ let aiContextUsageTimer = null;
3277
3559
  let aiContextUsageRequest = 0;
3278
3560
 
3279
3561
  function currentAiRequestScope() {
3280
- if (!state.work || !state.chapter) return null;
3562
+ if (!state.work) return null;
3281
3563
  const taskType = $("#ai-task").value;
3282
3564
  const scopeType = $("#ai-scope").value;
3283
- const selection = $("#chapter-content").value.slice($("#chapter-content").selectionStart, $("#chapter-content").selectionEnd);
3284
- 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;
3285
3569
  const includeBookSummary = scopeType === "chapter-summary";
3286
- const scope = taskType === "polish" ? { type: "chapter", chapterId: state.chapter.id, selection }
3287
- : 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 } : {}) }
3288
3572
  : scopeType === "book" ? { type: "book" }
3289
3573
  : scopeType === "volume" ? { type: "volume", volumeId: volume?.id }
3290
- : { type: "chapter", chapterId: state.chapter.id };
3574
+ : { type: "chapter", chapterId: state.chapter?.id };
3291
3575
  Object.assign(scope, buildAiReferenceScope(state.aiReferences));
3292
3576
  if (includeBookSummary) scope.includeBookSummary = true;
3293
3577
  return { taskType, scope, selection };
@@ -3383,11 +3667,12 @@ async function refreshAiContextUsage() {
3383
3667
  async function loadAiReferences() {
3384
3668
  const workId = state.work?.id;
3385
3669
  if (!workId) return;
3670
+ const generation = workScopedUiGeneration;
3386
3671
  const [characters, settings] = await Promise.all([
3387
3672
  canReadModule("characters") ? apiAllPages(`/api/works/${workId}/characters`) : Promise.resolve([]),
3388
3673
  canReadModule("settings") ? apiAllPages(`/api/works/${workId}/settings`) : Promise.resolve([])
3389
3674
  ]);
3390
- if (state.work?.id !== workId) return;
3675
+ if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
3391
3676
  state.characters = characters;
3392
3677
  state.settings = settings;
3393
3678
  loadedAiReferencesWorkId = workId;
@@ -3411,6 +3696,7 @@ async function ensureAiReferencesLoaded() {
3411
3696
 
3412
3697
  function field(name, label, type = "text", value = "", options = []) {
3413
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>`;
3414
3700
  if (type === "item-list") {
3415
3701
  const values = Array.isArray(value) && value.length ? value : [""];
3416
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>`;
@@ -3452,6 +3738,52 @@ function field(name, label, type = "text", value = "", options = []) {
3452
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>`;
3453
3739
  }
3454
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
+
3455
3787
  function bindDynamicListControls(container) {
3456
3788
  container.querySelectorAll("[data-item-list-add]").forEach((button) => button.addEventListener("click", () => {
3457
3789
  const rows = button.previousElementSibling;
@@ -3534,6 +3866,7 @@ function commitRelationshipKeywordInputs(container) {
3534
3866
  }
3535
3867
 
3536
3868
  function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
3869
+ void discardPendingMarkdownAttachments();
3537
3870
  $("#dialog-title").textContent = title;
3538
3871
  $("#dialog-eyebrow").textContent = eyebrow;
3539
3872
  $("#dialog-fields").innerHTML = fields;
@@ -3541,15 +3874,21 @@ function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
3541
3874
  $("#form-dialog").classList.toggle("wide-dialog", Boolean(options.wide));
3542
3875
  bindDynamicListControls($("#dialog-fields"));
3543
3876
  bindRelationshipKeywordControls($("#dialog-fields"));
3877
+ bindVditorEditors($("#dialog-fields"));
3544
3878
  const form = $("#dynamic-form");
3545
3879
  form.onsubmit = async (event) => {
3546
- if (event.submitter?.value === "cancel") return;
3880
+ if (event.submitter?.value === "cancel") {
3881
+ void discardPendingMarkdownAttachments();
3882
+ return;
3883
+ }
3547
3884
  event.preventDefault();
3548
3885
  const submit = $("#dialog-submit");
3549
3886
  submit.disabled = true;
3550
3887
  try {
3551
3888
  commitRelationshipKeywordInputs(form);
3552
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);
3553
3892
  $("#form-dialog").close();
3554
3893
  } catch (error) {
3555
3894
  toast(error.message, "error");
@@ -3621,12 +3960,21 @@ function bindWorkCoverControls(work) {
3621
3960
  function openWorkSettingsDialog(work) {
3622
3961
  if (!work) return;
3623
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
+ : "打开作品后可用";
3624
3968
  const accessField = `<section class="work-access-field" aria-labelledby="work-access-title">
3625
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>
3626
3970
  ${canManageAccess ? '<button id="work-access-manage" class="ghost-button" type="button">配置成员权限</button>' : '<small>仅作品创建者或系统管理员可以调整访问权限。</small>'}
3627
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>`;
3628
3976
  openDialog("作品信息",
3629
- 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,
3630
3978
  async (form) => {
3631
3979
  await api(`/api/works/${work.id}`, { method: "PATCH", body: { title: form.get("title"), author: form.get("author"), description: form.get("description") } });
3632
3980
  state.works = (await apiPage("/api/works")).items;
@@ -3644,13 +3992,17 @@ function openWorkSettingsDialog(work) {
3644
3992
  toast("作品信息已保存");
3645
3993
  }, "作品设置");
3646
3994
  bindWorkCoverControls(work);
3995
+ $("#import-history-button")?.addEventListener("click", () => {
3996
+ $("#form-dialog").close();
3997
+ void openImportHistory();
3998
+ });
3647
3999
  $("#work-access-manage")?.addEventListener("click", () => {
3648
4000
  $("#form-dialog").close();
3649
4001
  openMembersDialog(work);
3650
4002
  });
3651
4003
  }
3652
4004
 
3653
- async function openChapterDialog() {
4005
+ async function openChapterDialog(volumeId = null) {
3654
4006
  if (!state.work) return openWorkDialog();
3655
4007
  if (!canEditProse()) return toast("当前权限只能编辑设定资料,正文为只读", "error");
3656
4008
  if (!state.work.volumes.length) {
@@ -3658,7 +4010,8 @@ async function openChapterDialog() {
3658
4010
  state.work = await api(`/api/works/${state.work.id}`);
3659
4011
  renderTree();
3660
4012
  }
3661
- 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) => {
3662
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: "" } });
3663
4016
  state.work = await api(`/api/works/${state.work.id}`);
3664
4017
  await selectChapter(chapter.id);
@@ -3673,13 +4026,13 @@ function openVolumeDialog(item) {
3673
4026
  field("title", "分卷名称", "text", item?.title) +
3674
4027
  field("kind", "分卷类型", "select", item?.kind ?? "main", kindOptions) +
3675
4028
  field("description", "分卷简介", "textarea", item?.description) +
3676
- field("keywords", "分卷关键词(逐条填写)", "item-list", item?.keywords ?? []),
4029
+ field("keywords", "分卷关键词", "keyword-chips", item?.keywords ?? []),
3677
4030
  async (form) => {
3678
4031
  const body = {
3679
4032
  title: form.get("title"),
3680
4033
  kind: form.get("kind"),
3681
4034
  description: form.get("description"),
3682
- keywords: form.getAll("keywords").map((value) => String(value).trim()).filter(Boolean)
4035
+ keywords: uniqueRelationshipKeywords(form.getAll("keywords").map(String))
3683
4036
  };
3684
4037
  await api(item ? `/api/volumes/${item.id}` : `/api/works/${state.work.id}/volumes`, { method: item ? "PATCH" : "POST", body });
3685
4038
  state.work = await api(`/api/works/${state.work.id}`);
@@ -3689,9 +4042,10 @@ function openVolumeDialog(item) {
3689
4042
  }
3690
4043
 
3691
4044
  function openSettingEditor(item = null) {
4045
+ destroyVditorEditor(settingEditorVditor);
4046
+ settingEditorVditor = null;
3692
4047
  settingEditorItem = item;
3693
4048
  $("#setting-editor-eyebrow").textContent = item ? "人工修正" : "作者事实";
3694
- $("#setting-editor-title").textContent = item ? `编辑“${item.title}”` : "新建设定";
3695
4049
  $("#setting-editor-name").value = item?.title ?? "";
3696
4050
  $("#setting-editor-category").value = item?.category ?? "世界规则";
3697
4051
  $("#setting-editor-locked").checked = Boolean(item?.locked);
@@ -3710,16 +4064,29 @@ function openSettingEditor(item = null) {
3710
4064
  const submit = $("#setting-editor-submit");
3711
4065
  submit.disabled = true;
3712
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
+ }
3713
4079
  const locked = form.get("locked") === "on";
3714
4080
  const body = {
3715
- title: String(form.get("title") ?? "").trim(),
4081
+ title,
3716
4082
  category: String(form.get("category") ?? "世界规则"),
3717
- content: String(form.get("content") ?? ""),
4083
+ content,
3718
4084
  locked,
3719
4085
  status: locked ? "confirmed" : (item?.status ?? "draft"),
3720
4086
  ...(item ? { changeNote: String(form.get("changeNote") ?? "").trim() } : {})
3721
4087
  };
3722
4088
  await api(item ? `/api/settings/${item.id}` : `/api/works/${state.work.id}/settings`, { method: item ? "PATCH" : "POST", body });
4089
+ await cleanupPendingMarkdownAttachments(body.content);
3723
4090
  entityEditorDirty = false;
3724
4091
  await loadAiReferences();
3725
4092
  await closeEntityEditor({ force: true });
@@ -3731,6 +4098,10 @@ function openSettingEditor(item = null) {
3731
4098
  }
3732
4099
  };
3733
4100
  showEntityEditorPage("setting");
4101
+ settingEditorVditor = createVditorEditor($("#setting-editor-markdown"), item?.content ?? "", {
4102
+ onInput: (markdown) => { $("#setting-editor-body").value = markdown; markEntityEditorDirty(); },
4103
+ readOnly: viewOnly
4104
+ });
3734
4105
  $("#setting-editor-name").focus();
3735
4106
  }
3736
4107
 
@@ -3856,22 +4227,165 @@ async function discardPendingCharacterAttachments() {
3856
4227
  }));
3857
4228
  }
3858
4229
 
3859
- function scheduleCharacterSectionPreview() {
3860
- if (characterSectionPreviewTimer !== null) clearTimeout(characterSectionPreviewTimer);
3861
- characterSectionPreviewTimer = window.setTimeout(() => {
3862
- characterSectionPreviewTimer = null;
3863
- const preview = $("#character-section-preview");
3864
- const content = $("#character-section-markdown");
3865
- if (preview && content) preview.innerHTML = renderMarkdown(content.value) || '<p class="character-markdown-empty">预览区域暂无内容。</p>';
3866
- }, 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
+ }));
3867
4235
  }
3868
4236
 
3869
- async function closeCharacterSectionEditor({ force = false } = {}) {
3870
- if (!force && characterSectionEditorDirty && !window.confirm("当前 Markdown 章节有未保存修改,返回人物档案将丢弃这些修改。是否继续?")) return false;
3871
- if (characterSectionPreviewTimer !== null) {
3872
- clearTimeout(characterSectionPreviewTimer);
3873
- 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;
3874
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;
3875
4389
  await discardPendingCharacterAttachments();
3876
4390
  characterSectionEditorDirty = false;
3877
4391
  $("#character-section-editor-view").classList.add("hidden");
@@ -3881,6 +4395,82 @@ async function closeCharacterSectionEditor({ force = false } = {}) {
3881
4395
  return true;
3882
4396
  }
3883
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
+
3884
4474
  function characterSectionEditorHtml(section = null) {
3885
4475
  const options = Object.entries(characterSectionTypeLabels).map(([value, label]) => `<option value="${value}" ${section?.sectionType === value ? "selected" : ""}>${esc(label)}</option>`).join("");
3886
4476
  return `<div class="character-section-editor-shell">
@@ -3894,15 +4484,7 @@ function characterSectionEditorHtml(section = null) {
3894
4484
  <label>章节标题<input id="character-section-title" maxlength="200" value="${esc(section?.title ?? "")}" placeholder="例如:背景故事" required></label>
3895
4485
  <label class="character-markdown-summary-field">章节摘要<textarea id="character-section-summary" maxlength="20000" placeholder="用于角色列表和 AI 快速定位,不会替代正文">${esc(section?.summary ?? "")}</textarea></label>
3896
4486
  </div>
3897
- <div class="character-markdown-toolbar">
3898
- <label class="ghost-button" for="character-section-attachment">上传并插入图片</label>
3899
- <input id="character-section-attachment" class="hidden" type="file" accept=".png,.jpg,.jpeg,.webp,.gif,image/png,image/jpeg,image/webp,image/gif">
3900
- <span>图片会优先转换为体积更小的无损 WebP</span>
3901
- </div>
3902
- <div class="character-markdown-compose">
3903
- <label>Markdown 原文<textarea id="character-section-markdown" maxlength="500000" spellcheck="true" placeholder="支持标题、列表、引用、表格、链接和图片">${esc(section?.contentMarkdown ?? "")}</textarea></label>
3904
- <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>
3905
- </div>
4487
+ <div id="character-section-markdown" class="vditor-editor-host" data-vditor-editor aria-label="Markdown 编辑器"></div>
3906
4488
  <div class="character-markdown-editor-footer">
3907
4489
  <label class="character-markdown-change-note">版本说明<input id="character-section-change-note" maxlength="500" placeholder="可选,例如:补充远古时期经历"></label>
3908
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>
@@ -3913,41 +4495,17 @@ function characterSectionEditorHtml(section = null) {
3913
4495
 
3914
4496
  async function openCharacterSectionEditor(section = null) {
3915
4497
  await discardPendingCharacterAttachments();
4498
+ destroyVditorEditor(characterSectionVditor);
4499
+ characterSectionVditor = null;
3916
4500
  const host = $("#character-section-editor-host");
3917
4501
  host.innerHTML = characterSectionEditorHtml(section);
3918
4502
  characterSectionEditorDirty = false;
3919
4503
  $("#character-editor-form").classList.add("hidden");
3920
4504
  $("#character-section-editor-view").classList.remove("hidden");
3921
- const textarea = $("#character-section-markdown");
3922
4505
  host.querySelectorAll("input, textarea, select").forEach((control) => control.addEventListener("input", () => { characterSectionEditorDirty = true; }));
3923
- textarea.addEventListener("input", scheduleCharacterSectionPreview);
3924
- $("#character-section-attachment").addEventListener("change", async (event) => {
3925
- const file = event.target.files?.[0];
3926
- if (!file) return;
3927
- const input = event.currentTarget;
3928
- input.disabled = true;
3929
- const body = new FormData();
3930
- body.append("file", file);
3931
- try {
3932
- const attachment = await api(`/api/works/${state.work.id}/attachments`, { method: "POST", body });
3933
- if (!attachment.deduplicated) characterSectionPendingAttachments.push(String(attachment.id));
3934
- const imageLabel = file.name.replace(/[\]\r\n]/gu, "").trim() || "图片附件";
3935
- const insertion = `![${imageLabel}](attachment://${attachment.id})`;
3936
- const start = textarea.selectionStart ?? textarea.value.length;
3937
- const end = textarea.selectionEnd ?? start;
3938
- const prefix = start > 0 && !textarea.value.slice(0, start).endsWith("\n") ? "\n\n" : "";
3939
- const suffix = end < textarea.value.length && !textarea.value.slice(end).startsWith("\n") ? "\n\n" : "";
3940
- textarea.setRangeText(`${prefix}${insertion}${suffix}`, start, end, "end");
3941
- textarea.focus();
3942
- characterSectionEditorDirty = true;
3943
- scheduleCharacterSectionPreview();
3944
- toast(attachment.storedMimeType === "image/webp" ? "图片已转换为无损 WebP 并插入" : "图片已插入;转换后未变小,因此保留原格式");
3945
- } catch (error) {
3946
- toast(error.message, "error");
3947
- } finally {
3948
- input.disabled = false;
3949
- input.value = "";
3950
- }
4506
+ characterSectionVditor = createVditorEditor($("#character-section-markdown"), section?.contentMarkdown ?? "", {
4507
+ uploadAttachment: uploadCharacterSectionAttachment,
4508
+ onInput: () => { characterSectionEditorDirty = true; }
3951
4509
  });
3952
4510
  host.querySelector("[data-character-section-edit-close]").addEventListener("click", () => void closeCharacterSectionEditor());
3953
4511
  host.querySelector("[data-character-section-edit-cancel]").addEventListener("click", () => void closeCharacterSectionEditor());
@@ -3960,7 +4518,7 @@ async function openCharacterSectionEditor(section = null) {
3960
4518
  return;
3961
4519
  }
3962
4520
  button.disabled = true;
3963
- const contentMarkdown = textarea.value;
4521
+ const contentMarkdown = characterSectionVditor?.getValue() ?? "";
3964
4522
  try {
3965
4523
  const saved = await api(section ? `/api/character-sections/${section.id}` : `/api/characters/${characterEditorItem.id}/sections`, {
3966
4524
  method: section ? "PATCH" : "POST",
@@ -4018,7 +4576,7 @@ function renderCharacterMarkdownSections() {
4018
4576
  const host = $("#character-markdown-sections");
4019
4577
  if (!host) return;
4020
4578
  if (!characterEditorItem?.id) {
4021
- 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>';
4022
4580
  return;
4023
4581
  }
4024
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>`;
@@ -4284,44 +4842,115 @@ async function openCharacterEditor(item = null) {
4284
4842
  }
4285
4843
  }
4286
4844
 
4287
- 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();
4288
4882
  state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
4289
4883
  const memberOptions = state.characters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
4290
- const parentOptions = [["", "无(根种族)"], ...eligibleRaceParents(state.races, item?.id)
4291
- .sort((left, right) => racePathLabel(left).localeCompare(racePathLabel(right), "zh-CN"))
4292
- .map((race) => [race.id, racePathLabel(race)])];
4293
- openDialog(item ? "编辑种族" : "新建种族",
4294
- field("name", "种族名称", "text", item?.name) +
4295
- field("parentRaceId", "父种族", "select", item?.parentRaceId ?? "", parentOptions) +
4296
- field("description", "种族简介", "textarea", item?.description) +
4297
- field("settings", "种族共同设定(逐条填写)", "item-list", item?.settings ?? []) +
4298
- (memberOptions.length ? field("memberIds", "属于该种族的角色(可多选)", "chips", item?.memberIds ?? [], memberOptions) : ""),
4299
- async (form) => {
4300
- const settings = form.getAll("settings").map((value) => String(value).trim()).filter(Boolean);
4301
- const body = { name: form.get("name"), parentRaceId: form.get("parentRaceId") || null, description: form.get("description"), settings };
4302
- if (canReadModule("characters")) body.memberIds = form.getAll("memberIds").map(String);
4303
- await api(item ? `/api/races/${item.id}` : `/api/works/${state.work.id}/races`, { method: item ? "PATCH" : "POST", body });
4304
- 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;
4305
4935
  await loadAiReferences();
4306
- }, 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);
4307
4950
  }
4308
4951
 
4309
4952
  async function openOrganizationDialog(item) {
4310
- state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
4311
- const memberOptions = state.characters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
4312
- openDialog(item ? "编辑组织" : "新建组织",
4313
- field("name", "组织名称", "text", item?.name) +
4314
- field("description", "组织简介", "textarea", item?.description) +
4315
- field("settings", "组织设定(逐条填写)", "item-list", item?.settings ?? []) +
4316
- (memberOptions.length ? field("memberIds", "组织成员(可多选)", "chips", item?.memberIds ?? [], memberOptions) : ""),
4317
- async (form) => {
4318
- const settings = form.getAll("settings").map((value) => String(value).trim()).filter(Boolean);
4319
- const body = { name: form.get("name"), description: form.get("description"), settings };
4320
- if (canReadModule("characters")) body.memberIds = form.getAll("memberIds").map(String);
4321
- await api(item ? `/api/organizations/${item.id}` : `/api/works/${state.work.id}/organizations`, { method: item ? "PATCH" : "POST", body });
4322
- await renderOrganizations();
4323
- await loadAiReferences();
4324
- }, item ? "组织档案" : "世界内组织");
4953
+ await openKnowledgeEditor("organization", item);
4325
4954
  }
4326
4955
 
4327
4956
  function openTimelineTrackDialog(item) {
@@ -4462,16 +5091,29 @@ function openProviderDialog(item) {
4462
5091
 
4463
5092
  function openModelDialog(providerId, item = null) {
4464
5093
  const values = modelFormValues(item);
4465
- 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) => {
4466
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);
4467
5097
  await api(item ? `/api/models/${item.id}` : `/api/providers/${providerId}/models`, { method: item ? "PATCH" : "POST", body });
4468
5098
  await renderPlatformAiConfig();
4469
5099
  await loadModels();
4470
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();
4471
5113
  }
4472
5114
 
4473
5115
  async function sendAi() {
4474
- if (!state.work || !state.chapter) return toast("请先选择章节", "error");
5116
+ if (!state.work) return toast("请先选择作品", "error");
4475
5117
  try {
4476
5118
  await Promise.all([ensureAiModelsLoaded(), ensureAiConversationsLoaded()]);
4477
5119
  } catch (error) {
@@ -4727,7 +5369,10 @@ async function showVersions() {
4727
5369
  const versions = await api(`/api/chapters/${state.chapter.id}/versions`);
4728
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("");
4729
5371
  $("#versions-list").querySelectorAll("[data-restore-version]").forEach((button) => button.addEventListener("click", async () => {
4730
- if (!window.confirm(`将版本 v${button.dataset.restoreVersion} 恢复为一个新的保存版本?`)) return;
5372
+ if (!(await confirmToast(
5373
+ `将版本 v${button.dataset.restoreVersion} 恢复为一个新的保存版本?`,
5374
+ { title: "恢复历史版本", confirmLabel: "确认恢复" }
5375
+ ))) return;
4731
5376
  state.chapter = await api(`/api/chapters/${state.chapter.id}/restore`, { method: "POST", body: { versionNo: Number(button.dataset.restoreVersion) } });
4732
5377
  lastSavedChapterSnapshot = { chapterId: state.chapter.id, title: state.chapter.title, content: state.chapter.content };
4733
5378
  $("#chapter-title").value = state.chapter.title;
@@ -4741,6 +5386,120 @@ async function showVersions() {
4741
5386
  $("#versions-dialog").showModal();
4742
5387
  }
4743
5388
 
5389
+ function importHistoryRecordLabel(version) {
5390
+ const restorePrefix = "before-restore:";
5391
+ if (version.fileType === "snapshot" && version.fileName.startsWith(restorePrefix)) {
5392
+ return {
5393
+ title: `恢复操作前备份:${version.fileName.slice(restorePrefix.length)}`,
5394
+ kind: "自动备份",
5395
+ action: "恢复此操作前备份"
5396
+ };
5397
+ }
5398
+ return { title: version.fileName, kind: String(version.fileType).toUpperCase(), action: "恢复到此次导入前" };
5399
+ }
5400
+
5401
+ function renderImportHistory(versions, nextPage = null) {
5402
+ const host = $("#import-history-list");
5403
+ if (!versions.length) {
5404
+ host.innerHTML = '<p class="entity-history-empty">还没有正文导入记录。首次导入后会自动保存导入前快照。</p>';
5405
+ return;
5406
+ }
5407
+ host.innerHTML = versions.map((version) => {
5408
+ const label = importHistoryRecordLabel(version);
5409
+ return `<article class="entity-version-card import-history-card" data-file-version="${esc(version.id)}">
5410
+ <header><strong title="${esc(label.title)}">${esc(label.title)}</strong><span class="import-history-kind">${esc(label.kind)}</span></header>
5411
+ <time>${esc(formatDateTime(version.createdAt))} · ${esc(version.actor || "历史数据")}</time>
5412
+ <p>${version.fileType === "snapshot" ? "恢复操作执行前自动保存的完整正文。" : "保存的是这次文件导入开始前的完整正文。"}</p>
5413
+ <small>自动备份仅包含正文,不能撤销章节关联信息的变化。</small>
5414
+ <button type="button" data-file-version-restore="${esc(version.id)}" data-default-label="${esc(label.action)}">${esc(label.action)}</button>
5415
+ </article>`;
5416
+ }).join("") + (nextPage ? '<button class="import-history-load-more" type="button" data-import-history-load-more>加载更多记录</button>' : "");
5417
+ host.querySelectorAll("[data-file-version-restore]").forEach((button) => button.addEventListener("click", async () => {
5418
+ if (!state.work || !canReplaceProse()) return;
5419
+ const defaultLabel = button.dataset.defaultLabel;
5420
+ if (button.dataset.confirmed !== "true") {
5421
+ host.querySelectorAll("[data-file-version-restore]").forEach((other) => {
5422
+ other.dataset.confirmed = "false";
5423
+ other.classList.remove("is-confirming");
5424
+ other.textContent = other.dataset.defaultLabel;
5425
+ });
5426
+ button.dataset.confirmed = "true";
5427
+ button.classList.add("is-confirming");
5428
+ button.textContent = "再次点击确认恢复";
5429
+ window.setTimeout(() => {
5430
+ if (!button.isConnected || button.dataset.confirmed !== "true") return;
5431
+ button.dataset.confirmed = "false";
5432
+ button.classList.remove("is-confirming");
5433
+ button.textContent = defaultLabel;
5434
+ }, 5000);
5435
+ return;
5436
+ }
5437
+ if (!(await confirmDiscardChanges("当前章节有未保存修改,恢复正文会丢弃这些本地修改。是否继续?"))) return;
5438
+ button.disabled = true;
5439
+ cancelChapterAutoSave();
5440
+ const workId = state.work.id;
5441
+ try {
5442
+ await api(`/api/works/${encodeURIComponent(workId)}/file-versions/${encodeURIComponent(button.dataset.fileVersionRestore)}/restore`, {
5443
+ method: "POST",
5444
+ body: { expectedVersionNo: state.work.versionNo }
5445
+ });
5446
+ state.dirty = false;
5447
+ resetWorkScopedUiCaches();
5448
+ $("#import-history-dialog").close();
5449
+ await loadWorks(workId);
5450
+ toast("正文已恢复;恢复前正文已自动备份");
5451
+ } catch (error) {
5452
+ button.disabled = false;
5453
+ button.dataset.confirmed = "false";
5454
+ button.classList.remove("is-confirming");
5455
+ button.textContent = defaultLabel;
5456
+ if (state.dirty) scheduleChapterAutoSave();
5457
+ toast(error.message, "error");
5458
+ }
5459
+ }));
5460
+ host.querySelector("[data-import-history-load-more]")?.addEventListener("click", async (event) => {
5461
+ const button = event.currentTarget;
5462
+ button.disabled = true;
5463
+ button.textContent = "正在加载…";
5464
+ try {
5465
+ await loadImportHistoryPage(nextPage);
5466
+ } catch (error) {
5467
+ button.disabled = false;
5468
+ button.textContent = "加载更多记录";
5469
+ toast(error.message, "error");
5470
+ }
5471
+ });
5472
+ }
5473
+
5474
+ async function loadImportHistoryPage(page) {
5475
+ const workId = state.work?.id;
5476
+ if (!workId || !page) return;
5477
+ const requestId = ++importHistoryRequestId;
5478
+ const result = await apiPage(`/api/works/${encodeURIComponent(workId)}/file-versions`, page, 25);
5479
+ if (requestId !== importHistoryRequestId || state.work?.id !== workId || !$("#import-history-dialog").open) return;
5480
+ importHistoryRecords = page === 1 ? result.items : [...importHistoryRecords, ...result.items];
5481
+ importHistoryNextPage = result.nextPage;
5482
+ renderImportHistory(importHistoryRecords, importHistoryNextPage);
5483
+ }
5484
+
5485
+ async function openImportHistory() {
5486
+ if (!state.work || !canReplaceProse()) {
5487
+ toast("恢复整本正文需要所有受影响模块的编辑权限", "error");
5488
+ return;
5489
+ }
5490
+ importHistoryRecords = [];
5491
+ importHistoryNextPage = null;
5492
+ importHistoryRequestId += 1;
5493
+ $("#import-history-list").innerHTML = '<p class="entity-history-empty">正在读取导入历史…</p>';
5494
+ $("#import-history-dialog").showModal();
5495
+ try {
5496
+ await loadImportHistoryPage(1);
5497
+ } catch (error) {
5498
+ $("#import-history-dialog").close();
5499
+ toast(error.message, "error");
5500
+ }
5501
+ }
5502
+
4744
5503
  async function showChapterInsight() {
4745
5504
  if (!state.chapter) return;
4746
5505
  const panel = $("#chapter-insight");
@@ -4756,11 +5515,13 @@ async function showChapterInsight() {
4756
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(";"))}` : ""}`;
4757
5516
  }
4758
5517
 
4759
- $("#home-button").addEventListener("click", () => {
4760
- if (!confirmDiscardChanges()) return;
5518
+ $("#home-button").addEventListener("click", async () => {
5519
+ if (!(await confirmDiscardChanges())) return;
4761
5520
  loadWorks().catch((error) => toast(error.message, "error"));
4762
5521
  });
4763
- $("#settings-button").addEventListener("click", showSettingsHub);
5522
+ $("#settings-button").addEventListener("click", () => {
5523
+ void showSettingsHub();
5524
+ });
4764
5525
  $("#account-button").addEventListener("click", () => {
4765
5526
  const expanded = $("#account-menu").classList.toggle("hidden") === false;
4766
5527
  $("#account-button").setAttribute("aria-expanded", String(expanded));
@@ -4841,7 +5602,7 @@ $("#avatar-file").addEventListener("change", async (event) => {
4841
5602
  }
4842
5603
  });
4843
5604
  $("#avatar-remove-button").addEventListener("click", async () => {
4844
- if (!state.user?.avatarUrl || !window.confirm("确定移除当前头像吗?")) return;
5605
+ if (!state.user?.avatarUrl || !(await confirmToast("确定移除当前头像吗?", { title: "移除头像", confirmLabel: "确认移除" }))) return;
4845
5606
  $("#avatar-upload-button").disabled = true;
4846
5607
  $("#avatar-remove-button").disabled = true;
4847
5608
  try {
@@ -4869,14 +5630,35 @@ $("#profile-form").addEventListener("submit", async (event) => {
4869
5630
  $("#password-form").addEventListener("submit", async (event) => {
4870
5631
  event.preventDefault();
4871
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
+ }
4872
5642
  try {
4873
- 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 } });
4874
5644
  event.currentTarget.reset();
4875
5645
  toast("密码已更新,其他设备的会话已退出");
4876
5646
  } catch (error) { toast(error.message, "error"); }
4877
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);
4878
5657
  $("#api-key-reset-button").addEventListener("click", async () => {
4879
- 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;
4880
5662
  try {
4881
5663
  const result = await api("/api/auth/api-key/reset", { method: "POST", body: {} });
4882
5664
  $("#api-key-status").textContent = `已配置 ${result.prefix}…,尚未使用`;
@@ -5050,22 +5832,26 @@ $("#member-permission-form").addEventListener("submit", async (event) => {
5050
5832
  $("#platform-new-provider").addEventListener("click", () => openProviderDialog());
5051
5833
  $("#shelf-new-work").addEventListener("click", openWorkDialog);
5052
5834
  $("#welcome-new-work").addEventListener("click", () => state.work ? openChapterDialog() : openWorkDialog());
5053
- $("#new-chapter-button").addEventListener("click", openChapterDialog);
5054
5835
  $("#save-button").addEventListener("click", saveChapter);
5055
5836
  $("#tidy-blank-lines-button").addEventListener("click", tidyChapterBlankLines);
5056
5837
  $("#new-volume-button").addEventListener("click", () => openVolumeDialog());
5057
5838
  $("#insight-button").addEventListener("click", () => showChapterInsight().catch((error) => toast(error.message, "error")));
5058
5839
  $("#versions-button").addEventListener("click", showVersions);
5059
5840
  $("#versions-close").addEventListener("click", () => $("#versions-dialog").close());
5841
+ $("#import-history-close").addEventListener("click", () => $("#import-history-dialog").close());
5060
5842
  $("#entity-history-close").addEventListener("click", () => $("#entity-history-dialog").close());
5061
5843
  $("#ai-tool-call-close").addEventListener("click", () => $("#ai-tool-call-dialog").close());
5062
5844
  $("#setting-editor-back").addEventListener("click", () => { void closeEntityEditor(); });
5063
5845
  $("#character-editor-close").addEventListener("click", () => { void closeEntityEditor(); });
5064
5846
  $("#character-editor-cancel").addEventListener("click", () => { void closeEntityEditor(); });
5847
+ $("#knowledge-editor-close").addEventListener("click", () => { void closeEntityEditor(); });
5848
+ $("#knowledge-editor-cancel").addEventListener("click", () => { void closeEntityEditor(); });
5065
5849
  $("#setting-editor-form").addEventListener("input", markEntityEditorDirty);
5066
5850
  $("#setting-editor-form").addEventListener("change", markEntityEditorDirty);
5067
5851
  $("#character-editor-form").addEventListener("input", markEntityEditorDirty);
5068
5852
  $("#character-editor-form").addEventListener("change", markEntityEditorDirty);
5853
+ $("#knowledge-editor-form").addEventListener("input", markEntityEditorDirty);
5854
+ $("#knowledge-editor-form").addEventListener("change", markEntityEditorDirty);
5069
5855
  $("#character-editor-fields").addEventListener("click", (event) => {
5070
5856
  if (event.target.closest("[data-item-list-add], [data-structured-list-add], [data-item-list-remove], [data-structured-list-remove]")) markEntityEditorDirty();
5071
5857
  });
@@ -5192,6 +5978,13 @@ $("#ai-mention-menu").addEventListener("click", (event) => {
5192
5978
  const button = event.target.closest("[data-ai-reference-id]");
5193
5979
  if (button) selectAiMention(button);
5194
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
+ });
5195
5988
  $("#import-file").addEventListener("change", async (event) => {
5196
5989
  const file = event.target.files[0];
5197
5990
  if (!state.work || !file) return;
@@ -5205,6 +5998,13 @@ $("#import-file").addEventListener("change", async (event) => {
5205
5998
  event.target.value = "";
5206
5999
  return;
5207
6000
  }
6001
+ if (mode === "overwrite" && !(await confirmToast(
6002
+ `当前选项:覆盖正文。确认后将使用“${file.name}”替换《${state.work.title}》的现有正文目录。`,
6003
+ { title: "覆盖正文二次确认", confirmLabel: "确认覆盖" }
6004
+ ))) {
6005
+ event.target.value = "";
6006
+ return;
6007
+ }
5208
6008
  cancelChapterAutoSave();
5209
6009
  const body = new FormData();
5210
6010
  body.append("file", file);