@musnows/scriverse 0.3.11 → 0.4.1

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 (564) hide show
  1. package/README.en.md +3 -1
  2. package/README.md +5 -1
  3. package/dist/ai.js +292 -19
  4. package/dist/ai.js.map +1 -1
  5. package/dist/app.js +46 -4
  6. package/dist/app.js.map +1 -1
  7. package/dist/database.js +64 -0
  8. package/dist/database.js.map +1 -1
  9. package/dist/public/app.js +1088 -274
  10. package/dist/public/character-markdown.js +12 -0
  11. package/dist/public/character-version.js +3 -1
  12. package/dist/public/index.html +76 -29
  13. package/dist/public/keyboard-shortcuts.d.ts +9 -0
  14. package/dist/public/keyboard-shortcuts.js +4 -0
  15. package/dist/public/model-config.d.ts +1 -0
  16. package/dist/public/model-config.js +12 -3
  17. package/dist/public/module-layout.d.ts +17 -0
  18. package/dist/public/module-layout.js +28 -0
  19. package/dist/public/page-route.js +1 -1
  20. package/dist/public/relationship-graph.js +62 -2
  21. package/dist/public/styles.css +330 -98
  22. package/dist/public/vendor/vditor/dist/css/content-theme/ant-design.css +210 -0
  23. package/dist/public/vendor/vditor/dist/css/content-theme/dark.css +123 -0
  24. package/dist/public/vendor/vditor/dist/css/content-theme/light.css +101 -0
  25. package/dist/public/vendor/vditor/dist/css/content-theme/wechat.css +94 -0
  26. package/dist/public/vendor/vditor/dist/images/emoji/b3log.png +0 -0
  27. package/dist/public/vendor/vditor/dist/images/emoji/chainbook.png +0 -0
  28. package/dist/public/vendor/vditor/dist/images/emoji/doge.png +0 -0
  29. package/dist/public/vendor/vditor/dist/images/emoji/hacpai.png +0 -0
  30. package/dist/public/vendor/vditor/dist/images/emoji/huaji.gif +0 -0
  31. package/dist/public/vendor/vditor/dist/images/emoji/latke.png +0 -0
  32. package/dist/public/vendor/vditor/dist/images/emoji/liandi.png +0 -0
  33. package/dist/public/vendor/vditor/dist/images/emoji/lute.png +0 -0
  34. package/dist/public/vendor/vditor/dist/images/emoji/octocat.png +0 -0
  35. package/dist/public/vendor/vditor/dist/images/emoji/pipe.png +0 -0
  36. package/dist/public/vendor/vditor/dist/images/emoji/siyuan.png +0 -0
  37. package/dist/public/vendor/vditor/dist/images/emoji/solo.png +0 -0
  38. package/dist/public/vendor/vditor/dist/images/emoji/sym.png +0 -0
  39. package/dist/public/vendor/vditor/dist/images/emoji/trollface.png +0 -0
  40. package/dist/public/vendor/vditor/dist/images/emoji/vditor.png +0 -0
  41. package/dist/public/vendor/vditor/dist/images/emoji/wide.png +0 -0
  42. package/dist/public/vendor/vditor/dist/images/emoji/wulian.png +0 -0
  43. package/dist/public/vendor/vditor/dist/images/img-loading.svg +1 -0
  44. package/dist/public/vendor/vditor/dist/images/logo.png +0 -0
  45. package/dist/public/vendor/vditor/dist/index.css +1823 -0
  46. package/dist/public/vendor/vditor/dist/index.d.ts +82 -0
  47. package/dist/public/vendor/vditor/dist/index.js +15995 -0
  48. package/dist/public/vendor/vditor/dist/index.min.js +1 -0
  49. package/dist/public/vendor/vditor/dist/js/abcjs/abcjs_basic.min.js +3 -0
  50. package/dist/public/vendor/vditor/dist/js/echarts/echarts.min.js +45 -0
  51. package/dist/public/vendor/vditor/dist/js/flowchart.js/flowchart.min.js +8 -0
  52. package/dist/public/vendor/vditor/dist/js/graphviz/full.render.js +90 -0
  53. package/dist/public/vendor/vditor/dist/js/graphviz/viz.js +333 -0
  54. package/dist/public/vendor/vditor/dist/js/highlight.js/LICENSE +29 -0
  55. package/dist/public/vendor/vditor/dist/js/highlight.js/highlight.min.js +3835 -0
  56. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/a11y-dark.min.css +7 -0
  57. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/a11y-light.min.css +7 -0
  58. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/agate.min.css +20 -0
  59. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/an-old-hope.min.css +9 -0
  60. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/androidstudio.min.css +1 -0
  61. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/ant-design.min.css +84 -0
  62. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/arduino-light.min.css +1 -0
  63. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/arta.min.css +1 -0
  64. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/ascetic.min.css +1 -0
  65. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/atom-one-dark-reasonable.min.css +1 -0
  66. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/atom-one-dark.min.css +1 -0
  67. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/atom-one-light.min.css +1 -0
  68. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/3024.min.css +7 -0
  69. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/apathy.min.css +7 -0
  70. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/apprentice.min.css +7 -0
  71. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ashes.min.css +7 -0
  72. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-cave-light.min.css +7 -0
  73. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-cave.min.css +7 -0
  74. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-dune-light.min.css +7 -0
  75. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-dune.min.css +7 -0
  76. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-estuary-light.min.css +7 -0
  77. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-estuary.min.css +7 -0
  78. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-forest-light.min.css +7 -0
  79. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-forest.min.css +7 -0
  80. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-heath-light.min.css +7 -0
  81. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-heath.min.css +7 -0
  82. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-lakeside-light.min.css +7 -0
  83. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-lakeside.min.css +7 -0
  84. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-plateau-light.min.css +7 -0
  85. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-plateau.min.css +7 -0
  86. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-savanna-light.min.css +7 -0
  87. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-savanna.min.css +7 -0
  88. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-seaside-light.min.css +7 -0
  89. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-seaside.min.css +7 -0
  90. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-sulphurpool-light.min.css +7 -0
  91. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atelier-sulphurpool.min.css +7 -0
  92. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/atlas.min.css +7 -0
  93. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/bespin.min.css +7 -0
  94. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-bathory.min.css +7 -0
  95. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-burzum.min.css +7 -0
  96. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-dark-funeral.min.css +7 -0
  97. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-gorgoroth.min.css +7 -0
  98. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-immortal.min.css +7 -0
  99. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-khold.min.css +7 -0
  100. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-marduk.min.css +7 -0
  101. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-mayhem.min.css +7 -0
  102. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-nile.min.css +7 -0
  103. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal-venom.min.css +7 -0
  104. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/black-metal.min.css +7 -0
  105. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/brewer.min.css +7 -0
  106. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/bright.min.css +7 -0
  107. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/brogrammer.min.css +7 -0
  108. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/brush-trees-dark.min.css +7 -0
  109. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/brush-trees.min.css +7 -0
  110. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/chalk.min.css +7 -0
  111. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/circus.min.css +7 -0
  112. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/classic-dark.min.css +7 -0
  113. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/classic-light.min.css +7 -0
  114. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/codeschool.min.css +7 -0
  115. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/colors.min.css +7 -0
  116. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/cupcake.min.css +7 -0
  117. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/cupertino.min.css +7 -0
  118. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/danqing.min.css +7 -0
  119. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/darcula.min.css +7 -0
  120. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/dark-violet.min.css +7 -0
  121. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/darkmoss.min.css +7 -0
  122. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/darktooth.min.css +7 -0
  123. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/decaf.min.css +7 -0
  124. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/default-dark.min.css +7 -0
  125. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/default-light.min.css +7 -0
  126. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/dirtysea.min.css +7 -0
  127. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/dracula.min.css +7 -0
  128. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/edge-dark.min.css +7 -0
  129. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/edge-light.min.css +7 -0
  130. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/eighties.min.css +7 -0
  131. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/embers.min.css +7 -0
  132. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/equilibrium-dark.min.css +7 -0
  133. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/equilibrium-gray-dark.min.css +7 -0
  134. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/equilibrium-gray-light.min.css +7 -0
  135. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/equilibrium-light.min.css +7 -0
  136. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/espresso.min.css +7 -0
  137. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/eva-dim.min.css +7 -0
  138. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/eva.min.css +7 -0
  139. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/flat.min.css +7 -0
  140. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/framer.min.css +7 -0
  141. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/fruit-soda.min.css +7 -0
  142. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gigavolt.min.css +7 -0
  143. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/github.min.css +7 -0
  144. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/google-dark.min.css +7 -0
  145. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/google-light.min.css +7 -0
  146. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/grayscale-dark.min.css +7 -0
  147. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/grayscale-light.min.css +7 -0
  148. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/green-screen.min.css +7 -0
  149. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gruvbox-dark-hard.min.css +7 -0
  150. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gruvbox-dark-medium.min.css +7 -0
  151. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gruvbox-dark-pale.min.css +7 -0
  152. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gruvbox-dark-soft.min.css +7 -0
  153. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gruvbox-light-hard.min.css +7 -0
  154. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gruvbox-light-medium.min.css +7 -0
  155. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/gruvbox-light-soft.min.css +7 -0
  156. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/hardcore.min.css +7 -0
  157. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/harmonic16-dark.min.css +7 -0
  158. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/harmonic16-light.min.css +7 -0
  159. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/heetch-dark.min.css +7 -0
  160. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/heetch-light.min.css +7 -0
  161. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/helios.min.css +7 -0
  162. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/hopscotch.min.css +7 -0
  163. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/horizon-dark.min.css +7 -0
  164. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/horizon-light.min.css +7 -0
  165. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/humanoid-dark.min.css +7 -0
  166. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/humanoid-light.min.css +7 -0
  167. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ia-dark.min.css +7 -0
  168. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ia-light.min.css +7 -0
  169. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/icy-dark.min.css +7 -0
  170. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ir-black.min.css +7 -0
  171. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/isotope.min.css +7 -0
  172. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/kimber.min.css +7 -0
  173. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/london-tube.min.css +7 -0
  174. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/macintosh.min.css +7 -0
  175. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/marrakesh.min.css +7 -0
  176. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/materia.min.css +7 -0
  177. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/material-darker.min.css +7 -0
  178. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/material-lighter.min.css +7 -0
  179. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/material-palenight.min.css +7 -0
  180. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/material-vivid.min.css +7 -0
  181. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/material.min.css +7 -0
  182. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/mellow-purple.min.css +7 -0
  183. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/mexico-light.min.css +7 -0
  184. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/mocha.min.css +7 -0
  185. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/monokai.min.css +7 -0
  186. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/nebula.min.css +7 -0
  187. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/nord.min.css +7 -0
  188. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/nova.min.css +7 -0
  189. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ocean.min.css +7 -0
  190. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/oceanicnext.min.css +7 -0
  191. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/one-light.min.css +7 -0
  192. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/onedark.min.css +7 -0
  193. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/outrun-dark.min.css +7 -0
  194. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/papercolor-dark.min.css +7 -0
  195. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/papercolor-light.min.css +7 -0
  196. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/paraiso.min.css +7 -0
  197. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/pasque.min.css +7 -0
  198. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/phd.min.css +7 -0
  199. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/pico.min.css +7 -0
  200. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/pop.min.css +7 -0
  201. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/porple.min.css +7 -0
  202. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/qualia.min.css +7 -0
  203. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/railscasts.min.css +7 -0
  204. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/rebecca.min.css +7 -0
  205. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ros-pine-dawn.min.css +7 -0
  206. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ros-pine-moon.min.css +7 -0
  207. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/ros-pine.min.css +7 -0
  208. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/sagelight.min.css +7 -0
  209. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/sandcastle.min.css +7 -0
  210. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/seti-ui.min.css +7 -0
  211. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/shapeshifter.min.css +7 -0
  212. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/silk-dark.min.css +7 -0
  213. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/silk-light.min.css +7 -0
  214. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/snazzy.min.css +7 -0
  215. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/solar-flare-light.min.css +7 -0
  216. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/solar-flare.min.css +7 -0
  217. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/solarized-dark.min.css +7 -0
  218. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/solarized-light.min.css +7 -0
  219. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/spacemacs.min.css +7 -0
  220. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/summercamp.min.css +7 -0
  221. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/summerfruit-dark.min.css +7 -0
  222. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/summerfruit-light.min.css +7 -0
  223. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/synth-midnight-terminal-dark.min.css +7 -0
  224. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/synth-midnight-terminal-light.min.css +7 -0
  225. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/tango.min.css +7 -0
  226. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/tender.min.css +7 -0
  227. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/tomorrow-night.min.css +7 -0
  228. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/tomorrow.min.css +7 -0
  229. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/twilight.min.css +7 -0
  230. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/unikitty-dark.min.css +7 -0
  231. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/unikitty-light.min.css +7 -0
  232. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/vulcan.min.css +7 -0
  233. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-10-light.min.css +7 -0
  234. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-10.min.css +7 -0
  235. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-95-light.min.css +7 -0
  236. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-95.min.css +7 -0
  237. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-high-contrast-light.min.css +7 -0
  238. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-high-contrast.min.css +7 -0
  239. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-nt-light.min.css +7 -0
  240. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/windows-nt.min.css +7 -0
  241. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/woodland.min.css +7 -0
  242. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/xcode-dusk.min.css +7 -0
  243. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/base16/zenburn.min.css +7 -0
  244. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/brown-paper.min.css +1 -0
  245. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/brown-papersq.png +0 -0
  246. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/codepen-embed.min.css +1 -0
  247. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/color-brewer.min.css +1 -0
  248. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/dark.min.css +1 -0
  249. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/default.min.css +9 -0
  250. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/devibeans.min.css +7 -0
  251. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/docco.min.css +1 -0
  252. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/far.min.css +1 -0
  253. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/felipec.min.css +7 -0
  254. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/foundation.min.css +1 -0
  255. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/github-dark-dimmed.min.css +9 -0
  256. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/github-dark.min.css +10 -0
  257. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/github.min.css +10 -0
  258. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/gml.min.css +1 -0
  259. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/googlecode.min.css +1 -0
  260. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/gradient-dark.min.css +1 -0
  261. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/gradient-light.min.css +1 -0
  262. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/grayscale.min.css +1 -0
  263. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/hybrid.min.css +1 -0
  264. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/idea.min.css +1 -0
  265. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/intellij-light.min.css +1 -0
  266. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/ir-black.min.css +1 -0
  267. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/isbl-editor-dark.min.css +1 -0
  268. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/isbl-editor-light.min.css +1 -0
  269. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/kimbie-dark.min.css +1 -0
  270. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/kimbie-light.min.css +1 -0
  271. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/lightfair.min.css +1 -0
  272. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/lioshi.min.css +1 -0
  273. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/magula.min.css +1 -0
  274. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/mono-blue.min.css +1 -0
  275. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/monokai-sublime.min.css +1 -0
  276. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/monokai.min.css +1 -0
  277. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/night-owl.min.css +1 -0
  278. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/nnfx-dark.min.css +10 -0
  279. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/nnfx-light.min.css +10 -0
  280. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/nord.min.css +1 -0
  281. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/obsidian.min.css +1 -0
  282. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/panda-syntax-dark.min.css +1 -0
  283. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/panda-syntax-light.min.css +1 -0
  284. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/paraiso-dark.min.css +1 -0
  285. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/paraiso-light.min.css +1 -0
  286. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/pojoaque.jpg +0 -0
  287. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/pojoaque.min.css +1 -0
  288. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/purebasic.min.css +1 -0
  289. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/qtcreator-dark.min.css +1 -0
  290. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/qtcreator-light.min.css +1 -0
  291. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/rainbow.min.css +1 -0
  292. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/routeros.min.css +1 -0
  293. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/school-book.min.css +1 -0
  294. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/shades-of-purple.min.css +1 -0
  295. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/srcery.min.css +1 -0
  296. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/stackoverflow-dark.min.css +13 -0
  297. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/stackoverflow-light.min.css +13 -0
  298. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/sunburst.min.css +1 -0
  299. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/tokyo-night-dark.min.css +8 -0
  300. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/tokyo-night-light.min.css +8 -0
  301. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/tomorrow-night-blue.min.css +1 -0
  302. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/tomorrow-night-bright.min.css +1 -0
  303. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/vs.min.css +1 -0
  304. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/vs2015.min.css +1 -0
  305. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/xcode.min.css +1 -0
  306. package/dist/public/vendor/vditor/dist/js/highlight.js/styles/xt256.min.css +1 -0
  307. package/dist/public/vendor/vditor/dist/js/highlight.js/third-languages.js +135 -0
  308. package/dist/public/vendor/vditor/dist/js/i18n/de_DE.js +86 -0
  309. package/dist/public/vendor/vditor/dist/js/i18n/en_US.js +86 -0
  310. package/dist/public/vendor/vditor/dist/js/i18n/es_ES.js +86 -0
  311. package/dist/public/vendor/vditor/dist/js/i18n/fr_FR.js +86 -0
  312. package/dist/public/vendor/vditor/dist/js/i18n/ja_JP.js +86 -0
  313. package/dist/public/vendor/vditor/dist/js/i18n/ko_KR.js +86 -0
  314. package/dist/public/vendor/vditor/dist/js/i18n/pt_BR.js +86 -0
  315. package/dist/public/vendor/vditor/dist/js/i18n/ru_RU.js +86 -0
  316. package/dist/public/vendor/vditor/dist/js/i18n/sv_SE.js +86 -0
  317. package/dist/public/vendor/vditor/dist/js/i18n/vi_VN.js +86 -0
  318. package/dist/public/vendor/vditor/dist/js/i18n/zh_CN.js +86 -0
  319. package/dist/public/vendor/vditor/dist/js/i18n/zh_TW.js +86 -0
  320. package/dist/public/vendor/vditor/dist/js/icons/ant.js +174 -0
  321. package/dist/public/vendor/vditor/dist/js/icons/material.js +195 -0
  322. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
  323. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
  324. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  325. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  326. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  327. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  328. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  329. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  330. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  331. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  332. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  333. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  334. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  335. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  336. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  337. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
  338. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Bold.woff +0 -0
  339. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  340. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  341. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  342. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  343. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
  344. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Italic.woff +0 -0
  345. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  346. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
  347. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Regular.woff +0 -0
  348. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  349. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  350. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  351. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  352. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
  353. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Math-Italic.woff +0 -0
  354. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  355. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  356. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  357. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  358. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  359. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  360. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  361. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  362. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  363. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  364. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
  365. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Script-Regular.woff +0 -0
  366. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  367. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
  368. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
  369. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  370. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
  371. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
  372. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  373. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
  374. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
  375. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  376. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
  377. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
  378. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  379. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  380. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  381. package/dist/public/vendor/vditor/dist/js/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  382. package/dist/public/vendor/vditor/dist/js/katex/katex.min.css +1 -0
  383. package/dist/public/vendor/vditor/dist/js/katex/katex.min.js +1 -0
  384. package/dist/public/vendor/vditor/dist/js/katex/mhchem.min.js +1 -0
  385. package/dist/public/vendor/vditor/dist/js/lute/lute.min.js +72 -0
  386. package/dist/public/vendor/vditor/dist/js/markmap/katex.min.css +1 -0
  387. package/dist/public/vendor/vditor/dist/js/markmap/markmap.min.js +8251 -0
  388. package/dist/public/vendor/vditor/dist/js/markmap/prism.css +140 -0
  389. package/dist/public/vendor/vditor/dist/js/mathjax/LICENSE +202 -0
  390. package/dist/public/vendor/vditor/dist/js/mathjax/a11y/assistive-mml.js +1 -0
  391. package/dist/public/vendor/vditor/dist/js/mathjax/a11y/complexity.js +1 -0
  392. package/dist/public/vendor/vditor/dist/js/mathjax/a11y/explorer.js +1 -0
  393. package/dist/public/vendor/vditor/dist/js/mathjax/a11y/semantic-enrich.js +1 -0
  394. package/dist/public/vendor/vditor/dist/js/mathjax/input/asciimath.js +1 -0
  395. package/dist/public/vendor/vditor/dist/js/mathjax/input/mml/entities.js +1 -0
  396. package/dist/public/vendor/vditor/dist/js/mathjax/input/mml.js +1 -0
  397. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/action.js +1 -0
  398. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/all-packages.js +1 -0
  399. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/ams.js +1 -0
  400. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/amscd.js +1 -0
  401. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/autoload.js +1 -0
  402. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/bbox.js +1 -0
  403. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/boldsymbol.js +1 -0
  404. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/braket.js +1 -0
  405. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/bussproofs.js +1 -0
  406. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/cancel.js +1 -0
  407. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/color.js +1 -0
  408. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/colorV2.js +1 -0
  409. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/configMacros.js +1 -0
  410. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/enclose.js +1 -0
  411. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/extpfeil.js +1 -0
  412. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/html.js +1 -0
  413. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/mhchem.js +1 -0
  414. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/newcommand.js +1 -0
  415. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/noerrors.js +1 -0
  416. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/noundefined.js +1 -0
  417. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/physics.js +1 -0
  418. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/require.js +1 -0
  419. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/tagFormat.js +1 -0
  420. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/textmacros.js +1 -0
  421. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/unicode.js +1 -0
  422. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex/extensions/verb.js +1 -0
  423. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex-base.js +1 -0
  424. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex-full.js +1 -0
  425. package/dist/public/vendor/vditor/dist/js/mathjax/input/tex.js +1 -0
  426. package/dist/public/vendor/vditor/dist/js/mathjax/sre/mathmaps/de.js +104 -0
  427. package/dist/public/vendor/vditor/dist/js/mathjax/sre/mathmaps/en.js +110 -0
  428. package/dist/public/vendor/vditor/dist/js/mathjax/sre/mathmaps/es.js +104 -0
  429. package/dist/public/vendor/vditor/dist/js/mathjax/sre/mathmaps/fr.js +104 -0
  430. package/dist/public/vendor/vditor/dist/js/mathjax/sre/mathmaps/mathmaps_ie.js +518 -0
  431. package/dist/public/vendor/vditor/dist/js/mathjax/sre/mathmaps/nemeth.js +104 -0
  432. package/dist/public/vendor/vditor/dist/js/mathjax/sre/sre-node.js +11 -0
  433. package/dist/public/vendor/vditor/dist/js/mathjax/sre/sre_browser.js +1633 -0
  434. package/dist/public/vendor/vditor/dist/js/mathjax/tex-svg-full.js +1 -0
  435. package/dist/public/vendor/vditor/dist/js/mermaid/mermaid.min.js +2607 -0
  436. package/dist/public/vendor/vditor/dist/js/plantuml/plantuml-encoder.min.js +1 -0
  437. package/dist/public/vendor/vditor/dist/js/smiles-drawer/smiles-drawer.min.js +71 -0
  438. package/dist/public/vendor/vditor/dist/method.d.ts +51 -0
  439. package/dist/public/vendor/vditor/dist/method.js +2673 -0
  440. package/dist/public/vendor/vditor/dist/method.min.js +1 -0
  441. package/dist/public/vendor/vditor/dist/ts/constants.d.ts +45 -0
  442. package/dist/public/vendor/vditor/dist/ts/devtools/index.d.ts +7 -0
  443. package/dist/public/vendor/vditor/dist/ts/export/index.d.ts +4 -0
  444. package/dist/public/vendor/vditor/dist/ts/hint/index.d.ts +14 -0
  445. package/dist/public/vendor/vditor/dist/ts/ir/expandMarker.d.ts +1 -0
  446. package/dist/public/vendor/vditor/dist/ts/ir/highlightToolbarIR.d.ts +1 -0
  447. package/dist/public/vendor/vditor/dist/ts/ir/index.d.ts +13 -0
  448. package/dist/public/vendor/vditor/dist/ts/ir/input.d.ts +1 -0
  449. package/dist/public/vendor/vditor/dist/ts/ir/process.d.ts +8 -0
  450. package/dist/public/vendor/vditor/dist/ts/ir/processKeydown.d.ts +1 -0
  451. package/dist/public/vendor/vditor/dist/ts/markdown/SMILESRender.d.ts +1 -0
  452. package/dist/public/vendor/vditor/dist/ts/markdown/abcRender.d.ts +1 -0
  453. package/dist/public/vendor/vditor/dist/ts/markdown/adapterRender.d.ts +41 -0
  454. package/dist/public/vendor/vditor/dist/ts/markdown/anchorRender.d.ts +1 -0
  455. package/dist/public/vendor/vditor/dist/ts/markdown/chartRender.d.ts +1 -0
  456. package/dist/public/vendor/vditor/dist/ts/markdown/codeRender.d.ts +1 -0
  457. package/dist/public/vendor/vditor/dist/ts/markdown/flowchartRender.d.ts +1 -0
  458. package/dist/public/vendor/vditor/dist/ts/markdown/getHTML.d.ts +1 -0
  459. package/dist/public/vendor/vditor/dist/ts/markdown/getMarkdown.d.ts +1 -0
  460. package/dist/public/vendor/vditor/dist/ts/markdown/graphvizRender.d.ts +1 -0
  461. package/dist/public/vendor/vditor/dist/ts/markdown/highlightRender.d.ts +1 -0
  462. package/dist/public/vendor/vditor/dist/ts/markdown/lazyLoadImageRender.d.ts +6 -0
  463. package/dist/public/vendor/vditor/dist/ts/markdown/markmapRender.d.ts +1 -0
  464. package/dist/public/vendor/vditor/dist/ts/markdown/mathRender.d.ts +9 -0
  465. package/dist/public/vendor/vditor/dist/ts/markdown/mediaRender.d.ts +1 -0
  466. package/dist/public/vendor/vditor/dist/ts/markdown/mermaidRender.d.ts +1 -0
  467. package/dist/public/vendor/vditor/dist/ts/markdown/mindmapRender.d.ts +1 -0
  468. package/dist/public/vendor/vditor/dist/ts/markdown/outlineRender.d.ts +1 -0
  469. package/dist/public/vendor/vditor/dist/ts/markdown/plantumlRender.d.ts +1 -0
  470. package/dist/public/vendor/vditor/dist/ts/markdown/previewRender.d.ts +2 -0
  471. package/dist/public/vendor/vditor/dist/ts/markdown/setLute.d.ts +1 -0
  472. package/dist/public/vendor/vditor/dist/ts/markdown/speechRender.d.ts +6 -0
  473. package/dist/public/vendor/vditor/dist/ts/outline/index.d.ts +7 -0
  474. package/dist/public/vendor/vditor/dist/ts/preview/image.d.ts +1 -0
  475. package/dist/public/vendor/vditor/dist/ts/preview/index.d.ts +10 -0
  476. package/dist/public/vendor/vditor/dist/ts/resize/index.d.ts +6 -0
  477. package/dist/public/vendor/vditor/dist/ts/sv/combineFootnote.d.ts +6 -0
  478. package/dist/public/vendor/vditor/dist/ts/sv/index.d.ts +13 -0
  479. package/dist/public/vendor/vditor/dist/ts/sv/inputEvent.d.ts +1 -0
  480. package/dist/public/vendor/vditor/dist/ts/sv/process.d.ts +11 -0
  481. package/dist/public/vendor/vditor/dist/ts/sv/processKeydown.d.ts +1 -0
  482. package/dist/public/vendor/vditor/dist/ts/tip/index.d.ts +6 -0
  483. package/dist/public/vendor/vditor/dist/ts/toolbar/Both.d.ts +5 -0
  484. package/dist/public/vendor/vditor/dist/ts/toolbar/Br.d.ts +4 -0
  485. package/dist/public/vendor/vditor/dist/ts/toolbar/CodeTheme.d.ts +6 -0
  486. package/dist/public/vendor/vditor/dist/ts/toolbar/ContentTheme.d.ts +6 -0
  487. package/dist/public/vendor/vditor/dist/ts/toolbar/Counter.d.ts +6 -0
  488. package/dist/public/vendor/vditor/dist/ts/toolbar/Custom.d.ts +5 -0
  489. package/dist/public/vendor/vditor/dist/ts/toolbar/Devtools.d.ts +5 -0
  490. package/dist/public/vendor/vditor/dist/ts/toolbar/Divider.d.ts +4 -0
  491. package/dist/public/vendor/vditor/dist/ts/toolbar/EditMode.d.ts +8 -0
  492. package/dist/public/vendor/vditor/dist/ts/toolbar/Emoji.d.ts +7 -0
  493. package/dist/public/vendor/vditor/dist/ts/toolbar/Export.d.ts +6 -0
  494. package/dist/public/vendor/vditor/dist/ts/toolbar/Fullscreen.d.ts +6 -0
  495. package/dist/public/vendor/vditor/dist/ts/toolbar/Headings.d.ts +7 -0
  496. package/dist/public/vendor/vditor/dist/ts/toolbar/Help.d.ts +5 -0
  497. package/dist/public/vendor/vditor/dist/ts/toolbar/Indent.d.ts +5 -0
  498. package/dist/public/vendor/vditor/dist/ts/toolbar/Info.d.ts +5 -0
  499. package/dist/public/vendor/vditor/dist/ts/toolbar/InsertAfter.d.ts +5 -0
  500. package/dist/public/vendor/vditor/dist/ts/toolbar/InsertBefore.d.ts +5 -0
  501. package/dist/public/vendor/vditor/dist/ts/toolbar/MenuItem.d.ts +5 -0
  502. package/dist/public/vendor/vditor/dist/ts/toolbar/Outdent.d.ts +5 -0
  503. package/dist/public/vendor/vditor/dist/ts/toolbar/Outline.d.ts +5 -0
  504. package/dist/public/vendor/vditor/dist/ts/toolbar/Preview.d.ts +6 -0
  505. package/dist/public/vendor/vditor/dist/ts/toolbar/Record.d.ts +6 -0
  506. package/dist/public/vendor/vditor/dist/ts/toolbar/Redo.d.ts +5 -0
  507. package/dist/public/vendor/vditor/dist/ts/toolbar/Undo.d.ts +5 -0
  508. package/dist/public/vendor/vditor/dist/ts/toolbar/Upload.d.ts +6 -0
  509. package/dist/public/vendor/vditor/dist/ts/toolbar/index.d.ts +10 -0
  510. package/dist/public/vendor/vditor/dist/ts/toolbar/setToolbar.d.ts +20 -0
  511. package/dist/public/vendor/vditor/dist/ts/ui/initUI.d.ts +4 -0
  512. package/dist/public/vendor/vditor/dist/ts/ui/setCodeTheme.d.ts +1 -0
  513. package/dist/public/vendor/vditor/dist/ts/ui/setContentTheme.d.ts +1 -0
  514. package/dist/public/vendor/vditor/dist/ts/ui/setPreviewMode.d.ts +1 -0
  515. package/dist/public/vendor/vditor/dist/ts/ui/setTheme.d.ts +1 -0
  516. package/dist/public/vendor/vditor/dist/ts/undo/index.d.ts +19 -0
  517. package/dist/public/vendor/vditor/dist/ts/upload/getElement.d.ts +1 -0
  518. package/dist/public/vendor/vditor/dist/ts/upload/index.d.ts +8 -0
  519. package/dist/public/vendor/vditor/dist/ts/upload/setHeaders.d.ts +1 -0
  520. package/dist/public/vendor/vditor/dist/ts/util/Options.d.ts +8 -0
  521. package/dist/public/vendor/vditor/dist/ts/util/RecordMedia.d.ts +18 -0
  522. package/dist/public/vendor/vditor/dist/ts/util/addScript.d.ts +2 -0
  523. package/dist/public/vendor/vditor/dist/ts/util/addStyle.d.ts +1 -0
  524. package/dist/public/vendor/vditor/dist/ts/util/code160to32.d.ts +1 -0
  525. package/dist/public/vendor/vditor/dist/ts/util/compatibility.d.ts +7 -0
  526. package/dist/public/vendor/vditor/dist/ts/util/editorCommonEvent.d.ts +9 -0
  527. package/dist/public/vendor/vditor/dist/ts/util/fixBrowserBehavior.d.ts +39 -0
  528. package/dist/public/vendor/vditor/dist/ts/util/function.d.ts +3 -0
  529. package/dist/public/vendor/vditor/dist/ts/util/getSelectText.d.ts +1 -0
  530. package/dist/public/vendor/vditor/dist/ts/util/hasClosest.d.ts +9 -0
  531. package/dist/public/vendor/vditor/dist/ts/util/hasClosestByHeadings.d.ts +2 -0
  532. package/dist/public/vendor/vditor/dist/ts/util/highlightToolbar.d.ts +1 -0
  533. package/dist/public/vendor/vditor/dist/ts/util/hotKey.d.ts +1 -0
  534. package/dist/public/vendor/vditor/dist/ts/util/log.d.ts +1 -0
  535. package/dist/public/vendor/vditor/dist/ts/util/merge.d.ts +1 -0
  536. package/dist/public/vendor/vditor/dist/ts/util/processCode.d.ts +2 -0
  537. package/dist/public/vendor/vditor/dist/ts/util/selection.d.ts +14 -0
  538. package/dist/public/vendor/vditor/dist/ts/util/toc.d.ts +5 -0
  539. package/dist/public/vendor/vditor/dist/ts/wysiwyg/afterRenderEvent.d.ts +5 -0
  540. package/dist/public/vendor/vditor/dist/ts/wysiwyg/highlightToolbarWYSIWYG.d.ts +4 -0
  541. package/dist/public/vendor/vditor/dist/ts/wysiwyg/index.d.ts +22 -0
  542. package/dist/public/vendor/vditor/dist/ts/wysiwyg/inlineTag.d.ts +10 -0
  543. package/dist/public/vendor/vditor/dist/ts/wysiwyg/input.d.ts +1 -0
  544. package/dist/public/vendor/vditor/dist/ts/wysiwyg/processKeydown.d.ts +2 -0
  545. package/dist/public/vendor/vditor/dist/ts/wysiwyg/renderDomByMd.d.ts +5 -0
  546. package/dist/public/vendor/vditor/dist/ts/wysiwyg/setHeading.d.ts +2 -0
  547. package/dist/public/vendor/vditor/dist/ts/wysiwyg/showCode.d.ts +1 -0
  548. package/dist/public/vendor/vditor/dist/ts/wysiwyg/toolbarEvent.d.ts +1 -0
  549. package/dist/public/vendor/vditor/dist/types/index.d.ts +921 -0
  550. package/dist/public/work-permissions.d.ts +5 -2
  551. package/dist/public/work-permissions.js +40 -13
  552. package/dist/security.js +11 -0
  553. package/dist/security.js.map +1 -1
  554. package/dist/server-runtime.js +5 -2
  555. package/dist/server-runtime.js.map +1 -1
  556. package/dist/store.js +192 -34
  557. package/dist/store.js.map +1 -1
  558. package/dist/user-auth.js +36 -7
  559. package/dist/user-auth.js.map +1 -1
  560. package/dist/version.js +1 -1
  561. package/dist/version.js.map +1 -1
  562. package/dist/work-permissions.js +29 -6
  563. package/dist/work-permissions.js.map +1 -1
  564. package/package.json +2 -1
@@ -1,10 +1,10 @@
1
- import { buildRelationshipGraph, createGalaxyRenderer, renderRelationshipMindMap } from "/relationship-graph.js?v=20260721-release-0.3.6";
1
+ import { buildRelationshipGraph, createGalaxyRenderer, renderRelationshipMindMap } from "/relationship-graph.js?v=20260724-relationship-density";
2
2
  import { collapseExcessBlankLines, formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
3
3
  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,14 @@ 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";
24
+ import { isGlobalSearchShortcut } from "/keyboard-shortcuts.js?v=20260723-global-search";
23
25
 
24
26
  const state = {
25
27
  user: null,
@@ -51,6 +53,8 @@ const state = {
51
53
  contextChapterId: null
52
54
  };
53
55
 
56
+ let timelineMultiSelectEnabled = false;
57
+
54
58
  const chapterTypes = ["正文", "设定", "作者的话", "其他"];
55
59
 
56
60
  const taskTypeLabels = MODEL_PURPOSE_OPTIONS;
@@ -82,7 +86,7 @@ function analysisTaskStatusLabel(status) {
82
86
  }
83
87
 
84
88
  function canEditWork(work = state.work) {
85
- return WORK_PERMISSION_MODULES.some((item) => canWriteUiModule(work, item.uiModule));
89
+ return WORK_PERMISSION_MODULES.some((item) => canWritePermissionModule(work, item.id));
86
90
  }
87
91
 
88
92
  function canEditProse(work = state.work) {
@@ -92,7 +96,7 @@ function canEditProse(work = state.work) {
92
96
  function canReplaceProse(work = state.work) {
93
97
  return WORK_PERMISSION_MODULES
94
98
  .filter((item) => item.id !== "ai-settings")
95
- .every((item) => canWriteUiModule(work, item.uiModule));
99
+ .every((item) => canWritePermissionModule(work, item.id));
96
100
  }
97
101
 
98
102
  function canManageWork(work = state.work) {
@@ -116,8 +120,8 @@ function applyWorkAccessMode() {
116
120
  const viewOnly = Boolean(state.work) && !canEditWork();
117
121
  const proseReadOnly = Boolean(state.work) && !canEditProse();
118
122
  const proseHidden = Boolean(state.work) && !canReadModule("editor");
119
- const aiHidden = Boolean(state.work) && !canReadModule("tasks");
120
- const aiReadOnly = Boolean(state.work) && !canEditModule("tasks");
123
+ const aiHidden = Boolean(state.work) && !canReadPermissionModule(state.work, "ai-chat");
124
+ const aiReadOnly = Boolean(state.work) && !canWritePermissionModule(state.work, "ai-chat");
121
125
  const moduleReadOnly = Boolean(state.work) && !canEditModule(state.module);
122
126
  $("#app").classList.toggle("view-only-mode", viewOnly);
123
127
  $("#app").classList.toggle("prose-read-only-mode", proseReadOnly);
@@ -125,14 +129,16 @@ function applyWorkAccessMode() {
125
129
  $("#app").classList.toggle("ai-hidden-mode", aiHidden);
126
130
  document.body.classList.toggle("work-viewer-mode", moduleReadOnly);
127
131
  for (const item of WORK_PERMISSION_MODULES) {
132
+ if (!item.uiModule) continue;
128
133
  const button = $(`#module-nav [data-module="${item.uiModule}"]`);
129
134
  if (button) button.classList.toggle("permission-hidden", !canReadModule(item.uiModule));
130
135
  }
131
136
  $("#module-nav [data-work-settings]").classList.toggle("permission-hidden", Boolean(state.work) && !canManageWork());
132
- $("#import-file-button").classList.toggle("permission-hidden", proseReadOnly);
133
- $("#import-file-button").setAttribute("aria-hidden", String(proseReadOnly));
137
+ $("#new-volume-button").classList.toggle("permission-hidden", Boolean(state.work) && proseReadOnly);
138
+ $("#welcome-new-work").classList.toggle("permission-hidden", Boolean(state.work) && proseReadOnly);
139
+ $("#import-file-button").setAttribute("aria-disabled", String(proseReadOnly));
140
+ $("#import-file-button").setAttribute("title", proseReadOnly ? "当前权限不能导入正文" : "导入 TXT / DOCX");
134
141
  $("#import-file").disabled = proseReadOnly;
135
- $("#import-history-button").classList.toggle("permission-hidden", Boolean(state.work) && !canReplaceProse());
136
142
  $(".ai-panel").classList.toggle("permission-hidden", aiHidden);
137
143
  $("#chapter-title").readOnly = proseReadOnly;
138
144
  $("#chapter-content").readOnly = proseReadOnly;
@@ -226,7 +232,7 @@ const shelfOnboardingSteps = [
226
232
  const workspaceOnboardingSteps = [
227
233
  { selector: "#home-button", eyebrow: "作品入口", title: "随时返回书架", description: "点击叙界标志返回书架,切换作品或创建新的故事世界。", placement: "bottom" },
228
234
  { selector: ".file-button", eyebrow: "稿件导入", title: "导入 TXT 或 DOCX", description: "已有稿件可以直接导入,系统会解析分卷与章节结构。", placement: "right" },
229
- { selector: "#new-chapter-button", eyebrow: "正文结构", title: "新建章节", description: "使用分卷和章节组织长篇正文,章节会自动保存并保留版本。", placement: "right" },
235
+ { selector: "[data-new-chapter-volume]", eyebrow: "正文结构", title: "新建章节", description: "使用分卷和章节组织长篇正文,章节会自动保存并保留版本。", placement: "right" },
230
236
  { selector: "#versions-button", eyebrow: "版本安全", title: "查看章节版本", description: "每次保存都会生成可恢复版本,误改内容时可以随时回溯。", placement: "bottom" },
231
237
  { selector: "[data-module=\"characters\"]", eyebrow: "作品知识", title: "维护角色与世界资料", description: "角色、种族、组织、设定和时间线共同构成 AI 可引用的作品知识。", placement: "right" },
232
238
  { selector: "[data-module=\"outlines\"]", eyebrow: "创作规划", title: "跟踪大纲与伏笔", description: "记录剧情目标、冲突、转折和伏笔回收,避免长线遗漏。", placement: "right" },
@@ -408,7 +414,7 @@ function replacePageRoute(route) {
408
414
  function currentPageRoute() {
409
415
  const workId = state.work?.id ?? null;
410
416
  if (!$("#entity-editor-view").classList.contains("hidden") && workId && entityEditorType) {
411
- const entityId = entityEditorType === "setting" ? settingEditorItem?.id : characterEditorItem?.id;
417
+ const entityId = entityEditorType === "setting" ? settingEditorItem?.id : entityEditorType === "character" ? characterEditorItem?.id : knowledgeEditorItem?.id;
412
418
  return { view: "entity-editor", workId, entity: entityEditorType, entityId: entityId ?? null };
413
419
  }
414
420
  if (!$("#settings-hub-view").classList.contains("hidden")) return { view: "settings", workId, ...settingsRouteContext() };
@@ -524,50 +530,76 @@ let entityEditorType = null;
524
530
  let entityEditorDirty = false;
525
531
  let settingEditorItem = null;
526
532
  let characterEditorItem = null;
533
+ let knowledgeEditorItem = null;
534
+ let knowledgeEditorKind = null;
535
+ let knowledgeEditorSections = [];
536
+ let knowledgeSectionEditorIndex = null;
537
+ let knowledgeSectionEditorDirty = false;
527
538
  let characterEditorVersions = [];
528
539
  let characterEditorRelationships = [];
529
540
  let characterEditorRelationshipsLoading = false;
530
541
  let characterEditorSections = [];
531
- let characterSectionPreviewTimer = null;
532
542
  let characterSectionPendingAttachments = [];
543
+ let markdownEditorPendingAttachments = [];
533
544
  let characterSectionEditorDirty = false;
545
+ let settingEditorVditor = null;
546
+ let knowledgeSectionVditor = null;
547
+ let characterSectionVditor = null;
534
548
  let entityHistoryContext = null;
535
549
 
536
550
  function showEntityEditorPage(type) {
537
551
  entityEditorType = type;
538
552
  entityEditorDirty = false;
539
553
  characterSectionEditorDirty = false;
554
+ knowledgeSectionEditorDirty = false;
540
555
  $("#entity-editor-view").classList.remove("hidden");
541
556
  $("#setting-editor-form").classList.toggle("hidden", type !== "setting");
542
557
  $("#character-editor-form").classList.toggle("hidden", type !== "character");
558
+ $("#knowledge-editor-form").classList.toggle("hidden", !["race", "organization"].includes(type));
543
559
  $("#character-section-editor-view").classList.add("hidden");
560
+ $("#knowledge-section-editor-view").classList.add("hidden");
544
561
  $("#app").inert = true;
545
562
  document.body.classList.add("entity-editor-open");
546
563
  replacePageRoute(currentPageRoute());
547
564
  }
548
565
 
549
566
  function markEntityEditorDirty() {
550
- const module = entityEditorType === "setting" ? "settings" : "characters";
567
+ const module = entityEditorType === "setting" ? "settings" : entityEditorType === "character" ? "characters" : entityEditorType === "race" ? "races" : "organizations";
551
568
  if (entityEditorType && canEditModule(module)) entityEditorDirty = true;
552
569
  }
553
570
 
554
- function confirmEntityEditorDiscard(message) {
571
+ async function confirmEntityEditorDiscard(message) {
555
572
  if (!entityEditorDirty) return true;
556
- return window.confirm(message ?? "当前资料有未保存修改,返回列表将丢弃这些修改。是否继续?");
573
+ return confirmToast(message ?? "当前资料有未保存修改,返回列表将丢弃这些修改。是否继续?", {
574
+ title: "放弃未保存修改",
575
+ confirmLabel: "放弃并继续",
576
+ cancelLabel: "继续编辑"
577
+ });
557
578
  }
558
579
 
559
580
  async function closeEntityEditor({ force = false } = {}) {
560
581
  if (!$("#character-section-editor-view").classList.contains("hidden")) return closeCharacterSectionEditor({ force });
561
- if (!force && !confirmEntityEditorDiscard()) return false;
582
+ if (!$("#knowledge-section-editor-view").classList.contains("hidden")) return closeKnowledgeSectionEditor({ force });
583
+ if (!force && !(await confirmEntityEditorDiscard())) return false;
562
584
  await discardPendingCharacterAttachments();
563
- const module = entityEditorType === "setting" ? "settings" : "characters";
585
+ await discardPendingMarkdownAttachments();
586
+ destroyVditorEditor(settingEditorVditor);
587
+ settingEditorVditor = null;
588
+ const module = entityEditorType === "setting" ? "settings" : entityEditorType === "character" ? "characters" : entityEditorType === "race" ? "races" : "organizations";
564
589
  entityEditorType = null;
565
590
  entityEditorDirty = false;
566
591
  settingEditorItem = null;
567
592
  characterEditorItem = null;
593
+ knowledgeEditorItem = null;
594
+ knowledgeEditorKind = null;
595
+ knowledgeEditorSections = [];
596
+ knowledgeSectionEditorIndex = null;
597
+ knowledgeSectionEditorDirty = false;
568
598
  $("#entity-editor-view").classList.add("hidden");
569
599
  $("#setting-editor-form").classList.add("hidden");
570
600
  $("#character-editor-form").classList.add("hidden");
601
+ $("#knowledge-editor-form").classList.add("hidden");
602
+ $("#knowledge-section-editor-view").classList.add("hidden");
571
603
  $("#app").inert = false;
572
604
  document.body.classList.remove("entity-editor-open");
573
605
  await showModule(module);
@@ -594,14 +626,25 @@ function syncChapterLineNumberScroll() {
594
626
  }
595
627
  }
596
628
 
629
+ function syncChapterWhitespaceControls() {
630
+ document.querySelectorAll("[data-toggle-whitespace]").forEach((button) => {
631
+ button.setAttribute("aria-pressed", String(chapterWhitespaceVisible));
632
+ button.textContent = chapterWhitespaceVisible ? "隐藏空白符" : "显示空白符";
633
+ });
634
+ }
635
+
636
+ function toggleChapterWhitespaceVisibility() {
637
+ chapterWhitespaceVisible = !chapterWhitespaceVisible;
638
+ syncChapterWhitespaceControls();
639
+ scheduleChapterLineNumbers();
640
+ }
641
+
597
642
  function renderChapterWhitespaceMarkers(input, style) {
598
643
  const overlay = $("#chapter-whitespace-overlay");
599
644
  const inner = $("#chapter-whitespace-inner");
600
- const button = $("#toggle-whitespace-button");
601
- if (!overlay || !inner || !button) return;
645
+ syncChapterWhitespaceControls();
646
+ if (!overlay || !inner) return;
602
647
  overlay.classList.toggle("is-visible", chapterWhitespaceVisible);
603
- button.setAttribute("aria-pressed", String(chapterWhitespaceVisible));
604
- button.textContent = chapterWhitespaceVisible ? "隐藏空白符" : "显示空白符";
605
648
  if (!chapterWhitespaceVisible) {
606
649
  inner.replaceChildren();
607
650
  return;
@@ -1407,6 +1450,7 @@ function applyColorTheme(theme) {
1407
1450
  button.setAttribute("aria-pressed", String(normalized === "dark"));
1408
1451
  const themeColor = document.querySelector('meta[name="theme-color"]');
1409
1452
  if (themeColor) themeColor.content = normalized === "dark" ? "#171714" : "#8b3d2c";
1453
+ [settingEditorVditor, knowledgeSectionVditor, characterSectionVditor].forEach((editor) => editor?.setTheme(normalized === "dark" ? "dark" : "classic"));
1410
1454
  }
1411
1455
 
1412
1456
  function saveColorTheme(theme) {
@@ -1474,6 +1518,7 @@ function renderTypographyPreview() {
1474
1518
  function openAppearanceDialog() {
1475
1519
  fillAppearanceForm(typographySettings);
1476
1520
  renderTypographyPreview();
1521
+ syncChapterWhitespaceControls();
1477
1522
  $("#appearance-dialog").showModal();
1478
1523
  }
1479
1524
 
@@ -1701,6 +1746,47 @@ function toast(message, type = "info") {
1701
1746
  }, 3600);
1702
1747
  }
1703
1748
 
1749
+ function confirmToast(message, { title = "请再次确认", confirmLabel = "确认", cancelLabel = "取消" } = {}) {
1750
+ const region = $("#toast-region");
1751
+ const element = document.createElement("section");
1752
+ element.className = "toast toast-confirmation";
1753
+ element.setAttribute("role", "alertdialog");
1754
+ element.setAttribute("aria-label", title);
1755
+ const heading = document.createElement("strong");
1756
+ heading.textContent = title;
1757
+ const description = document.createElement("p");
1758
+ description.textContent = message;
1759
+ const actions = document.createElement("div");
1760
+ actions.className = "toast-confirmation-actions";
1761
+ const cancel = document.createElement("button");
1762
+ cancel.className = "ghost-button";
1763
+ cancel.type = "button";
1764
+ cancel.textContent = cancelLabel;
1765
+ const confirm = document.createElement("button");
1766
+ confirm.className = "primary-button";
1767
+ confirm.type = "button";
1768
+ confirm.textContent = confirmLabel;
1769
+ actions.append(cancel, confirm);
1770
+ element.append(heading, description, actions);
1771
+ region.append(element);
1772
+ raiseToastRegion();
1773
+ cancel.focus();
1774
+ return new Promise((resolve) => {
1775
+ const finish = (confirmed) => {
1776
+ element.remove();
1777
+ if (!region.childElementCount && typeof region.hidePopover === "function" && region.matches(":popover-open")) region.hidePopover();
1778
+ resolve(confirmed);
1779
+ };
1780
+ cancel.addEventListener("click", () => finish(false), { once: true });
1781
+ confirm.addEventListener("click", () => finish(true), { once: true });
1782
+ element.addEventListener("keydown", (event) => {
1783
+ if (event.key !== "Escape") return;
1784
+ event.preventDefault();
1785
+ finish(false);
1786
+ });
1787
+ });
1788
+ }
1789
+
1704
1790
  document.addEventListener("toggle", (event) => {
1705
1791
  const target = event.target;
1706
1792
  if (target instanceof HTMLDialogElement && target.open && $("#toast-region").childElementCount) {
@@ -1806,23 +1892,34 @@ async function persistChapter({ automatic = false } = {}) {
1806
1892
  }
1807
1893
  }
1808
1894
 
1809
- function confirmDiscardChanges(message = "当前章节有未保存修改,继续将丢弃这些修改。是否继续?") {
1895
+ async function confirmDiscardChanges(message = "当前章节有未保存修改,继续将丢弃这些修改。是否继续?") {
1810
1896
  if (!state.dirty) return true;
1811
- return window.confirm(message);
1897
+ return confirmToast(message, {
1898
+ title: "放弃未保存修改",
1899
+ confirmLabel: "放弃并继续",
1900
+ cancelLabel: "继续编辑"
1901
+ });
1812
1902
  }
1813
1903
 
1814
1904
  function chooseExistingWorkImportMode(file) {
1815
1905
  const dialog = $("#import-mode-dialog");
1906
+ const form = dialog.querySelector("form");
1907
+ const confirm = $("#import-mode-confirm");
1816
1908
  const canOverwrite = canReplaceProse();
1817
1909
  $("#import-mode-file-summary").textContent = `文件:${file.name};当前作品:《${state.work.title}》`;
1818
1910
  $("#import-mode-unsaved-warning").classList.toggle("hidden", !state.dirty);
1911
+ form.reset();
1819
1912
  $("#import-mode-overwrite").disabled = !canOverwrite;
1820
1913
  $("#import-mode-overwrite-permission").classList.toggle("hidden", canOverwrite);
1914
+ confirm.disabled = true;
1915
+ form.onchange = () => { confirm.disabled = !form.querySelector('input[name="importMode"]:checked'); };
1821
1916
  dialog.returnValue = "cancel";
1822
1917
  dialog.showModal();
1918
+ $("#import-mode-dialog-title").focus();
1823
1919
  return new Promise((resolve) => {
1824
1920
  dialog.addEventListener("close", () => {
1825
- resolve(["append", "overwrite"].includes(dialog.returnValue) ? dialog.returnValue : null);
1921
+ const mode = form.querySelector('input[name="importMode"]:checked')?.value;
1922
+ resolve(dialog.returnValue === "confirm" && ["append", "overwrite"].includes(mode) ? mode : null);
1826
1923
  }, { once: true });
1827
1924
  });
1828
1925
  }
@@ -1874,7 +1971,7 @@ async function initializePage() {
1874
1971
  state.module = route.view === "module"
1875
1972
  ? route.module
1876
1973
  : route.view === "entity-editor"
1877
- ? (route.entity === "setting" ? "settings" : "characters")
1974
+ ? ({ setting: "settings", character: "characters", race: "races", organization: "organizations" }[route.entity] ?? "characters")
1878
1975
  : "editor";
1879
1976
  await selectWork(requestedWork.id, route.view === "editor" ? route.chapterId : null);
1880
1977
  }
@@ -1885,14 +1982,16 @@ async function initializePage() {
1885
1982
  }
1886
1983
  if (route.view === "module") return;
1887
1984
  if (route.view === "entity-editor") {
1888
- const records = route.entity === "setting" ? state.settings : state.characters;
1985
+ const records = route.entity === "setting" ? state.settings : route.entity === "character" ? state.characters : route.entity === "race" ? state.races : state.organizations;
1889
1986
  const item = route.entityId ? records.find((record) => record.id === route.entityId) : null;
1890
1987
  if (route.entityId && !item) {
1891
- toast(route.entity === "setting" ? "未找到要编辑的设定" : "未找到要编辑的角色", "error");
1988
+ toast(({ setting: "未找到要编辑的设定", character: "未找到要编辑的角色", race: "未找到要编辑的种族", organization: "未找到要编辑的组织" }[route.entity] ?? "未找到要编辑的档案"), "error");
1892
1989
  return;
1893
1990
  }
1894
1991
  if (route.entity === "setting") openSettingEditor(item);
1895
- else await openCharacterEditor(item);
1992
+ else if (route.entity === "character") await openCharacterEditor(item);
1993
+ else if (route.entity === "race") await openRaceDialog(item);
1994
+ else if (route.entity === "organization") await openOrganizationDialog(item);
1896
1995
  return;
1897
1996
  }
1898
1997
  if (route.view === "welcome") {
@@ -1900,7 +1999,7 @@ async function initializePage() {
1900
1999
  return;
1901
2000
  }
1902
2001
  if (route.view === "settings") {
1903
- showSettingsHub();
2002
+ await showSettingsHub();
1904
2003
  settingsReturnContext = restoredSettingsReturnContext(route);
1905
2004
  renderSettingsHub();
1906
2005
  return;
@@ -2125,6 +2224,11 @@ async function openSearchDialog() {
2125
2224
  toast("请先打开一部作品", "error");
2126
2225
  return;
2127
2226
  }
2227
+ if ($("#search-dialog").open) {
2228
+ $("#search-query").focus();
2229
+ $("#search-query").select();
2230
+ return;
2231
+ }
2128
2232
  $("#search-dialog .eyebrow").textContent = `当前作品 · 《${state.work.title}》`;
2129
2233
  $("#search-query").value = "";
2130
2234
  $("#search-results").innerHTML = '<p class="search-results-empty">输入关键词后开始检索。</p>';
@@ -2195,10 +2299,10 @@ async function openSearchResult(result) {
2195
2299
  }
2196
2300
  }
2197
2301
 
2198
- function showSettingsHub() {
2302
+ async function showSettingsHub() {
2199
2303
  const alreadyInSettings = !$("#settings-hub-view").classList.contains("hidden") || !$("#platform-ai-view").classList.contains("hidden");
2200
2304
  if (!alreadyInSettings) {
2201
- if (state.dirty && !confirmDiscardChanges("当前章节有未保存修改,进入设置将放弃本地修改。是否继续?")) return false;
2305
+ if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入设置将放弃本地修改。是否继续?"))) return false;
2202
2306
  settingsReturnContext = captureSettingsReturnContext();
2203
2307
  state.dirty = false;
2204
2308
  }
@@ -2228,14 +2332,14 @@ async function returnFromSettings() {
2228
2332
  $("#app").classList.remove("shelf-mode");
2229
2333
  $("#shelf-view").classList.add("hidden");
2230
2334
  updateDocumentTitle(state.work);
2231
- $("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${state.work.wordCount} 字`;
2335
+ $("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${Number(state.work.wordCount ?? 0).toLocaleString("zh-CN")} 字`;
2232
2336
  if (context.view === "module") return showModule(context.module);
2233
2337
  if (context.view === "editor" && context.chapterId) return selectChapter(context.chapterId);
2234
2338
  return showWelcome(true);
2235
2339
  }
2236
2340
 
2237
2341
  async function showPlatformAi() {
2238
- if (state.dirty && !confirmDiscardChanges("当前章节有未保存修改,进入平台 AI 管理将放弃本地修改。是否继续?")) return false;
2342
+ if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,进入平台 AI 管理将放弃本地修改。是否继续?"))) return false;
2239
2343
  state.dirty = false;
2240
2344
  updateDocumentTitle();
2241
2345
  $("#app").classList.add("shelf-mode");
@@ -2262,7 +2366,7 @@ function renderShelf() {
2262
2366
  <span class="book-cover-fallback">${esc(Array.from(work.title)[0] ?? "书")}</span>
2263
2367
  ${work.coverUrl ? `<img src="${esc(work.coverUrl)}" alt="${esc(work.title)} 封面">` : ""}
2264
2368
  </span>
2265
- <span class="book-info"><strong>${esc(work.title)}</strong><small>${esc(work.author || "未署名")} · ${work.chapterCount} 章 · ${work.wordCount} 字</small><span>${esc(work.description || "尚未填写作品简介")}</span><em class="book-access-badge">${work.accessRole === "viewer" ? "全部只读" : work.accessRole === "settings-editor" ? "设定协作" : work.accessRole === "editor" ? "全部可编辑" : work.accessRole === "custom" ? "自定义权限" : work.accessRole === "admin" ? "管理员访问" : "我的作品"}</em></span>
2369
+ <span class="book-info"><strong>${esc(work.title)}</strong><small>${esc(work.author || "未署名")} · ${work.chapterCount} 章 · ${Number(work.wordCount ?? 0).toLocaleString("zh-CN")} 字</small><span>${esc(work.description || "尚未填写作品简介")}</span><em class="book-access-badge">${work.accessRole === "viewer" ? "全部只读" : work.accessRole === "settings-editor" ? "设定协作" : work.accessRole === "editor" ? "全部可编辑" : work.accessRole === "custom" ? "自定义权限" : work.accessRole === "admin" ? "管理员访问" : "我的作品"}</em></span>
2266
2370
  </button>
2267
2371
  ${canManageWork(work) ? `<button class="book-card-settings" type="button" data-edit-work="${esc(work.id)}" aria-label="作品设置" title="作品设置">设置</button>` : ""}
2268
2372
  </article>`).join("")}
@@ -2311,7 +2415,7 @@ function resetWorkScopedUiCaches() {
2311
2415
 
2312
2416
  async function selectWork(workId, preferredChapterId = null) {
2313
2417
  const discarding = state.work?.id !== workId && state.dirty;
2314
- if (discarding && !confirmDiscardChanges()) return false;
2418
+ if (discarding && !(await confirmDiscardChanges())) return false;
2315
2419
  const nextWork = await api(`/api/works/${workId}?page=1&limit=100`);
2316
2420
  if (state.work?.id !== nextWork.id) resetWorkScopedUiCaches();
2317
2421
  if (discarding) setSaveState("就绪");
@@ -2326,7 +2430,7 @@ async function selectWork(workId, preferredChapterId = null) {
2326
2430
  if (!canReadModule(state.module)) state.module = firstReadableUiModule(state.work) ?? "editor";
2327
2431
  applyWorkAccessMode();
2328
2432
  updateDocumentTitle(state.work);
2329
- $("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${state.work.wordCount} 字`;
2433
+ $("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${Number(state.work.wordCount ?? 0).toLocaleString("zh-CN")} 字`;
2330
2434
  $("#top-search-button").disabled = !canReadAggregateContent();
2331
2435
  renderTree();
2332
2436
  const chapters = state.work.volumes.flatMap((volume) => volume.chapters);
@@ -2342,11 +2446,15 @@ async function selectWork(workId, preferredChapterId = null) {
2342
2446
  function renderTree() {
2343
2447
  if (!state.work) return;
2344
2448
  const count = state.work.volumes.reduce((total, volume) => total + volume.chapters.length, 0);
2449
+ const proseEditable = canEditProse();
2345
2450
  $("#chapter-count").textContent = `${count} 章`;
2346
2451
  $("#novel-tree").classList.remove("empty-copy");
2347
2452
  $("#novel-tree").innerHTML = state.work.volumes.map((volume) => `
2348
2453
  <div class="volume-node ${state.collapsedVolumeIds.has(volume.id) ? "is-collapsed" : ""}" data-volume-id="${esc(volume.id)}">
2349
- <button class="volume-title" type="button" data-volume-toggle="${esc(volume.id)}" aria-expanded="${state.collapsedVolumeIds.has(volume.id) ? "false" : "true"}" title="左键折叠,右键设置分卷"><span>${esc(volume.title)}</span><span>${volume.chapters.length} 章</span></button>
2454
+ <div class="volume-title">
2455
+ <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>
2456
+ ${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>` : ""}
2457
+ </div>
2350
2458
  <div class="volume-chapters">
2351
2459
  ${volume.chapters.map((chapter) => `
2352
2460
  <button class="chapter-node ${state.chapter?.id === chapter.id ? "active" : ""}" type="button" data-chapter-id="${esc(chapter.id)}">
@@ -2367,6 +2475,9 @@ function renderTree() {
2367
2475
  openVolumeDialog(state.work.volumes.find((volume) => volume.id === button.dataset.volumeToggle));
2368
2476
  });
2369
2477
  });
2478
+ $("#novel-tree").querySelectorAll("[data-new-chapter-volume]").forEach((button) => {
2479
+ button.addEventListener("click", () => openChapterDialog(button.dataset.newChapterVolume));
2480
+ });
2370
2481
  $("#novel-tree").querySelectorAll("[data-chapter-id]").forEach((button) => {
2371
2482
  button.addEventListener("click", () => selectChapter(button.dataset.chapterId));
2372
2483
  button.addEventListener("contextmenu", (event) => {
@@ -2399,7 +2510,7 @@ function openChapterTypeMenu(chapterId, clientX, clientY) {
2399
2510
  }
2400
2511
 
2401
2512
  async function selectChapter(chapterId) {
2402
- if (state.chapter?.id !== chapterId && !confirmDiscardChanges("当前章节有未保存修改,仍要切换吗?")) return;
2513
+ if (state.chapter?.id !== chapterId && !(await confirmDiscardChanges("当前章节有未保存修改,仍要切换吗?"))) return;
2403
2514
  cancelChapterAutoSave();
2404
2515
  state.chapter = await api(`/api/chapters/${chapterId}`);
2405
2516
  lastSavedChapterSnapshot = { chapterId: state.chapter.id, title: state.chapter.title, content: state.chapter.content };
@@ -2487,7 +2598,7 @@ async function showModule(module) {
2487
2598
  }
2488
2599
  module = fallback;
2489
2600
  }
2490
- if (module !== "editor" && state.module === "editor" && !confirmDiscardChanges()) return;
2601
+ if (module !== "editor" && state.module === "editor" && !(await confirmDiscardChanges())) return;
2491
2602
  if (module !== "editor" && state.module === "editor" && state.dirty) setSaveState("已放弃修改");
2492
2603
  state.module = module;
2493
2604
  applyWorkAccessMode();
@@ -2508,6 +2619,7 @@ async function showModule(module) {
2508
2619
  $("#module-eyebrow").textContent = meta[0];
2509
2620
  $("#module-title").textContent = meta[1];
2510
2621
  $("#module-description").textContent = meta[2];
2622
+ $("#module-header-actions").querySelectorAll("[data-module-header-action]").forEach((action) => action.remove());
2511
2623
  $("#module-create-button").textContent = meta[3];
2512
2624
  $("#module-create-button").classList.toggle("hidden", module === "ai-settings" || !canEditModule(module));
2513
2625
  $("#module-content").innerHTML = '<div class="empty-state">正在载入……</div>';
@@ -2598,28 +2710,82 @@ function bindEntityHistoryButtons(refresh) {
2598
2710
  }));
2599
2711
  }
2600
2712
 
2713
+ function pencilIconMarkup() {
2714
+ return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 20h9"></path><path d="m16.5 3.5 1.4-1.4a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4L16.5 3.5Z"></path></svg>';
2715
+ }
2716
+
2717
+ function recordCardEditButton(attribute, id, label) {
2718
+ return `<button class="record-card-edit" type="button" data-${attribute}="${esc(id)}" aria-label="编辑${esc(label)}" title="编辑">${pencilIconMarkup()}</button>`;
2719
+ }
2720
+
2721
+ function recordHistoryButton(type, id, title) {
2722
+ return `<button type="button" data-entity-history="${esc(type)}" data-entity-id="${esc(id)}" data-entity-title="${esc(title)}">版本历史</button>`;
2723
+ }
2724
+
2725
+ function entityDialogManagementHtml({ typeLabel, canMerge, canDelete }) {
2726
+ return `<section class="entity-dialog-management" aria-label="${esc(typeLabel)}档案操作">
2727
+ <div><strong>档案操作</strong><small>版本历史和高风险操作集中在编辑面板内。</small></div>
2728
+ <div class="entity-dialog-management-actions">
2729
+ <button class="ghost-button" type="button" data-dialog-entity-history>版本历史</button>
2730
+ ${canMerge ? `<button class="ghost-button" type="button" data-dialog-entity-merge>合并</button>` : ""}
2731
+ ${canDelete ? `<button class="danger-button" type="button" data-dialog-entity-delete>删除</button>` : ""}
2732
+ </div>
2733
+ </section>`;
2734
+ }
2735
+
2736
+ function bindManagedEntityDialogActions({ type, typeLabel, item, candidates, endpoint, body, impact, refresh, deleteEndpoint, warning }) {
2737
+ if (!item) return;
2738
+ const fields = $("#dialog-fields");
2739
+ fields.querySelector("[data-dialog-entity-history]")?.addEventListener("click", () => {
2740
+ $("#form-dialog").close();
2741
+ openEntityHistory(type, item.id, item.name, async () => { await refresh(); await loadAiReferences(); });
2742
+ });
2743
+ fields.querySelector("[data-dialog-entity-merge]")?.addEventListener("click", () => {
2744
+ $("#form-dialog").close();
2745
+ openEntityMergeDialog({ typeLabel, source: item, candidates, endpoint, body, refresh, impact });
2746
+ });
2747
+ fields.querySelector("[data-dialog-entity-delete]")?.addEventListener("click", () => {
2748
+ void deleteManagedEntity({
2749
+ typeLabel,
2750
+ item,
2751
+ endpoint: deleteEndpoint,
2752
+ refresh,
2753
+ warning,
2754
+ onDeleted: () => $("#form-dialog").close()
2755
+ });
2756
+ });
2757
+ }
2758
+
2601
2759
  function openEntityMergeDialog({ typeLabel, source, candidates, endpoint, body, refresh, impact }) {
2602
2760
  const targetOptions = candidates
2603
2761
  .filter((candidate) => candidate.id !== source.id)
2604
2762
  .map((candidate) => [candidate.id, candidate.name]);
2605
2763
  openDialog(`合并${typeLabel}`,
2606
2764
  `<p class="merge-dialog-note">“${esc(source.name)}”将合并到所选档案,目标档案会保留。${esc(impact)}</p>` +
2607
- field("targetId", `目标${typeLabel}`, "select", targetOptions[0]?.[0] ?? "", targetOptions),
2765
+ field("targetId", `目标${typeLabel}`, "select", targetOptions[0]?.[0] ?? "", targetOptions) +
2766
+ `<label class="danger-confirm-field"><input name="mergeConfirm" type="checkbox" value="confirmed"><span><strong>我确认执行合并</strong><small>来源档案及关联资料将迁移到目标档案,合并后不可撤销。</small></span></label>`,
2608
2767
  async (form) => {
2609
2768
  const target = candidates.find((candidate) => candidate.id === form.get("targetId"));
2610
2769
  if (!target) throw new Error(`请选择目标${typeLabel}`);
2770
+ if (form.get("mergeConfirm") !== "confirmed") throw new Error("请先完成合并二次确认");
2611
2771
  await api(endpoint(source), { method: "POST", body: body(target) });
2612
2772
  await refresh();
2613
2773
  await loadAiReferences();
2614
2774
  toast(`已将“${source.name}”合并到“${target.name}”`);
2615
2775
  }, "人工资料管理", { submitLabel: "确认合并" });
2776
+ const confirmation = $("#dialog-fields").querySelector("[name='mergeConfirm']");
2777
+ const submit = $("#dialog-submit");
2778
+ submit.disabled = true;
2779
+ confirmation?.addEventListener("change", () => { submit.disabled = !confirmation.checked; });
2616
2780
  }
2617
2781
 
2618
- async function deleteManagedEntity({ typeLabel, item, endpoint, refresh, warning = "" }) {
2782
+ async function deleteManagedEntity({ typeLabel, item, endpoint, refresh, warning = "", onDeleted }) {
2619
2783
  const detail = warning ? `\n${warning}` : "";
2620
- if (!window.confirm(`确认删除${typeLabel}“${item.name}”吗?${detail}`)) return;
2784
+ if (!await confirmToast(`确认删除${typeLabel}“${item.name}”吗?${detail}`, { title: `删除${typeLabel}`, confirmLabel: "继续删除" })) return;
2785
+ if (!await confirmToast(`删除${typeLabel}“${item.name}”后无法恢复。${detail}`, { title: "删除操作需要再次确认", confirmLabel: "确认删除" })) return;
2621
2786
  try {
2622
2787
  await api(endpoint(item), { method: "DELETE" });
2788
+ await onDeleted?.();
2623
2789
  await refresh();
2624
2790
  await loadAiReferences();
2625
2791
  toast(`已删除${typeLabel}“${item.name}”`);
@@ -2628,19 +2794,84 @@ async function deleteManagedEntity({ typeLabel, item, endpoint, refresh, warning
2628
2794
  }
2629
2795
  }
2630
2796
 
2797
+ function readModuleLayout() {
2798
+ try {
2799
+ const stored = localStorage.getItem(MODULE_LAYOUT_STORAGE_KEY) ?? localStorage.getItem(LEGACY_SETTINGS_LAYOUT_STORAGE_KEY);
2800
+ return normalizeModuleLayout(stored);
2801
+ } catch {
2802
+ return "cards";
2803
+ }
2804
+ }
2805
+
2806
+ function saveModuleLayout(layout) {
2807
+ const normalized = normalizeModuleLayout(layout);
2808
+ try {
2809
+ localStorage.setItem(MODULE_LAYOUT_STORAGE_KEY, normalized);
2810
+ } catch {
2811
+ /* 浏览器禁用存储时仅保留当前会话选择 */
2812
+ }
2813
+ return normalized;
2814
+ }
2815
+
2816
+ function moduleRowPreview(text, max = 180) {
2817
+ const preview = String(text ?? "").replace(/\s+/g, " ").trim();
2818
+ return preview.length > max ? `${preview.slice(0, max)}…` : preview;
2819
+ }
2820
+
2821
+ function renderModuleLayoutToggle(layout, ariaLabel = "列表样式") {
2822
+ return `<div class="module-layout-toolbar" data-module-header-action="layout-toggle">
2823
+ <div class="module-layout-toggle" role="group" aria-label="${esc(ariaLabel)}">
2824
+ <button type="button" data-module-layout="cards" aria-pressed="${layout === "cards"}">卡片</button>
2825
+ <button type="button" data-module-layout="rows" aria-pressed="${layout === "rows"}">列表</button>
2826
+ </div>
2827
+ <span class="module-layout-hint">当前:${esc(moduleLayoutLabel(layout))}</span>
2828
+ </div>`;
2829
+ }
2830
+
2831
+ function mountModuleLayoutToggle(layout, ariaLabel) {
2832
+ $("#module-header-actions").querySelector('[data-module-header-action="layout-toggle"]')?.remove();
2833
+ $("#module-header-actions").insertAdjacentHTML("beforeend", renderModuleLayoutToggle(layout, ariaLabel));
2834
+ }
2835
+
2836
+ function bindModuleLayoutToggle(refresh) {
2837
+ $("#module-header-actions").querySelectorAll("[data-module-layout]").forEach((button) => button.addEventListener("click", async () => {
2838
+ saveModuleLayout(button.dataset.moduleLayout);
2839
+ await refresh();
2840
+ }));
2841
+ }
2842
+
2843
+ function settingRecordActions(item) {
2844
+ return canEditModule("settings")
2845
+ ? recordCardEditButton("edit-setting", item.id, `设定“${item.title}”`)
2846
+ : recordHistoryButton("setting", item.id, item.title);
2847
+ }
2848
+
2849
+ function renderSettingCards(records) {
2850
+ return `<div class="card-grid">${records.map((item) => `
2851
+ <article class="record-card"><small>${esc(item.category)} · ${item.locked ? "已锁定" : esc(item.status)}</small>
2852
+ <h3>${esc(item.title)}</h3><div class="record-markdown-preview message-body">${renderMarkdown(item.content) || '<p class="markdown-editor-empty">暂无正文</p>'}</div>
2853
+ <div class="card-actions">${settingRecordActions(item)}</div></article>`).join("")}</div>`;
2854
+ }
2855
+
2856
+ function renderSettingRows(records) {
2857
+ return `<div class="module-row-list">${records.map((item) => {
2858
+ const preview = moduleRowPreview(item.content);
2859
+ return `
2860
+ <article class="record-card module-row"><small>${esc(item.category)} · ${item.locked ? "已锁定" : esc(item.status)}</small>
2861
+ <h3>${esc(item.title)}</h3><p class="module-row-preview" title="${esc(preview)}">${esc(preview)}</p>
2862
+ <div class="card-actions">${settingRecordActions(item)}</div></article>`;
2863
+ }).join("")}</div>`;
2864
+ }
2865
+
2631
2866
  async function renderSettings() {
2632
2867
  const records = (await apiPage(`/api/works/${state.work.id}/settings`)).items;
2633
2868
  state.settings = records;
2634
- $("#module-content").innerHTML = records.length ? `<div class="card-grid">${records.map((item) => `
2635
- <article class="record-card"><small>${esc(item.category)} · ${item.locked ? "已锁定" : esc(item.status)}</small>
2636
- <h3>${esc(item.title)}</h3><p>${esc(item.content)}</p>
2637
- <div class="card-actions">${item.status === "pending" ? `<button data-setting-status="confirmed" data-setting-id="${esc(item.id)}">确认候选</button><button data-setting-status="deprecated" data-setting-id="${esc(item.id)}">弃用</button>` : ""}<button data-edit-setting="${esc(item.id)}">编辑</button><button data-entity-history="setting" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.title)}">版本历史</button></div></article>`).join("")}</div>`
2869
+ const layout = readModuleLayout();
2870
+ if (records.length) mountModuleLayoutToggle(layout, "设定列表样式");
2871
+ $("#module-content").innerHTML = records.length
2872
+ ? `${layout === "rows" ? renderSettingRows(records) : renderSettingCards(records)}`
2638
2873
  : emptyModule("还没有世界观设定", "新建规则、地点、组织、科技或创作约束。AI 提取的候选也会进入这里。");
2639
- $("#module-content").querySelectorAll("[data-setting-status]").forEach((button) => button.addEventListener("click", async () => {
2640
- await api(`/api/settings/${button.dataset.settingId}`, { method: "PATCH", body: { status: button.dataset.settingStatus, changeNote: button.dataset.settingStatus === "confirmed" ? "确认 AI 设定候选" : "弃用 AI 设定候选" } });
2641
- await renderSettings();
2642
- await loadAiReferences();
2643
- }));
2874
+ bindModuleLayoutToggle(renderSettings);
2644
2875
  $("#module-content").querySelectorAll("[data-edit-setting]").forEach((button) => button.addEventListener("click", () => openSettingEditor(records.find((item) => item.id === button.dataset.editSetting))));
2645
2876
  bindEntityHistoryButtons(async () => { await renderSettings(); await loadAiReferences(); });
2646
2877
  }
@@ -2651,21 +2882,46 @@ async function renderCharacters() {
2651
2882
  canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
2652
2883
  canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
2653
2884
  ]);
2654
- const auditPanel = canEditModule("tasks") ? `<section class="character-audit-panel"><div><strong>角色身份确认</strong><small>让 AI 查询角色档案并搜索正文,找出可能被误建成两个档案的同一角色。AI 只提交审核建议,不会自动合并。</small></div><button id="create-character-audit-task" class="ghost-button" type="button" ${state.characters.length < 2 ? "disabled" : ""}>AI 角色查重</button></section>` : "";
2655
- $("#module-content").innerHTML = auditPanel + (state.characters.length ? `<div class="card-grid">${state.characters.map((item) => {
2885
+ const layout = readModuleLayout();
2886
+ const characterActions = (item) => recordCardEditButton("edit-character", item.id, `角色“${item.name}”`);
2887
+ const characterCards = () => `<div class="card-grid">${state.characters.map((item) => {
2656
2888
  const details = normalizeCharacterDetails(item.attributes?.details);
2657
2889
  return `
2658
- <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>
2659
- <h3>${esc(item.name)}</h3><div>${item.aliases.map((alias) => `<span class="pill">${esc(alias)}</span>`).join("")}</div>
2660
- ${item.species ? `<div class="character-species"><b>种族</b><span class="pill">${esc(racePathLabel(item.race) || item.species)}</span></div>` : ""}
2890
+ <article class="record-card character-card has-card-edit" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}<small>${item.lockedFields.length ? `锁定 ${item.lockedFields.length} 项` : esc(item.visibility)}</small>
2891
+ <h3>${esc(item.name)}</h3>
2661
2892
  ${item.attributes?.identity ? `<p class="character-identity">${esc(item.attributes.identity)}</p>` : ""}
2893
+ ${item.aliases.length ? `<div class="character-aliases">${item.aliases.map((alias) => `<span class="pill">${esc(alias)}</span>`).join("")}</div>` : ""}
2894
+ ${item.code ? `<div class="character-code"><b>编号</b><span class="pill">${esc(item.code)}</span></div>` : ""}
2895
+ ${item.species ? `<div class="character-species"><b>种族</b><span class="pill">${esc(racePathLabel(item.race) || item.species)}</span></div>` : ""}
2662
2896
  ${details.length ? `<dl class="character-detail-list">${details.slice(0, 4).map((detail) => `<div><dt>${esc(detail.label)}</dt><dd>${esc(detail.value)}</dd></div>`).join("")}</dl>` : ""}
2663
2897
  <div class="organization-links"><b>所属组织</b>${(item.organizations ?? []).length ? item.organizations.map((organization) => `<span class="pill organization-pill">${esc(organization.name)}</span>`).join("") : '<span class="organization-empty">未加入组织</span>'}</div>
2664
2898
  ${item.profile?.summary ? `<p class="character-summary">${esc(item.profile.summary)}</p>` : `<p>${esc(Object.entries(item.currentState).map(([key, value]) => `${key}:${value}`).join("\n") || "尚未记录当前状态")}</p>`}
2665
2899
  ${item.profileSectionCount ? `<small class="character-section-count">${item.profileSectionCount} 个设定章节</small>` : ""}
2666
- <div class="card-actions"><button data-edit-character="${esc(item.id)}">编辑</button>${canEditModule("characters") && state.characters.length > 1 ? `<button data-merge-character="${esc(item.id)}">合并</button>` : ""}${canEditModule("characters") ? `<button class="danger-button" data-delete-character="${esc(item.id)}">删除</button>` : ""}</div></article>`;
2667
- }).join("")}</div>`
2900
+ </article>`;
2901
+ }).join("")}</div>`;
2902
+ const characterRows = () => `<div class="module-row-list">${state.characters.map((item) => {
2903
+ const preview = moduleRowPreview(item.profile?.summary || item.attributes?.identity || Object.entries(item.currentState).map(([key, value]) => `${key}:${value}`).join(" ") || "尚未记录当前状态");
2904
+ const meta = [
2905
+ item.code ? `编号 ${item.code}` : "",
2906
+ item.species ? (racePathLabel(item.race) || item.species) : "",
2907
+ ...(item.aliases ?? []).slice(0, 3),
2908
+ (item.organizations ?? []).length ? (item.organizations ?? []).map((organization) => organization.name).join("、") : ""
2909
+ ].filter(Boolean).join(" · ");
2910
+ const line = meta ? `${meta} · ${preview}` : preview;
2911
+ return `
2912
+ <article class="record-card module-row character-card" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">
2913
+ <small>${item.lockedFields.length ? `锁定 ${item.lockedFields.length} 项` : esc(item.visibility)}</small>
2914
+ <h3>${esc(item.name)}</h3>
2915
+ <p class="module-row-preview" title="${esc(line)}">${esc(line)}</p>
2916
+ <div class="card-actions">${characterActions(item)}</div>
2917
+ </article>`;
2918
+ }).join("")}</div>`;
2919
+ 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>` : "";
2920
+ if (state.characters.length) mountModuleLayoutToggle(layout, "角色列表样式");
2921
+ $("#module-content").innerHTML = auditPanel + (state.characters.length
2922
+ ? `${layout === "rows" ? characterRows() : characterCards()}`
2668
2923
  : emptyModule("还没有角色档案", "创建主要人物,并维护别名、身份、动机和当前状态。"));
2924
+ bindModuleLayoutToggle(renderCharacters);
2669
2925
  $("#create-character-audit-task")?.addEventListener("click", async () => {
2670
2926
  const button = $("#create-character-audit-task");
2671
2927
  button.disabled = true;
@@ -2688,34 +2944,6 @@ async function renderCharacters() {
2688
2944
  });
2689
2945
  });
2690
2946
  $("#module-content").querySelectorAll("[data-edit-character]").forEach((button) => button.addEventListener("click", () => openCharacterEditor(state.characters.find((item) => item.id === button.dataset.editCharacter))));
2691
- $("#module-content").querySelectorAll("[data-merge-character]").forEach((button) => button.addEventListener("click", () => {
2692
- const source = state.characters.find((item) => item.id === button.dataset.mergeCharacter);
2693
- if (!source) return;
2694
- openEntityMergeDialog({
2695
- typeLabel: "角色",
2696
- source,
2697
- candidates: state.characters,
2698
- endpoint: (item) => `/api/characters/${encodeURIComponent(item.id)}/merge`,
2699
- body: (target) => ({
2700
- targetCharacterId: target.id,
2701
- expectedTargetVersionNo: target.versionNo,
2702
- expectedSourceVersionNo: source.versionNo
2703
- }),
2704
- refresh: renderCharacters,
2705
- impact: "来源角色的别名、组织、档案章节、时间线与人物关系会迁移到目标角色。"
2706
- });
2707
- }));
2708
- $("#module-content").querySelectorAll("[data-delete-character]").forEach((button) => button.addEventListener("click", () => {
2709
- const item = state.characters.find((character) => character.id === button.dataset.deleteCharacter);
2710
- if (!item) return;
2711
- void deleteManagedEntity({
2712
- typeLabel: "角色",
2713
- item,
2714
- endpoint: (character) => `/api/characters/${encodeURIComponent(character.id)}`,
2715
- refresh: renderCharacters,
2716
- warning: "相关人物关系会删除,时间线中的参与者引用会移除。"
2717
- });
2718
- }));
2719
2947
  }
2720
2948
 
2721
2949
  async function renderRaces() {
@@ -2723,45 +2951,44 @@ async function renderRaces() {
2723
2951
  apiAllPages(`/api/works/${state.work.id}/races`),
2724
2952
  canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
2725
2953
  ]);
2954
+ const layout = readModuleLayout();
2955
+ const canEditRaces = canEditModule("races");
2956
+ const raceActions = (item) => canEditRaces
2957
+ ? recordCardEditButton("edit-race", item.id, `种族“${item.name}”`)
2958
+ : recordHistoryButton("race", item.id, item.name);
2959
+ const raceCardActions = (item) => canEditRaces
2960
+ ? raceActions(item)
2961
+ : `<div class="card-actions">${raceActions(item)}</div>`;
2726
2962
  const renderRaceNode = (item) => `<details class="race-tree-node" open data-race-node="${esc(item.id)}">
2727
2963
  <summary><span>${esc(item.name)}</span><small>${item.children.length} 个直接子种族</small></summary>
2728
2964
  <div class="race-tree-branch">
2729
- <article class="record-card race-card"><small>${item.memberIds.length} 位直接角色 · ${item.settings.length} 条自身设定</small>
2965
+ <article class="record-card race-card${canEditRaces ? " has-card-edit" : ""}"><small>${item.memberIds.length} 位直接角色 · ${item.settings.length} 条自身设定</small>
2730
2966
  <div class="race-path" aria-label="种族路径">${esc(racePathLabel(item))}</div>
2731
2967
  <p>${esc(item.description || "尚未填写种族简介")}</p>
2732
- <div class="race-settings">${item.effectiveSettings.length ? item.effectiveSettings.map((setting) => `<span class="pill${setting.inherited ? " inherited" : ""}" title="${esc(setting.inherited ? `继承自 ${setting.sourceRaceName}` : `定义于 ${setting.sourceRaceName}`)}">${esc(setting.value)}<small>${esc(setting.sourceRaceName)}</small></span>`).join("") : '<span class="pill">暂无共同设定</span>'}</div>
2968
+ <div class="race-settings">${item.effectiveSettings.length ? item.effectiveSettings.map((setting) => `<section class="knowledge-markdown-block${setting.inherited ? " inherited" : ""}"><div class="knowledge-markdown-block-heading"><h4>${esc(setting.title || "未命名章节")}</h4><small>${esc(setting.inherited ? `继承自 ${setting.sourceRaceName}` : `定义于 ${setting.sourceRaceName}`)}</small></div><div class="message-body">${renderMarkdown(setting.value) || '<p class="markdown-editor-empty">暂无内容</p>'}</div></section>`).join("") : '<span class="pill">暂无共同设定</span>'}</div>
2733
2969
  <p class="race-members">直接角色:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
2734
- <div class="card-actions"><button data-edit-race="${esc(item.id)}">编辑</button><button data-entity-history="race" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button>${canEditModule("races") && state.races.length > 1 ? `<button data-merge-race="${esc(item.id)}">合并</button>` : ""}${canEditModule("races") ? `<button class="danger-button" data-delete-race="${esc(item.id)}">删除</button>` : ""}</div>
2970
+ ${raceCardActions(item)}
2735
2971
  </article>
2736
2972
  ${item.children.length ? `<div class="race-tree-children">${item.children.map(renderRaceNode).join("")}</div>` : ""}
2737
2973
  </div>
2738
2974
  </details>`;
2739
- $("#module-content").innerHTML = state.races.length ? `<section class="race-tree" aria-label="种族层级">${buildRaceForest(state.races).map(renderRaceNode).join("")}</section>` : emptyModule("还没有种族档案", "先创建种族及共同设定,之后角色编辑器才能选择该种族。");
2975
+ const raceRows = () => `<div class="module-row-list">${state.races.map((item) => {
2976
+ const preview = moduleRowPreview(item.description || "尚未填写种族简介");
2977
+ const meta = `${item.memberIds.length} 位直接角色 · ${item.settings.length ? "已填写共同设定" : "暂无共同设定"}`;
2978
+ return `
2979
+ <article class="record-card module-row race-card">
2980
+ <small>${esc(meta)}</small>
2981
+ <h3>${esc(item.name)}<span class="module-row-path">${esc(racePathLabel(item))}</span></h3>
2982
+ <p class="module-row-preview" title="${esc(preview)}">${esc(preview)}${item.members.length ? ` · ${esc(item.members.map((member) => member.name).join("、"))}` : ""}</p>
2983
+ <div class="card-actions">${raceActions(item)}</div>
2984
+ </article>`;
2985
+ }).join("")}</div>`;
2986
+ if (state.races.length) mountModuleLayoutToggle(layout, "种族列表样式");
2987
+ $("#module-content").innerHTML = state.races.length
2988
+ ? `${layout === "rows" ? raceRows() : `<section class="race-tree" aria-label="种族层级">${buildRaceForest(state.races).map(renderRaceNode).join("")}</section>`}`
2989
+ : emptyModule("还没有种族档案", "先创建种族及共同设定,之后角色编辑器才能选择该种族。");
2990
+ bindModuleLayoutToggle(renderRaces);
2740
2991
  $("#module-content").querySelectorAll("[data-edit-race]").forEach((button) => button.addEventListener("click", () => openRaceDialog(state.races.find((item) => item.id === button.dataset.editRace))));
2741
- $("#module-content").querySelectorAll("[data-merge-race]").forEach((button) => button.addEventListener("click", () => {
2742
- const source = state.races.find((item) => item.id === button.dataset.mergeRace);
2743
- if (!source) return;
2744
- openEntityMergeDialog({
2745
- typeLabel: "种族",
2746
- source,
2747
- candidates: state.races,
2748
- endpoint: (item) => `/api/races/${encodeURIComponent(item.id)}/merge`,
2749
- body: (target) => ({ targetRaceId: target.id }),
2750
- refresh: renderRaces,
2751
- impact: "来源种族的角色、子种族、简介与共同设定会迁移到目标种族。"
2752
- });
2753
- }));
2754
- $("#module-content").querySelectorAll("[data-delete-race]").forEach((button) => button.addEventListener("click", () => {
2755
- const item = state.races.find((race) => race.id === button.dataset.deleteRace);
2756
- if (!item) return;
2757
- void deleteManagedEntity({
2758
- typeLabel: "种族",
2759
- item,
2760
- endpoint: (race) => `/api/races/${encodeURIComponent(race.id)}`,
2761
- refresh: renderRaces,
2762
- warning: "已绑定角色将变为未指定种族;有子种族时需先迁移或合并。"
2763
- });
2764
- }));
2765
2992
  bindEntityHistoryButtons(async () => { await renderRaces(); await loadAiReferences(); });
2766
2993
  }
2767
2994
 
@@ -2770,54 +2997,82 @@ async function renderOrganizations() {
2770
2997
  apiAllPages(`/api/works/${state.work.id}/organizations`),
2771
2998
  canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
2772
2999
  ]);
2773
- $("#module-content").innerHTML = state.organizations.length ? `<div class="card-grid organization-grid">${state.organizations.map((item) => `
2774
- <article class="record-card organization-card"><small>${item.memberIds.length} 位成员 · ${item.settings.length} 条设定</small>
3000
+ const layout = readModuleLayout();
3001
+ const canEditOrganizations = canEditModule("organizations");
3002
+ const organizationActions = (item) => canEditOrganizations
3003
+ ? recordCardEditButton("edit-organization", item.id, `组织“${item.name}”`)
3004
+ : recordHistoryButton("organization", item.id, item.name);
3005
+ const organizationCardActions = (item) => canEditOrganizations
3006
+ ? organizationActions(item)
3007
+ : `<div class="card-actions">${organizationActions(item)}</div>`;
3008
+ const organizationCards = () => `<div class="card-grid organization-grid">${state.organizations.map((item) => `
3009
+ <article class="record-card organization-card${canEditOrganizations ? " has-card-edit" : ""}"><small>${item.memberIds.length} 位成员 · ${item.settings.length ? "已填写组织设定" : "暂无组织设定"}</small>
2775
3010
  <h3>${esc(item.name)}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
2776
- <div class="organization-settings">${item.settings.map((setting) => `<span class="pill">${esc(setting)}</span>`).join("") || '<span class="pill">暂无组织设定</span>'}</div>
3011
+ <div class="organization-settings">${item.settingsSections?.length ? item.settingsSections.map((section) => `<article class="knowledge-markdown-block"><div class="knowledge-markdown-block-heading"><h4>${esc(section.title || "未命名章节")}</h4></div><div class="message-body">${renderMarkdown(section.contentMarkdown) || '<p class="markdown-editor-empty">暂无内容</p>'}</div></article>`).join("") : '<span class="pill">暂无组织设定</span>'}</div>
2777
3012
  <p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
2778
- <div class="card-actions"><button data-edit-organization="${esc(item.id)}">编辑</button><button data-entity-history="organization" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button>${canEditModule("organizations") && state.organizations.length > 1 ? `<button data-merge-organization="${esc(item.id)}">合并</button>` : ""}${canEditModule("organizations") ? `<button class="danger-button" data-delete-organization="${esc(item.id)}">删除</button>` : ""}</div>
2779
- </article>`).join("")}</div>` : emptyModule("还没有组织", "创建国家、机构、阵营或团队,并维护组织设定与成员。");
3013
+ ${organizationCardActions(item)}
3014
+ </article>`).join("")}</div>`;
3015
+ const organizationRows = () => `<div class="module-row-list">${state.organizations.map((item) => {
3016
+ const preview = moduleRowPreview(item.description || "尚未填写组织简介");
3017
+ const members = item.members.length ? item.members.map((member) => member.name).join("、") : "暂无绑定角色";
3018
+ return `
3019
+ <article class="record-card module-row organization-card">
3020
+ <small>${item.memberIds.length} 位成员 · ${item.settings.length ? "已填写组织设定" : "暂无组织设定"}</small>
3021
+ <h3>${esc(item.name)}</h3>
3022
+ <p class="module-row-preview" title="${esc(`${preview} · 成员:${members}`)}">${esc(preview)} · ${esc(members)}</p>
3023
+ <div class="card-actions">${organizationActions(item)}</div>
3024
+ </article>`;
3025
+ }).join("")}</div>`;
3026
+ if (state.organizations.length) mountModuleLayoutToggle(layout, "组织列表样式");
3027
+ $("#module-content").innerHTML = state.organizations.length
3028
+ ? `${layout === "rows" ? organizationRows() : organizationCards()}`
3029
+ : emptyModule("还没有组织", "创建国家、机构、阵营或团队,并维护组织设定与成员。");
3030
+ bindModuleLayoutToggle(renderOrganizations);
2780
3031
  $("#module-content").querySelectorAll("[data-edit-organization]").forEach((button) => button.addEventListener("click", () => openOrganizationDialog(state.organizations.find((item) => item.id === button.dataset.editOrganization))));
2781
- $("#module-content").querySelectorAll("[data-merge-organization]").forEach((button) => button.addEventListener("click", () => {
2782
- const source = state.organizations.find((item) => item.id === button.dataset.mergeOrganization);
2783
- if (!source) return;
2784
- openEntityMergeDialog({
2785
- typeLabel: "组织",
2786
- source,
2787
- candidates: state.organizations,
2788
- endpoint: (item) => `/api/organizations/${encodeURIComponent(item.id)}/merge`,
2789
- body: (target) => ({ targetOrganizationId: target.id }),
2790
- refresh: renderOrganizations,
2791
- impact: "来源组织的成员、简介与组织设定会迁移到目标组织。"
2792
- });
2793
- }));
2794
- $("#module-content").querySelectorAll("[data-delete-organization]").forEach((button) => button.addEventListener("click", () => {
2795
- const item = state.organizations.find((organization) => organization.id === button.dataset.deleteOrganization);
2796
- if (!item) return;
2797
- void deleteManagedEntity({
2798
- typeLabel: "组织",
2799
- item,
2800
- endpoint: (organization) => `/api/organizations/${encodeURIComponent(organization.id)}`,
2801
- refresh: renderOrganizations,
2802
- warning: "角色与该组织的成员关系会一并移除。"
2803
- });
2804
- }));
2805
3032
  bindEntityHistoryButtons(async () => { await renderOrganizations(); await loadAiReferences(); });
2806
3033
  }
2807
3034
 
3035
+ function updateTimelineMultiSelectControls() {
3036
+ const toggle = $("#timeline-multi-select-toggle");
3037
+ if (!toggle) return;
3038
+ const selectedCount = $("#module-content").querySelectorAll("[data-event-select]:checked").length;
3039
+ toggle.setAttribute("aria-pressed", String(timelineMultiSelectEnabled));
3040
+ toggle.textContent = timelineMultiSelectEnabled ? "退出多选" : "多选";
3041
+ $("#module-content").querySelectorAll("[data-event-select]").forEach((input) => {
3042
+ input.hidden = !timelineMultiSelectEnabled;
3043
+ if (!timelineMultiSelectEnabled) input.checked = false;
3044
+ });
3045
+ const merge = $("#merge-events");
3046
+ if (merge) {
3047
+ merge.hidden = !timelineMultiSelectEnabled;
3048
+ merge.disabled = !timelineMultiSelectEnabled || selectedCount < 2;
3049
+ }
3050
+ }
3051
+
3052
+ function setTimelineMultiSelectMode(enabled) {
3053
+ timelineMultiSelectEnabled = enabled;
3054
+ updateTimelineMultiSelectControls();
3055
+ }
3056
+
2808
3057
  async function renderTimeline() {
2809
3058
  const [events, tracks] = await Promise.all([
2810
3059
  apiPage(`/api/works/${state.work.id}/timeline`).then((result) => result.items),
2811
3060
  apiAllPages(`/api/works/${state.work.id}/timeline-tracks`)
2812
3061
  ]);
3062
+ timelineMultiSelectEnabled = false;
3063
+ $("#timeline-tools")?.remove();
3064
+ $("#module-header-actions").insertAdjacentHTML("beforeend", `<div id="timeline-tools" class="timeline-tools" data-module-header-action="timeline-tools" role="group" aria-label="时间轴操作"><button id="create-timeline-track" class="ghost-button" type="button">新建独立时间轴</button><button id="timeline-multi-select-toggle" class="ghost-button" type="button" aria-pressed="false">多选</button>${events.length > 1 ? '<button id="merge-events" class="ghost-button" type="button" hidden>合并所选事件</button>' : ""}</div>`);
2813
3065
  state.timelineTracks = tracks;
2814
3066
  const lanes = [...tracks, { id: "", name: "未分组时间轴", description: "尚未归入独立大事件的时间节点。", sortOrder: Number.MAX_SAFE_INTEGER }];
2815
- const eventCard = (item) => `<article class="timeline-kanban-card"><div class="timeline-card-meta"><input type="checkbox" data-event-select="${esc(item.id)}" aria-label="选择 ${esc(item.name)}"><small>${esc(item.timeLabel)} · ${esc(item.status)}</small></div><h4>${esc(item.name)}</h4><p>${esc(item.description || "暂无说明")}</p>${item.location ? `<span>地点:${esc(item.location)}</span>` : ""}<div class="card-actions"><button data-edit-event="${esc(item.id)}">编辑与排序</button><button data-split-event="${esc(item.id)}">拆分</button><button data-entity-history="timeline-event" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button></div></article>`;
2816
- $("#module-content").innerHTML = `<div class="timeline-tools"><button id="create-timeline-track" class="primary-button" type="button">新建独立时间轴</button>${events.length > 1 ? '<button id="merge-events" class="ghost-button" type="button">合并所选事件</button>' : ""}</div><div class="timeline-kanban" data-testid="timeline-kanban">${lanes.map((track) => {
3067
+ const eventCard = (item) => `<article class="timeline-kanban-card"><div class="timeline-card-meta"><input type="checkbox" data-event-select="${esc(item.id)}" aria-label="选择 ${esc(item.name)}" hidden><small>${esc(item.timeLabel)} · ${esc(item.status)}</small></div><h4>${esc(item.name)}</h4><p>${esc(item.description || "暂无说明")}</p>${item.location ? `<span>地点:${esc(item.location)}</span>` : ""}<div class="card-actions"><button data-edit-event="${esc(item.id)}">编辑与排序</button><button data-split-event="${esc(item.id)}">拆分</button><button data-entity-history="timeline-event" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button></div></article>`;
3068
+ $("#module-content").innerHTML = `<div class="timeline-kanban" data-testid="timeline-kanban">${lanes.map((track) => {
2817
3069
  const laneEvents = events.filter((item) => (item.trackId ?? "") === track.id);
2818
3070
  return `<section class="timeline-lane" data-track-id="${esc(track.id)}"><header><div><small>${laneEvents.length} 个节点</small><h3>${esc(track.name)}</h3></div>${track.id ? `<div class="timeline-track-actions"><button class="timeline-track-menu" data-edit-timeline-track="${esc(track.id)}" type="button">编辑</button><button class="timeline-track-menu" data-entity-history="timeline-track" data-entity-id="${esc(track.id)}" data-entity-title="${esc(track.name)}" type="button">历史</button></div>` : ""}</header><p class="timeline-track-description">${esc(track.description || "暂无说明")}</p><div class="timeline-lane-events">${laneEvents.map(eventCard).join("") || '<div class="timeline-lane-empty">还没有时间节点</div>'}</div><button class="timeline-add-event" data-add-event-track="${esc(track.id)}" type="button">添加事件</button></section>`;
2819
3071
  }).join("")}</div>`;
2820
3072
  $("#create-timeline-track").addEventListener("click", () => openTimelineTrackDialog());
3073
+ $("#timeline-multi-select-toggle").addEventListener("click", () => setTimelineMultiSelectMode(!timelineMultiSelectEnabled));
3074
+ $("#module-content").querySelectorAll("[data-event-select]").forEach((input) => input.addEventListener("change", updateTimelineMultiSelectControls));
3075
+ setTimelineMultiSelectMode(false);
2821
3076
  $("#module-content").querySelectorAll("[data-edit-timeline-track]").forEach((button) => button.addEventListener("click", () => openTimelineTrackDialog(tracks.find((track) => track.id === button.dataset.editTimelineTrack))));
2822
3077
  $("#module-content").querySelectorAll("[data-add-event-track]").forEach((button) => button.addEventListener("click", () => openTimelineDialog(null, button.dataset.addEventTrack || null)));
2823
3078
  $("#module-content").querySelectorAll("[data-edit-event]").forEach((button) => button.addEventListener("click", () => openTimelineDialog(events.find((item) => item.id === button.dataset.editEvent))));
@@ -2844,17 +3099,36 @@ async function renderOutlines() {
2844
3099
  apiPage(`/api/works/${state.work.id}/outlines`).then((result) => result.items),
2845
3100
  apiPage(`/api/works/${state.work.id}/foreshadows?status=all${currentChapterId ? `&currentChapterId=${encodeURIComponent(currentChapterId)}` : ""}`).then((result) => result.items)
2846
3101
  ]);
3102
+ const layout = readModuleLayout();
2847
3103
  const unresolved = foreshadows.filter((item) => item.unresolved);
2848
3104
  const overdue = unresolved.filter((item) => item.overdue);
2849
3105
  const navButton = $("#module-nav [data-module=outlines] .nav-label");
2850
3106
  if (navButton) navButton.textContent = unresolved.length ? `大纲与伏笔 · ${unresolved.length}` : "大纲与伏笔";
2851
- const foreshadowHtml = foreshadows.length ? `<div class="card-grid foreshadow-grid">${foreshadows.map((item) => `
3107
+ 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>`;
3108
+ const foreshadowCards = () => `<div class="card-grid foreshadow-grid">${foreshadows.map((item) => `
2852
3109
  <article class="record-card foreshadow-card ${item.overdue ? "is-overdue" : ""}">
2853
3110
  <small>${esc(item.importance)} · ${esc(item.status)}${item.overdue ? " · 已逾期" : ""}</small>
2854
3111
  <h3>${esc(item.title)}</h3><p>${esc(item.description || "暂无说明")}</p>
2855
3112
  <div class="foreshadow-links">${item.occurrences.length ? item.occurrences.map((link) => `<span class="pill">${esc({ setup: "埋设", reminder: "提醒", payoff: "回收" }[link.role] ?? link.role)} · ${esc(link.volumeTitle)} / ${esc(link.chapterTitle)}</span>`).join("") : '<span class="pill">尚未关联章节</span>'}</div>
2856
- <div class="card-actions"><button data-edit-foreshadow="${esc(item.id)}">编辑伏笔</button><button data-entity-history="foreshadow" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.title)}">版本历史</button></div>
2857
- </article>`).join("")}</div>` : emptyModule("还没有伏笔", "创建伏笔并关联埋设、提醒与回收章节,未回收项会持续显示。\n");
3113
+ <div class="card-actions">${foreshadowActions(item)}</div>
3114
+ </article>`).join("")}</div>`;
3115
+ const foreshadowRows = () => `<div class="module-row-list">${foreshadows.map((item) => {
3116
+ const preview = moduleRowPreview(item.description || "暂无说明");
3117
+ const links = item.occurrences.length
3118
+ ? item.occurrences.map((link) => `${({ setup: "埋设", reminder: "提醒", payoff: "回收" }[link.role] ?? link.role)} · ${link.volumeTitle} / ${link.chapterTitle}`).join(";")
3119
+ : "尚未关联章节";
3120
+ return `
3121
+ <article class="record-card module-row foreshadow-card ${item.overdue ? "is-overdue" : ""}">
3122
+ <small>${esc(item.importance)} · ${esc(item.status)}${item.overdue ? " · 已逾期" : ""}</small>
3123
+ <h3>${esc(item.title)}</h3>
3124
+ <p class="module-row-preview" title="${esc(`${preview} · ${links}`)}">${esc(preview)} · ${esc(links)}</p>
3125
+ <div class="card-actions">${foreshadowActions(item)}</div>
3126
+ </article>`;
3127
+ }).join("")}</div>`;
3128
+ const foreshadowHtml = foreshadows.length
3129
+ ? `${layout === "rows" ? foreshadowRows() : foreshadowCards()}`
3130
+ : emptyModule("还没有伏笔", "创建伏笔并关联埋设、提醒与回收章节,未回收项会持续显示。\n");
3131
+ if (foreshadows.length) mountModuleLayoutToggle(layout, "伏笔列表样式");
2858
3132
  const outlineHtml = outlines.length ? `<div class="outline-list">${outlines.map((item) => `
2859
3133
  <article class="outline-row ${item.status === "completed" ? "is-complete" : ""}">
2860
3134
  <div><small>${esc(item.volumeTitle)} · ${esc(item.status)}</small><h3>${esc(item.chapterTitle)}</h3></div>
@@ -2864,6 +3138,7 @@ async function renderOutlines() {
2864
3138
  <div class="outline-actions">${item.unresolvedForeshadowCount ? `<span>${item.unresolvedForeshadowCount} 个未回收伏笔</span>` : ""}<button data-edit-outline="${esc(item.chapterId)}">编辑</button><button data-entity-history="chapter-outline" data-entity-id="${esc(item.chapterId)}" data-entity-title="${esc(item.chapterTitle)}">版本历史</button></div>
2865
3139
  </article>`).join("")}</div>` : emptyModule("还没有章节", "先创建章节,再为每章维护目标、冲突和转折。\n");
2866
3140
  $("#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>`;
3141
+ bindModuleLayoutToggle(renderOutlines);
2867
3142
  $("#module-content").querySelectorAll("[data-edit-outline]").forEach((button) => button.addEventListener("click", () => openOutlineDialog(outlines.find((item) => item.chapterId === button.dataset.editOutline))));
2868
3143
  $("#module-content").querySelectorAll("[data-edit-foreshadow]").forEach((button) => button.addEventListener("click", () => openForeshadowDialog(foreshadows.find((item) => item.id === button.dataset.editForeshadow))));
2869
3144
  bindEntityHistoryButtons(renderOutlines);
@@ -2880,7 +3155,7 @@ async function renderRelationships() {
2880
3155
  state.relationshipGraph = graph;
2881
3156
  $("#module-content").innerHTML = `<div id="relationship-map-host"></div>${relationships.length ? `<table class="table-list relationship-table"><thead><tr><th>人物</th><th>关系</th><th>关键词</th><th>证据</th><th>置信度</th><th>状态</th><th>操作</th></tr></thead><tbody>${relationships.map((item) => `
2882
3157
  <tr><td>${esc(nameOf(item.fromCharacterId))} ${item.directed ? "→" : "—"} ${esc(nameOf(item.toCharacterId))}</td>
2883
- <td>${esc(item.category)} / ${esc(item.subtype || "未细分")}</td><td>${(item.keywords ?? []).map((keyword) => `<span class="pill relationship-keyword">${esc(keyword)}</span>`).join("") || "—"}</td><td>${item.evidence.length} 条</td><td>${Math.round(item.confidence * 100)}%</td><td>${esc(item.confirmationStatus)}</td><td class="relationship-actions"><button data-edit-relationship="${esc(item.id)}">编辑</button><button data-entity-history="relationship" data-entity-id="${esc(item.id)}" data-entity-title="${esc(`${nameOf(item.fromCharacterId)} / ${nameOf(item.toCharacterId)}`)}">历史</button></td></tr>`).join("")}</tbody></table>` : '<div class="relationship-empty-note">尚无关系边;孤立角色仍显示在力导向图谱中。可人工新建关系,或运行全书人物关系分析。</div>'}`;
3158
+ <td>${esc(item.category)} / ${esc(item.subtype || "未细分")}</td><td>${(item.keywords ?? []).map((keyword) => `<span class="pill relationship-keyword">${esc(keyword)}</span>`).join("") || "—"}</td><td>${item.evidence.length}</td><td>${Math.round(item.confidence * 100)}%</td><td>${esc(item.confirmationStatus)}</td><td class="relationship-actions"><button data-edit-relationship="${esc(item.id)}">编辑</button><button data-entity-history="relationship" data-entity-id="${esc(item.id)}" data-entity-title="${esc(`${nameOf(item.fromCharacterId)} / ${nameOf(item.toCharacterId)}`)}">历史</button></td></tr>`).join("")}</tbody></table>` : '<div class="relationship-empty-note">尚无关系边;孤立角色仍显示在力导向图谱中。可人工新建关系,或运行全书人物关系分析。</div>'}`;
2884
3159
  const openGalaxy = () => {
2885
3160
  state.galaxy?.destroy();
2886
3161
  state.galaxy = createGalaxyRenderer($("#relationship-galaxy-dialog"), graph, { workId: state.work.id });
@@ -2926,11 +3201,29 @@ async function renderReviews() {
2926
3201
  : "";
2927
3202
  return `<article class="record-card character-duplicate-review"><small>角色查重 · ${esc(item.severity)} · ${esc(item.status)}</small><h3>${esc(item.title)}</h3><div class="character-duplicate-pair">${sideHtml}</div><p>${esc(item.description)}${item.suggestion ? `\n建议:${esc(item.suggestion)}` : ""}</p>${evidenceHtml ? `<ul class="character-duplicate-evidence">${evidenceHtml}</ul>` : ""}${actions}${item.resolutionNote ? `<p class="review-resolution-note">处理结果:${esc(item.resolutionNote)}</p>` : ""}</article>`;
2928
3203
  };
2929
- $("#module-content").innerHTML = reviews.length ? `<div class="card-grid">${reviews.map((item) => item.itemType === "character-duplicate" ? duplicateCard(item) : `
3204
+ const layout = readModuleLayout();
3205
+ const reviewCard = (item) => item.itemType === "character-duplicate" ? duplicateCard(item) : `
2930
3206
  <article class="record-card"><small>${esc(item.itemType)} · ${esc(item.severity)} · ${esc(item.status)}</small><h3>${esc(item.title)}</h3>
2931
3207
  <p>${esc(item.description)}${item.suggestion ? `\n建议:${esc(item.suggestion)}` : ""}</p>
2932
- ${item.status === "pending" && canResolveReview ? `<div class="card-actions"><button data-review-status="fixed" data-review-id="${esc(item.id)}">标为已修复</button><button data-review-status="ignored" data-review-id="${esc(item.id)}">忽略</button></div>` : ""}</article>`).join("")}</div>`
3208
+ ${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>`;
3209
+ const reviewRow = (item) => {
3210
+ if (item.itemType === "character-duplicate") {
3211
+ return `<div class="module-row-span">${duplicateCard(item)}</div>`;
3212
+ }
3213
+ const preview = moduleRowPreview(`${item.description || ""}${item.suggestion ? ` 建议:${item.suggestion}` : ""}`);
3214
+ return `
3215
+ <article class="record-card module-row">
3216
+ <small>${esc(item.itemType)} · ${esc(item.severity)} · ${esc(item.status)}</small>
3217
+ <h3>${esc(item.title)}</h3>
3218
+ <p class="module-row-preview" title="${esc(preview)}">${esc(preview)}</p>
3219
+ <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>
3220
+ </article>`;
3221
+ };
3222
+ if (reviews.length) mountModuleLayoutToggle(layout, "审核列表样式");
3223
+ $("#module-content").innerHTML = reviews.length
3224
+ ? `${layout === "rows" ? `<div class="module-row-list">${reviews.map(reviewRow).join("")}</div>` : `<div class="card-grid">${reviews.map(reviewCard).join("")}</div>`}`
2933
3225
  : emptyModule("没有待审核事项", "候选设定、冲突与低置信度结论会集中显示在这里。");
3226
+ bindModuleLayoutToggle(renderReviews);
2934
3227
  $("#module-content").querySelectorAll("[data-review-id]").forEach((button) => button.addEventListener("click", async () => {
2935
3228
  await api(`/api/reviews/${button.dataset.reviewId}`, { method: "PATCH", body: { status: button.dataset.reviewStatus } });
2936
3229
  await renderReviews();
@@ -2938,7 +3231,10 @@ async function renderReviews() {
2938
3231
  $("#module-content").querySelectorAll("[data-merge-review]").forEach((button) => button.addEventListener("click", async () => {
2939
3232
  const target = characterById.get(button.dataset.mergeTarget);
2940
3233
  const source = characterById.get(button.dataset.mergeSource);
2941
- if (!target || !source || !window.confirm(`确认把“${source.name}”合并到“${target.name}”?来源角色的别名、组织、时间线和关系会迁移到目标角色。`)) return;
3234
+ if (!target || !source || !(await confirmToast(
3235
+ `确认把“${source.name}”合并到“${target.name}”?来源角色的别名、组织、时间线和关系会迁移到目标角色。`,
3236
+ { title: "确认合并角色", confirmLabel: "确认合并" }
3237
+ ))) return;
2942
3238
  button.disabled = true;
2943
3239
  try {
2944
3240
  await api(`/api/reviews/${button.dataset.mergeReview}/character-resolution`, { method: "POST", body: {
@@ -2971,7 +3267,7 @@ async function renderReviews() {
2971
3267
 
2972
3268
  async function renderTasks() {
2973
3269
  const [tasks, settings] = await Promise.all([
2974
- apiPage(`/api/works/${state.work.id}/tasks`).then((result) => result.items),
3270
+ apiPage(`/api/works/${state.work.id}/tasks?view=summary`).then((result) => result.items),
2975
3271
  canReadModule("ai-settings")
2976
3272
  ? api(`/api/works/${state.work.id}/ai-settings`)
2977
3273
  : Promise.resolve({ autoRunEnabled: false, autoRunConcurrency: 2, autoRunBatchLimit: 20 })
@@ -3042,9 +3338,13 @@ async function renderTasks() {
3042
3338
  }
3043
3339
  });
3044
3340
 
3045
- const taskById = new Map(tasks.map((item) => [item.id, item]));
3046
3341
  $("#module-content").querySelectorAll("[data-task-detail]").forEach((button) => button.addEventListener("click", () => {
3047
- openTaskDetailDialog(taskById.get(button.dataset.taskDetail));
3342
+ if (button.disabled) return;
3343
+ button.disabled = true;
3344
+ api(`/api/tasks/${encodeURIComponent(button.dataset.taskDetail)}`)
3345
+ .then((task) => openTaskDetailDialog(task))
3346
+ .catch((error) => toast(error.message, "error"))
3347
+ .finally(() => { button.disabled = false; });
3048
3348
  }));
3049
3349
  $("#module-content").querySelectorAll("[data-run-task]").forEach((button) => button.addEventListener("click", async () => {
3050
3350
  const workId = state.work.id;
@@ -3307,17 +3607,19 @@ let aiContextUsageTimer = null;
3307
3607
  let aiContextUsageRequest = 0;
3308
3608
 
3309
3609
  function currentAiRequestScope() {
3310
- if (!state.work || !state.chapter) return null;
3610
+ if (!state.work) return null;
3311
3611
  const taskType = $("#ai-task").value;
3312
3612
  const scopeType = $("#ai-scope").value;
3313
- const selection = $("#chapter-content").value.slice($("#chapter-content").selectionStart, $("#chapter-content").selectionEnd);
3314
- const volume = state.work.volumes.find((item) => item.id === state.chapter.volumeId);
3613
+ const requiresChapter = taskType === "polish" || taskType === "continue" || scopeType !== "none";
3614
+ if (requiresChapter && !state.chapter) return null;
3615
+ const selection = state.chapter ? $("#chapter-content").value.slice($("#chapter-content").selectionStart, $("#chapter-content").selectionEnd) : "";
3616
+ const volume = state.chapter ? state.work.volumes.find((item) => item.id === state.chapter.volumeId) : null;
3315
3617
  const includeBookSummary = scopeType === "chapter-summary";
3316
- const scope = taskType === "polish" ? { type: "chapter", chapterId: state.chapter.id, selection }
3317
- : scopeType === "none" ? { type: "none", ...(taskType === "continue" ? { chapterId: state.chapter.id } : {}) }
3618
+ const scope = taskType === "polish" ? { type: "chapter", chapterId: state.chapter?.id, selection }
3619
+ : scopeType === "none" ? { type: "none", ...(taskType === "continue" && state.chapter ? { chapterId: state.chapter.id } : {}) }
3318
3620
  : scopeType === "book" ? { type: "book" }
3319
3621
  : scopeType === "volume" ? { type: "volume", volumeId: volume?.id }
3320
- : { type: "chapter", chapterId: state.chapter.id };
3622
+ : { type: "chapter", chapterId: state.chapter?.id };
3321
3623
  Object.assign(scope, buildAiReferenceScope(state.aiReferences));
3322
3624
  if (includeBookSummary) scope.includeBookSummary = true;
3323
3625
  return { taskType, scope, selection };
@@ -3442,6 +3744,7 @@ async function ensureAiReferencesLoaded() {
3442
3744
 
3443
3745
  function field(name, label, type = "text", value = "", options = []) {
3444
3746
  if (type === "textarea") return `<label>${esc(label)}<textarea name="${esc(name)}">${esc(value)}</textarea></label>`;
3747
+ if (type === "markdown") return `<div class="form-field markdown-editor-field" data-vditor-editor-field><div class="vditor-editor-host" data-vditor-editor data-placeholder="${esc(options.placeholder ?? `在这里编辑${label}`)}" aria-label="Markdown 编辑器"></div><textarea class="hidden" name="${esc(name)}" data-vditor-value maxlength="200000" aria-label="Markdown 原文">${esc(value)}</textarea></div>`;
3445
3748
  if (type === "item-list") {
3446
3749
  const values = Array.isArray(value) && value.length ? value : [""];
3447
3750
  return `<div class="form-field item-list-field"><span>${esc(label)}</span><div class="item-list-rows" data-item-list-rows data-name="${esc(name)}" data-label="${esc(label)}">${values.map((item) => `<div class="item-list-row"><input name="${esc(name)}" value="${esc(item)}" aria-label="${esc(label)}"><button type="button" data-item-list-remove aria-label="删除此条">删除</button></div>`).join("")}</div><button class="item-list-add" type="button" data-item-list-add>添加一条</button></div>`;
@@ -3483,6 +3786,52 @@ function field(name, label, type = "text", value = "", options = []) {
3483
3786
  return `<label>${esc(label)}<input name="${esc(name)}" type="${esc(type)}" value="${esc(value)}" ${type === "password" ? 'autocomplete="new-password"' : ""} ${type === "number" ? 'step="any"' : ""}></label>`;
3484
3787
  }
3485
3788
 
3789
+ function knowledgeSectionTitleFromMarkdown(content, index) {
3790
+ return String(content).match(/^\s{0,3}#{1,6}\s+(.+?)\s*#*\s*$/mu)?.[1]?.trim() || `设定 ${index + 1}`;
3791
+ }
3792
+
3793
+ function normalizeKnowledgeEditorSections(item) {
3794
+ const sections = Array.isArray(item?.settingsSections)
3795
+ ? item.settingsSections.map((section, index) => ({
3796
+ title: String(section.title ?? "").trim() || knowledgeSectionTitleFromMarkdown(section.contentMarkdown ?? "", index),
3797
+ contentMarkdown: String(section.contentMarkdown ?? section.content ?? ""),
3798
+ summary: String(section.summary ?? ""),
3799
+ sortOrder: Number.isFinite(Number(section.sortOrder)) ? Number(section.sortOrder) : index
3800
+ }))
3801
+ : [];
3802
+ if (sections.length > 0) return sections;
3803
+ const legacy = String(item?.settingsMarkdown ?? (Array.isArray(item?.settings) ? item.settings.join("\n\n") : ""));
3804
+ return legacy.trim() ? [{ title: knowledgeSectionTitleFromMarkdown(legacy, 0), contentMarkdown: legacy, summary: "", sortOrder: 0 }] : [];
3805
+ }
3806
+
3807
+ function knowledgeSectionPreviewText(section) {
3808
+ const content = String(section.contentMarkdown ?? "").replace(/!\[[^\]]*\]\([^)]*\)/gu, "图片").replace(/[`*_>#-]/gu, "").replace(/\s+/gu, " ").trim();
3809
+ return content ? `${content.slice(0, 180)}${content.length > 180 ? "…" : ""}` : "暂无正文内容";
3810
+ }
3811
+
3812
+ function renderKnowledgeMarkdownSections() {
3813
+ const host = $("#knowledge-markdown-sections");
3814
+ if (!host) return;
3815
+ const label = knowledgeEditorKind === "race" ? "种族" : "组织";
3816
+ const canEdit = canEditModule(knowledgeEditorKind === "race" ? "races" : "organizations");
3817
+ const sections = Array.isArray(knowledgeEditorSections) ? knowledgeEditorSections : [];
3818
+ 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>'}`;
3819
+ host.querySelector("[data-knowledge-section-create]")?.addEventListener("click", () => void openKnowledgeSectionEditor());
3820
+ host.querySelectorAll("[data-knowledge-section-edit]").forEach((button) => button.addEventListener("click", () => void openKnowledgeSectionEditor(Number(button.dataset.knowledgeSectionEdit))));
3821
+ host.querySelectorAll("[data-knowledge-section-delete]").forEach((button) => button.addEventListener("click", async () => {
3822
+ const index = Number(button.dataset.knowledgeSectionDelete);
3823
+ const section = knowledgeEditorSections[index];
3824
+ if (!section || !(await confirmToast(
3825
+ `确定删除“${section.title || `设定 ${index + 1}`}”吗?`,
3826
+ { title: "删除设定", confirmLabel: "确认删除" }
3827
+ ))) return;
3828
+ knowledgeEditorSections.splice(index, 1);
3829
+ knowledgeEditorSections.forEach((item, sortOrder) => { item.sortOrder = sortOrder; });
3830
+ markEntityEditorDirty();
3831
+ renderKnowledgeMarkdownSections();
3832
+ }));
3833
+ }
3834
+
3486
3835
  function bindDynamicListControls(container) {
3487
3836
  container.querySelectorAll("[data-item-list-add]").forEach((button) => button.addEventListener("click", () => {
3488
3837
  const rows = button.previousElementSibling;
@@ -3565,6 +3914,7 @@ function commitRelationshipKeywordInputs(container) {
3565
3914
  }
3566
3915
 
3567
3916
  function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
3917
+ void discardPendingMarkdownAttachments();
3568
3918
  $("#dialog-title").textContent = title;
3569
3919
  $("#dialog-eyebrow").textContent = eyebrow;
3570
3920
  $("#dialog-fields").innerHTML = fields;
@@ -3572,15 +3922,21 @@ function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
3572
3922
  $("#form-dialog").classList.toggle("wide-dialog", Boolean(options.wide));
3573
3923
  bindDynamicListControls($("#dialog-fields"));
3574
3924
  bindRelationshipKeywordControls($("#dialog-fields"));
3925
+ bindVditorEditors($("#dialog-fields"));
3575
3926
  const form = $("#dynamic-form");
3576
3927
  form.onsubmit = async (event) => {
3577
- if (event.submitter?.value === "cancel") return;
3928
+ if (event.submitter?.value === "cancel") {
3929
+ void discardPendingMarkdownAttachments();
3930
+ return;
3931
+ }
3578
3932
  event.preventDefault();
3579
3933
  const submit = $("#dialog-submit");
3580
3934
  submit.disabled = true;
3581
3935
  try {
3582
3936
  commitRelationshipKeywordInputs(form);
3583
3937
  await onSubmit(new FormData(form));
3938
+ const markdown = [...form.querySelectorAll("[data-vditor-value]")].map((textarea) => textarea.value).join("\n\n");
3939
+ await cleanupPendingMarkdownAttachments(markdown);
3584
3940
  $("#form-dialog").close();
3585
3941
  } catch (error) {
3586
3942
  toast(error.message, "error");
@@ -3652,12 +4008,25 @@ function bindWorkCoverControls(work) {
3652
4008
  function openWorkSettingsDialog(work) {
3653
4009
  if (!work) return;
3654
4010
  const canManageAccess = ["admin", "owner"].includes(String(work.accessRole));
4011
+ const isCurrentWork = state.work?.id === work.id;
4012
+ const canOpenImportHistory = isCurrentWork && canReplaceProse(work);
4013
+ const importHistoryAction = isCurrentWork
4014
+ ? canOpenImportHistory ? "查看导入历史" : "需要完整编辑权限"
4015
+ : "打开作品后可用";
3655
4016
  const accessField = `<section class="work-access-field" aria-labelledby="work-access-title">
3656
4017
  <div><strong id="work-access-title">成员权限</strong><small>选择成员后,可为每个作品模块单独设置无权限、只读或可编辑。</small><div class="work-access-options" aria-label="成员权限配置方式"><span>按成员配置</span><span>按模块授权</span><span>读写分离</span></div></div>
3657
4018
  ${canManageAccess ? '<button id="work-access-manage" class="ghost-button" type="button">配置成员权限</button>' : '<small>仅作品创建者或系统管理员可以调整访问权限。</small>'}
3658
4019
  </section>`;
4020
+ const importHistoryField = `<section class="work-access-field" aria-labelledby="import-history-settings-title">
4021
+ <div><strong id="import-history-settings-title">正文导入历史</strong><small>查看导入前快照并恢复被覆盖的分卷、章节标题和正文;大纲、伏笔等章节关联资料不在快照中。</small></div>
4022
+ <button id="import-history-button" class="ghost-button" type="button" aria-controls="import-history-dialog" aria-haspopup="dialog" ${canOpenImportHistory ? "" : "disabled"}>${importHistoryAction}</button>
4023
+ </section>`;
4024
+ const whitespaceField = isCurrentWork ? `<section class="work-access-field" aria-labelledby="whitespace-settings-title">
4025
+ <div><strong id="whitespace-settings-title">正文空白符</strong><small>在编辑器正文中显示或隐藏空格、全角空格和 Tab 的可视标记。</small></div>
4026
+ <button id="toggle-whitespace-settings" class="ghost-button" data-toggle-whitespace type="button" aria-pressed="${chapterWhitespaceVisible}" title="用点标记半角空格,用方框标记全角空格,用箭头标记 Tab">${chapterWhitespaceVisible ? "隐藏空白符" : "显示空白符"}</button>
4027
+ </section>` : "";
3659
4028
  openDialog("作品信息",
3660
- workCoverFieldHtml(work) + field("title", "作品名称", "text", work.title) + field("author", "作者", "text", work.author) + field("description", "简介", "textarea", work.description) + accessField,
4029
+ workCoverFieldHtml(work) + field("title", "作品名称", "text", work.title) + field("author", "作者", "text", work.author) + field("description", "简介", "textarea", work.description) + whitespaceField + accessField + importHistoryField,
3661
4030
  async (form) => {
3662
4031
  await api(`/api/works/${work.id}`, { method: "PATCH", body: { title: form.get("title"), author: form.get("author"), description: form.get("description") } });
3663
4032
  state.works = (await apiPage("/api/works")).items;
@@ -3669,19 +4038,24 @@ function openWorkSettingsDialog(work) {
3669
4038
  state.work.description = String(form.get("description") ?? state.work.description);
3670
4039
  if (updated?.coverUrl !== undefined) state.work.coverUrl = updated.coverUrl;
3671
4040
  updateDocumentTitle(state.work);
3672
- $("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${state.work.wordCount} 字`;
4041
+ $("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${Number(state.work.wordCount ?? 0).toLocaleString("zh-CN")} 字`;
3673
4042
  }
3674
4043
  renderShelf();
3675
4044
  toast("作品信息已保存");
3676
4045
  }, "作品设置");
3677
4046
  bindWorkCoverControls(work);
4047
+ $("#toggle-whitespace-settings")?.addEventListener("click", toggleChapterWhitespaceVisibility);
4048
+ $("#import-history-button")?.addEventListener("click", () => {
4049
+ $("#form-dialog").close();
4050
+ void openImportHistory();
4051
+ });
3678
4052
  $("#work-access-manage")?.addEventListener("click", () => {
3679
4053
  $("#form-dialog").close();
3680
4054
  openMembersDialog(work);
3681
4055
  });
3682
4056
  }
3683
4057
 
3684
- async function openChapterDialog() {
4058
+ async function openChapterDialog(volumeId = null) {
3685
4059
  if (!state.work) return openWorkDialog();
3686
4060
  if (!canEditProse()) return toast("当前权限只能编辑设定资料,正文为只读", "error");
3687
4061
  if (!state.work.volumes.length) {
@@ -3689,7 +4063,8 @@ async function openChapterDialog() {
3689
4063
  state.work = await api(`/api/works/${state.work.id}`);
3690
4064
  renderTree();
3691
4065
  }
3692
- openDialog("新建章节", field("title", "章节标题") + field("volumeId", "所属卷", "select", state.work.volumes[0].id, state.work.volumes.map((volume) => [volume.id, volume.title])) + field("chapterType", "章节类型", "select", "正文", chapterTypes.map((value) => [value, value])), async (form) => {
4066
+ const selectedVolumeId = state.work.volumes.some((volume) => volume.id === volumeId) ? volumeId : state.work.volumes[0].id;
4067
+ openDialog("新建章节", field("title", "章节标题") + field("volumeId", "所属卷", "select", selectedVolumeId, state.work.volumes.map((volume) => [volume.id, volume.title])) + field("chapterType", "章节类型", "select", "正文", chapterTypes.map((value) => [value, value])), async (form) => {
3693
4068
  const chapter = await api(`/api/works/${state.work.id}/chapters`, { method: "POST", body: { title: form.get("title"), volumeId: form.get("volumeId"), chapterType: form.get("chapterType"), content: "" } });
3694
4069
  state.work = await api(`/api/works/${state.work.id}`);
3695
4070
  await selectChapter(chapter.id);
@@ -3704,13 +4079,13 @@ function openVolumeDialog(item) {
3704
4079
  field("title", "分卷名称", "text", item?.title) +
3705
4080
  field("kind", "分卷类型", "select", item?.kind ?? "main", kindOptions) +
3706
4081
  field("description", "分卷简介", "textarea", item?.description) +
3707
- field("keywords", "分卷关键词(逐条填写)", "item-list", item?.keywords ?? []),
4082
+ field("keywords", "分卷关键词", "keyword-chips", item?.keywords ?? []),
3708
4083
  async (form) => {
3709
4084
  const body = {
3710
4085
  title: form.get("title"),
3711
4086
  kind: form.get("kind"),
3712
4087
  description: form.get("description"),
3713
- keywords: form.getAll("keywords").map((value) => String(value).trim()).filter(Boolean)
4088
+ keywords: uniqueRelationshipKeywords(form.getAll("keywords").map(String))
3714
4089
  };
3715
4090
  await api(item ? `/api/volumes/${item.id}` : `/api/works/${state.work.id}/volumes`, { method: item ? "PATCH" : "POST", body });
3716
4091
  state.work = await api(`/api/works/${state.work.id}`);
@@ -3720,9 +4095,10 @@ function openVolumeDialog(item) {
3720
4095
  }
3721
4096
 
3722
4097
  function openSettingEditor(item = null) {
4098
+ destroyVditorEditor(settingEditorVditor);
4099
+ settingEditorVditor = null;
3723
4100
  settingEditorItem = item;
3724
4101
  $("#setting-editor-eyebrow").textContent = item ? "人工修正" : "作者事实";
3725
- $("#setting-editor-title").textContent = item ? `编辑“${item.title}”` : "新建设定";
3726
4102
  $("#setting-editor-name").value = item?.title ?? "";
3727
4103
  $("#setting-editor-category").value = item?.category ?? "世界规则";
3728
4104
  $("#setting-editor-locked").checked = Boolean(item?.locked);
@@ -3734,6 +4110,43 @@ function openSettingEditor(item = null) {
3734
4110
  $("#setting-editor-form").querySelectorAll("input, textarea").forEach((control) => { control.readOnly = viewOnly; });
3735
4111
  $("#setting-editor-form").querySelectorAll("select, input[type='checkbox']").forEach((control) => { control.disabled = viewOnly; });
3736
4112
  $("#setting-editor-submit").classList.toggle("hidden", viewOnly);
4113
+ const management = $("#setting-editor-management");
4114
+ management.classList.toggle("hidden", !item || viewOnly);
4115
+ const statusButtons = [$("#setting-editor-confirm"), $("#setting-editor-deprecate")];
4116
+ $("#setting-editor-confirm").classList.toggle("hidden", item?.status !== "pending");
4117
+ $("#setting-editor-deprecate").classList.toggle("hidden", item?.status !== "pending");
4118
+ $("#setting-editor-history").onclick = async () => {
4119
+ if (!item) return;
4120
+ if (!(await closeEntityEditor())) return;
4121
+ openEntityHistory("setting", item.id, item.title, async () => { await renderSettings(); await loadAiReferences(); });
4122
+ };
4123
+ $("#setting-editor-delete").onclick = () => {
4124
+ if (!item) return;
4125
+ void deleteManagedEntity({
4126
+ typeLabel: "设定",
4127
+ item,
4128
+ endpoint: (setting) => `/api/settings/${encodeURIComponent(setting.id)}`,
4129
+ refresh: () => closeEntityEditor({ force: true }),
4130
+ warning: "这条设定及其版本历史会被删除。"
4131
+ });
4132
+ };
4133
+ statusButtons.forEach((button) => {
4134
+ button.onclick = async () => {
4135
+ if (!item) return;
4136
+ button.disabled = true;
4137
+ const status = button.id === "setting-editor-confirm" ? "confirmed" : "deprecated";
4138
+ try {
4139
+ await api(`/api/settings/${item.id}`, { method: "PATCH", body: { status, changeNote: status === "confirmed" ? "确认 AI 设定候选" : "弃用 AI 设定候选" } });
4140
+ await closeEntityEditor({ force: true });
4141
+ await loadAiReferences();
4142
+ toast(status === "confirmed" ? "设定候选已确认" : "设定候选已弃用");
4143
+ } catch (error) {
4144
+ toast(error.message, "error");
4145
+ } finally {
4146
+ button.disabled = false;
4147
+ }
4148
+ };
4149
+ });
3737
4150
  $("#setting-editor-form").onsubmit = async (event) => {
3738
4151
  event.preventDefault();
3739
4152
  if (!canEditModule("settings")) return;
@@ -3741,16 +4154,29 @@ function openSettingEditor(item = null) {
3741
4154
  const submit = $("#setting-editor-submit");
3742
4155
  submit.disabled = true;
3743
4156
  try {
4157
+ const title = String(form.get("title") ?? "").trim();
4158
+ if (!title) {
4159
+ toast("请填写设定标题", "error");
4160
+ $("#setting-editor-name").focus();
4161
+ return;
4162
+ }
4163
+ const content = String(form.get("content") ?? "");
4164
+ if (!content.trim()) {
4165
+ toast("请填写设定正文", "error");
4166
+ $("#setting-editor-body").focus();
4167
+ return;
4168
+ }
3744
4169
  const locked = form.get("locked") === "on";
3745
4170
  const body = {
3746
- title: String(form.get("title") ?? "").trim(),
4171
+ title,
3747
4172
  category: String(form.get("category") ?? "世界规则"),
3748
- content: String(form.get("content") ?? ""),
4173
+ content,
3749
4174
  locked,
3750
4175
  status: locked ? "confirmed" : (item?.status ?? "draft"),
3751
4176
  ...(item ? { changeNote: String(form.get("changeNote") ?? "").trim() } : {})
3752
4177
  };
3753
4178
  await api(item ? `/api/settings/${item.id}` : `/api/works/${state.work.id}/settings`, { method: item ? "PATCH" : "POST", body });
4179
+ await cleanupPendingMarkdownAttachments(body.content);
3754
4180
  entityEditorDirty = false;
3755
4181
  await loadAiReferences();
3756
4182
  await closeEntityEditor({ force: true });
@@ -3762,6 +4188,10 @@ function openSettingEditor(item = null) {
3762
4188
  }
3763
4189
  };
3764
4190
  showEntityEditorPage("setting");
4191
+ settingEditorVditor = createVditorEditor($("#setting-editor-markdown"), item?.content ?? "", {
4192
+ onInput: (markdown) => { $("#setting-editor-body").value = markdown; markEntityEditorDirty(); },
4193
+ readOnly: viewOnly
4194
+ });
3765
4195
  $("#setting-editor-name").focus();
3766
4196
  }
3767
4197
 
@@ -3887,22 +4317,165 @@ async function discardPendingCharacterAttachments() {
3887
4317
  }));
3888
4318
  }
3889
4319
 
3890
- function scheduleCharacterSectionPreview() {
3891
- if (characterSectionPreviewTimer !== null) clearTimeout(characterSectionPreviewTimer);
3892
- characterSectionPreviewTimer = window.setTimeout(() => {
3893
- characterSectionPreviewTimer = null;
3894
- const preview = $("#character-section-preview");
3895
- const content = $("#character-section-markdown");
3896
- if (preview && content) preview.innerHTML = renderMarkdown(content.value) || '<p class="character-markdown-empty">预览区域暂无内容。</p>';
3897
- }, 260);
4320
+ async function discardPendingMarkdownAttachments() {
4321
+ const pending = markdownEditorPendingAttachments.splice(0);
4322
+ await Promise.all(pending.map(async (attachmentId) => {
4323
+ try { await api(`/api/attachments/${attachmentId}`, { method: "DELETE" }); } catch { /* 已被引用的附件由正常引用生命周期管理。 */ }
4324
+ }));
3898
4325
  }
3899
4326
 
3900
- async function closeCharacterSectionEditor({ force = false } = {}) {
3901
- if (!force && characterSectionEditorDirty && !window.confirm("当前 Markdown 章节有未保存修改,返回人物档案将丢弃这些修改。是否继续?")) return false;
3902
- if (characterSectionPreviewTimer !== null) {
3903
- clearTimeout(characterSectionPreviewTimer);
3904
- characterSectionPreviewTimer = null;
4327
+ async function cleanupPendingMarkdownAttachments(contentMarkdown) {
4328
+ const referenced = new Set([...String(contentMarkdown).matchAll(/attachment:\/\/([A-Za-z0-9_-]{1,300})/gu)].map((match) => String(match[1])));
4329
+ const pending = markdownEditorPendingAttachments.splice(0);
4330
+ await Promise.all(pending.filter((attachmentId) => !referenced.has(attachmentId)).map((attachmentId) => api(`/api/attachments/${attachmentId}`, { method: "DELETE" }).catch(() => null)));
4331
+ }
4332
+
4333
+ function markdownImageLabel(file, fallback = "图片附件") {
4334
+ return String(file?.name ?? "").replace(/[\[\]\r\n]/gu, "").trim() || fallback;
4335
+ }
4336
+
4337
+ async function uploadMarkdownAttachment(file) {
4338
+ const body = new FormData();
4339
+ body.append("file", file);
4340
+ const attachment = await api(`/api/works/${state.work.id}/attachments`, { method: "POST", body });
4341
+ if (!attachment.deduplicated) markdownEditorPendingAttachments.push(String(attachment.id));
4342
+ return { attachment, imageLabel: markdownImageLabel(file) };
4343
+ }
4344
+
4345
+ function createVditorUploadHandler(uploadAttachment, getEditor) {
4346
+ return async (files) => {
4347
+ const selection = window.getSelection();
4348
+ const range = selection?.rangeCount ? selection.getRangeAt(0).cloneRange() : null;
4349
+ const insertions = [];
4350
+ for (const file of files) {
4351
+ try {
4352
+ const { attachment, imageLabel } = await uploadAttachment(file);
4353
+ insertions.push(`![${imageLabel}](attachment://${attachment.id})`);
4354
+ } catch (error) {
4355
+ toast(error.message, "error");
4356
+ }
4357
+ }
4358
+ const editor = getEditor();
4359
+ if (editor && insertions.length > 0) {
4360
+ if (range && editor.vditor?.element?.contains(range.startContainer)) {
4361
+ selection?.removeAllRanges();
4362
+ selection?.addRange(range);
4363
+ }
4364
+ editor.insertMD(insertions.join("\n\n"));
4365
+ normalizeVditorAttachmentImages(editor);
4366
+ }
4367
+ return null;
4368
+ };
4369
+ }
4370
+
4371
+ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment = uploadMarkdownAttachment, placeholder = "", readOnly = false } = {}) {
4372
+ if (!window.Vditor) {
4373
+ toast("Markdown 编辑器资源加载失败,请刷新页面后重试", "error");
4374
+ return null;
3905
4375
  }
4376
+ ensureVditorIconScript();
4377
+ let editor = null;
4378
+ editor = new window.Vditor(host, {
4379
+ cdn: "/vendor/vditor",
4380
+ lang: "zh_CN",
4381
+ theme: currentColorTheme() === "dark" ? "dark" : "classic",
4382
+ mode: "ir",
4383
+ value: String(value ?? ""),
4384
+ height: "100%",
4385
+ minHeight: 260,
4386
+ placeholder,
4387
+ preview: { transform: transformVditorPreview },
4388
+ cache: { enable: false },
4389
+ toolbar: ["headings", "bold", "italic", "strike", "|", "line", "quote", "list", "ordered-list", "check", "|", "code", "inline-code", "link", "table", "upload", "|", "undo", "redo", "edit-mode", "fullscreen"],
4390
+ upload: {
4391
+ accept: "image/*",
4392
+ max: 10 * 1024 * 1024,
4393
+ multiple: true,
4394
+ handler: createVditorUploadHandler(uploadAttachment, () => editor)
4395
+ },
4396
+ input: (markdown) => {
4397
+ normalizeVditorAttachmentImages(editor);
4398
+ onInput(markdown);
4399
+ },
4400
+ after: () => {
4401
+ normalizeVditorAttachmentImages(editor);
4402
+ if (readOnly) editor?.disabled();
4403
+ }
4404
+ });
4405
+ const attachmentObserver = new MutationObserver(() => normalizeVditorAttachmentImages(editor));
4406
+ attachmentObserver.observe(host, { subtree: true, childList: true, attributes: true, attributeFilter: ["src"] });
4407
+ editor.__attachmentObserver = attachmentObserver;
4408
+ host.__vditor = editor;
4409
+ if (readOnly) editor.disabled();
4410
+ return editor;
4411
+ }
4412
+
4413
+ function transformVditorPreview(html) {
4414
+ 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}`);
4415
+ }
4416
+
4417
+ function normalizeVditorAttachmentImages(editor) {
4418
+ const root = editor?.vditor?.element;
4419
+ if (!root) return;
4420
+ root.querySelectorAll('img[src^="attachment://"]').forEach((image) => {
4421
+ const target = image.getAttribute("src")?.trim() ?? "";
4422
+ const attachment = target.match(/^attachment:\/\/([A-Za-z0-9_-]{1,300})$/u);
4423
+ if (attachment) image.setAttribute("src", `/api/attachments/${encodeURIComponent(attachment[1])}/content`);
4424
+ });
4425
+ }
4426
+
4427
+ function ensureVditorIconScript() {
4428
+ if (document.getElementById("vditorIconScript")) return;
4429
+ const script = document.createElement("script");
4430
+ script.id = "vditorIconScript";
4431
+ script.src = "/vendor/vditor/dist/js/icons/ant.js?v=3.11.2";
4432
+ document.body.appendChild(script);
4433
+ }
4434
+
4435
+ function destroyVditorEditor(editor) {
4436
+ if (!editor) return;
4437
+ editor.__attachmentObserver?.disconnect();
4438
+ const host = editor.vditor?.element;
4439
+ editor.destroy();
4440
+ if (host) delete host.__vditor;
4441
+ }
4442
+
4443
+ function bindVditorEditors(container) {
4444
+ container.querySelectorAll("[data-vditor-editor]").forEach((host) => {
4445
+ const valueField = host.parentElement?.querySelector("[data-vditor-value]");
4446
+ const editor = createVditorEditor(host, valueField?.value ?? "", {
4447
+ onInput: (markdown) => {
4448
+ if (valueField) valueField.value = markdown;
4449
+ markEntityEditorDirty();
4450
+ },
4451
+ placeholder: "",
4452
+ readOnly: Boolean(valueField?.readOnly)
4453
+ });
4454
+ });
4455
+ }
4456
+
4457
+ function characterSectionImageLabel(file, fallback = "图片附件") {
4458
+ return String(file?.name ?? "").replace(/[\[\]\r\n]/gu, "").trim() || fallback;
4459
+ }
4460
+
4461
+ async function uploadCharacterSectionAttachment(file) {
4462
+ const body = new FormData();
4463
+ body.append("file", file);
4464
+ const attachment = await api(`/api/works/${state.work.id}/attachments`, { method: "POST", body });
4465
+ if (!attachment.deduplicated) characterSectionPendingAttachments.push(String(attachment.id));
4466
+ return {
4467
+ attachment,
4468
+ imageLabel: characterSectionImageLabel(file)
4469
+ };
4470
+ }
4471
+
4472
+ async function closeCharacterSectionEditor({ force = false } = {}) {
4473
+ if (!force && characterSectionEditorDirty && !(await confirmToast(
4474
+ "当前 Markdown 章节有未保存修改,返回人物档案将丢弃这些修改。是否继续?",
4475
+ { title: "放弃未保存修改", confirmLabel: "放弃并继续", cancelLabel: "继续编辑" }
4476
+ ))) return false;
4477
+ destroyVditorEditor(characterSectionVditor);
4478
+ characterSectionVditor = null;
3906
4479
  await discardPendingCharacterAttachments();
3907
4480
  characterSectionEditorDirty = false;
3908
4481
  $("#character-section-editor-view").classList.add("hidden");
@@ -3912,6 +4485,82 @@ async function closeCharacterSectionEditor({ force = false } = {}) {
3912
4485
  return true;
3913
4486
  }
3914
4487
 
4488
+ function knowledgeSectionEditorHtml(section = null) {
4489
+ const label = knowledgeEditorKind === "race" ? "种族" : "组织";
4490
+ return `<div class="character-section-editor-shell knowledge-section-editor-shell">
4491
+ <header class="character-section-editor-header">
4492
+ <div><span class="eyebrow">${label} Markdown 设定</span><h2 id="knowledge-section-editor-title">${section ? `编辑“${esc(section.title)}”` : "新建设定"}</h2></div>
4493
+ <button class="entity-editor-back" type="button" data-knowledge-section-edit-close>返回设定列表</button>
4494
+ </header>
4495
+ <section class="character-markdown-editor" aria-label="${section ? "编辑" : "新建"}${label} Markdown 设定">
4496
+ <div class="character-markdown-editor-meta">
4497
+ <label>设定标题<input id="knowledge-section-title" maxlength="200" value="${esc(section?.title ?? "")}" placeholder="例如:组织章程、种族特征" required></label>
4498
+ </div>
4499
+ <div id="knowledge-section-markdown" class="vditor-editor-host" data-vditor-editor aria-label="Markdown 编辑器"></div>
4500
+ <div class="character-markdown-editor-footer">
4501
+ <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>
4502
+ </div>
4503
+ </section>
4504
+ </div>`;
4505
+ }
4506
+
4507
+ async function closeKnowledgeSectionEditor({ force = false } = {}) {
4508
+ if (!force && knowledgeSectionEditorDirty && !(await confirmToast(
4509
+ "当前 Markdown 设定有未保存修改,返回设定列表将丢弃这些修改。是否继续?",
4510
+ { title: "放弃未保存修改", confirmLabel: "放弃并继续", cancelLabel: "继续编辑" }
4511
+ ))) return false;
4512
+ destroyVditorEditor(knowledgeSectionVditor);
4513
+ knowledgeSectionVditor = null;
4514
+ knowledgeSectionEditorDirty = false;
4515
+ knowledgeSectionEditorIndex = null;
4516
+ $("#knowledge-section-editor-view").classList.add("hidden");
4517
+ $("#knowledge-editor-form").classList.remove("hidden");
4518
+ $("#knowledge-section-editor-host").innerHTML = "";
4519
+ renderKnowledgeMarkdownSections();
4520
+ replacePageRoute(currentPageRoute());
4521
+ return true;
4522
+ }
4523
+
4524
+ async function openKnowledgeSectionEditor(index = null) {
4525
+ if (!canEditModule(knowledgeEditorKind === "race" ? "races" : "organizations")) return;
4526
+ destroyVditorEditor(knowledgeSectionVditor);
4527
+ knowledgeSectionVditor = null;
4528
+ const section = Number.isInteger(index) ? knowledgeEditorSections[index] : null;
4529
+ if (Number.isInteger(index) && !section) return;
4530
+ knowledgeSectionEditorIndex = Number.isInteger(index) ? index : null;
4531
+ knowledgeSectionEditorDirty = false;
4532
+ const host = $("#knowledge-section-editor-host");
4533
+ host.innerHTML = knowledgeSectionEditorHtml(section);
4534
+ $("#knowledge-editor-form").classList.add("hidden");
4535
+ $("#knowledge-section-editor-view").classList.remove("hidden");
4536
+ const titleInput = $("#knowledge-section-title");
4537
+ host.querySelectorAll("input, textarea").forEach((control) => control.addEventListener("input", () => { knowledgeSectionEditorDirty = true; }));
4538
+ knowledgeSectionVditor = createVditorEditor($("#knowledge-section-markdown"), section?.contentMarkdown ?? "", {
4539
+ onInput: () => { knowledgeSectionEditorDirty = true; }
4540
+ });
4541
+ host.querySelector("[data-knowledge-section-edit-close]").addEventListener("click", () => void closeKnowledgeSectionEditor());
4542
+ host.querySelector("[data-knowledge-section-edit-cancel]").addEventListener("click", () => void closeKnowledgeSectionEditor());
4543
+ host.querySelector("[data-knowledge-section-edit-save]").addEventListener("click", async (event) => {
4544
+ const button = event.currentTarget;
4545
+ const wasEditing = knowledgeSectionEditorIndex !== null;
4546
+ const title = titleInput.value.trim();
4547
+ if (!title) {
4548
+ toast("请填写设定标题", "error");
4549
+ titleInput.focus();
4550
+ return;
4551
+ }
4552
+ button.disabled = true;
4553
+ const nextSection = { title, contentMarkdown: knowledgeSectionVditor?.getValue() ?? "", summary: String(section?.summary ?? ""), sortOrder: knowledgeSectionEditorIndex ?? knowledgeEditorSections.length };
4554
+ if (knowledgeSectionEditorIndex === null) knowledgeEditorSections.push(nextSection);
4555
+ else knowledgeEditorSections[knowledgeSectionEditorIndex] = nextSection;
4556
+ knowledgeEditorSections.forEach((item, sortOrder) => { item.sortOrder = sortOrder; });
4557
+ markEntityEditorDirty();
4558
+ await closeKnowledgeSectionEditor({ force: true });
4559
+ toast(wasEditing ? "设定已更新" : "设定已添加");
4560
+ });
4561
+ titleInput.focus();
4562
+ }
4563
+
3915
4564
  function characterSectionEditorHtml(section = null) {
3916
4565
  const options = Object.entries(characterSectionTypeLabels).map(([value, label]) => `<option value="${value}" ${section?.sectionType === value ? "selected" : ""}>${esc(label)}</option>`).join("");
3917
4566
  return `<div class="character-section-editor-shell">
@@ -3925,15 +4574,7 @@ function characterSectionEditorHtml(section = null) {
3925
4574
  <label>章节标题<input id="character-section-title" maxlength="200" value="${esc(section?.title ?? "")}" placeholder="例如:背景故事" required></label>
3926
4575
  <label class="character-markdown-summary-field">章节摘要<textarea id="character-section-summary" maxlength="20000" placeholder="用于角色列表和 AI 快速定位,不会替代正文">${esc(section?.summary ?? "")}</textarea></label>
3927
4576
  </div>
3928
- <div class="character-markdown-toolbar">
3929
- <label class="ghost-button" for="character-section-attachment">上传并插入图片</label>
3930
- <input id="character-section-attachment" class="hidden" type="file" accept=".png,.jpg,.jpeg,.webp,.gif,image/png,image/jpeg,image/webp,image/gif">
3931
- <span>图片会优先转换为体积更小的无损 WebP</span>
3932
- </div>
3933
- <div class="character-markdown-compose">
3934
- <label>Markdown 原文<textarea id="character-section-markdown" maxlength="500000" spellcheck="true" placeholder="支持标题、列表、引用、表格、链接和图片">${esc(section?.contentMarkdown ?? "")}</textarea></label>
3935
- <div><span class="character-markdown-preview-label">安全预览</span><article id="character-section-preview" class="character-markdown-document message-body">${renderMarkdown(section?.contentMarkdown ?? "") || '<p class="character-markdown-empty">预览区域暂无内容。</p>'}</article></div>
3936
- </div>
4577
+ <div id="character-section-markdown" class="vditor-editor-host" data-vditor-editor aria-label="Markdown 编辑器"></div>
3937
4578
  <div class="character-markdown-editor-footer">
3938
4579
  <label class="character-markdown-change-note">版本说明<input id="character-section-change-note" maxlength="500" placeholder="可选,例如:补充远古时期经历"></label>
3939
4580
  <div class="character-markdown-editor-actions"><button type="button" data-character-section-edit-cancel>取消</button><button type="button" class="primary-button" data-character-section-edit-save>${section ? "保存章节版本" : "创建章节"}</button></div>
@@ -3944,41 +4585,17 @@ function characterSectionEditorHtml(section = null) {
3944
4585
 
3945
4586
  async function openCharacterSectionEditor(section = null) {
3946
4587
  await discardPendingCharacterAttachments();
4588
+ destroyVditorEditor(characterSectionVditor);
4589
+ characterSectionVditor = null;
3947
4590
  const host = $("#character-section-editor-host");
3948
4591
  host.innerHTML = characterSectionEditorHtml(section);
3949
4592
  characterSectionEditorDirty = false;
3950
4593
  $("#character-editor-form").classList.add("hidden");
3951
4594
  $("#character-section-editor-view").classList.remove("hidden");
3952
- const textarea = $("#character-section-markdown");
3953
4595
  host.querySelectorAll("input, textarea, select").forEach((control) => control.addEventListener("input", () => { characterSectionEditorDirty = true; }));
3954
- textarea.addEventListener("input", scheduleCharacterSectionPreview);
3955
- $("#character-section-attachment").addEventListener("change", async (event) => {
3956
- const file = event.target.files?.[0];
3957
- if (!file) return;
3958
- const input = event.currentTarget;
3959
- input.disabled = true;
3960
- const body = new FormData();
3961
- body.append("file", file);
3962
- try {
3963
- const attachment = await api(`/api/works/${state.work.id}/attachments`, { method: "POST", body });
3964
- if (!attachment.deduplicated) characterSectionPendingAttachments.push(String(attachment.id));
3965
- const imageLabel = file.name.replace(/[\]\r\n]/gu, "").trim() || "图片附件";
3966
- const insertion = `![${imageLabel}](attachment://${attachment.id})`;
3967
- const start = textarea.selectionStart ?? textarea.value.length;
3968
- const end = textarea.selectionEnd ?? start;
3969
- const prefix = start > 0 && !textarea.value.slice(0, start).endsWith("\n") ? "\n\n" : "";
3970
- const suffix = end < textarea.value.length && !textarea.value.slice(end).startsWith("\n") ? "\n\n" : "";
3971
- textarea.setRangeText(`${prefix}${insertion}${suffix}`, start, end, "end");
3972
- textarea.focus();
3973
- characterSectionEditorDirty = true;
3974
- scheduleCharacterSectionPreview();
3975
- toast(attachment.storedMimeType === "image/webp" ? "图片已转换为无损 WebP 并插入" : "图片已插入;转换后未变小,因此保留原格式");
3976
- } catch (error) {
3977
- toast(error.message, "error");
3978
- } finally {
3979
- input.disabled = false;
3980
- input.value = "";
3981
- }
4596
+ characterSectionVditor = createVditorEditor($("#character-section-markdown"), section?.contentMarkdown ?? "", {
4597
+ uploadAttachment: uploadCharacterSectionAttachment,
4598
+ onInput: () => { characterSectionEditorDirty = true; }
3982
4599
  });
3983
4600
  host.querySelector("[data-character-section-edit-close]").addEventListener("click", () => void closeCharacterSectionEditor());
3984
4601
  host.querySelector("[data-character-section-edit-cancel]").addEventListener("click", () => void closeCharacterSectionEditor());
@@ -3991,7 +4608,7 @@ async function openCharacterSectionEditor(section = null) {
3991
4608
  return;
3992
4609
  }
3993
4610
  button.disabled = true;
3994
- const contentMarkdown = textarea.value;
4611
+ const contentMarkdown = characterSectionVditor?.getValue() ?? "";
3995
4612
  try {
3996
4613
  const saved = await api(section ? `/api/character-sections/${section.id}` : `/api/characters/${characterEditorItem.id}/sections`, {
3997
4614
  method: section ? "PATCH" : "POST",
@@ -4049,7 +4666,7 @@ function renderCharacterMarkdownSections() {
4049
4666
  const host = $("#character-markdown-sections");
4050
4667
  if (!host) return;
4051
4668
  if (!characterEditorItem?.id) {
4052
- host.innerHTML = '<div class="character-editor-empty-field"><b>Markdown 档案章节</b><span>创建人物档案后即可添加背景故事、能力、经历和研究记录。</span></div>';
4669
+ host.innerHTML = '<div class="character-editor-empty-field" role="note" aria-label="保存角色提示"><b>请先保存当前角色</b><span>完成基础资料后,请点击页面底部的“创建人物档案”。保存成功后即可新建 Markdown 档案章节。</span></div>';
4053
4670
  return;
4054
4671
  }
4055
4672
  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>`;
@@ -4127,6 +4744,7 @@ function renderCharacterEditorFields(item) {
4127
4744
  ? field("firstChapterId", "首次登场章节", "select", item?.firstChapterId ?? "", chapterOptions)
4128
4745
  : '<div class="character-editor-empty-field"><b>首次登场章节</b><span>当前账户没有正文读取权限,原有绑定不会被修改。</span></div>')),
4129
4746
  characterEditorSection("profile", "人物档案", "记录人物定位、行为动力和便于创作时快速理解的简介。",
4747
+ field("code", "编号", "text", item?.code) +
4130
4748
  field("identity", "身份与定位", "text", item?.attributes?.identity) +
4131
4749
  field("motivation", "核心动机", "textarea", item?.profile?.motivation) +
4132
4750
  field("summary", "人物简介", "textarea", item?.profile?.summary)),
@@ -4163,6 +4781,7 @@ function collectCharacterBody(form) {
4163
4781
  delete profile.sections;
4164
4782
  const body = {
4165
4783
  name: String(form.get("name") ?? "").trim(),
4784
+ code: String(form.get("code") ?? "").trim(),
4166
4785
  aliases: form.getAll("aliases").map((value) => String(value).trim()).filter(Boolean),
4167
4786
  attributes: {
4168
4787
  ...(item?.attributes ?? {}),
@@ -4269,6 +4888,34 @@ async function openCharacterEditor(item = null) {
4269
4888
  $("#character-editor-submit").textContent = item ? "保存新版本" : "创建人物档案";
4270
4889
  $("#character-history-button").disabled = !item;
4271
4890
  $("#character-history-button").title = item ? "查看、比较和回滚历史版本" : "创建人物档案后即可查看版本历史";
4891
+ const characterMergeButton = $("#character-merge-button");
4892
+ const characterDeleteButton = $("#character-delete-button");
4893
+ const canManageCharacter = Boolean(item && canEditModule("characters"));
4894
+ characterMergeButton.classList.toggle("hidden", !canManageCharacter || state.characters.length < 2);
4895
+ characterDeleteButton.classList.toggle("hidden", !canManageCharacter);
4896
+ characterMergeButton.onclick = async () => {
4897
+ if (!item) return;
4898
+ await closeEntityEditor({ force: true });
4899
+ openEntityMergeDialog({
4900
+ typeLabel: "角色",
4901
+ source: item,
4902
+ candidates: state.characters,
4903
+ endpoint: (character) => `/api/characters/${encodeURIComponent(character.id)}/merge`,
4904
+ body: (target) => ({ targetCharacterId: target.id, expectedTargetVersionNo: target.versionNo, expectedSourceVersionNo: item.versionNo }),
4905
+ refresh: renderCharacters,
4906
+ impact: "来源角色的别名、组织、档案章节、时间线与人物关系会迁移到目标角色。"
4907
+ });
4908
+ };
4909
+ characterDeleteButton.onclick = () => {
4910
+ if (!item) return;
4911
+ void deleteManagedEntity({
4912
+ typeLabel: "角色",
4913
+ item,
4914
+ endpoint: (character) => `/api/characters/${encodeURIComponent(character.id)}`,
4915
+ refresh: () => closeEntityEditor({ force: true }),
4916
+ warning: "相关人物关系会删除,时间线中的参与者引用会移除。"
4917
+ });
4918
+ };
4272
4919
  setCharacterHistoryVisible(false);
4273
4920
  renderCharacterEditorFields(item);
4274
4921
  const viewOnly = !canEditModule("characters");
@@ -4315,44 +4962,152 @@ async function openCharacterEditor(item = null) {
4315
4962
  }
4316
4963
  }
4317
4964
 
4318
- async function openRaceDialog(item) {
4965
+ function knowledgeEditorSection(key, title, description, content) {
4966
+ 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>`;
4967
+ }
4968
+
4969
+ function activateKnowledgeEditorTab(key) {
4970
+ document.querySelectorAll("[data-knowledge-editor-tab]").forEach((button) => {
4971
+ const active = button.dataset.knowledgeEditorTab === key;
4972
+ button.setAttribute("aria-selected", String(active));
4973
+ button.tabIndex = active ? 0 : -1;
4974
+ });
4975
+ document.querySelectorAll("[data-knowledge-editor-panel]").forEach((panel) => panel.classList.toggle("hidden", panel.dataset.knowledgeEditorPanel !== key));
4976
+ }
4977
+
4978
+ function renderKnowledgeEditorFields(kind, item, memberOptions, parentOptions) {
4979
+ const isRace = kind === "race";
4980
+ const label = isRace ? "种族" : "组织";
4981
+ const title = isRace ? "种族共同设定" : "组织设定";
4982
+ const tabs = isRace
4983
+ ? [["basic", "基础资料", "名称、层级与简介"], ["settings", "共同设定", "Markdown 设定章节"], ["members", "种族成员", "角色归属"]]
4984
+ : [["basic", "基础资料", "名称与简介"], ["settings", "组织设定", "Markdown 设定章节"], ["members", "组织成员", "角色归属"]];
4985
+ $("#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("");
4986
+ const basicFields = field("name", `${label}名称`, "text", item?.name, []) + (isRace ? field("parentRaceId", "父种族", "select", item?.parentRaceId ?? "", parentOptions) : "") + field("description", `${label}简介`, "textarea", item?.description, []);
4987
+ const memberField = memberOptions.length
4988
+ ? field("memberIds", isRace ? "属于该种族的角色(可多选)" : "组织成员(可多选)", "chips", item?.memberIds ?? [], memberOptions)
4989
+ : `<div class="character-editor-empty-field"><strong>${isRace ? "种族成员" : "组织成员"}</strong><span>当前还没有可绑定的角色。</span></div>`;
4990
+ $("#knowledge-editor-fields").innerHTML = knowledgeEditorSection("basic", "基础资料", isRace ? "先定义名称、层级和简介,再补充共同设定。" : "先定义组织名称和简介,再补充完整的组织设定。", basicFields)
4991
+ + knowledgeEditorSection("settings", title, "", '<div id="knowledge-markdown-sections" class="knowledge-markdown-sections"></div>')
4992
+ + knowledgeEditorSection("members", isRace ? "种族成员" : "组织成员", "成员关系会同步到角色档案中。", memberField);
4993
+ document.querySelectorAll("[data-knowledge-editor-tab]").forEach((button) => {
4994
+ button.onclick = () => activateKnowledgeEditorTab(button.dataset.knowledgeEditorTab);
4995
+ });
4996
+ renderKnowledgeMarkdownSections();
4997
+ activateKnowledgeEditorTab("basic");
4998
+ }
4999
+
5000
+ async function openKnowledgeEditor(kind, item) {
5001
+ await discardPendingMarkdownAttachments();
4319
5002
  state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
4320
5003
  const memberOptions = state.characters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
4321
- const parentOptions = [["", "无(根种族)"], ...eligibleRaceParents(state.races, item?.id)
4322
- .sort((left, right) => racePathLabel(left).localeCompare(racePathLabel(right), "zh-CN"))
4323
- .map((race) => [race.id, racePathLabel(race)])];
4324
- openDialog(item ? "编辑种族" : "新建种族",
4325
- field("name", "种族名称", "text", item?.name) +
4326
- field("parentRaceId", "父种族", "select", item?.parentRaceId ?? "", parentOptions) +
4327
- field("description", "种族简介", "textarea", item?.description) +
4328
- field("settings", "种族共同设定(逐条填写)", "item-list", item?.settings ?? []) +
4329
- (memberOptions.length ? field("memberIds", "属于该种族的角色(可多选)", "chips", item?.memberIds ?? [], memberOptions) : ""),
4330
- async (form) => {
4331
- const settings = form.getAll("settings").map((value) => String(value).trim()).filter(Boolean);
4332
- const body = { name: form.get("name"), parentRaceId: form.get("parentRaceId") || null, description: form.get("description"), settings };
4333
- if (canReadModule("characters")) body.memberIds = form.getAll("memberIds").map(String);
4334
- await api(item ? `/api/races/${item.id}` : `/api/works/${state.work.id}/races`, { method: item ? "PATCH" : "POST", body });
4335
- await renderRaces();
5004
+ const isRace = kind === "race";
5005
+ const module = isRace ? "races" : "organizations";
5006
+ const label = isRace ? "种族" : "组织";
5007
+ const parentOptions = isRace
5008
+ ? [["", "无(根种族)"], ...eligibleRaceParents(state.races, item?.id)
5009
+ .sort((left, right) => racePathLabel(left).localeCompare(racePathLabel(right), "zh-CN"))
5010
+ .map((race) => [race.id, racePathLabel(race)])]
5011
+ : [];
5012
+ knowledgeEditorItem = item ?? null;
5013
+ knowledgeEditorKind = kind;
5014
+ knowledgeEditorSections = normalizeKnowledgeEditorSections(item);
5015
+ knowledgeSectionEditorIndex = null;
5016
+ knowledgeSectionEditorDirty = false;
5017
+ $("#knowledge-editor-eyebrow").textContent = item ? `${label}档案` : `建立${label}档案`;
5018
+ $("#knowledge-editor-title").textContent = item?.name || `新建${label}`;
5019
+ $("#knowledge-editor-version").textContent = item ? `v${item.versionNo ?? 1}` : "新档案";
5020
+ $("#knowledge-editor-header-note").textContent = isRace ? "层级、共同设定与角色归属" : "简介、组织设定与成员归属";
5021
+ $("#knowledge-editor-footer-note").textContent = `保存${label}档案后返回${isRace ? "种族" : "组织"}列表。`;
5022
+ $("#knowledge-editor-submit").textContent = item ? `保存${label}档案` : `创建${label}档案`;
5023
+ const historyButton = $("#knowledge-editor-history");
5024
+ const mergeButton = $("#knowledge-editor-merge");
5025
+ const deleteButton = $("#knowledge-editor-delete");
5026
+ const refresh = isRace ? renderRaces : renderOrganizations;
5027
+ const candidates = isRace ? state.races : state.organizations;
5028
+ const typeLabel = label;
5029
+ historyButton.classList.toggle("hidden", !item);
5030
+ mergeButton.classList.toggle("hidden", !item || !canEditModule(module) || candidates.length < 2);
5031
+ deleteButton.classList.toggle("hidden", !item || !canEditModule(module));
5032
+ historyButton.onclick = async () => {
5033
+ if (!item) return;
5034
+ if (!(await closeEntityEditor())) return;
5035
+ openEntityHistory(kind, item.id, item.name, async () => { await refresh(); await loadAiReferences(); });
5036
+ };
5037
+ mergeButton.onclick = async () => {
5038
+ if (!item) return;
5039
+ await closeEntityEditor({ force: true });
5040
+ openEntityMergeDialog({
5041
+ typeLabel,
5042
+ source: item,
5043
+ candidates,
5044
+ endpoint: (source) => `/api/${module}/${encodeURIComponent(source.id)}/merge`,
5045
+ body: (target) => isRace ? { targetRaceId: target.id } : { targetOrganizationId: target.id },
5046
+ refresh,
5047
+ impact: isRace ? "来源种族的角色、子种族、简介与共同设定会迁移到目标种族。" : "来源组织的成员、简介与组织设定会迁移到目标组织。"
5048
+ });
5049
+ };
5050
+ deleteButton.onclick = () => {
5051
+ if (!item) return;
5052
+ void deleteManagedEntity({
5053
+ typeLabel,
5054
+ item,
5055
+ endpoint: (source) => `/api/${module}/${encodeURIComponent(source.id)}`,
5056
+ refresh: () => closeEntityEditor({ force: true }),
5057
+ warning: isRace ? "已绑定角色将变为未指定种族;有子种族时需先迁移或合并。" : "角色与该组织的成员关系会一并移除。"
5058
+ });
5059
+ };
5060
+ renderKnowledgeEditorFields(kind, item, memberOptions, parentOptions);
5061
+ const viewOnly = !canEditModule(module);
5062
+ if (viewOnly) {
5063
+ $("#knowledge-editor-eyebrow").textContent = `${label}档案`;
5064
+ $("#knowledge-editor-fields").querySelectorAll("input, textarea").forEach((control) => { control.readOnly = true; });
5065
+ $("#knowledge-editor-fields").querySelectorAll("select, input[type='checkbox']").forEach((control) => { control.disabled = true; });
5066
+ }
5067
+ $("#knowledge-editor-submit").classList.toggle("hidden", viewOnly);
5068
+ const form = $("#knowledge-editor-form");
5069
+ form.onsubmit = async (event) => {
5070
+ event.preventDefault();
5071
+ if (!canEditModule(module)) return;
5072
+ const submit = $("#knowledge-editor-submit");
5073
+ submit.disabled = true;
5074
+ try {
5075
+ const data = new FormData(form);
5076
+ const name = String(data.get("name") ?? "").trim();
5077
+ if (!name) throw new Error(`请填写${label}名称`);
5078
+ const settingsSections = knowledgeEditorSections
5079
+ .map((section, index) => ({ title: String(section.title ?? "").trim(), contentMarkdown: String(section.contentMarkdown ?? ""), summary: String(section.summary ?? "").trim(), sortOrder: index }))
5080
+ .filter((section) => section.title || section.contentMarkdown.trim());
5081
+ const untitled = settingsSections.findIndex((section) => !section.title);
5082
+ if (untitled >= 0) throw new Error(`请填写第 ${untitled + 1} 条 Markdown 设定的标题`);
5083
+ const settingsMarkdown = settingsSections.map((section) => section.contentMarkdown).join("\n\n");
5084
+ const body = isRace
5085
+ ? { name, parentRaceId: data.get("parentRaceId") || null, description: data.get("description"), settingsMarkdown, settingsSections }
5086
+ : { name, description: data.get("description"), settingsMarkdown, settingsSections };
5087
+ if (canReadModule("characters")) body.memberIds = data.getAll("memberIds").map(String);
5088
+ const wasEditing = Boolean(knowledgeEditorItem);
5089
+ await api(wasEditing ? `/api/${module}/${knowledgeEditorItem.id}` : `/api/works/${state.work.id}/${module}`, { method: wasEditing ? "PATCH" : "POST", body });
5090
+ await cleanupPendingMarkdownAttachments(settingsMarkdown);
5091
+ entityEditorDirty = false;
4336
5092
  await loadAiReferences();
4337
- }, item ? "种族档案" : "作品内种族");
5093
+ await closeEntityEditor({ force: true });
5094
+ toast(wasEditing ? `${label}档案已保存` : `${label}档案已创建`);
5095
+ } catch (error) {
5096
+ toast(error.message, "error");
5097
+ } finally {
5098
+ submit.disabled = false;
5099
+ }
5100
+ };
5101
+ showEntityEditorPage(kind);
5102
+ $("#knowledge-editor-fields").querySelector("input:not([type='checkbox']), textarea")?.focus();
5103
+ }
5104
+
5105
+ async function openRaceDialog(item) {
5106
+ await openKnowledgeEditor("race", item);
4338
5107
  }
4339
5108
 
4340
5109
  async function openOrganizationDialog(item) {
4341
- state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
4342
- const memberOptions = state.characters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
4343
- openDialog(item ? "编辑组织" : "新建组织",
4344
- field("name", "组织名称", "text", item?.name) +
4345
- field("description", "组织简介", "textarea", item?.description) +
4346
- field("settings", "组织设定(逐条填写)", "item-list", item?.settings ?? []) +
4347
- (memberOptions.length ? field("memberIds", "组织成员(可多选)", "chips", item?.memberIds ?? [], memberOptions) : ""),
4348
- async (form) => {
4349
- const settings = form.getAll("settings").map((value) => String(value).trim()).filter(Boolean);
4350
- const body = { name: form.get("name"), description: form.get("description"), settings };
4351
- if (canReadModule("characters")) body.memberIds = form.getAll("memberIds").map(String);
4352
- await api(item ? `/api/organizations/${item.id}` : `/api/works/${state.work.id}/organizations`, { method: item ? "PATCH" : "POST", body });
4353
- await renderOrganizations();
4354
- await loadAiReferences();
4355
- }, item ? "组织档案" : "世界内组织");
5110
+ await openKnowledgeEditor("organization", item);
4356
5111
  }
4357
5112
 
4358
5113
  function openTimelineTrackDialog(item) {
@@ -4493,16 +5248,29 @@ function openProviderDialog(item) {
4493
5248
 
4494
5249
  function openModelDialog(providerId, item = null) {
4495
5250
  const values = modelFormValues(item);
4496
- openDialog(item ? "编辑模型" : "添加模型", field("displayName", "显示名称", "text", values.displayName) + field("modelId", "模型标识符", "text", values.modelId) + field("purposes", "支持用途(可多选)", "chips", values.purposes, MODEL_PURPOSE_OPTIONS) + field("contextWindow", "模型上下文总量(Token)", "number", values.contextWindow) + field("temperature", "默认温度", "number", values.temperature) + field("maxTokens", "默认 max_tokens", "number", values.maxTokens) + field("thinkingEnabled", "开启 Thinking(供应商需支持 thinking 参数)", "checkbox", values.thinkingEnabled) + field("enabled", "启用模型", "checkbox", values.enabled), async (form) => {
5251
+ 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>`;
5252
+ openDialog(item ? "编辑模型" : "添加模型", field("displayName", "显示名称", "text", values.displayName) + field("modelId", "模型标识符", "text", values.modelId) + field("purposes", "支持用途(可多选)", "chips", values.purposes, MODEL_PURPOSE_OPTIONS) + field("contextWindow", "模型上下文总量(Token)", "number", values.contextWindow) + temperatureField + field("maxTokens", "默认 max_tokens", "number", values.maxTokens) + field("thinkingEnabled", "开启 Thinking(供应商需支持 thinking 参数)", "checkbox", values.thinkingEnabled) + field("enabled", "启用模型", "checkbox", values.enabled), async (form) => {
4497
5253
  const body = modelPayload({ displayName: form.get("displayName"), modelId: form.get("modelId"), purposes: form.getAll("purposes"), contextWindow: form.get("contextWindow"), temperature: form.get("temperature"), maxTokens: form.get("maxTokens"), thinkingEnabled: form.get("thinkingEnabled") === "on", enabled: form.get("enabled") === "on" }, item?.preset);
4498
5254
  await api(item ? `/api/models/${item.id}` : `/api/providers/${providerId}/models`, { method: item ? "PATCH" : "POST", body });
4499
5255
  await renderPlatformAiConfig();
4500
5256
  await loadModels();
4501
5257
  }, item ? "模型配置" : "供应商模型");
5258
+ const modelIdInput = $("#dialog-fields input[name='modelId']");
5259
+ const temperatureInput = $("#dialog-fields input[name='temperature']");
5260
+ const temperatureHint = $("#model-temperature-hint");
5261
+ const syncKimiTemperature = () => {
5262
+ const isKimi = isKimiModelId(modelIdInput.value);
5263
+ temperatureHint.hidden = !isKimi;
5264
+ };
5265
+ modelIdInput.addEventListener("input", () => {
5266
+ if (isKimiModelId(modelIdInput.value)) temperatureInput.value = "1";
5267
+ syncKimiTemperature();
5268
+ });
5269
+ syncKimiTemperature();
4502
5270
  }
4503
5271
 
4504
5272
  async function sendAi() {
4505
- if (!state.work || !state.chapter) return toast("请先选择章节", "error");
5273
+ if (!state.work) return toast("请先选择作品", "error");
4506
5274
  try {
4507
5275
  await Promise.all([ensureAiModelsLoaded(), ensureAiConversationsLoaded()]);
4508
5276
  } catch (error) {
@@ -4758,7 +5526,10 @@ async function showVersions() {
4758
5526
  const versions = await api(`/api/chapters/${state.chapter.id}/versions`);
4759
5527
  $("#versions-list").innerHTML = versions.map((version) => `<div class="version-row"><div><b>v${version.versionNo}</b><small>${esc(version.source)} · ${esc(version.actor || "历史数据")}</small></div><p>${esc(version.content.slice(0, 300) || "空白章节")}</p>${canEditProse() ? `<button class="ghost-button" data-restore-version="${version.versionNo}">恢复</button>` : ""}</div>`).join("");
4760
5528
  $("#versions-list").querySelectorAll("[data-restore-version]").forEach((button) => button.addEventListener("click", async () => {
4761
- if (!window.confirm(`将版本 v${button.dataset.restoreVersion} 恢复为一个新的保存版本?`)) return;
5529
+ if (!(await confirmToast(
5530
+ `将版本 v${button.dataset.restoreVersion} 恢复为一个新的保存版本?`,
5531
+ { title: "恢复历史版本", confirmLabel: "确认恢复" }
5532
+ ))) return;
4762
5533
  state.chapter = await api(`/api/chapters/${state.chapter.id}/restore`, { method: "POST", body: { versionNo: Number(button.dataset.restoreVersion) } });
4763
5534
  lastSavedChapterSnapshot = { chapterId: state.chapter.id, title: state.chapter.title, content: state.chapter.content };
4764
5535
  $("#chapter-title").value = state.chapter.title;
@@ -4820,7 +5591,7 @@ function renderImportHistory(versions, nextPage = null) {
4820
5591
  }, 5000);
4821
5592
  return;
4822
5593
  }
4823
- if (!confirmDiscardChanges("当前章节有未保存修改,恢复正文会丢弃这些本地修改。是否继续?")) return;
5594
+ if (!(await confirmDiscardChanges("当前章节有未保存修改,恢复正文会丢弃这些本地修改。是否继续?"))) return;
4824
5595
  button.disabled = true;
4825
5596
  cancelChapterAutoSave();
4826
5597
  const workId = state.work.id;
@@ -4901,11 +5672,13 @@ async function showChapterInsight() {
4901
5672
  panel.innerHTML = `<strong>章节概览${esc(stale)}</strong>${esc(insight.summary || "暂无梗概")}${eventNames.length ? `<br><strong>事件</strong>${esc(eventNames.join(";"))}` : ""}${insight.uncertainties.length ? `<br><strong>待确认</strong>${esc(insight.uncertainties.map((item) => typeof item === "string" ? item : JSON.stringify(item)).join(";"))}` : ""}`;
4902
5673
  }
4903
5674
 
4904
- $("#home-button").addEventListener("click", () => {
4905
- if (!confirmDiscardChanges()) return;
5675
+ $("#home-button").addEventListener("click", async () => {
5676
+ if (!(await confirmDiscardChanges())) return;
4906
5677
  loadWorks().catch((error) => toast(error.message, "error"));
4907
5678
  });
4908
- $("#settings-button").addEventListener("click", showSettingsHub);
5679
+ $("#settings-button").addEventListener("click", () => {
5680
+ void showSettingsHub();
5681
+ });
4909
5682
  $("#account-button").addEventListener("click", () => {
4910
5683
  const expanded = $("#account-menu").classList.toggle("hidden") === false;
4911
5684
  $("#account-button").setAttribute("aria-expanded", String(expanded));
@@ -4986,7 +5759,7 @@ $("#avatar-file").addEventListener("change", async (event) => {
4986
5759
  }
4987
5760
  });
4988
5761
  $("#avatar-remove-button").addEventListener("click", async () => {
4989
- if (!state.user?.avatarUrl || !window.confirm("确定移除当前头像吗?")) return;
5762
+ if (!state.user?.avatarUrl || !(await confirmToast("确定移除当前头像吗?", { title: "移除头像", confirmLabel: "确认移除" }))) return;
4990
5763
  $("#avatar-upload-button").disabled = true;
4991
5764
  $("#avatar-remove-button").disabled = true;
4992
5765
  try {
@@ -5014,14 +5787,35 @@ $("#profile-form").addEventListener("submit", async (event) => {
5014
5787
  $("#password-form").addEventListener("submit", async (event) => {
5015
5788
  event.preventDefault();
5016
5789
  const form = new FormData(event.currentTarget);
5790
+ const newPassword = String(form.get("newPassword") ?? "");
5791
+ const passwordConfirmation = String(form.get("passwordConfirmation") ?? "");
5792
+ if (newPassword !== passwordConfirmation) {
5793
+ const confirmationInput = $("#password-form input[name='passwordConfirmation']");
5794
+ confirmationInput.setCustomValidity("两次输入的密码不一致");
5795
+ confirmationInput.reportValidity();
5796
+ confirmationInput.focus();
5797
+ return;
5798
+ }
5017
5799
  try {
5018
- await api("/api/auth/password", { method: "PATCH", body: { currentPassword: form.get("currentPassword"), newPassword: form.get("newPassword") } });
5800
+ await api("/api/auth/password", { method: "PATCH", body: { currentPassword: form.get("currentPassword"), newPassword, passwordConfirmation } });
5019
5801
  event.currentTarget.reset();
5020
5802
  toast("密码已更新,其他设备的会话已退出");
5021
5803
  } catch (error) { toast(error.message, "error"); }
5022
5804
  });
5805
+ function validatePasswordChangeConfirmation() {
5806
+ const newPasswordInput = $("#password-form input[name='newPassword']");
5807
+ const confirmationInput = $("#password-form input[name='passwordConfirmation']");
5808
+ const matches = !confirmationInput.value || newPasswordInput.value === confirmationInput.value;
5809
+ confirmationInput.setCustomValidity(matches ? "" : "两次输入的密码不一致");
5810
+ return matches;
5811
+ }
5812
+ $("#password-form input[name='newPassword']").addEventListener("input", validatePasswordChangeConfirmation);
5813
+ $("#password-form input[name='passwordConfirmation']").addEventListener("input", validatePasswordChangeConfirmation);
5023
5814
  $("#api-key-reset-button").addEventListener("click", async () => {
5024
- if ($("#api-key-reset-button").textContent.includes("重置") && !window.confirm("重置后,所有使用旧 API Key 的 CLI 会立即退出登录。确定继续吗?")) return;
5815
+ if ($("#api-key-reset-button").textContent.includes("重置") && !(await confirmToast(
5816
+ "重置后,所有使用旧 API Key 的 CLI 会立刻退出登录。确定继续吗?",
5817
+ { title: "重置 API Key", confirmLabel: "确认重置" }
5818
+ ))) return;
5025
5819
  try {
5026
5820
  const result = await api("/api/auth/api-key/reset", { method: "POST", body: {} });
5027
5821
  $("#api-key-status").textContent = `已配置 ${result.prefix}…,尚未使用`;
@@ -5195,24 +5989,26 @@ $("#member-permission-form").addEventListener("submit", async (event) => {
5195
5989
  $("#platform-new-provider").addEventListener("click", () => openProviderDialog());
5196
5990
  $("#shelf-new-work").addEventListener("click", openWorkDialog);
5197
5991
  $("#welcome-new-work").addEventListener("click", () => state.work ? openChapterDialog() : openWorkDialog());
5198
- $("#new-chapter-button").addEventListener("click", openChapterDialog);
5199
5992
  $("#save-button").addEventListener("click", saveChapter);
5200
5993
  $("#tidy-blank-lines-button").addEventListener("click", tidyChapterBlankLines);
5201
5994
  $("#new-volume-button").addEventListener("click", () => openVolumeDialog());
5202
5995
  $("#insight-button").addEventListener("click", () => showChapterInsight().catch((error) => toast(error.message, "error")));
5203
5996
  $("#versions-button").addEventListener("click", showVersions);
5204
5997
  $("#versions-close").addEventListener("click", () => $("#versions-dialog").close());
5205
- $("#import-history-button").addEventListener("click", () => openImportHistory());
5206
5998
  $("#import-history-close").addEventListener("click", () => $("#import-history-dialog").close());
5207
5999
  $("#entity-history-close").addEventListener("click", () => $("#entity-history-dialog").close());
5208
6000
  $("#ai-tool-call-close").addEventListener("click", () => $("#ai-tool-call-dialog").close());
5209
6001
  $("#setting-editor-back").addEventListener("click", () => { void closeEntityEditor(); });
5210
6002
  $("#character-editor-close").addEventListener("click", () => { void closeEntityEditor(); });
5211
6003
  $("#character-editor-cancel").addEventListener("click", () => { void closeEntityEditor(); });
6004
+ $("#knowledge-editor-close").addEventListener("click", () => { void closeEntityEditor(); });
6005
+ $("#knowledge-editor-cancel").addEventListener("click", () => { void closeEntityEditor(); });
5212
6006
  $("#setting-editor-form").addEventListener("input", markEntityEditorDirty);
5213
6007
  $("#setting-editor-form").addEventListener("change", markEntityEditorDirty);
5214
6008
  $("#character-editor-form").addEventListener("input", markEntityEditorDirty);
5215
6009
  $("#character-editor-form").addEventListener("change", markEntityEditorDirty);
6010
+ $("#knowledge-editor-form").addEventListener("input", markEntityEditorDirty);
6011
+ $("#knowledge-editor-form").addEventListener("change", markEntityEditorDirty);
5216
6012
  $("#character-editor-fields").addEventListener("click", (event) => {
5217
6013
  if (event.target.closest("[data-item-list-add], [data-structured-list-add], [data-item-list-remove], [data-structured-list-remove]")) markEntityEditorDirty();
5218
6014
  });
@@ -5228,6 +6024,7 @@ function cleanupExpandedRelationshipMap() {
5228
6024
  $("#relationship-map-close").addEventListener("click", () => $("#relationship-map-dialog").close());
5229
6025
  $("#relationship-map-dialog").addEventListener("close", cleanupExpandedRelationshipMap);
5230
6026
  $("#appearance-button").addEventListener("click", openAppearanceDialog);
6027
+ $("#toggle-whitespace-appearance").addEventListener("click", toggleChapterWhitespaceVisibility);
5231
6028
  $("#theme-toggle").addEventListener("click", () => {
5232
6029
  const theme = nextTheme(currentColorTheme());
5233
6030
  const persisted = saveColorTheme(theme);
@@ -5259,10 +6056,6 @@ $("#chapter-content").addEventListener("input", (event) => {
5259
6056
  });
5260
6057
  $("#chapter-content").addEventListener("select", scheduleAiContextUsage);
5261
6058
  $("#chapter-content").addEventListener("scroll", syncChapterLineNumberScroll);
5262
- $("#toggle-whitespace-button").addEventListener("click", () => {
5263
- chapterWhitespaceVisible = !chapterWhitespaceVisible;
5264
- scheduleChapterLineNumbers();
5265
- });
5266
6059
  $("#chapter-line-numbers-inner").addEventListener("pointerdown", (event) => {
5267
6060
  const row = event.target.closest(".chapter-line-number");
5268
6061
  if (!row || event.button !== 0) return;
@@ -5339,6 +6132,13 @@ $("#ai-mention-menu").addEventListener("click", (event) => {
5339
6132
  const button = event.target.closest("[data-ai-reference-id]");
5340
6133
  if (button) selectAiMention(button);
5341
6134
  });
6135
+ $("#import-file-button").addEventListener("click", (event) => {
6136
+ if (!state.work || canEditProse()) return;
6137
+ event.preventDefault();
6138
+ event.stopPropagation();
6139
+ $("#import-file").value = "";
6140
+ toast("当前权限只能编辑设定资料,不能导入正文", "error");
6141
+ });
5342
6142
  $("#import-file").addEventListener("change", async (event) => {
5343
6143
  const file = event.target.files[0];
5344
6144
  if (!state.work || !file) return;
@@ -5352,6 +6152,13 @@ $("#import-file").addEventListener("change", async (event) => {
5352
6152
  event.target.value = "";
5353
6153
  return;
5354
6154
  }
6155
+ if (mode === "overwrite" && !(await confirmToast(
6156
+ `当前选项:覆盖正文。确认后将使用“${file.name}”替换《${state.work.title}》的现有正文目录。`,
6157
+ { title: "覆盖正文二次确认", confirmLabel: "确认覆盖" }
6158
+ ))) {
6159
+ event.target.value = "";
6160
+ return;
6161
+ }
5355
6162
  cancelChapterAutoSave();
5356
6163
  const body = new FormData();
5357
6164
  body.append("file", file);
@@ -5450,6 +6257,13 @@ document.addEventListener("keydown", (event) => {
5450
6257
  hideAiMentionMenu();
5451
6258
  }
5452
6259
  });
6260
+ document.addEventListener("keydown", (event) => {
6261
+ if (!isGlobalSearchShortcut(event) || !state.work) return;
6262
+ event.preventDefault();
6263
+ event.stopPropagation();
6264
+ if (event.repeat) return;
6265
+ openSearchDialog().catch((error) => toast(error.message, "error"));
6266
+ }, { capture: true });
5453
6267
  $("#ai-send").addEventListener("click", sendAi);
5454
6268
  $("#ai-new-conversation").addEventListener("click", async () => {
5455
6269
  const button = $("#ai-new-conversation");