@gientech/modual 2.0.8 → 2.0.9

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 (415) hide show
  1. package/.editorconfig +38 -0
  2. package/.prettierignore +16 -0
  3. package/.prettierrc +17 -0
  4. package/INSTALL_TROUBLESHOOTING.md +316 -0
  5. package/USAGE.md +247 -0
  6. package/bash.exe.stackdump +40 -0
  7. package/components.json +21 -0
  8. package/dist/README.md +761 -0
  9. package/dist/package.json +68 -0
  10. package/doc_assets/2.png +0 -0
  11. package/doc_assets/demo.md +27 -0
  12. package/doc_assets/demos/dist-app/assets/index.Dh-ZAS9Z.css +2 -0
  13. package/doc_assets/demos/dist-app/assets/index.Dv8KVW18.js +23699 -0
  14. package/doc_assets/demos/dist-app/assets/index.Dv8KVW18.js.map +1 -0
  15. package/doc_assets/demos/dist-app/index.html +14 -0
  16. package/doc_assets/demos/dist-app/vite.svg +1 -0
  17. package/doc_assets/images/1.png +0 -0
  18. package/doc_assets/images/3.png +0 -0
  19. package/doc_assets/images/component-screenshot.png +1 -0
  20. package/doc_assets/install.md +5 -0
  21. package/doc_assets/v2.0.7/345/217/230/346/233/264/346/200/273/347/273/223.md +115 -0
  22. package/doc_assets//346/226/271/346/241/210//344/274/230/345/214/226/346/226/271/346/241/210-/345/244/232/344/274/232/350/257/235SSE/350/277/236/346/216/245/347/256/241/347/220/206.md +504 -0
  23. package/eslint.config.js +92 -0
  24. package/index.html +13 -0
  25. package/package.json +103 -41
  26. package/package.json.demo-backup +109 -0
  27. package/postcss.config.cjs +19 -0
  28. package/public/icons/answerAwartar.png +0 -0
  29. package/public/icons/docx-file.png +0 -0
  30. package/public/icons/folder.png +0 -0
  31. package/public/icons/html.png +0 -0
  32. package/public/icons/image.png +0 -0
  33. package/public/icons/jpg-file.png +0 -0
  34. package/public/icons/json.png +0 -0
  35. package/public/icons/md.png +0 -0
  36. package/public/icons/pdf.png +0 -0
  37. package/public/icons/pptx.png +0 -0
  38. package/public/icons/questionAwartar.png +0 -0
  39. package/public/icons/sheets.png +0 -0
  40. package/public/icons/txt.png +0 -0
  41. package/public/icons/xlsx.png +0 -0
  42. package/public/vite.svg +1 -0
  43. package/public/worker/pdf.worker.min.js +22 -0
  44. package/scripts/README.md +133 -0
  45. package/scripts/build-demo.js +88 -0
  46. package/scripts/decrypt-api-key.js +95 -0
  47. package/scripts/demo-selector.js +216 -0
  48. package/scripts/dev-demo.js +76 -0
  49. package/scripts/preview-demo.js +130 -0
  50. package/scripts/run-demo.bat +34 -0
  51. package/src/assets/img/close.png +0 -0
  52. package/src/assets/img/database.png +0 -0
  53. package/src/assets/img/downArrow.png +0 -0
  54. package/src/assets/img/downLoad.png +0 -0
  55. package/src/assets/img/excel.png +0 -0
  56. package/src/assets/img/graphIcon.png +0 -0
  57. package/src/assets/img/img.png +0 -0
  58. package/src/assets/img/pdf.png +0 -0
  59. package/src/assets/img/ppt.png +0 -0
  60. package/src/assets/img/singleQa.png +0 -0
  61. package/src/assets/img/txt.png +0 -0
  62. package/src/assets/img/webSearch.png +0 -0
  63. package/src/assets/img/word.png +0 -0
  64. package/src/assets/login/homeBg.png +0 -0
  65. package/src/assets/login/left.jpg +0 -0
  66. package/src/assets/login/logoImg.png +0 -0
  67. package/src/examples/ConversationAssistantPage/index.tsx +41 -0
  68. package/src/examples/Demo/index.tsx +12 -0
  69. package/src/examples/LoginPage/index.tsx +18 -0
  70. package/src/examples/chat/components/DrawerGraphPreview.tsx +78 -0
  71. package/src/examples/chat/index.tsx +166 -0
  72. package/src/examples/chat/logo03.png +0 -0
  73. package/src/examples/gientechStreamFilesReader/index.tsx +951 -0
  74. package/src/examples/headlessChat/assets/mind.svg +6 -0
  75. package/src/examples/headlessChat/assets/net.svg +7 -0
  76. package/src/examples/headlessChat/index.tsx +285 -0
  77. package/src/examples/ragDatabaseDataPage/index.tsx +40 -0
  78. package/src/examples/ragDatabaseIdPage/index.tsx +47 -0
  79. package/src/examples/ragDatabasePage/index.tsx +36 -0
  80. package/src/examples/ragModelManagePage/index.tsx +38 -0
  81. package/src/examples/ragSearchPage/index.tsx +0 -0
  82. package/src/examples/ragSensitiveWordsPage/index.tsx +32 -0
  83. package/src/examples/streamFiles/index.tsx +417 -0
  84. package/src/lib_enter.ts +43 -0
  85. package/src/main.tsx +5 -0
  86. package/src/main.tsx.backup +5 -0
  87. package/src/modules/CHAT_UNIFICATION_PLAN.md +324 -0
  88. package/src/modules/assistantConfig/assets/databse.svg +6 -0
  89. package/src/modules/assistantConfig/assets/empty.png +0 -0
  90. package/src/modules/assistantConfig/assets/graph.svg +4 -0
  91. package/src/modules/assistantConfig/assets/knowledge.svg +4 -0
  92. package/src/modules/assistantConfig/assets/sensitive.svg +5 -0
  93. package/src/modules/assistantConfig/components/Database.tsx +171 -0
  94. package/src/modules/assistantConfig/components/Graph.tsx +177 -0
  95. package/src/modules/assistantConfig/components/Knowledge.tsx +276 -0
  96. package/src/modules/assistantConfig/components/NotFoundContent.tsx +21 -0
  97. package/src/modules/assistantConfig/components/Paragraph.tsx +51 -0
  98. package/src/modules/assistantConfig/components/ParamsItem.tsx +39 -0
  99. package/src/modules/assistantConfig/components/ResourceBinderItem.tsx +133 -0
  100. package/src/modules/assistantConfig/components/SearchableSelector.tsx +500 -0
  101. package/src/modules/assistantConfig/components/Sensitive.tsx +221 -0
  102. package/src/modules/assistantConfig/components/SliderInput.tsx +65 -0
  103. package/src/modules/assistantConfig/constants.tsx +75 -0
  104. package/src/modules/assistantConfig/index.tsx +710 -0
  105. package/src/modules/assistantConfig/server.ts +262 -0
  106. package/src/modules/chat/Conversations/Item.tsx +167 -0
  107. package/src/modules/chat/Conversations/List.tsx +210 -0
  108. package/src/modules/chat/Conversations/groupByTime.ts +39 -0
  109. package/src/modules/chat/Conversations/index.tsx +252 -0
  110. package/src/modules/chat/ReferenceBar.tsx +622 -0
  111. package/src/modules/chat/constants.tsx +57 -0
  112. package/src/modules/chat/index.tsx +2449 -0
  113. package/src/modules/chat/referenceCom/DeleteModal.tsx +75 -0
  114. package/src/modules/chat/referenceCom/DrawerContent.tsx +136 -0
  115. package/src/modules/chat/referenceCom/DrawerDatabase.tsx +102 -0
  116. package/src/modules/chat/referenceCom/DrawerGraphPreview.tsx +86 -0
  117. package/src/modules/chat/referenceCom/DrawerPreview.tsx +73 -0
  118. package/src/modules/chat/referenceCom/DrawerTitle.tsx +26 -0
  119. package/src/modules/chat/referenceCom/RenameModal.tsx +86 -0
  120. package/src/modules/chat/referenceCom/TagCom.tsx +30 -0
  121. package/src/modules/chat/style.less +3 -0
  122. package/src/modules/chat/types.ts +17 -0
  123. package/src/modules/chat/utils/index.ts +348 -0
  124. package/src/modules/database/CreateModal.tsx +403 -0
  125. package/src/modules/database/assets/Doris.png +0 -0
  126. package/src/modules/database/assets/PostgreSQL.png +0 -0
  127. package/src/modules/database/assets/SQLServer.png +0 -0
  128. package/src/modules/database/assets/database.svg +11 -0
  129. package/src/modules/database/assets/database_add.svg +53 -0
  130. package/src/modules/database/assets/database_connect.svg +66 -0
  131. package/src/modules/database/assets/database_upload.svg +29 -0
  132. package/src/modules/database/assets/empty.png +0 -0
  133. package/src/modules/database/assets/mysql.svg +14 -0
  134. package/src/modules/database/index.tsx +477 -0
  135. package/src/modules/database/server.ts +196 -0
  136. package/src/modules/databaseId/CustomCom.tsx +156 -0
  137. package/src/modules/databaseId/EditConfig.tsx +280 -0
  138. package/src/modules/databaseId/UploadDrawer.tsx +535 -0
  139. package/src/modules/databaseId/assets/aiOptimize.svg +10 -0
  140. package/src/modules/databaseId/assets/empty.png +0 -0
  141. package/src/modules/databaseId/assets/template.svg +6 -0
  142. package/src/modules/databaseId/assets/upload.svg +9 -0
  143. package/src/modules/databaseId/assets/useTemp.svg +6 -0
  144. package/src/modules/databaseId/index.tsx +769 -0
  145. package/src/modules/databaseId/server.ts +286 -0
  146. package/src/modules/databaseId/style.css +5 -0
  147. package/src/modules/databaseTable/EditRowDrawer.tsx +124 -0
  148. package/src/modules/databaseTable/index.tsx +359 -0
  149. package/src/modules/databaseTable/server.ts +180 -0
  150. package/src/modules/headlessChat/ReferenceBar.tsx +783 -0
  151. package/src/modules/headlessChat/constants.tsx +54 -0
  152. package/src/modules/headlessChat/index.tsx +1782 -0
  153. package/src/modules/headlessChat/referenceCom/DeleteModal.tsx +75 -0
  154. package/src/modules/headlessChat/referenceCom/DrawerContent.tsx +136 -0
  155. package/src/modules/headlessChat/referenceCom/DrawerDatabase.tsx +102 -0
  156. package/src/modules/headlessChat/referenceCom/DrawerGraphPreview.tsx +86 -0
  157. package/src/modules/headlessChat/referenceCom/DrawerPreview.tsx +73 -0
  158. package/src/modules/headlessChat/referenceCom/DrawerTitle.tsx +26 -0
  159. package/src/modules/headlessChat/referenceCom/RenameModal.tsx +86 -0
  160. package/src/modules/headlessChat/referenceCom/TagCom.tsx +30 -0
  161. package/src/modules/headlessChat/style.less +3 -0
  162. package/src/modules/headlessChat/types.ts +23 -0
  163. package/src/modules/headlessChat/utils/index.ts +348 -0
  164. package/src/modules/login/components/Login/LoginBox/index.tsx +102 -0
  165. package/src/modules/login/components/Login/RegisterBox/index.tsx +180 -0
  166. package/src/modules/login/components/Login/index.tsx +100 -0
  167. package/src/modules/login/index.tsx +106 -0
  168. package/src/modules/login/style.css +3 -0
  169. package/src/modules/login/useServices.ts +53 -0
  170. package/src/modules/login/utils.ts +42 -0
  171. package/src/modules/modelManage/ConfigDrawer.tsx +249 -0
  172. package/src/modules/modelManage/ReplaceModal.tsx +124 -0
  173. package/src/modules/modelManage/assets/empty.png +0 -0
  174. package/src/modules/modelManage/const.ts +51 -0
  175. package/src/modules/modelManage/index.tsx +606 -0
  176. package/src/modules/modelManage/server.ts +223 -0
  177. package/src/modules/nodegraph/index.tsx +1 -0
  178. package/src/modules/search/assets/Icon-history.svg +8 -0
  179. package/src/modules/search/assets/answerAwartar.png +0 -0
  180. package/src/modules/search/assets/doc.png +0 -0
  181. package/src/modules/search/assets/genera.gif +0 -0
  182. package/src/modules/search/assets/icon-robot.svg +9 -0
  183. package/src/modules/search/assets/icon-search-bar.svg +14 -0
  184. package/src/modules/search/assets/icon-sub-title.svg +3 -0
  185. package/src/modules/search/assets/icon-title.svg +9 -0
  186. package/src/modules/search/assets/icon-zoomOut.svg +9 -0
  187. package/src/modules/search/assets/iconAi.svg +9 -0
  188. package/src/modules/search/assets/pdf.png +0 -0
  189. package/src/modules/search/assets/ppt.png +0 -0
  190. package/src/modules/search/assets/search.svg +3 -0
  191. package/src/modules/search/assets/selected.svg +4 -0
  192. package/src/modules/search/assets/txt.png +0 -0
  193. package/src/modules/search/assets/xls.png +0 -0
  194. package/src/modules/search/components/AssisSelect.tsx +137 -0
  195. package/src/modules/search/components/Editor/ChatViewEditor.tsx +261 -0
  196. package/src/modules/search/components/Editor/aichat.css +1 -0
  197. package/src/modules/search/components/Editor/constant.ts +13 -0
  198. package/src/modules/search/components/Editor/index.tsx +113 -0
  199. package/src/modules/search/components/Editor/plugins/autofomatRules.ts +332 -0
  200. package/src/modules/search/components/Editor/plugins/convertImgPlugins.tsx +20 -0
  201. package/src/modules/search/components/Editor/plugins/createIndexes.tsx +38 -0
  202. package/src/modules/search/components/Editor/plugins/displayer.ts +298 -0
  203. package/src/modules/search/components/Editor/plugins/imageClick.tsx +32 -0
  204. package/src/modules/search/components/Editor/plugins/myplugin.tsx +98 -0
  205. package/src/modules/search/components/Editor/ui/avatar.tsx +19 -0
  206. package/src/modules/search/components/Editor/ui/blockquote-element.tsx +21 -0
  207. package/src/modules/search/components/Editor/ui/button.tsx +58 -0
  208. package/src/modules/search/components/Editor/ui/calendar.tsx +68 -0
  209. package/src/modules/search/components/Editor/ui/caption.tsx +46 -0
  210. package/src/modules/search/components/Editor/ui/checkbox.tsx +27 -0
  211. package/src/modules/search/components/Editor/ui/code-block-combobox.tsx +188 -0
  212. package/src/modules/search/components/Editor/ui/code-block-element.css +434 -0
  213. package/src/modules/search/components/Editor/ui/code-block-element.tsx +39 -0
  214. package/src/modules/search/components/Editor/ui/code-leaf.tsx +24 -0
  215. package/src/modules/search/components/Editor/ui/code-line-element.tsx +10 -0
  216. package/src/modules/search/components/Editor/ui/code-syntax-leaf.tsx +21 -0
  217. package/src/modules/search/components/Editor/ui/column-element.tsx +30 -0
  218. package/src/modules/search/components/Editor/ui/column-group-element.tsx +94 -0
  219. package/src/modules/search/components/Editor/ui/command.tsx +75 -0
  220. package/src/modules/search/components/Editor/ui/comment-avatar.tsx +22 -0
  221. package/src/modules/search/components/Editor/ui/comment-create-form.tsx +37 -0
  222. package/src/modules/search/components/Editor/ui/comment-item.tsx +74 -0
  223. package/src/modules/search/components/Editor/ui/comment-leaf.tsx +49 -0
  224. package/src/modules/search/components/Editor/ui/comment-more-dropdown.tsx +42 -0
  225. package/src/modules/search/components/Editor/ui/comment-reply-items.tsx +22 -0
  226. package/src/modules/search/components/Editor/ui/comment-resolve-button.tsx +32 -0
  227. package/src/modules/search/components/Editor/ui/comment-value.tsx +34 -0
  228. package/src/modules/search/components/Editor/ui/comments-popover.tsx +63 -0
  229. package/src/modules/search/components/Editor/ui/date-element.tsx +83 -0
  230. package/src/modules/search/components/Editor/ui/dialog.tsx +63 -0
  231. package/src/modules/search/components/Editor/ui/draggable.tsx +177 -0
  232. package/src/modules/search/components/Editor/ui/dropdown-menu.tsx +180 -0
  233. package/src/modules/search/components/Editor/ui/emoji-input-element.tsx +85 -0
  234. package/src/modules/search/components/Editor/ui/excalidraw-element.tsx +28 -0
  235. package/src/modules/search/components/Editor/ui/fixed-toolbar-buttons.tsx +76 -0
  236. package/src/modules/search/components/Editor/ui/fixed-toolbar.tsx +8 -0
  237. package/src/modules/search/components/Editor/ui/floating-toolbar-buttons.tsx +51 -0
  238. package/src/modules/search/components/Editor/ui/floating-toolbar.tsx +77 -0
  239. package/src/modules/search/components/Editor/ui/heading-element.tsx +48 -0
  240. package/src/modules/search/components/Editor/ui/highlight-leaf.tsx +17 -0
  241. package/src/modules/search/components/Editor/ui/hr-element.tsx +30 -0
  242. package/src/modules/search/components/Editor/ui/icons.tsx +267 -0
  243. package/src/modules/search/components/Editor/ui/image-element.tsx +74 -0
  244. package/src/modules/search/components/Editor/ui/inline-combobox.tsx +368 -0
  245. package/src/modules/search/components/Editor/ui/input.tsx +25 -0
  246. package/src/modules/search/components/Editor/ui/insert-dropdown-menu.tsx +218 -0
  247. package/src/modules/search/components/Editor/ui/kbd-leaf.tsx +20 -0
  248. package/src/modules/search/components/Editor/ui/link-element.tsx +29 -0
  249. package/src/modules/search/components/Editor/ui/link-floating-toolbar.tsx +161 -0
  250. package/src/modules/search/components/Editor/ui/list-element.tsx +30 -0
  251. package/src/modules/search/components/Editor/ui/mark-toolbar-button.tsx +24 -0
  252. package/src/modules/search/components/Editor/ui/media-embed-element.tsx +133 -0
  253. package/src/modules/search/components/Editor/ui/media-popover.tsx +97 -0
  254. package/src/modules/search/components/Editor/ui/mention-element.tsx +43 -0
  255. package/src/modules/search/components/Editor/ui/mention-input-element.tsx +141 -0
  256. package/src/modules/search/components/Editor/ui/mode-dropdown-menu.tsx +93 -0
  257. package/src/modules/search/components/Editor/ui/more-dropdown-menu.tsx +67 -0
  258. package/src/modules/search/components/Editor/ui/paragraph-element.tsx +4 -0
  259. package/src/modules/search/components/Editor/ui/placeholder.tsx +52 -0
  260. package/src/modules/search/components/Editor/ui/popover.tsx +32 -0
  261. package/src/modules/search/components/Editor/ui/resizable.tsx +66 -0
  262. package/src/modules/search/components/Editor/ui/separator.tsx +25 -0
  263. package/src/modules/search/components/Editor/ui/style.less +12 -0
  264. package/src/modules/search/components/Editor/ui/table-cell-element.tsx +143 -0
  265. package/src/modules/search/components/Editor/ui/table-element.tsx +243 -0
  266. package/src/modules/search/components/Editor/ui/table-row-element.tsx +22 -0
  267. package/src/modules/search/components/Editor/ui/tableValue.tsx +135 -0
  268. package/src/modules/search/components/Editor/ui/todo-list-element.tsx +43 -0
  269. package/src/modules/search/components/Editor/ui/toggle-element.tsx +31 -0
  270. package/src/modules/search/components/Editor/ui/toolbar.tsx +157 -0
  271. package/src/modules/search/components/Editor/ui/tooltip.tsx +65 -0
  272. package/src/modules/search/components/Editor/ui/turn-into-dropdown-menu.tsx +160 -0
  273. package/src/modules/search/components/Editor/ui/with-draggables.tsx +175 -0
  274. package/src/modules/search/components/FileList.tsx +287 -0
  275. package/src/modules/search/components/ImageGroupView/index.tsx +85 -0
  276. package/src/modules/search/components/ResultContent.tsx +232 -0
  277. package/src/modules/search/components/SearchInput.tsx +232 -0
  278. package/src/modules/search/components/SearchLanding.tsx +74 -0
  279. package/src/modules/search/components/SearchView.tsx +563 -0
  280. package/src/modules/search/components/SimpleEditor.tsx +158 -0
  281. package/src/modules/search/components/SimpleFileList.tsx +215 -0
  282. package/src/modules/search/index.tsx +10 -0
  283. package/src/modules/search/reademe.md +1 -0
  284. package/src/modules/search/servers/apis.tsx +19 -0
  285. package/src/modules/search/servers/index.ts +184 -0
  286. package/src/modules/search/style.less +503 -0
  287. package/src/modules/search/type.ts +22 -0
  288. package/src/modules/search/utils.ts +34 -0
  289. package/src/modules/sensitive/index.tsx +313 -0
  290. package/src/modules/sensitive/server.ts +122 -0
  291. package/src/modules/streamFilesReader/GientechStreamReader.tsx +1625 -0
  292. package/src/modules/streamFilesReader/components/Header/Toolbar.tsx +0 -0
  293. package/src/modules/streamFilesReader/components/Header/index.tsx +297 -0
  294. package/src/modules/streamFilesReader/index.tsx +3 -0
  295. package/src/style.css +6 -0
  296. package/src/type.d.ts +0 -0
  297. package/src/utils/commonFn.tsx +111 -0
  298. package/src/utils/decryptApiKey.ts +40 -0
  299. package/src/utils/gientechCommon/components/AppError.tsx +32 -0
  300. package/src/utils/gientechCommon/components/AppLoading.tsx +75 -0
  301. package/src/utils/gientechCommon/components/DeleteModal.tsx +75 -0
  302. package/src/utils/gientechCommon/components/DisplayError.tsx +33 -0
  303. package/src/utils/gientechCommon/components/DisplayLoading.tsx +38 -0
  304. package/src/utils/gientechCommon/components/FeedBackModal.tsx +319 -0
  305. package/src/utils/gientechCommon/components/FileCardCommon.tsx +82 -0
  306. package/src/utils/gientechCommon/components/FileManager/index.tsx +418 -0
  307. package/src/utils/gientechCommon/components/FileManager/style.css +5 -0
  308. package/src/utils/gientechCommon/components/Messages/GientechNewChatWelcome.tsx +581 -0
  309. package/src/utils/gientechCommon/components/Messages/ReferenceCard.tsx +359 -0
  310. package/src/utils/gientechCommon/components/Messages/RetriveItem.tsx +245 -0
  311. package/src/utils/gientechCommon/components/Messages/WebRetriveItem.tsx +209 -0
  312. package/src/utils/gientechCommon/components/Messages/defaultBot.png +0 -0
  313. package/src/utils/gientechCommon/components/Messages/defaultStyleSet.tsx +148 -0
  314. package/src/utils/gientechCommon/components/Messages/defaultWeLogo.svg +14 -0
  315. package/src/utils/gientechCommon/components/RenameModal.tsx +86 -0
  316. package/src/utils/gientechCommon/components/style.less +11 -0
  317. package/src/utils/gientechCommon/configs/commonConfig.ts +2 -0
  318. package/src/utils/gientechCommon/configs/senderConfig.ts +0 -0
  319. package/src/utils/gientechCommon/configs/stylesConfig.ts +142 -0
  320. package/src/utils/gientechCommon/hooks/AichatUseController.tsx +417 -0
  321. package/src/utils/gientechCommon/hooks/useFileDisplayTools.tsx +251 -0
  322. package/src/utils/gientechCommon/hooks/useFileManager.ts +208 -0
  323. package/src/utils/gientechCommon/slate/converters/deserializers.ts +763 -0
  324. package/src/utils/gientechCommon/slate/converters/mockData.ts +232 -0
  325. package/src/utils/gientechCommon/slate/converters/slateConverters.ts +258 -0
  326. package/src/utils/gientechCommon/slate/richElements/index.tsx +499 -0
  327. package/src/utils/gientechCommon/utils/fileUtils.ts +86 -0
  328. package/src/utils/gientechCommon/utils/request.ts +37 -0
  329. package/src/utils/gientechCommon/utils/serverFn.ts +172 -0
  330. package/src/utils/index.tsx +142 -0
  331. package/src/utils/testconfigs/demologin/index.tsx +32 -0
  332. package/src/utils/testconfigs/index.ts +59 -0
  333. package/src/vite-env.d.ts +42 -0
  334. package/stats.html +4949 -0
  335. package/tailwind.config.js +170 -0
  336. package/tsconfig.app.json +30 -0
  337. package/tsconfig.app.tsbuildinfo +11 -0
  338. package/tsconfig.json +13 -0
  339. package/tsconfig.node.json +22 -0
  340. package/tsconfig.node.tsbuildinfo +1 -0
  341. package/vite.config.app.ts +93 -0
  342. package/vite.config.ts +232 -0
  343. package/workflows/release.yml +60 -0
  344. package//346/215/242/350/241/214/346/240/274/345/274/217/344/277/235/346/214/201/344/274/230/345/214/226/346/226/271/346/241/210.md +359 -0
  345. /package/{assets → dist/assets}/Doris.png +0 -0
  346. /package/{assets → dist/assets}/GientechStreamReader-CNj6Rcm7.js +0 -0
  347. /package/{assets → dist/assets}/PostgreSQL.png +0 -0
  348. /package/{assets → dist/assets}/SQLServer.png +0 -0
  349. /package/{assets → dist/assets}/database.svg +0 -0
  350. /package/{assets → dist/assets}/database_add.svg +0 -0
  351. /package/{assets → dist/assets}/database_connect.svg +0 -0
  352. /package/{assets → dist/assets}/database_upload.svg +0 -0
  353. /package/{assets → dist/assets}/databse.svg +0 -0
  354. /package/{assets → dist/assets}/defaultWeLogo.svg +0 -0
  355. /package/{assets → dist/assets}/empty.png +0 -0
  356. /package/{assets → dist/assets}/graph.svg +0 -0
  357. /package/{assets → dist/assets}/homeBg.png +0 -0
  358. /package/{assets → dist/assets}/index-B2yNvzjy.js +0 -0
  359. /package/{assets → dist/assets}/index-BJv0t0eJ.js +0 -0
  360. /package/{assets → dist/assets}/index-BKe5FgcC.js +0 -0
  361. /package/{assets → dist/assets}/index-CU45mVZ4.js +0 -0
  362. /package/{assets → dist/assets}/index-CpW6Dhpp.js +0 -0
  363. /package/{assets → dist/assets}/index-D72cKELw.js +0 -0
  364. /package/{assets → dist/assets}/index-DdVFXD_y.js +0 -0
  365. /package/{assets → dist/assets}/index-DrkSoKz6.js +0 -0
  366. /package/{assets → dist/assets}/index-ZopkeZtI.js +0 -0
  367. /package/{assets → dist/assets}/index-g-SUxfJH.js +0 -0
  368. /package/{assets → dist/assets}/index-j0kQJd0a.js +0 -0
  369. /package/{assets → dist/assets}/knowledge.svg +0 -0
  370. /package/{assets → dist/assets}/left.jpg +0 -0
  371. /package/{assets → dist/assets}/logoImg.png +0 -0
  372. /package/{assets → dist/assets}/mysql.svg +0 -0
  373. /package/{assets → dist/assets}/plus-V9zUoSq6.js +0 -0
  374. /package/{assets → dist/assets}/sensitive.svg +0 -0
  375. /package/{assets → dist/assets}/style-CGmZ5osp.js +0 -0
  376. /package/{assets → dist/assets}/style.css +0 -0
  377. /package/{assets → dist/assets}/style2.css +0 -0
  378. /package/{assets → dist/assets}/style3.css +0 -0
  379. /package/{assets → dist/assets}/style4.css +0 -0
  380. /package/{assets → dist/assets}/worker-BbpylX7l.js +0 -0
  381. /package/{assets → dist/assets}/x-CnaaLGJF.js +0 -0
  382. /package/{assistantConfig.d.ts → dist/assistantConfig.d.ts} +0 -0
  383. /package/{assistantConfig.js → dist/assistantConfig.js} +0 -0
  384. /package/{chat.d.ts → dist/chat.d.ts} +0 -0
  385. /package/{chat.js → dist/chat.js} +0 -0
  386. /package/{database.d.ts → dist/database.d.ts} +0 -0
  387. /package/{database.js → dist/database.js} +0 -0
  388. /package/{databaseId.d.ts → dist/databaseId.d.ts} +0 -0
  389. /package/{databaseId.js → dist/databaseId.js} +0 -0
  390. /package/{databaseTable.d.ts → dist/databaseTable.d.ts} +0 -0
  391. /package/{databaseTable.js → dist/databaseTable.js} +0 -0
  392. /package/{icons → dist/icons}/answerAwartar.png +0 -0
  393. /package/{icons → dist/icons}/docx-file.png +0 -0
  394. /package/{icons → dist/icons}/folder.png +0 -0
  395. /package/{icons → dist/icons}/html.png +0 -0
  396. /package/{icons → dist/icons}/image.png +0 -0
  397. /package/{icons → dist/icons}/jpg-file.png +0 -0
  398. /package/{icons → dist/icons}/json.png +0 -0
  399. /package/{icons → dist/icons}/md.png +0 -0
  400. /package/{icons → dist/icons}/pdf.png +0 -0
  401. /package/{icons → dist/icons}/pptx.png +0 -0
  402. /package/{icons → dist/icons}/questionAwartar.png +0 -0
  403. /package/{icons → dist/icons}/sheets.png +0 -0
  404. /package/{icons → dist/icons}/txt.png +0 -0
  405. /package/{icons → dist/icons}/xlsx.png +0 -0
  406. /package/{index.d.ts → dist/index.d.ts} +0 -0
  407. /package/{index.js → dist/index.js} +0 -0
  408. /package/{modelManage.d.ts → dist/modelManage.d.ts} +0 -0
  409. /package/{modelManage.js → dist/modelManage.js} +0 -0
  410. /package/{sensitive.d.ts → dist/sensitive.d.ts} +0 -0
  411. /package/{sensitive.js → dist/sensitive.js} +0 -0
  412. /package/{streamFilesReader.d.ts → dist/streamFilesReader.d.ts} +0 -0
  413. /package/{streamFilesReader.js → dist/streamFilesReader.js} +0 -0
  414. /package/{vite.svg → dist/vite.svg} +0 -0
  415. /package/{worker → dist/worker}/pdf.worker.min.js +0 -0
@@ -0,0 +1,2449 @@
1
+ import React, { useEffect, useState, useMemo, useRef } from 'react';
2
+ import axios from 'axios';
3
+ import { AiChat, type AppStatusManager, validateFileUpload, showValidationErrors } from '@mxmweb/aichat';
4
+ import { ChatMessageAdapter } from '@mxmweb/rtext';
5
+ import { ConfigProvider, Drawer, message } from 'antd';
6
+ import { uid } from 'uid';
7
+ import { History } from 'lucide-react';
8
+
9
+ import { defaultTheme, deepMergeTheme, type Styles } from '@mxmweb/zui';
10
+ import { DefaultSenderConfig } from './constants';
11
+ import GientechConversationPanel from './Conversations';
12
+ import {
13
+ convertQueryReplyPairListToMessages,
14
+ getFileTypeByName,
15
+ toCopy,
16
+ } from '../../utils/commonFn';
17
+ import {
18
+ getFeedbackList,
19
+ handleCancelFeedBack,
20
+ handleDeleteConversation,
21
+ handleFeedBack,
22
+ handleRename,
23
+ handleCreateConversation,
24
+ } from '../../utils/gientechCommon/utils/serverFn';
25
+
26
+ import GientechNewChatWelcome from '../../utils/gientechCommon/components/Messages/GientechNewChatWelcome';
27
+ import AichatUseController from '../../utils/gientechCommon/hooks/AichatUseController';
28
+ import { useFileManager } from '../../utils/gientechCommon/hooks/useFileManager';
29
+ import { useFileDisplayTools } from '../../utils/gientechCommon/hooks/useFileDisplayTools';
30
+ import FeedBackModal from '../../utils/gientechCommon/components/FeedBackModal';
31
+ import FileManager from '../../utils/gientechCommon/components/FileManager';
32
+ import AppLoading from '../../utils/gientechCommon/components/AppLoading';
33
+ import { Icon } from '@mxmweb/zui';
34
+ import DisplayLoading from '../../utils/gientechCommon/components/DisplayLoading';
35
+ import AppError from '../../utils/gientechCommon/components/AppError';
36
+ import DisplayError from '../../utils/gientechCommon/components/DisplayError';
37
+ import { maxPollCount, maxPollInterval } from '../../utils/gientechCommon/configs/commonConfig';
38
+ import { mergeFiles } from '../../utils/gientechCommon/utils/fileUtils';
39
+ import { RenameModal } from '../../utils/gientechCommon/components/RenameModal';
40
+ import { DeleteModal } from '../../utils/gientechCommon/components/DeleteModal';
41
+ import { ReferenceBar } from './ReferenceBar';
42
+ import '@mxmweb/rtext/style.css';
43
+ import '@mxmweb/aichat/style.css';
44
+ import '@mxmweb/zui/style.css';
45
+ import { fileViewTypes, getUrlPrefix, TempBaseUrl } from './utils';
46
+ import DrawerPreview from './referenceCom/DrawerPreview';
47
+ import { DrawerContent } from './referenceCom/DrawerContent';
48
+ export enum DrawerType {
49
+ REFERENCELIST = 'referencelist',
50
+ }
51
+
52
+ interface SenderConfig {
53
+ actions?: Array<{
54
+ name: string;
55
+ icon?: React.ReactNode;
56
+ badgeCount?: number;
57
+ enabled?: boolean;
58
+ [key: string]: any;
59
+ }>;
60
+ switchs?: Array<{
61
+ name: string;
62
+ label: string;
63
+ enabled?: boolean;
64
+ [key: string]: any;
65
+ }>;
66
+ [key: string]: any;
67
+ }
68
+
69
+ // 自定义组件接口
70
+ interface CustomComponents {
71
+ LogoBox?: React.ComponentType<any> | null;
72
+ AiChatBox?: React.ComponentType<any>;
73
+ UserChatBox?: React.ComponentType<any>;
74
+ WelcomeComponent?: React.ComponentType<any>;
75
+ DisplayLoading?: React.ComponentType<any>;
76
+ DisplayError?: React.ComponentType<any>;
77
+ AppError?: React.ComponentType<any>;
78
+ AppLoading?: React.ComponentType<any>;
79
+ [key: string]: React.ComponentType<any> | null | undefined;
80
+ }
81
+
82
+ interface GientechChatAdopterProps {
83
+ token: string;
84
+ url?: string;
85
+ CSRFToken?: string;
86
+ styles?: Styles;
87
+ eventsEmit?: (eventName: string, data: any) => void;
88
+ CustomComponents?: CustomComponents;
89
+ senderConfig?: SenderConfig;
90
+ appLoadingConfig?: {
91
+ title?: string;
92
+ subtitle?: string;
93
+ };
94
+ [key: string]: any;
95
+ }
96
+
97
+ export interface SidebarTabConfig {
98
+ key: string;
99
+ type: 'custom' | 'form';
100
+ label: string;
101
+ icon: React.ReactNode;
102
+ enabled?: boolean;
103
+ component?: React.ReactNode; // type=custom
104
+ formConfig?: any[]; // type=form
105
+ initialValues?: Record<string, any>;
106
+ onChange?: (values: any) => void;
107
+ [key: string]: any;
108
+ }
109
+
110
+ export default function withGientechChatAdopter(WrappedComponent = AiChat) {
111
+ /**
112
+ * GientechChatAdopter 高阶组件
113
+ * 封装了与Gientech后端服务的接口对接、状态管理、事件处理等核心业务逻辑
114
+ * @param {React.ComponentType} WrappedComponent - 需要包裹的基础聊天组件,默认为 AiChat
115
+ */
116
+ return function GientechChatAdopter({
117
+ token,
118
+ url = 'http://localhost:8888',
119
+ styles,
120
+ CSRFToken,
121
+ eventsEmit,
122
+ scrollOld,
123
+ ...rest
124
+ }: GientechChatAdopterProps) {
125
+ // 统一归一化样式:使用 UI 库的 deepMergeTheme
126
+ const normalizedStyles: Styles = useMemo(() => {
127
+ const baseStyles: Styles = { theme: defaultTheme, mode: 'light' };
128
+ return deepMergeTheme(baseStyles, styles);
129
+ }, [styles]);
130
+ const [isRenameModalOpen, setisRenameModalOpen] = useState(false);
131
+ const [isRemoveModalOpen, setisRemoveModalOpen] = useState(false);
132
+ const [curEditData, setCurEditData] = useState<any>(null);
133
+ const [open, setOpen] = useState(false);
134
+ const [drawerType, setDrawerType] = useState<string>('mark');
135
+ const [curFileInfo, setCurFileInfo] = useState({
136
+ url: '',
137
+ parse_url: '',
138
+ file_type: '',
139
+ file_name: '',
140
+ });
141
+ // =================================================================
142
+ // State Management - 状态管理
143
+ // =================================================================
144
+ const [conversationList, setConversationList] = useState<any[]>([]); // 对话列表
145
+ // 会话列表分页状态
146
+ const [convPageNo, setConvPageNo] = useState<number>(1);
147
+ const [convPageSize] = useState<number>(16);
148
+ const [convTotal, setConvTotal] = useState<number>(0);
149
+ const [convLoading, setConvLoading] = useState<boolean>(false);
150
+ const [chatData, setChatData] = useState<any[]>([]); // 所有对话的聊天记录
151
+ const [activeSessionId, setActiveSessionId] = useState<string | undefined>(undefined); // 当前激活的对话ID
152
+ const [recommandQuestions, setRecommandQuestions] = useState<any[]>([]); // 推荐问题列表
153
+ const [assistantList, setAssistantList] = useState<any[]>([]); // 智能体(助手)列表
154
+ const [newAssistantId, setNewAssistantId] = useState<string | undefined>(undefined); // 新建对话时选择的智能体ID
155
+ const [appStatus, setAppStatus] = useState<AppStatusManager>({
156
+ display: 'ready', // 主聊天区域状态: ready, loading, processing, error, uploading
157
+ sender: 'ready', // 发送器状态: ready, processing, error, uploading
158
+ app: 'initializing', // 应用整体状态: initializing, ready, error
159
+ });
160
+ // 发送器配置支持外部传入
161
+ // 深度合并工具,customConfig 有的字段覆盖 defaultConfig
162
+ function deepMerge(defaultConfig: any, customConfig: any): any {
163
+ if (!customConfig) return defaultConfig;
164
+ const result: any = Array.isArray(defaultConfig) ? [...defaultConfig] : { ...defaultConfig };
165
+ for (const key in customConfig) {
166
+ if (
167
+ customConfig[key] !== undefined &&
168
+ customConfig[key] !== null &&
169
+ Object.prototype.hasOwnProperty.call(customConfig, key)
170
+ ) {
171
+ // 特殊处理 actions 数组:合并而不是替换
172
+ if (
173
+ key === 'actions' &&
174
+ Array.isArray(defaultConfig[key]) &&
175
+ Array.isArray(customConfig[key])
176
+ ) {
177
+ // 创建一个以 name 为 key 的 Map,优先使用 customConfig 中的 action
178
+ const actionsMap = new Map();
179
+ // 先添加 defaultConfig 中的 actions
180
+ defaultConfig[key].forEach((action: any) => {
181
+ if (action.name) {
182
+ actionsMap.set(action.name, action);
183
+ }
184
+ });
185
+ // 然后用 customConfig 中的 actions 覆盖或添加
186
+ customConfig[key].forEach((action: any) => {
187
+ if (action.name) {
188
+ actionsMap.set(action.name, action);
189
+ }
190
+ });
191
+ result[key] = Array.from(actionsMap.values());
192
+ } else if (
193
+ typeof defaultConfig[key] === 'object' &&
194
+ defaultConfig[key] !== null &&
195
+ !Array.isArray(defaultConfig[key]) &&
196
+ typeof customConfig[key] === 'object' &&
197
+ customConfig[key] !== null &&
198
+ !Array.isArray(customConfig[key])
199
+ ) {
200
+ result[key] = deepMerge(defaultConfig[key], customConfig[key]);
201
+ } else {
202
+ result[key] = customConfig[key];
203
+ }
204
+ }
205
+ }
206
+ return result;
207
+ }
208
+
209
+ // 发送器配置支持外部传入,merge 默认和外部
210
+ const [dynamicSenderConfig, setDynamicSenderConfig] = useState(() => {
211
+ const merged = deepMerge(DefaultSenderConfig, rest.senderConfig);
212
+
213
+ return merged;
214
+ });
215
+
216
+ // 监听外部 senderConfig 变化自动 merge
217
+ useEffect(() => {
218
+ if (rest.senderConfig) {
219
+ const merged = deepMerge(DefaultSenderConfig, rest.senderConfig);
220
+
221
+ setDynamicSenderConfig(merged);
222
+ }
223
+ }, [rest.senderConfig]);
224
+ // 使用统一的文件管理 Hook
225
+ const {
226
+ fileManagerData,
227
+ fileStatuses,
228
+ setFileManagerData,
229
+ setFileStatuses,
230
+ pollFileStatus: pollFileStatusHook,
231
+ handleRemoveFromTemp,
232
+ resetStopFlag,
233
+ setStopFlag,
234
+ stoppedRef,
235
+ } = useFileManager({
236
+ url,
237
+ token,
238
+ CSRFToken,
239
+ activeSessionId,
240
+ chatData,
241
+ });
242
+
243
+ // 新增:全局管理助手选中状态,彻底解决子组件重建跳回默认
244
+ const [selectedAssistantId, setSelectedAssistantId] = React.useState(() => {
245
+ if (!assistantList || assistantList.length === 0) return undefined;
246
+ const defaultItem = assistantList.find(item => item.isDefault);
247
+ return defaultItem ? defaultItem.id : assistantList[0]?.id;
248
+ });
249
+
250
+ // 监听 assistantList 变化,自动选中默认助手
251
+ React.useEffect(() => {
252
+ if (!assistantList || assistantList.length === 0) return;
253
+ const exists = assistantList.some(item => item.id === selectedAssistantId);
254
+ if (!selectedAssistantId || !exists) {
255
+ const defaultItem = assistantList.find(item => item.isDefault);
256
+ setSelectedAssistantId(defaultItem ? defaultItem.id : assistantList[0].id);
257
+ }
258
+ }, [assistantList]);
259
+
260
+ // 新增:记录 sender switchs 的选中状态
261
+ const [senderSwitchValues, setSenderSwitchValues] = useState<{ [key: string]: boolean }>({});
262
+
263
+ // =================================================================
264
+ // Refs - 引用
265
+ // =================================================================
266
+ const prevActiveSessionId = useRef<string | undefined>(undefined); // 记录上一个激活的对话ID,用于对比变化
267
+ const setUploadedFilesRef = useRef<(files: any[]) => void>(undefined); // 引用核心组件的文件上传方法
268
+ const removeFileRef = useRef<(fileObj: any, idx: number, _type: string) => void>(undefined); // 引用核心组件的文件移除方法
269
+ const fileInputRef = useRef<HTMLInputElement>(null); // 文件上传input引用
270
+ const senderConfigRef = useRef<SenderConfig | undefined>(undefined); // 引用核心组件的上传限制配置
271
+ const uploadedFilesRef = useRef<any[]>([]); // 引用核心组件的已上传文件列表
272
+ const deleteData = useRef({ sessionId: '' }); // 暂存待删除的对话数据
273
+ const confirmed = useRef<any>(null); // 暂存 antd Modal.confirm 的引用
274
+ // 待上传文件状态(从 AiChat 内部获取)
275
+ const [pendingFiles, setPendingFiles] = useState<any[]>([]);
276
+ const [feedParam, setFeedParam] = useState<{
277
+ queryId?: number | string | undefined;
278
+ restult?: number;
279
+ }>({});
280
+ const [openFeed, setOpenFeed] = useState(false);
281
+ const [feedBackList, setFeedBackList] = useState<any>([]);
282
+ // FileManager Modal 状态
283
+ const [fileManagerOpen, setFileManagerOpen] = useState(false);
284
+ // stoppedRef 已由 useFileManager hook 管理
285
+
286
+ // =================================================================
287
+ // Custom Hooks - 自定义钩子
288
+ // =================================================================
289
+ // 引入SSE控制器,封装了流式请求、消息管理等复杂逻辑
290
+ const {
291
+ api_startChat_re,
292
+ addEmptyMessage,
293
+ setLastEmptyMessage,
294
+ stopStream,
295
+ cancelStreamBySessionId,
296
+ } = AichatUseController({
297
+ baseUrl: url,
298
+ CSRFToken: CSRFToken,
299
+ token,
300
+ setChatData,
301
+ activeSessionId,
302
+ setAppStatus,
303
+ setConversationList,
304
+ });
305
+
306
+ // =================================================================
307
+ // Memoized Values - 缓存计算值
308
+ // =================================================================
309
+
310
+ /**
311
+ * 将智能体列表转换为以ID为键的Map,便于快速查找
312
+ */
313
+ const assistantMap = useMemo(() => {
314
+ const map: Record<string, any> = {};
315
+ (assistantList || []).forEach(item => {
316
+ map[item.id] = item;
317
+ });
318
+ return map;
319
+ }, [assistantList]);
320
+
321
+ /**
322
+ * 根据 activeSessionId 从 chatData 中筛选出当前对话的数据, 并合并智能体信息
323
+ */
324
+ const currentChatData = useMemo(() => {
325
+ const data = chatData.find((c: any) => c.id === activeSessionId) || {
326
+ id: activeSessionId,
327
+ messages: [],
328
+ };
329
+ const assistantInfo = data.configId ? assistantMap[data.configId] : null;
330
+ return {
331
+ ...data,
332
+ assistantInfo,
333
+ };
334
+ }, [chatData, activeSessionId, assistantMap]);
335
+
336
+ /**
337
+ * 从当前会话的所有用户消息的 filePaths 中收集所有文件
338
+ * filePaths 应该包含所有历史文件,而 currentFiles 只是当前对话的上传文件
339
+ */
340
+ const allFilesFromQuery = useMemo(() => {
341
+ const filesMap = new Map<string, any>(); // 使用 Map 去重,以 uid 为 key
342
+
343
+ // 遍历当前会话的所有用户消息
344
+ const userMessages = (currentChatData?.messages || []).filter(
345
+ (msg: any) => msg.istype === 'user'
346
+ );
347
+
348
+ for (const msg of userMessages) {
349
+ // 获取 filePaths(可能是 JSON 字符串或数组)
350
+ const filePaths = msg.filePaths;
351
+ if (!filePaths) continue;
352
+
353
+ try {
354
+ // 解析 filePaths(如果是字符串则解析,如果是数组则直接使用)
355
+ const parsedFiles = typeof filePaths === 'string'
356
+ ? JSON.parse(filePaths)
357
+ : filePaths;
358
+
359
+ if (Array.isArray(parsedFiles)) {
360
+ // 将所有文件添加到 Map 中,以 uid 为 key 去重
361
+ parsedFiles.forEach((file: any) => {
362
+ if (file && file.uid) {
363
+ filesMap.set(file.uid, file);
364
+ }
365
+ });
366
+ }
367
+ } catch (error) {
368
+ console.warn('[FileManager] 解析 filePaths 失败:', error, filePaths);
369
+ }
370
+ }
371
+
372
+ return Array.from(filesMap.values());
373
+ }, [currentChatData?.messages]);
374
+
375
+ const is_download = useMemo(() => {
376
+ if (currentChatData.configId && assistantMap) {
377
+ const curAssist = assistantMap[currentChatData.configId];
378
+ if (curAssist?.configJson) {
379
+ const config = JSON.parse(curAssist.configJson);
380
+ return config.is_download;
381
+ }
382
+ return false;
383
+ } else {
384
+ return false;
385
+ }
386
+ }, [currentChatData.configId, assistantMap]);
387
+
388
+ const is_enableThinking = useMemo(() => {
389
+ //console.log('is_enableThinking',currentChatData.configId,assistantMap,newAssistantId)
390
+ if ((currentChatData.configId || newAssistantId) && assistantMap) {
391
+ const curAssist = assistantMap[currentChatData.configId];
392
+ if (curAssist?.configJson) {
393
+ const config = JSON.parse(curAssist.configJson);
394
+ console.log('is_enableThinking', currentChatData.configId, assistantMap, newAssistantId);
395
+ return config.thinking ? true : false;
396
+ } else {
397
+ // const curAssist = assistantMap[newAssistantId]
398
+ //console.log('newAssistantId',newAssistantId)
399
+ if (newAssistantId) {
400
+ const curAssist = assistantMap[newAssistantId];
401
+ if (curAssist?.configJson) {
402
+ const config = JSON.parse(curAssist.configJson);
403
+ return config.thinking ? true : false;
404
+ }
405
+ }
406
+ }
407
+ }
408
+ return false;
409
+ }, [currentChatData.configId, assistantMap, newAssistantId]);
410
+
411
+ // 文件工具函数已提取到 useFileManager hook 中
412
+
413
+ // =================================================================
414
+ // Side Effects (useEffect) - 副作用钩子
415
+ // =================================================================
416
+
417
+ /**
418
+ * 跟踪 activeSessionId 的变化:
419
+ * 1. 清理上一个无消息的临时会话
420
+ * 2. 更新推荐问题
421
+ */
422
+ useEffect(() => {
423
+ // 切换会话时,检查上一个会话是否为临时且无消息
424
+ if (prevActiveSessionId.current && prevActiveSessionId.current !== activeSessionId) {
425
+ const prevId = prevActiveSessionId.current;
426
+ const prevConv = conversationList.find(c => c.sessionId === prevId && c.isNew);
427
+ const prevChat = chatData.find(c => c.id === prevId && c.isNew);
428
+ const prevHasMsg = prevChat && prevChat.messages && prevChat.messages.length > 0;
429
+ // 如果是临时的且没有消息,则自动删除
430
+ if (prevConv && !prevHasMsg) {
431
+ setConversationList(list => list.filter(c => c.sessionId !== prevId));
432
+ setChatData(list => list.filter(c => c.id !== prevId));
433
+ }
434
+ }
435
+ prevActiveSessionId.current = activeSessionId;
436
+
437
+ // 更新推荐问题为当前对话的最后一个推荐问题
438
+ const historyRQ =
439
+ currentChatData.messages && currentChatData.messages.length > 0
440
+ ? currentChatData.messages[currentChatData.messages.length - 1].recommendQuestion
441
+ : [];
442
+ setRecommandQuestions(historyRQ);
443
+ }, [activeSessionId, conversationList, chatData, currentChatData.messages]);
444
+
445
+ /**
446
+ * 拉取会话列表分页
447
+ */
448
+ const fetchConversationPage = async (pageNo: number) => {
449
+ if (convLoading) return;
450
+ setConvLoading(true);
451
+ try {
452
+ const res = await axios.get(
453
+ `${url}/qa/dialogue/list?pageNo=${pageNo}&pageSize=${convPageSize}&sysType=INT_SESSION`,
454
+ { headers: { Authorization: token } }
455
+ );
456
+ const records = res?.data?.data?.records || [];
457
+ const total = res?.data?.data?.total || 0;
458
+ setConvTotal(total);
459
+ setConversationList(prev => {
460
+ // 去重合并(以 sessionId 为准)
461
+ const map = new Map<string, any>();
462
+ [...prev, ...records].forEach((it: any) => {
463
+ map.set(it.sessionId, it);
464
+ });
465
+ return Array.from(map.values());
466
+ });
467
+ setConvPageNo(pageNo);
468
+ } catch (_) {
469
+ // 忽略,错误态交由上层处理
470
+ } finally {
471
+ setConvLoading(false);
472
+ }
473
+ };
474
+
475
+ /**
476
+ * 首次加载:并发拉取助手列表与会话第一页
477
+ */
478
+ useEffect(() => {
479
+ setAppStatus(prev => ({ ...prev, app: 'initializing' }));
480
+ // 助手列表
481
+ axios
482
+ .get(`${url}/qa/search/config/helper/list?pageNo=1&pageSize=3000`, {
483
+ headers: { Authorization: token },
484
+ })
485
+ .then(assistantRes => {
486
+ setAssistantList(assistantRes.data.data || []);
487
+ })
488
+ .catch(() => {});
489
+
490
+ // 会话第一页
491
+ fetchConversationPage(1)
492
+ .then(() => {
493
+ if (!activeSessionId) {
494
+ handleConversationCreate({});
495
+ }
496
+ setAppStatus(prev => ({ ...prev, app: 'ready' }));
497
+ })
498
+ .catch(() => {
499
+ setAppStatus(prev => ({ ...prev, app: 'error' }));
500
+ });
501
+ }, []);
502
+
503
+ /**
504
+ * 当 activeSessionId 变化时,获取对应的聊天记录
505
+ */
506
+ useEffect(() => {
507
+ if (!activeSessionId) return;
508
+
509
+ setAppStatus(prev => ({ ...prev, display: 'loading' }));
510
+ axios
511
+ .get(`${url}/qa/dialogue/getDialogBySessionId?sessionId=${activeSessionId}`, {
512
+ headers: { Authorization: token },
513
+ })
514
+ .then(res => {
515
+ // const r = {
516
+ // data: {
517
+ // success: true,
518
+ // errorMsg: null,
519
+ // errorCode: null,
520
+ // data: {
521
+ // label: '控制器起不来(控制器右上角RDY灯不亮)',
522
+ // sessionId: '3d752532b3d4510867288e5ac5cb3833',
523
+ // gmtCreate: '2025-11-06 14:30:43',
524
+ // gmtModified: '2025-11-06 14:30:43',
525
+ // createBy: 'admin',
526
+ // updateBy: 'admin',
527
+ // filePath: 'http://10.160.33.221:9000',
528
+ // configId: 703,
529
+ // kbId: null,
530
+ // queryReplyPairList: [
531
+ // {
532
+ // query: {
533
+ // id: 27665,
534
+ // gmtCreate: '2025-11-06T06:31:05.000+00:00',
535
+ // content: '控制器起不来(控制器右上角RDY灯不亮)',
536
+ // type: 'text',
537
+ // queryId: 'query-2532b3d4510867288e5ac5cb38338870',
538
+ // audioUrl: '',
539
+ // filePaths: null,
540
+ // currentFiles: null,
541
+ // },
542
+ // reply: {
543
+ // id: 27638,
544
+ // gmtCreate: '2025-11-06T06:31:15.000+00:00',
545
+ // content:
546
+ // '<think>\n好的,我现在需要回答用户的问题:“控制器起不来(控制器右上角RDY灯不亮)”。首先,我要查看提供的参考资料,看看是否有相关信息。\n\n参考资料0提到了现象“控制器起不来(控制器右上角RDY灯不亮)”,并关联到“包含”和“产生”关系,但没有提供处理方法。参考资料1和2都详细描述了这个现象,并且都给出了处理方法:检查NT255模块是否报红,更换模块,并特别提到机舱检查FS211/N模块。这些处理方法的ID都是10001,说明是同一个解决方案。\n\n用户的问题没有指定版本,根据规则2,我应该使用最新的版本信息。不过,参考资料中没有明确的版本日期,所以只能假设它们都是最新的。因此,我应该综合所有参考资料的信息,提供处理方法。\n\n最后,我需要按照要求,用简洁的语言回答,并标注来源。由于处理方法在参考资料1和2中都有,我会引用这两个来源。\n\n\n</think>\n\n控制器起不来(控制器右上角RDY灯不亮)的处理方法如下:\n\n1. 检查NT255模块是否报红(ERR灯是否亮红)[retrive-tag id="2290607639633892359" image_urls="" fromType="2"][1][/retrive-tag][retrive-tag id="439368868506707299" image_urls="" fromType="2"][2][/retrive-tag]。\n2. 更换NT255模块(注:机舱检查FS211/N模块)[retrive-tag id="2290607639633892359" image_urls="" fromType="2"][1][/retrive-tag][retrive-tag id="439368868506707299" image_urls="" fromType="2"][2][/retrive-tag]。\n\n这些步骤可以帮助解决控制器无法启动的问题。',
547
+ // type: 'text',
548
+ // reference: '[]',
549
+ // webReference: '[]',
550
+ // graphReference:
551
+ // '[{"cyphers":["match (n:`组件`)-[e]-(m) where id(n) == \\"组件101\\" return n,e,m;","match (n:`现象`)-[e]-(m) where id(n) == \\"现象1001\\" return n,e,m;","match (n:`现象`)-[e]-(m) where id(n) == \\"现象1001\\" return n,e,m;"],"fetchUrl":"https://10.160.33.150:30001/jinxin_graph/proxy/search/graph/search/cypherSearch4Rag","graphName":"故障处理图谱","id":"1952195957278367755","traceNodes":[{"graphId":"1952195957278367755","id":"3484145002858895870","name":"控制器","nodeType":"组件","vid":"组件101"},{"graphId":"1952195957278367755","id":"2290607639633892359","name":"控制器起不来(控制器右上角RDY灯不亮)","nodeType":"现象","vid":"现象1001"},{"graphId":"1952195957278367755","id":"439368868506707299","name":"控制器起不来(控制器右上角RDY灯不亮)","nodeType":"现象","vid":"现象1001"}]}]',
552
+ // queryId: 'query-2532b3d4510867288e5ac5cb38338870',
553
+ // feedbackResult: null,
554
+ // feedbackId: null,
555
+ // parsedFilePaths: null,
556
+ // recommendQuestion:
557
+ // '["在检查NT255模块时,ERR灯亮红可能表示哪些具体问题?","更换NT255模块时需要注意哪些步骤或注意事项?","除了检查和更换NT255模块,还有哪些方法可以排查控制器无法启动的问题?"]',
558
+ // llmType: 1,
559
+ // resultType: 1,
560
+ // respTime: 32453,
561
+ // tokens: 824,
562
+ // },
563
+ // },
564
+ // ],
565
+ // searchConfigDTO: {
566
+ // id: 703,
567
+ // status: 1,
568
+ // isDefault: 0,
569
+ // configJson:
570
+ // '{"status":1,"defaultAnswer":"您好!我注意到您的消息中可能包含了一些敏感的内容。为了保证社区的友好氛围,我们无法继续就此话题进行讨论。如果您有其他问题或需要帮助,我很乐意继续为您服务!谢谢您的理解和支持。","dependOnKb":1,"isDel":1,"config_name":"图谱测试","model_name":"deepseek-distill-qwen32b","plan_model_name":"deepseek-distill-qwen32b","prologue":"你好","system_prompt":"","recommend_model_name":"deepseek-distill-qwen32b","temperature":0.3,"top_p":0.8,"top_k":10,"score_threshold":0.1,"max_tokens":4096,"max_round":5,"rerank_model_name":"bge-reranker-v2-m3_xinference","knowledge_base_name_source":"","graph_nb_name":[{"graph_name":"故障处理图谱","index_name":"1952195957278367755"}],"thinking":false,"knowledge_base_name":"","selectedKnowledgeBase":[],"enabled_graph":false,"enabled_nb_graph":true,"need_trace_source":true,"trace_by_llm":true,"is_download":false,"search_policy":1,"enabled_question_rewrite":false,"enabled_faq":true,"enabled_rerank":true,"rerank":false}',
571
+ // name: '图谱测试',
572
+ // des: null,
573
+ // createBy: 'admin',
574
+ // gmtCreate: '2025-11-05 19:37:54',
575
+ // permission: null,
576
+ // sensitiveWordIds: '',
577
+ // defaultAnswer:
578
+ // '您好!我注意到您的消息中可能包含了一些敏感的内容。为了保证社区的友好氛围,我们无法继续就此话题进行讨论。如果您有其他问题或需要帮助,我很乐意继续为您服务!谢谢您的理解和支持。',
579
+ // model: false,
580
+ // enabledFaq: 0,
581
+ // dependOnKb: 1,
582
+ // isDel: 1,
583
+ // },
584
+ // },
585
+ // },
586
+ // };
587
+ // 如果会话没有数据(例如新建的),直接返回
588
+
589
+ if (!res.data.data) {
590
+ setAppStatus(prev => ({ ...prev, display: 'ready' }));
591
+ return;
592
+ }
593
+ // 解析消息数据
594
+ const queryReplyPairList = res.data.data?.queryReplyPairList || [];
595
+ const messages = convertQueryReplyPairListToMessages(queryReplyPairList);
596
+ const {
597
+ sessionId,
598
+ searchConfigDTO,
599
+ filePath,
600
+ configId,
601
+ reference,
602
+ webReference,
603
+ uploadedFiles,
604
+ fileList,
605
+ currentFiles,
606
+ } = res.data.data;
607
+
608
+ // === 文件数据补全 ===
609
+ // 1. reference(AI消息)
610
+ if (messages.length > 0) {
611
+ // 找到最后一条 AI 消息的索引(从后往前找)
612
+ const lastAiIndex =
613
+ messages.length - 1 - [...messages].reverse().findIndex(m => m.istype === 'ai');
614
+
615
+ // 确保找到的是有效的 AI 消息
616
+ if (
617
+ lastAiIndex >= 0 &&
618
+ lastAiIndex < messages.length &&
619
+ messages[lastAiIndex].istype === 'ai'
620
+ ) {
621
+ if (reference) {
622
+ messages[lastAiIndex].reference = reference;
623
+ }
624
+ if (webReference) {
625
+ messages[lastAiIndex].webReference = webReference;
626
+ }
627
+ }
628
+ }
629
+ // 2. currentFiles/fileList/uploadedFiles(user消息)
630
+ const filesData = currentFiles || fileList || uploadedFiles;
631
+ if (filesData && messages.length > 0) {
632
+ const lastUserIdx = [...messages].reverse().findIndex(m => m.istype === 'user');
633
+ if (lastUserIdx !== -1) {
634
+ console.log(messages[messages.length - 1 - lastUserIdx].currentFiles, 'filesData');
635
+ messages[messages.length - 1 - lastUserIdx].currentFiles = filesData;
636
+ }
637
+ }
638
+
639
+ // 更新对话列表中的对应项的元数据
640
+ const item = conversationList.find(item => item.sessionId === sessionId);
641
+ if (item) {
642
+ // console.log(item,'你大爷')
643
+ setConversationList(list =>
644
+ list.map(item =>
645
+ item.sessionId === sessionId
646
+ ? { ...item, searchConfigDTO, filePath, configId }
647
+ : item
648
+ )
649
+ );
650
+ }
651
+
652
+ // 构造当前对话的完整数据
653
+ const conversationConfig = {
654
+ configId: res.data.data.configId,
655
+ filePath: res.data.data.filePath,
656
+ searchConfigDTO: res.data.data.searchConfigDTO || {},
657
+ sessionId: res.data.data.sessionId,
658
+ label: res.data.data.label,
659
+ kbId: res.data.data.kbId,
660
+ };
661
+
662
+ // 更新聊天数据
663
+ setChatData(prev => {
664
+ const others = prev.filter((c: any) => c.id !== activeSessionId);
665
+
666
+ const newChatData = [
667
+ { id: activeSessionId, messages, ...conversationConfig },
668
+ ...others,
669
+ ];
670
+ return newChatData;
671
+ });
672
+ setAppStatus(prev => ({ ...prev, display: 'ready' }));
673
+ })
674
+ .catch(err => {
675
+ console.error('获取聊天数据失败:', err);
676
+ if (err.response && err.response.status === 404) {
677
+ // 404 表示会话在后端不存在,按新建会话处理
678
+ handleConversationCreate({});
679
+ }
680
+ setAppStatus(prev => ({ ...prev, display: 'error' }));
681
+ });
682
+ }, [activeSessionId, url, token]);
683
+
684
+ /**
685
+ * 监听文件数据变化,更新Sender上的角标
686
+ */
687
+ useEffect(() => {
688
+ const totalFiles = fileManagerData.uploadedFiles.length;
689
+ setDynamicSenderConfig((prev: any) => {
690
+ const actions = Array.isArray(prev.actions) ? prev.actions : [];
691
+ const newConfig = {
692
+ ...prev,
693
+ actions: actions.map((action: any) =>
694
+ action.name === 'history' ? { ...action, badgeCount: totalFiles } : action
695
+ ),
696
+ };
697
+ return newConfig;
698
+ });
699
+ }, [fileManagerData.uploadedFiles]);
700
+ useEffect(() => {
701
+ getFeedbackList({ url, token }, data => {
702
+ setFeedBackList(data);
703
+ });
704
+ }, []);
705
+
706
+ /**
707
+ * 实时同步 fileStatuses 到当前消息的 currentFiles,便于消息区细致显示每个文件的处理状态
708
+ */
709
+ React.useEffect(() => {
710
+ if (!fileStatuses.length) return;
711
+ setChatData(chatList => {
712
+ if (!chatList[0]) return chatList;
713
+ const { messages } = chatList[0];
714
+ for (let i = messages.length - 1; i >= 0; i--) {
715
+ if (messages[i].istype === 'user') {
716
+ const oldFiles = messages[i].currentFiles || [];
717
+ messages[i].currentFiles = fileStatuses.map((f: any) => {
718
+ const old = oldFiles.find((of: any) => of.uid === f.uid);
719
+ return {
720
+ name: f.file?.name || f.name,
721
+ size: f.file?.size || f.size,
722
+ type: f.file?.type || f.type,
723
+ status: f.status,
724
+ uid: f.uid,
725
+ url: f.url || old?.url || '', // 优先用已有 url
726
+ };
727
+ });
728
+ break;
729
+ }
730
+ }
731
+ return [...chatList];
732
+ });
733
+ }, [fileStatuses]);
734
+
735
+ // =================================================================
736
+ // Core Functions - 核心功能函数
737
+ // =================================================================
738
+
739
+ // 1. 在组件作用域定义控制器和轮询停止标志
740
+ let uploadController: AbortController | null = null;
741
+ let stopPoll: (() => void) | null = null;
742
+
743
+ /**
744
+ * 轮询文件解析状态(使用统一的 hook)
745
+ */
746
+ const pollFileStatus = (
747
+ uids: string[],
748
+ onStatusUpdate: (serverStatuses: any[]) => void,
749
+ onComplete: () => void,
750
+ onFail: (errorMsg: string) => void
751
+ ) => {
752
+ stopPoll = pollFileStatusHook(uids, {
753
+ onStatusUpdate,
754
+ onComplete,
755
+ onFail,
756
+ });
757
+ };
758
+
759
+ /**
760
+ * 设置消息的反馈结果(赞/踩)
761
+ * @param {string} queryId - 消息ID
762
+ * @param {number} result - 反馈结果 (1: 赞, -1: 踩)
763
+ */
764
+ const setFeed = (queryId: any, restult: any) => {
765
+ setChatData(chatList => {
766
+ const newList = [...chatList];
767
+ if (!newList[0]) return newList;
768
+ newList[0] = {
769
+ ...newList[0],
770
+ messages: newList[0].messages.map((item: any) => {
771
+ if (item.queryId === queryId) {
772
+ return {
773
+ ...item,
774
+ feedbackResult: restult,
775
+ };
776
+ }
777
+ return item;
778
+ }),
779
+ };
780
+ return newList;
781
+ });
782
+ };
783
+
784
+ /**
785
+ * 获取指定queryId的推荐问题
786
+ * @param {string} queryId - 消息ID
787
+ */
788
+ const fetchRecommendQuestions = async (queryId: string) => {
789
+ if (!queryId) return;
790
+ try {
791
+ // 需要携带当前 configId
792
+ const configId =
793
+ currentChatData?.configId || assistantMap?.[currentChatData?.configId || '']?.id || '';
794
+ const res = await axios.get(
795
+ `${url}/qa/ai/recommendQuestion?queryId=${queryId}&configId=${configId || ''}`.replace(
796
+ /\?&/,
797
+ '?'
798
+ ),
799
+ {
800
+ headers: { Authorization: token },
801
+ }
802
+ );
803
+ setRecommandQuestions(res.data?.data || []);
804
+ } catch (e) {
805
+ setRecommandQuestions([]);
806
+ }
807
+ };
808
+
809
+ /**
810
+ * 处理重新发送消息逻辑
811
+ */
812
+ function handleReSenderSend(data: any) {
813
+ const { content, audioUrl, filePaths, queryId } = data;
814
+ if (!activeSessionId || !queryId) return;
815
+
816
+ // 从当前会话中按 queryId 定位本次 AI 回复
817
+ const msgs = currentChatData?.messages || [];
818
+ const aiIdx = msgs.findIndex(
819
+ (m: any) => m?.queryId === queryId && (m?.istype === 'ai' || m?.isUser === false)
820
+ );
821
+
822
+ // 回溯上一条用户消息
823
+ let userMsg: any | undefined;
824
+ if (aiIdx > 0) {
825
+ for (let i = aiIdx - 1; i >= 0; i -= 1) {
826
+ const m = msgs[i];
827
+ if (m?.istype === 'user' || m?.isUser === true) {
828
+ userMsg = m;
829
+ break;
830
+ }
831
+ }
832
+ }
833
+
834
+ const question = userMsg?.content ?? content;
835
+ if (!question) return;
836
+
837
+ const sourceFilePaths = userMsg?.filePaths ?? filePaths;
838
+ const _fileUids = (
839
+ typeof sourceFilePaths === 'string' && sourceFilePaths
840
+ ? JSON.parse(sourceFilePaths)
841
+ : sourceFilePaths || []
842
+ ).map((item: any) => item.uid);
843
+ const now = Date.now();
844
+ setLastEmptyMessage({ now });
845
+ setAppStatus(pre => {
846
+ return {
847
+ ...pre,
848
+ display: 'processing',
849
+ sender: 'processing',
850
+ };
851
+ });
852
+
853
+ data.clearFn && data.clearFn();
854
+ setConversationList(list =>
855
+ list.map(c => (c.sessionId === activeSessionId ? { ...c, gmtModified: now } : c))
856
+ );
857
+
858
+ api_startChat_re(
859
+ {
860
+ configId: currentChatData.configId,
861
+ content: question,
862
+ fileUids: _fileUids,
863
+ lastDate: now,
864
+ name: currentChatData.label || '',
865
+ queryId: queryId,
866
+ sessionId: String(activeSessionId || ''),
867
+ type: (userMsg?.audioUrl ?? audioUrl) ? 'audioUrl' : 'text',
868
+ audioUrl: (userMsg?.audioUrl ?? audioUrl) || '',
869
+ enableThinking: !!senderSwitchValues.reasoning,
870
+ enableWebsearch: !!senderSwitchValues.netSearch,
871
+ qaChannel: senderSwitchValues.qaChannel,
872
+ } as any,
873
+ gmtModified => {
874
+ // 只在AI回复完成后请求推荐问题
875
+ fetchRecommendQuestions(queryId);
876
+ }
877
+ );
878
+ }
879
+
880
+ // 提取发消息逻辑,确保只有有sessionId时才调用
881
+ async function handleSenderSend(data: any, clearFn: () => void) {
882
+ // 找到当前会话
883
+ if (!activeSessionId) return;
884
+ const openEvent = new CustomEvent('aichat:right_set', {
885
+ detail: { collapsed: true },
886
+ });
887
+ window.dispatchEvent(openEvent);
888
+ const conv = conversationList.find(c => c.sessionId === activeSessionId);
889
+ //console.log('zheli是啥',conv)
890
+ if (conv && conv.isNew) {
891
+ // 还没落库,先调 create
892
+ let createSuccess = false;
893
+ // 优先用用户选择的助手newAssistantId,否则用当前会话的configId
894
+ const configIdToUse = newAssistantId || conv.configId;
895
+ // 用用户输入内容截取20字作为label
896
+ const labelToUse =
897
+ data && data.content ? data.content.slice(0, 20) : conv.label || '未命名会话';
898
+ await new Promise<void>((resolve, reject) => {
899
+ handleCreateConversation(
900
+ { url, token, CSRFToken },
901
+ {
902
+ label: labelToUse,
903
+ sessionId: conv.sessionId,
904
+ sysType: 'INT_SESSION',
905
+ configId: configIdToUse,
906
+ },
907
+ (res: any) => {
908
+ if (res && res.data && res.data.sessionId) {
909
+ // create成功,标记已落库并更新label/configId
910
+ setConversationList(list =>
911
+ list.map(c =>
912
+ c.sessionId === conv.sessionId
913
+ ? { ...c, isNew: false, configId: configIdToUse, label: labelToUse }
914
+ : c
915
+ )
916
+ );
917
+ setChatData(list =>
918
+ list.map(c =>
919
+ c.id === conv.sessionId
920
+ ? { ...c, isNew: false, configId: configIdToUse, label: labelToUse }
921
+ : c
922
+ )
923
+ );
924
+ createSuccess = true;
925
+ resolve();
926
+ } else {
927
+ message.error('创建会话失败,请重试');
928
+ reject();
929
+ }
930
+ }
931
+ );
932
+ });
933
+ if (!createSuccess) return;
934
+ // 继续发消息
935
+ sendMessageWithSessionId(activeSessionId as string, data, clearFn, configIdToUse);
936
+ return;
937
+ }
938
+ // 已落库,直接发消息
939
+ sendMessageWithSessionId(activeSessionId as string, data, clearFn);
940
+ }
941
+
942
+ // 修改sendMessageWithSessionId,发消息时用本地会话的configId
943
+ function sendMessageWithSessionId(
944
+ sessionId: string,
945
+ data: any,
946
+ clearFn: () => void,
947
+ configIdOverride?: string
948
+ ) {
949
+ const queryId = 'query-' + uid(32);
950
+ // 优先用参数传递的 configId
951
+ const conv = conversationList.find(c => c.sessionId === sessionId);
952
+ const configIdToUse = configIdOverride || conv?.configId || currentChatData.configId;
953
+ console.log('助手配置详情', configIdToUse);
954
+
955
+ // === 数据流保障:明确保留内容格式,不进行任何 trim 或替换处理 ===
956
+ // 确保换行符、空格等格式完整保留,直接传递到后端
957
+ const rawContent = data.content || data.text || '';
958
+ const preservedContent = typeof rawContent === 'string' ? rawContent : String(rawContent);
959
+
960
+ const label = preservedContent ? preservedContent.slice(0, 20) : '未命名会话';
961
+ let localCurrentFiles = undefined;
962
+ if (data.files && data.files.length > 0) {
963
+ localCurrentFiles = data.files.map((fileItem: any) => ({
964
+ name: fileItem.file.name,
965
+ size: fileItem.file.size,
966
+ type: fileItem.file.type,
967
+ filePath: fileItem.filePath || '',
968
+ convertedFilePath: fileItem.convertedFilePath || '',
969
+ uid: fileItem.uid || '',
970
+ }));
971
+ }
972
+ console.log('本地文件上传', data.files);
973
+ addEmptyMessage({
974
+ content: preservedContent, // 使用保留格式的内容,确保换行和空格完整保留
975
+ queryId,
976
+ currentFiles: localCurrentFiles,
977
+ filePaths: localCurrentFiles ? JSON.stringify(localCurrentFiles) : undefined,
978
+ });
979
+ // === 新增:如果当前会话是未命名且isNew,首次提问后自动改名并去除isNew ===
980
+ setConversationList(list =>
981
+ list.map(c =>
982
+ c.sessionId === sessionId && c.label === '未命名会话' && c.isNew
983
+ ? { ...c, label, gmtModified: Date.now(), isNew: false }
984
+ : c
985
+ )
986
+ );
987
+ setChatData(list =>
988
+ list.map(c =>
989
+ c.id === sessionId && c.label === '未命名会话' && c.isNew
990
+ ? { ...c, label, isNew: false }
991
+ : c
992
+ )
993
+ );
994
+ // 2. 文件上传逻辑 ---
995
+ if (data.files && data.files.length > 0) {
996
+ setAppStatus(pre => ({ ...pre, sender: 'uploading', display: 'uploading' }));
997
+ uploadController = new AbortController();
998
+ resetStopFlag(); // 使用 hook 提供的重置函数
999
+ const clientFileUids = data.files.map(
1000
+ () => `rc-upload-${Date.now()}-${Math.random().toString(36).slice(2)}`
1001
+ );
1002
+ const initialFileStatuses = data.files.map((fileItem: any, index: number) => ({
1003
+ ...fileItem,
1004
+ uid: clientFileUids[index],
1005
+ status: 'uploading',
1006
+ }));
1007
+ setFileStatuses(initialFileStatuses);
1008
+ const formData = new FormData();
1009
+ data.files.forEach((fileItem: any) => {
1010
+ formData.append('files', fileItem.file);
1011
+ });
1012
+ const sessionIdStr = String(sessionId || '');
1013
+ formData.append('sessionId', sessionIdStr);
1014
+ formData.append('uids', clientFileUids.join(','));
1015
+ axios
1016
+ .post(`${url}/index/knowledgeBase/file/uploadFiles`, formData, {
1017
+ headers: {
1018
+ Authorization: token,
1019
+ 'Content-Type': 'multipart/form-data',
1020
+ },
1021
+ signal: uploadController.signal,
1022
+ })
1023
+ .then(response => {
1024
+ console.log('文件上传的打印结果', response);
1025
+ console.log('文件上传响应详情:', {
1026
+ status: response.status,
1027
+ success: response.data?.success,
1028
+ dataType: Array.isArray(response.data?.data) ? 'array' : typeof response.data?.data,
1029
+ dataLength: Array.isArray(response.data?.data) ? response.data.data.length : 'N/A',
1030
+ errorMsg: response.data?.errorMsg,
1031
+ errorCode: response.data?.errorCode,
1032
+ fullResponse: response.data,
1033
+ });
1034
+
1035
+ if (stoppedRef.current) return;
1036
+
1037
+ // 检查响应数据结构 - 严格按照 headlessChat 的逻辑:必须有有效数据才继续
1038
+ const responseData = response.data?.data;
1039
+ const isSuccess = response.data?.success === true;
1040
+ const isValidData = Array.isArray(responseData) && responseData.length > 0;
1041
+
1042
+ if (isSuccess && isValidData) {
1043
+ // 后端返回了有效数据,使用后端数据
1044
+ const uploadedFilesData = responseData;
1045
+ const serverUids = uploadedFilesData.map((f: any) => f.uid);
1046
+
1047
+ // === 合并后端返回字段到 fileStatuses ===
1048
+ setFileStatuses(prev => {
1049
+ return prev.map(localFile => {
1050
+ const serverFile = responseData.find((f: any) => f.uid === localFile.uid);
1051
+ return serverFile ? { ...localFile, ...serverFile } : localFile;
1052
+ });
1053
+ });
1054
+
1055
+ setAppStatus(pre => ({ ...pre, sender: 'processing', display: 'analyzing' }));
1056
+ setFileStatuses(prev => prev.map(f => ({ ...f, status: 'parsing' })));
1057
+
1058
+ pollFileStatus(
1059
+ serverUids,
1060
+ (serverStatuses: any[]) => {
1061
+ // 添加空值检查,防止 serverStatuses 为 null 或 undefined
1062
+ if (!serverStatuses || !Array.isArray(serverStatuses)) {
1063
+ console.warn('pollFileStatus 回调返回的 serverStatuses 无效:', serverStatuses);
1064
+ return;
1065
+ }
1066
+
1067
+ setFileStatuses(currentLocalStatuses => {
1068
+ return currentLocalStatuses.map(localFile => {
1069
+ const correspondingServerFile = serverStatuses.find(
1070
+ (sf: any) => sf && sf.uid === localFile.uid
1071
+ );
1072
+ if (correspondingServerFile) {
1073
+ switch (correspondingServerFile.status) {
1074
+ case 3:
1075
+ return { ...localFile, status: 'done' };
1076
+ case 4:
1077
+ return { ...localFile, status: 'error' };
1078
+ case 1:
1079
+ case 2:
1080
+ default:
1081
+ return { ...localFile, status: 'parsing' };
1082
+ }
1083
+ }
1084
+ return localFile;
1085
+ });
1086
+ });
1087
+ // === 新增:全部完成时清空 sender 区 ===
1088
+ const allDone =
1089
+ Array.isArray(serverStatuses) &&
1090
+ serverStatuses.length > 0 &&
1091
+ serverStatuses.every((file: any) => file && file.status === 3);
1092
+ if (allDone) {
1093
+ setFileStatuses([]);
1094
+ // 上传完成后合并新文件到 uploadedFiles,保证结构统一
1095
+ const map = new Map();
1096
+ (serverStatuses || []).forEach((item: any) => {
1097
+ map.set(item.id, item);
1098
+ });
1099
+ setFileManagerData(prev => {
1100
+ const r = mergeFiles(prev.uploadedFiles, uploadedFilesData);
1101
+
1102
+ return {
1103
+ uploadedFiles: r.map(item => {
1104
+ return {
1105
+ ...item,
1106
+ pdfPages: map.get(item.id)?.pdfPages || 0, // 保留后端返回的 pdfPages 字段
1107
+ };
1108
+ }),
1109
+ };
1110
+ });
1111
+ }
1112
+ },
1113
+ // onComplete: 全部解析成功后才发起AI请求
1114
+ () => {
1115
+ clearFn?.();
1116
+ setAppStatus(pre => ({ ...pre, sender: 'processing', display: 'processing' }));
1117
+ // === 新增:回填 filePath 到 fileStatuses ===
1118
+ setFileStatuses(prev => {
1119
+ return prev.map(localFile => {
1120
+ const serverFile = uploadedFilesData.find(
1121
+ (f: any) => f.uid === localFile.uid
1122
+ );
1123
+
1124
+ if (serverFile) {
1125
+ return {
1126
+ ...localFile,
1127
+ ...serverFile, // 合并后端所有字段
1128
+ url: serverFile.filePath, // 兼容 url 字段
1129
+ filePath: serverFile.filePath,
1130
+ name: serverFile.fileName || localFile.name,
1131
+ convertedFilePath:
1132
+ serverFile.convertedFilePath || localFile.convertedFilePath,
1133
+ };
1134
+ }
1135
+ return localFile;
1136
+ });
1137
+ });
1138
+ // === 同步 currentFiles 字段到 chatData ===
1139
+ setChatData(chatList => {
1140
+ if (!chatList[0]) return chatList;
1141
+ const { messages } = chatList[0];
1142
+ for (let i = messages.length - 1; i >= 0; i--) {
1143
+ if (messages[i].istype === 'user') {
1144
+ const oldFiles = messages[i].currentFiles || [];
1145
+ messages[i].currentFiles = oldFiles.map((localFile: any) => {
1146
+ const serverFile = uploadedFilesData.find(
1147
+ (f: any) => f.uid === localFile.uid
1148
+ );
1149
+ return serverFile ? { ...localFile, ...serverFile } : localFile;
1150
+ });
1151
+ break;
1152
+ }
1153
+ }
1154
+ return [...chatList];
1155
+ });
1156
+ // === 发起AI请求 ===
1157
+ const now = Date.now();
1158
+ api_startChat_re(
1159
+ {
1160
+ configId: configIdToUse,
1161
+ content: preservedContent, // 使用保留格式的内容,确保换行和空格完整传递到后端
1162
+ fileUids: serverUids,
1163
+ lastDate: now,
1164
+ name: label,
1165
+ queryId: queryId,
1166
+ sessionId: String(sessionId || ''),
1167
+ type: 'text',
1168
+ audioUrl: '',
1169
+ enableThinking: !!senderSwitchValues.reasoning,
1170
+ enableWebsearch: !!senderSwitchValues.netSearch,
1171
+ qaChannel: senderSwitchValues.qaChannel,
1172
+ } as any,
1173
+ gmtModified => {
1174
+ fetchRecommendQuestions(queryId);
1175
+ }
1176
+ );
1177
+ },
1178
+ (errorMsg: string) => {
1179
+ setAppStatus(pre => ({ ...pre, display: 'ready', sender: 'ready' }));
1180
+ setFileStatuses(prev =>
1181
+ prev.map(f => ({ ...f, status: 'error', message: errorMsg }))
1182
+ );
1183
+ }
1184
+ );
1185
+ } else {
1186
+ // 详细的错误信息
1187
+ let errorMsg = '文件上传失败';
1188
+ if (response.data?.errorMsg) {
1189
+ errorMsg = response.data.errorMsg;
1190
+ } else if (!isSuccess) {
1191
+ errorMsg = `上传失败: success=${response.data?.success}`;
1192
+ } else if (!isValidData) {
1193
+ if (!Array.isArray(responseData)) {
1194
+ errorMsg = `上传失败: 返回数据格式错误,期望数组,实际为 ${typeof responseData}`;
1195
+ } else if (responseData.length === 0) {
1196
+ errorMsg = '上传失败: 返回数据为空';
1197
+ }
1198
+ }
1199
+
1200
+ console.error('文件上传失败详情:', {
1201
+ errorMsg,
1202
+ response: response.data,
1203
+ isSuccess,
1204
+ isValidData,
1205
+ });
1206
+
1207
+ message.error(errorMsg);
1208
+ setAppStatus(pre => ({ ...pre, display: 'error', sender: 'ready' }));
1209
+ setFileStatuses(prev =>
1210
+ prev.map(f => ({ ...f, status: 'error', message: errorMsg }))
1211
+ );
1212
+ }
1213
+ })
1214
+ .catch(error => {
1215
+ // 检查是否为文件大小超限错误 (413)
1216
+ if (error?.response?.status === 413) {
1217
+ if (error?.response?.statusText.includes('Request Entity Too Large')) {
1218
+ message.error('文件超过最大可上传限制');
1219
+ }
1220
+ } else {
1221
+ // 其他错误显示通用网络错误提示
1222
+ message.error('网络错误');
1223
+ }
1224
+
1225
+ setAppStatus(pre => ({ ...pre, display: 'error', sender: 'ready' }));
1226
+ setFileStatuses(prev =>
1227
+ prev.map(f => ({ ...f, status: 'error', message: '网络错误' }))
1228
+ );
1229
+ });
1230
+ return;
1231
+ }
1232
+ // 3. 无文件,直接发起AI请求
1233
+ setAppStatus(pre => ({ ...pre, display: 'processing', sender: 'processing' }));
1234
+ const now = Date.now();
1235
+ setConversationList(list =>
1236
+ list.map(c => (c.sessionId === sessionId ? { ...c, gmtModified: now } : c))
1237
+ );
1238
+ clearFn?.();
1239
+ api_startChat_re(
1240
+ {
1241
+ configId: configIdToUse,
1242
+ content: preservedContent, // 使用保留格式的内容,确保换行和空格完整传递到后端
1243
+ fileUids: [],
1244
+ lastDate: now,
1245
+ name: label,
1246
+ queryId: queryId,
1247
+ sessionId: String(sessionId || ''),
1248
+ type: 'text',
1249
+ audioUrl: '',
1250
+ enableThinking: !!senderSwitchValues.reasoning,
1251
+ enableWebsearch: !!senderSwitchValues.netSearch,
1252
+ qaChannel: senderSwitchValues.qaChannel,
1253
+ } as any,
1254
+ gmtModified => {
1255
+ fetchRecommendQuestions(queryId);
1256
+ }
1257
+ );
1258
+ }
1259
+
1260
+ // =================================================================
1261
+ // Event Handlers - 事件处理器
1262
+ // =================================================================
1263
+
1264
+ /**
1265
+ * 处理点赞反馈
1266
+ */
1267
+ function handleThumbsup(data: any) {
1268
+ if (data.restult == 1) {
1269
+ handleCancelFeedBack({ url, token, CSRFToken }, { queryId: data.queryId }, () => {
1270
+ setFeed(data.queryId, null);
1271
+ });
1272
+ } else {
1273
+ handleFeedBack({ url, token, CSRFToken }, { restult: 1, queryId: data.queryId }, () => {
1274
+ setFeed(data.queryId, 1);
1275
+ });
1276
+ }
1277
+ }
1278
+
1279
+ /**
1280
+ * 处理新建一个临时会话
1281
+ */
1282
+ function handleConversationCreate(data: any) {
1283
+ const sessionId = uid(32);
1284
+ // 新会话对象,等待用户输入后再发起创建请求
1285
+ const newConversation = {
1286
+ sessionId,
1287
+ label: '未命名会话',
1288
+ gmtModified: Date.now(),
1289
+ searchConfigDTO: {},
1290
+ filePath: '',
1291
+ configId: '',
1292
+ isNew: true, // 标记为新会话
1293
+ };
1294
+ // 在列表顶部插入新会话
1295
+ setConversationList(list => [{ ...newConversation, messages: [] }, ...list]);
1296
+ setChatData(list => [{ id: sessionId, messages: [], ...newConversation }, ...list]);
1297
+ // 激活新会话
1298
+ setActiveSessionId(sessionId);
1299
+ }
1300
+
1301
+ /**
1302
+ * 处理会话列表项点击
1303
+ */
1304
+ function handleItemClick(data: any) {
1305
+ if (data?.sessionId && data.sessionId !== activeSessionId) {
1306
+ // 如果切换会话时,有正在进行的流式请求(且请求属于当前会话),则取消它
1307
+ // 这样可以防止在流式输出开始前切换会话时,后续数据写入错误的会话
1308
+ if (activeSessionId) {
1309
+ cancelStreamBySessionId(activeSessionId);
1310
+ }
1311
+ setActiveSessionId(data.sessionId);
1312
+ setAppStatus(prev => ({ ...prev, sender: 'ready' }));
1313
+ // 点击会话时,使用其最后一条AI消息的推荐问题
1314
+ const conv = chatData.find(c => c.id === data.sessionId);
1315
+ if (conv && conv.messages && conv.messages.length > 0) {
1316
+ const lastAiMsg = [...conv.messages].reverse().find((m: any) => m.istype === 'ai');
1317
+ if (lastAiMsg && lastAiMsg.recommendQuestion) {
1318
+ setRecommandQuestions(lastAiMsg.recommendQuestion);
1319
+ } else {
1320
+ setRecommandQuestions([]);
1321
+ }
1322
+ } else {
1323
+ setRecommandQuestions([]);
1324
+ }
1325
+ }
1326
+ }
1327
+
1328
+ // =================================================================
1329
+ // Async Data Operations - 异步数据操作 (供事件发射器使用)
1330
+ // =================================================================
1331
+
1332
+ /**
1333
+ * 异步删除会话接口
1334
+ */
1335
+ const async_deleteConversation = async (sessionId: string) => {
1336
+ return new Promise((resolve, reject) => {
1337
+ handleDeleteConversation({ url, token, CSRFToken }, { sessionId }, (res: any) => {
1338
+ if (res) {
1339
+ message.success('删除成功!');
1340
+ resolve(res);
1341
+ } else {
1342
+ message.error('删除失败!');
1343
+ reject(res);
1344
+ }
1345
+ });
1346
+ });
1347
+ };
1348
+ /**
1349
+ * 异步刷新会话列表接口
1350
+ */
1351
+ const async_refreshConversationList = async () => {
1352
+ try {
1353
+ // 重置为第一页
1354
+ setConversationList([]);
1355
+ setConvTotal(0);
1356
+ await fetchConversationPage(1);
1357
+ return [];
1358
+ } catch (e) {
1359
+ message.error('刷新会话列表失败');
1360
+ throw e;
1361
+ }
1362
+ };
1363
+
1364
+ /**
1365
+ * 异步重命名会话接口
1366
+ */
1367
+ const async_renameConversation = async (sessionId: string, newLabel: string) => {
1368
+ return new Promise((resolve, reject) => {
1369
+ handleRename({ url, token, CSRFToken }, { label: newLabel, sessionId }, res => {
1370
+ if (res) {
1371
+ // 成功后立即更新本地状态
1372
+ const now = Date.now();
1373
+ setConversationList(list =>
1374
+ list.map(item =>
1375
+ item.sessionId === sessionId ? { ...item, label: newLabel, gmtModified: now } : item
1376
+ )
1377
+ );
1378
+ // 同步更新 chatData,确保 currentChatData?.label 也能更新
1379
+ setChatData(list =>
1380
+ list.map(item =>
1381
+ item.id === sessionId ? { ...item, label: newLabel } : item
1382
+ )
1383
+ );
1384
+ message.success('重命名成功!');
1385
+ resolve(res);
1386
+ } else {
1387
+ message.error('重命名失败');
1388
+ reject(res);
1389
+ }
1390
+ });
1391
+ });
1392
+ };
1393
+
1394
+ // =================================================================
1395
+ // Local State Updaters - 本地状态更新 (供事件发射器使用)
1396
+ // =================================================================
1397
+
1398
+ /**
1399
+ * 从本地状态中移除会话项
1400
+ */
1401
+ const fn_removeItemFromList = (sessionId: string) => {
1402
+ setConversationList(list => {
1403
+ const newList = list.filter(c => c.sessionId !== sessionId);
1404
+ // 如果删除的是当前会话,则激活列表中的第一个
1405
+ if (activeSessionId === sessionId) {
1406
+ if (newList.length > 0) {
1407
+ setActiveSessionId(newList[0].sessionId);
1408
+ } else {
1409
+ // 如果列表空了,则新建一个
1410
+ handleConversationCreate({});
1411
+ }
1412
+ }
1413
+ return newList;
1414
+ });
1415
+ };
1416
+
1417
+ /**
1418
+ * 在本地状态中重命名会话项
1419
+ */
1420
+ const fn_renameItemInList = (sessionId: string, newLabel: string) => {
1421
+ const now = Date.now();
1422
+ setConversationList(list =>
1423
+ list.map(item =>
1424
+ item.sessionId === sessionId ? { ...item, label: newLabel, gmtModified: now } : item
1425
+ )
1426
+ );
1427
+ // 同步更新 chatData,确保 currentChatData?.label 也能更新
1428
+ setChatData(list =>
1429
+ list.map(item =>
1430
+ item.id === sessionId ? { ...item, label: newLabel } : item
1431
+ )
1432
+ );
1433
+ };
1434
+
1435
+ const handleFeedDownConfirm = (data: any) => {
1436
+ handleFeedBack({ url, token, CSRFToken }, { ...data, ...feedParam, restult: 0 }, () => {
1437
+ setFeed(feedParam.queryId, 0);
1438
+ });
1439
+ };
1440
+
1441
+ const editDialog = (data: any) => {
1442
+ const newLabel = data.new_name;
1443
+ const oldLabel = curEditData.data.label;
1444
+ if (newLabel && newLabel !== oldLabel) {
1445
+ curEditData
1446
+ .async_renameConversation(newLabel)
1447
+ .then(() => {
1448
+ curEditData.fn_renameItemInList(newLabel);
1449
+ setisRenameModalOpen(false);
1450
+ })
1451
+ .catch(() => {
1452
+ // 重命名失败
1453
+ });
1454
+ } else {
1455
+ setisRenameModalOpen(false);
1456
+ }
1457
+ };
1458
+
1459
+ const editRemove = (data: any) => {
1460
+ curEditData
1461
+ .async_deleteConversation()
1462
+ .then(() => {
1463
+ curEditData.fn_removeItemFromList();
1464
+ setisRemoveModalOpen(false);
1465
+ })
1466
+ .catch(() => {
1467
+ // 删除失败
1468
+ });
1469
+ };
1470
+
1471
+ const handleEvent = (type: string, data?: any) => {
1472
+ console.log(type, data);
1473
+ switch (type) {
1474
+ case 'close_remove':
1475
+ setisRemoveModalOpen(false);
1476
+ break;
1477
+ case 'close_rename':
1478
+ setisRenameModalOpen(false);
1479
+ break;
1480
+ case 'ok_rename':
1481
+ editDialog(data);
1482
+ break;
1483
+ case 'ok_remove':
1484
+ editRemove(data);
1485
+ break;
1486
+ }
1487
+ };
1488
+
1489
+ // =================================================================
1490
+ // Event Emitter - 事件分发器
1491
+ // =================================================================
1492
+
1493
+ const getDataBaseData = async ({ url, params, replacePredataFunction }: any) => {
1494
+ try {
1495
+ const res = await axios.post(url, params, {
1496
+ headers: { Authorization: token },
1497
+ });
1498
+ const data = res?.data?.data?.data;
1499
+ if (data) {
1500
+ replacePredataFunction(data);
1501
+ }
1502
+ } catch (error) {
1503
+ console.log('error', error);
1504
+ }
1505
+ };
1506
+ const api_getFileStatusByUid = async (data: any) => {
1507
+ try {
1508
+ const res: any = await axios.get(
1509
+ `${getUrlPrefix()}/index/knowledgeBase/file/getFileStatusByUids?uids=${data.data.uids}`,
1510
+ { headers: { Authorization: token } }
1511
+ );
1512
+ console.log('resslkdjgalsg', res);
1513
+ return res?.data?.data;
1514
+ } catch (error) {}
1515
+ };
1516
+ const referenceFile_question_upload = async (data: any) => {
1517
+ if (fileViewTypes.includes(data.file_type)) {
1518
+ // 优先使用 metadata.source,否则使用 url 或 convertedFilePath
1519
+ const previewUrl = data.metadata?.source
1520
+ ? `${TempBaseUrl}${data.metadata.source}`
1521
+ : (data.url || data.convertedFilePath || data.filePath);
1522
+ setCurFileInfo({
1523
+ ...data,
1524
+ parse_url: previewUrl,
1525
+ });
1526
+
1527
+ setDrawerType('preview');
1528
+ setOpen(true);
1529
+ return;
1530
+ }
1531
+ try {
1532
+ const res: any = await api_getFileStatusByUid({
1533
+ data: { uids: data.uid },
1534
+ config: {
1535
+ url: getUrlPrefix(),
1536
+ token,
1537
+ enableMock: false,
1538
+ },
1539
+ });
1540
+ if (res?.length) {
1541
+ const d = res[0];
1542
+ setCurFileInfo({
1543
+ ...data,
1544
+ parse_url: d.convertedFilePath,
1545
+ });
1546
+
1547
+ setDrawerType('preview');
1548
+ setOpen(true);
1549
+ }
1550
+ } catch (error) {}
1551
+ };
1552
+ /**
1553
+ * 统一事件分发器,连接业务逻辑与所有子组件的事件
1554
+ * @param {string} eventName - 事件名称
1555
+ * @param {any} data - 事件数据
1556
+ */
1557
+ const mergedEventsEmit = (eventName: string, data: any) => {
1558
+ console.log(`[Event] ${eventName}`, data);
1559
+ switch (eventName) {
1560
+ case 'reference_file:click':
1561
+ if (data.file_type === 'image') return;
1562
+
1563
+ if (!data.parse_url) {
1564
+ if (!data.uid) {
1565
+ if (data.references) return;
1566
+ return message.warning('文件还未解析完成,请稍后预览');
1567
+ }
1568
+ referenceFile_question_upload(data);
1569
+ return;
1570
+ }
1571
+ setCurFileInfo({
1572
+ ...data,
1573
+ file_type: data.type,
1574
+ file_name: data.file,
1575
+ });
1576
+ setDrawerType('preview');
1577
+ setOpen(true);
1578
+
1579
+ break;
1580
+ case 'conversations:load_more': {
1581
+ const hasMore = conversationList.length < convTotal;
1582
+ if (hasMore && !convLoading) {
1583
+ fetchConversationPage(convPageNo + 1);
1584
+ }
1585
+ break;
1586
+ }
1587
+ case 'retrieve-sql-data:pagingation_click':
1588
+ const { page, pageSize, fetchUrl, source, replacePredataFunction } = data;
1589
+ getDataBaseData({
1590
+ url: `${url}${fetchUrl}`,
1591
+ replacePredataFunction,
1592
+ params: { pageNo: page, pageSize, convertedFilePath: source, stream: false },
1593
+ });
1594
+ break;
1595
+ case 'web_referenceFile_title:click':
1596
+ if (!data?.data?.link) return console.error('没有网页地址');
1597
+ window.open(data.data.link, '_blank');
1598
+ break;
1599
+ // --- 对话列表事件 ---
1600
+ case 'conversation:item_click':
1601
+ handleItemClick(data);
1602
+ break;
1603
+
1604
+ case 'files:finished_animation_complete':
1605
+ setFileStatuses([]);
1606
+ break;
1607
+
1608
+ case 'conversation:create':
1609
+ handleConversationCreate(data);
1610
+ break;
1611
+ case 'conversation:new_assistant_change':
1612
+ setNewAssistantId(data.assistantId);
1613
+ break;
1614
+ case 'conversations:rename_icon_clicked':
1615
+ setCurEditData({
1616
+ data,
1617
+ async_renameConversation: (newLabel: string) =>
1618
+ async_renameConversation(data.sessionId, newLabel),
1619
+ fn_renameItemInList: (newLabel: string) =>
1620
+ fn_renameItemInList(data.sessionId, newLabel),
1621
+ });
1622
+ setisRenameModalOpen(true);
1623
+ break;
1624
+ case 'conversations:delete_icon_clicked':
1625
+ setCurEditData({
1626
+ data,
1627
+ async_deleteConversation: () => async_deleteConversation(data.sessionId),
1628
+ async_refreshConversationList,
1629
+ fn_removeItemFromList: () => fn_removeItemFromList(data.sessionId),
1630
+ });
1631
+ setisRemoveModalOpen(true);
1632
+ break;
1633
+ case 'sender:send':
1634
+ handleSenderSend(data, data.clearFn);
1635
+ break;
1636
+ case 'sender:send_recommandQuestion': {
1637
+ // 旧链路:来自 Sender 的推荐问题,可能已经在 AiChat 内转发为 sender:send
1638
+ // 这里兜底处理,确保能直接触发发送
1639
+ const content = (data?.content ?? '').toString();
1640
+ if (!content) break;
1641
+ handleSenderSend({ content }, () => {});
1642
+ break;
1643
+ }
1644
+ case 'chatbox:follow_up_question_click': {
1645
+ // 新链路:来自 ChatMessageAdapter 的追问点击
1646
+ const content = (data?.question ?? data?.content ?? '').toString();
1647
+ if (!content) break;
1648
+ handleSenderSend({ content }, () => {});
1649
+ break;
1650
+ }
1651
+ case 'sender:stop':
1652
+ setStopFlag(); // 使用 hook 提供的停止函数
1653
+ if (uploadController) {
1654
+ uploadController.abort();
1655
+ uploadController = null;
1656
+ }
1657
+ if (stopPoll) {
1658
+ stopPoll();
1659
+ stopPoll = null;
1660
+ }
1661
+ setAppStatus(pre => ({ ...pre, display: 'ready', sender: 'ready' }));
1662
+ setFileStatuses([]);
1663
+ stopStream?.();
1664
+ break;
1665
+ case 'sender:clear':
1666
+ data.clearFn?.();
1667
+ break;
1668
+ case 'sender:configChange':
1669
+ // 配置项变化,记录最新 switch 状态
1670
+ if (data && data.all) {
1671
+ setSenderSwitchValues(data.all);
1672
+ }
1673
+ break;
1674
+ case 'sender:action_upload':
1675
+ // 触发文件上传按钮点击,打开文件选择对话框
1676
+ if (fileInputRef.current) {
1677
+ fileInputRef.current.click();
1678
+ }
1679
+ break;
1680
+ case 'sender:action_history':
1681
+ // 打开文件管理器 Modal
1682
+ setFileManagerOpen(true);
1683
+ break;
1684
+ case 'uploaded_file:item_click': {
1685
+ // 处理文件列表项点击,关闭 Popover(chat 模块使用 AiChat 内置右侧栏,不需要预览 Drawer)
1686
+ // 注意:setFileListPopoverOpen 由 useFileDisplayTools hook 管理,这里通过 eventsEmit 触发关闭
1687
+ // 可以在这里添加其他处理逻辑,比如在右侧栏显示文件详情
1688
+ break;
1689
+ }
1690
+
1691
+ // --- 消息卡片事件 ---
1692
+ case 'action_copy:click':
1693
+ if (data) {
1694
+ toCopy(data.content, data.event);
1695
+ }
1696
+ break;
1697
+ case 'chatbox:copy': {
1698
+ const text = data?.content || '';
1699
+ if (!text) break;
1700
+ // 直接使用 Clipboard API,避免依赖 event.currentTarget
1701
+ const copyByClipboardApi = async (t: string) => {
1702
+ try {
1703
+ await navigator.clipboard.writeText(t);
1704
+ message.success('已复制到剪贴板');
1705
+ return true;
1706
+ } catch (_) {
1707
+ return false;
1708
+ }
1709
+ };
1710
+ const fallbackCopy = (t: string) => {
1711
+ const textarea = document.createElement('textarea');
1712
+ textarea.value = t;
1713
+ textarea.style.position = 'fixed';
1714
+ textarea.style.opacity = '0';
1715
+ document.body.appendChild(textarea);
1716
+ textarea.focus();
1717
+ textarea.select();
1718
+ try {
1719
+ document.execCommand('copy');
1720
+ message.success('已复制到剪贴板');
1721
+ } catch (_) {
1722
+ message.error('复制失败');
1723
+ }
1724
+ document.body.removeChild(textarea);
1725
+ };
1726
+ copyByClipboardApi(text).then(ok => {
1727
+ if (!ok) fallbackCopy(text);
1728
+ });
1729
+ break;
1730
+ }
1731
+ case 'chatbox:repeat':
1732
+ console.log(data, 'sdkflskdjflksdjfklsdjlfkjsdklfjsdklfjlks');
1733
+ handleReSenderSend(data);
1734
+ break;
1735
+ case 'action_thumbsup':
1736
+ handleThumbsup(data);
1737
+ break;
1738
+ case 'chatbox:like':
1739
+ handleThumbsup({ queryId: data?.queryId, restult: data?.result });
1740
+ break;
1741
+ case 'action_thumbsdown':
1742
+ if (data.restult === 0) {
1743
+ handleCancelFeedBack({ url, token, CSRFToken }, { queryId: data.queryId }, () => {
1744
+ setFeed(data.queryId, null);
1745
+ });
1746
+ } else {
1747
+ setFeedParam(data);
1748
+ setOpenFeed(true);
1749
+ }
1750
+ break;
1751
+ case 'chatbox:dislike':
1752
+ if (data?.result === 0) {
1753
+ handleCancelFeedBack({ url, token, CSRFToken }, { queryId: data.queryId }, () => {
1754
+ setFeed(data.queryId, null);
1755
+ });
1756
+ } else {
1757
+ setFeedParam({ queryId: data?.queryId });
1758
+ setOpenFeed(true);
1759
+ }
1760
+ break;
1761
+ case 'action_retrive_tag':
1762
+ // 处理 retrive-tag 点击事件,透传给父组件
1763
+ eventsEmit?.('action_retrive_tag', data);
1764
+ break;
1765
+ case 'referenceFile_view':
1766
+ // 处理文件预览事件,透传给父组件
1767
+ eventsEmit?.('referenceFile_view', data);
1768
+ break;
1769
+ case 'referenceFile_question':
1770
+ // 处理单文档对话事件,先处理预览,再透传给父组件
1771
+ console.log(data, 'zhezhhzhzh');
1772
+ // 调用预览处理函数
1773
+ referenceFile_question_upload(data.data || data);
1774
+ // 同时透传给父组件(如果需要)
1775
+ eventsEmit?.('referenceFile_question', data);
1776
+ break;
1777
+ case 'referenceFile_download':
1778
+ // 处理文件下载事件,透传给父组件
1779
+ eventsEmit?.('referenceFile_download', data);
1780
+ break;
1781
+ case 'references:click': {
1782
+ // 使用 AiChat 内置的右侧栏事件
1783
+ const Content = () => {
1784
+ // const safe = (() => {
1785
+ // try {
1786
+ // return typeof data === 'string' ? data : JSON.stringify(data, null, 2);
1787
+ // } catch (e) {
1788
+ // return String(data);
1789
+ // }
1790
+ // })();
1791
+ return (
1792
+ <ReferenceBar
1793
+ type={DrawerType.REFERENCELIST}
1794
+ data={data}
1795
+ token={token}
1796
+ eventsEmit={eventsEmit}
1797
+ is_download={is_download}
1798
+ />
1799
+ );
1800
+ };
1801
+ const setContentEvent = new CustomEvent('aichat:right_set_content', {
1802
+ detail: { content: <Content /> },
1803
+ });
1804
+ window.dispatchEvent(setContentEvent);
1805
+ const openEvent = new CustomEvent('aichat:right_set', {
1806
+ detail: { collapsed: false },
1807
+ });
1808
+ window.dispatchEvent(openEvent);
1809
+ break;
1810
+ }
1811
+ case 'retrive_tag:click':
1812
+ // 处理 retrive-tag 点击事件,透传给父组件
1813
+ eventsEmit?.('retrive_tag:click', data);
1814
+ break;
1815
+ // --- 文件管理器事件 ---
1816
+ case 'fileManager:change': {
1817
+ // 保存核心组件传递的文件处理函数引用
1818
+ if (typeof data?.setUploadedFiles === 'function') {
1819
+ setUploadedFilesRef.current = data.setUploadedFiles;
1820
+ removeFileRef.current = data.removeFile;
1821
+ // 保存上传限制配置和已上传文件列表引用,供 handleFileSelect 使用
1822
+ senderConfigRef.current = data?.senderConfig;
1823
+ uploadedFilesRef.current = Array.isArray(data?.uploadedFiles) ? data.uploadedFiles : [];
1824
+ // 同步待上传文件状态(从 AiChat 内部获取)
1825
+ if (Array.isArray(data?.uploadedFiles)) {
1826
+ setPendingFiles(data.uploadedFiles);
1827
+ }
1828
+ console.log('[DEBUG] fileManager:change 事件触发,setUploadedFilesRef 已设置', {
1829
+ hasSetUploadedFiles: !!setUploadedFilesRef.current,
1830
+ uploadedFilesCount: data?.uploadedFiles?.length || 0,
1831
+ pendingFilesCount: data?.uploadedFiles?.length || 0,
1832
+ hasSenderConfig: !!data?.senderConfig,
1833
+ senderConfig: data?.senderConfig,
1834
+ upload_number_limit: data?.senderConfig?.upload_number_limit,
1835
+ upload_size_limit: data?.senderConfig?.upload_size_limit,
1836
+ });
1837
+ }
1838
+ break;
1839
+ }
1840
+
1841
+ // --- 文件管理器上传文件删除事件,透传到业务层 ---
1842
+ case 'uploaded_file:removeFromTemp': {
1843
+ console.log('[DEBUG] mergedEventsEmit uploaded_file:removeFromTemp 事件', data);
1844
+
1845
+ const uid = data?.file?.uid;
1846
+ if (!uid) {
1847
+ console.warn('[DEBUG] 缺少uid,无法删除临时文件', data);
1848
+ break;
1849
+ }
1850
+ // 获取当前对话最后一条用户消息的 queryId
1851
+ const lastUserMsg = [...(currentChatData?.messages || [])]
1852
+ .reverse()
1853
+ .find((msg: any) => msg.istype === 'user');
1854
+ const queryId = lastUserMsg?.queryId;
1855
+
1856
+ // 使用统一的文件删除处理函数
1857
+ handleRemoveFromTemp(uid, queryId);
1858
+ break;
1859
+ }
1860
+ case 'uploaded_file:remove_batch':
1861
+ break;
1862
+
1863
+ default:
1864
+ // 其他未处理事件可继续向上层抛出
1865
+ eventsEmit?.(eventName, data);
1866
+ }
1867
+ };
1868
+
1869
+ // =================================================================
1870
+ // Component & UI Configuration - 组件及UI配置
1871
+ // =================================================================
1872
+
1873
+ /**
1874
+ * Sidebar 配置
1875
+ */
1876
+ const sidebar: SidebarTabConfig[] = [
1877
+ {
1878
+ key: 'conversations',
1879
+ type: 'custom',
1880
+ label: '对话历史',
1881
+ icon: <History size={16} />,
1882
+ enabled: true,
1883
+ component: (
1884
+ <GientechConversationPanel
1885
+ data={conversationList}
1886
+ assistantList={assistantList}
1887
+ activedItem={activeSessionId || ''}
1888
+ hasMore={conversationList.length < convTotal}
1889
+ loadingMore={convLoading}
1890
+ sortRules={[
1891
+ { label: '当天', dayPeriod: 1 },
1892
+ { label: '3天', dayPeriod: 3 },
1893
+ { label: '7天', dayPeriod: 7 },
1894
+ { label: '30天', dayPeriod: 30 },
1895
+ ]}
1896
+ eventsEmit={mergedEventsEmit}
1897
+ styles={normalizedStyles.theme}
1898
+ />
1899
+ ),
1900
+ },
1901
+ ];
1902
+
1903
+ /**
1904
+ * 业务层自定义组件
1905
+ */
1906
+ const WelcomeComponent = React.useCallback(() => {
1907
+ return (
1908
+ <GientechNewChatWelcome
1909
+ eventsEmit={mergedEventsEmit}
1910
+ assistantList={assistantList}
1911
+ styles={normalizedStyles}
1912
+ selectedId={selectedAssistantId}
1913
+ onSelectAssistant={setSelectedAssistantId}
1914
+ productLogo={rest?.productLogo}
1915
+ />
1916
+ );
1917
+ }, [mergedEventsEmit, assistantList, normalizedStyles, selectedAssistantId, rest?.productLogo]);
1918
+
1919
+ const businessCustomComponents = React.useMemo(
1920
+ () => ({
1921
+ AiChatBox: (msg: any, idx: number) => {
1922
+ const isLastMessage = idx === (currentChatData?.messages?.length || 0) - 1;
1923
+
1924
+ // 根据消息的 status 和 appStatus 来确定 displayStatus
1925
+ let displayStatus = 'ready';
1926
+ if (isLastMessage) {
1927
+ // 优先使用消息的 status 字段
1928
+ if (msg.status !== undefined) {
1929
+ switch (msg.status) {
1930
+ case 0: // StatusType.Pending
1931
+ displayStatus = 'thinking';
1932
+ break;
1933
+ case 1: // StatusType.Process
1934
+ displayStatus = 'processing';
1935
+ break;
1936
+ case 2: // StatusType.Done
1937
+ displayStatus = 'ready';
1938
+ break;
1939
+ case 3: // StatusType.Error
1940
+ displayStatus = 'error';
1941
+ break;
1942
+ default:
1943
+ displayStatus = appStatus.display;
1944
+ }
1945
+ } else {
1946
+ // 如果没有 status 字段,使用 appStatus.display
1947
+ displayStatus = appStatus.display;
1948
+ }
1949
+ }
1950
+ // 将推荐问题强制转换为数组,避免切换会话时报 .map 错误
1951
+ let safeFollowUps: string[] = [];
1952
+ if (Array.isArray(recommandQuestions)) {
1953
+ safeFollowUps = recommandQuestions as string[];
1954
+ } else if (typeof recommandQuestions === 'string') {
1955
+ try {
1956
+ safeFollowUps = JSON.parse(recommandQuestions) || [];
1957
+ } catch {
1958
+ safeFollowUps = [];
1959
+ }
1960
+ }
1961
+ // 保证传入 ChatMessageAdapter 的 reference/webReference/graphReference 始终是有效的 JSON 字符串
1962
+ const safeReference = (() => {
1963
+ const r = (msg as any)?.reference;
1964
+ if (!r) return '[]';
1965
+ if (typeof r === 'string') {
1966
+ try {
1967
+ JSON.parse(r);
1968
+ return r;
1969
+ } catch {
1970
+ return '[]';
1971
+ }
1972
+ }
1973
+ try {
1974
+ return JSON.stringify(r);
1975
+ } catch {
1976
+ return '[]';
1977
+ }
1978
+ })();
1979
+ const safeWebReference = (() => {
1980
+ const r = (msg as any)?.webReference;
1981
+ if (!r) return '[]';
1982
+ if (typeof r === 'string') {
1983
+ try {
1984
+ JSON.parse(r);
1985
+ return r;
1986
+ } catch {
1987
+ return '[]';
1988
+ }
1989
+ }
1990
+ try {
1991
+ return JSON.stringify(r);
1992
+ } catch {
1993
+ return '[]';
1994
+ }
1995
+ })();
1996
+ const safeGraphReference = (() => {
1997
+ const r = (msg as any)?.graphReference;
1998
+ if (!r) return '[]';
1999
+ if (typeof r === 'string') {
2000
+ try {
2001
+ JSON.parse(r);
2002
+ return r;
2003
+ } catch {
2004
+ return '[]';
2005
+ }
2006
+ }
2007
+ try {
2008
+ return JSON.stringify(r);
2009
+ } catch {
2010
+ return '[]';
2011
+ }
2012
+ })();
2013
+ return (
2014
+ <ChatMessageAdapter
2015
+ key={`${msg?.queryId || msg?.id || idx}-${msg?.istype || 'unknown'}`}
2016
+ {...msg}
2017
+ referenceMode="button"
2018
+ reference={safeReference}
2019
+ webReference={safeWebReference}
2020
+ graphReference={safeGraphReference}
2021
+ contentType="stream" // 添加 contentType 确保 slateContent 能正确生成
2022
+ displayStatus={displayStatus}
2023
+ eventsEmit={mergedEventsEmit}
2024
+ // 仅最后一条 AI 消息展示追问
2025
+ isLast={isLastMessage && msg?.istype === 'ai'}
2026
+ followUpQuestions={isLastMessage && msg?.istype === 'ai' ? safeFollowUps : []}
2027
+ defaultAnswer={
2028
+ currentChatData?.searchConfigDTO?.defaultAnswer ||
2029
+ '不知道什么原因我们没能查到你要的问题*-*'
2030
+ }
2031
+ is_download={is_download}
2032
+ styles={normalizedStyles as any}
2033
+ />
2034
+ );
2035
+ },
2036
+ LogoBox: (props: any) => {
2037
+ if (props && props.LogoBox === null) return null;
2038
+ if (props && typeof props.LogoBox === 'function') return <props.LogoBox />;
2039
+ return <div className="text-2xl w-full text-center font-bold">小鲸智能助手</div>;
2040
+ },
2041
+ DisplayLoading: () => <DisplayLoading />,
2042
+ DisplayError: () => <DisplayError />,
2043
+ AppError: () => (
2044
+ <AppError
2045
+ msg="应用初始化失败"
2046
+ subMsg="无法完成应用初始化,这可能是由于网络连接问题或服务配置错误导致的。请检查您的网络连接并重试。"
2047
+ />
2048
+ ),
2049
+ AppLoading: () => (
2050
+ <AppLoading
2051
+ title={rest?.appLoadingConfig?.title}
2052
+ subtitle={rest?.appLoadingConfig?.subtitle || ''}
2053
+ />
2054
+ ),
2055
+ UserChatBox: (msg: any, idx: number) => {
2056
+ return (
2057
+ <ChatMessageAdapter
2058
+ key={`${msg?.queryId || msg?.id || idx}-user`}
2059
+ {...msg}
2060
+ referenceMode="button"
2061
+ contentType="plainText"
2062
+ isUser={true}
2063
+ displayStatus="ready"
2064
+ eventsEmit={mergedEventsEmit}
2065
+ styles={normalizedStyles as any}
2066
+ fileManagerData={fileManagerData}
2067
+ // 确保 currentFiles 被正确传递(用于渲染上传文件卡片)
2068
+ currentFiles={msg?.currentFiles}
2069
+ />
2070
+ );
2071
+ },
2072
+ WelcomeComponent,
2073
+ }),
2074
+ [
2075
+ WelcomeComponent,
2076
+ currentChatData,
2077
+ appStatus.display,
2078
+ mergedEventsEmit,
2079
+ normalizedStyles,
2080
+ is_download,
2081
+ fileManagerData,
2082
+ ]
2083
+ );
2084
+
2085
+ /**
2086
+ * 合并外部传入和业务默认的自定义组件,外部优先
2087
+ */
2088
+ const mergedCustomComponents: { [key: string]: any } = {
2089
+ ...businessCustomComponents,
2090
+ ...(rest.CustomComponents || {}),
2091
+ };
2092
+
2093
+ useEffect(() => {
2094
+ setDynamicSenderConfig((prev: any) => {
2095
+ const switchs = Array.isArray(prev.switchs)
2096
+ ? prev.switchs.map((item: any) =>
2097
+ item.name === 'reasoning' ? { ...item, enabled: is_enableThinking } : item
2098
+ )
2099
+ : [];
2100
+ return {
2101
+ ...prev,
2102
+ switchs,
2103
+ };
2104
+ });
2105
+ }, [is_enableThinking]);
2106
+ // =================================================================
2107
+ // Render - 渲染
2108
+ // =================================================================
2109
+
2110
+ // 使用统一的文件展示工具 Hook
2111
+ // 使用 allFilesFromQuery(从 query.filePaths 收集的所有文件)而不是 fileManagerData.uploadedFiles
2112
+ const { FileDisplayTools, setFileListPopoverOpen } = useFileDisplayTools({
2113
+ fileManagerData: {
2114
+ uploadedFiles: (allFilesFromQuery.length > 0 ? allFilesFromQuery : (fileManagerData.uploadedFiles || [])) as any[],
2115
+ },
2116
+ styles: normalizedStyles,
2117
+ eventsEmit: mergedEventsEmit,
2118
+ });
2119
+
2120
+ // 处理文件选择
2121
+ const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
2122
+ const files = e.target.files;
2123
+ if (files && files.length > 0) {
2124
+ console.log('[DEBUG] handleFileSelect 触发', {
2125
+ filesCount: files.length,
2126
+ hasSetUploadedFilesRef: !!setUploadedFilesRef.current,
2127
+ hasSenderConfig: !!senderConfigRef.current,
2128
+ dynamicSenderConfig: dynamicSenderConfig,
2129
+ dynamicSenderConfigKeys: dynamicSenderConfig ? Object.keys(dynamicSenderConfig) : [],
2130
+ senderConfigRef: senderConfigRef.current,
2131
+ senderConfigRefKeys: senderConfigRef.current ? Object.keys(senderConfigRef.current) : [],
2132
+ });
2133
+
2134
+ const fileArray = Array.from(files);
2135
+
2136
+ // 应用上传限制配置验证
2137
+ // 优先使用 senderConfigRef.current,但如果它没有上传限制字段,则使用 dynamicSenderConfig
2138
+ let configSource: any = senderConfigRef.current;
2139
+ if (!configSource ||
2140
+ (configSource.upload_size_limit === undefined &&
2141
+ configSource.upload_number_limit === undefined &&
2142
+ (!configSource.upload_file_type_limit || configSource.upload_file_type_limit.length === 0))) {
2143
+ configSource = dynamicSenderConfig;
2144
+ }
2145
+ const existingFiles = uploadedFilesRef.current || [];
2146
+
2147
+ console.log('[DEBUG] 验证配置检查', {
2148
+ configSource,
2149
+ upload_number_limit: configSource?.upload_number_limit,
2150
+ upload_size_limit: configSource?.upload_size_limit,
2151
+ existingFilesCount: existingFiles.length,
2152
+ newFilesCount: fileArray.length,
2153
+ });
2154
+
2155
+ // 检查是否有上传限制配置(只要有一个限制配置存在就执行验证)
2156
+ const hasUploadLimitConfig =
2157
+ configSource &&
2158
+ (configSource.upload_size_limit !== undefined ||
2159
+ (configSource.upload_number_limit !== undefined && configSource.upload_number_limit > 0) ||
2160
+ (configSource.upload_file_type_limit && configSource.upload_file_type_limit.length > 0));
2161
+
2162
+ console.log('[DEBUG] 配置检查结果', {
2163
+ hasUploadLimitConfig,
2164
+ upload_number_limit: configSource?.upload_number_limit,
2165
+ upload_size_limit: configSource?.upload_size_limit,
2166
+ upload_file_type_limit: configSource?.upload_file_type_limit,
2167
+ });
2168
+
2169
+ if (hasUploadLimitConfig) {
2170
+ // 提取上传限制相关配置
2171
+ const uploadConfig = {
2172
+ upload_size_limit: configSource.upload_size_limit,
2173
+ upload_number_limit: configSource.upload_number_limit,
2174
+ upload_file_type_limit: configSource.upload_file_type_limit,
2175
+ upload_limit_message: configSource.upload_limit_message,
2176
+ };
2177
+
2178
+ console.log('[DEBUG] 执行文件验证', {
2179
+ uploadConfig,
2180
+ existingFilesCount: existingFiles.length,
2181
+ newFilesCount: fileArray.length,
2182
+ totalCount: existingFiles.length + fileArray.length,
2183
+ });
2184
+
2185
+ const validationResult = validateFileUpload(
2186
+ fileArray,
2187
+ existingFiles,
2188
+ uploadConfig
2189
+ );
2190
+
2191
+ console.log('[DEBUG] 验证结果', {
2192
+ valid: validationResult.valid,
2193
+ errors: validationResult.errors,
2194
+ rejectedFilesCount: validationResult.rejectedFiles.length,
2195
+ });
2196
+
2197
+ if (!validationResult.valid) {
2198
+ // 显示错误提示
2199
+ showValidationErrors(validationResult);
2200
+ // 只保留通过验证的文件
2201
+ const validFiles = fileArray.filter(
2202
+ file => !validationResult.rejectedFiles.includes(file)
2203
+ );
2204
+
2205
+ if (validFiles.length === 0) {
2206
+ // 所有文件都被拒绝,清空 input 并返回
2207
+ if (fileInputRef.current) {
2208
+ fileInputRef.current.value = '';
2209
+ }
2210
+ return;
2211
+ }
2212
+
2213
+ // 使用通过验证的文件
2214
+ fileArray.splice(0, fileArray.length, ...validFiles);
2215
+ }
2216
+ }
2217
+
2218
+ // 通过 setUploadedFilesRef 直接设置文件到 aichat 内部
2219
+ // 这个 ref 在 fileManager:change 事件中设置
2220
+ if (setUploadedFilesRef.current) {
2221
+ const fileList = fileArray.map(file => ({
2222
+ file,
2223
+ type: file.type.startsWith('image/') ? ('image' as const) : ('document' as const),
2224
+ status: 'pending' as const, // 添加 status 字段,确保文件可以正确显示
2225
+ name: file.name, // 添加 name 字段,方便 FilesDisplay 使用
2226
+ }));
2227
+ console.log('[DEBUG] 通过 setUploadedFilesRef 设置文件', fileList);
2228
+ setUploadedFilesRef.current(fileList);
2229
+ } else {
2230
+ console.warn('[DEBUG] setUploadedFilesRef 未设置,延迟重试');
2231
+ // 如果 ref 还没有设置,延迟重试
2232
+ setTimeout(() => {
2233
+ if (setUploadedFilesRef.current) {
2234
+ const fileList = fileArray.map(file => ({
2235
+ file,
2236
+ type: file.type.startsWith('image/') ? ('image' as const) : ('document' as const),
2237
+ status: 'pending' as const, // 添加 status 字段,确保文件可以正确显示
2238
+ name: file.name, // 添加 name 字段,方便 FilesDisplay 使用
2239
+ }));
2240
+ console.log('[DEBUG] 延迟后通过 setUploadedFilesRef 设置文件', fileList);
2241
+ setUploadedFilesRef.current(fileList);
2242
+ } else {
2243
+ console.error('[DEBUG] 延迟后 setUploadedFilesRef 仍未设置');
2244
+ }
2245
+ }, 100);
2246
+ }
2247
+ }
2248
+ // 清空 input,以便可以重复选择同一文件
2249
+ if (fileInputRef.current) {
2250
+ fileInputRef.current.value = '';
2251
+ }
2252
+ };
2253
+
2254
+ return (
2255
+ <div style={{ height: '100%', minWidth: '1116px', overflow: 'hidden' }}>
2256
+ {/* 隐藏的文件上传input */}
2257
+ <input
2258
+ ref={fileInputRef}
2259
+ type="file"
2260
+ multiple
2261
+ accept="image/*,.pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.txt,.md,.json,.csv,.zip,.rar,.7z"
2262
+ style={{ display: 'none' }}
2263
+ onChange={handleFileSelect}
2264
+ />
2265
+ {/* 右侧栏由 AiChat 内置事件控制,此处不再使用 antd Drawer */}
2266
+
2267
+ <WrappedComponent
2268
+ {...rest}
2269
+ status={appStatus}
2270
+ chatData={currentChatData}
2271
+ sidebar={sidebar}
2272
+ recommandQuestions={[]}
2273
+ eventsEmit={mergedEventsEmit}
2274
+ styles={normalizedStyles as any}
2275
+ senderConfig={dynamicSenderConfig}
2276
+ fileUploadStatus={fileStatuses}
2277
+ CustomComponents={mergedCustomComponents}
2278
+ activeSessionId={activeSessionId}
2279
+ scrollOld={scrollOld}
2280
+ rightbarWidth={'400px'}
2281
+ {...({
2282
+ chatHeader: {
2283
+ title: currentChatData?.label || '新对话',
2284
+ tools: FileDisplayTools,
2285
+ },
2286
+ } as any)}
2287
+ />
2288
+ <FeedBackModal
2289
+ open={openFeed}
2290
+ feedBackList={feedBackList}
2291
+ setOpen={setOpenFeed}
2292
+ handleConfirm={handleFeedDownConfirm}
2293
+ />
2294
+ <RenameModal
2295
+ isModalOpen={isRenameModalOpen}
2296
+ handleOk={handleEvent}
2297
+ handleCancel={() => handleEvent('close_rename')}
2298
+ data={curEditData?.data || {}}
2299
+ />
2300
+ <DeleteModal
2301
+ isModalOpen={isRemoveModalOpen}
2302
+ handleOk={handleEvent}
2303
+ handleCancel={() => handleEvent('close_remove')}
2304
+ data={curEditData?.data || {}}
2305
+ />
2306
+ {/* <FileManager
2307
+ open={fileManagerOpen}
2308
+ onCancel={() => setFileManagerOpen(false)}
2309
+ files={allFilesFromQuery.length > 0 ? allFilesFromQuery : (fileManagerData.uploadedFiles || [])}
2310
+ currentFiles={pendingFiles}
2311
+ styles={normalizedStyles}
2312
+ eventsEmit={mergedEventsEmit}
2313
+ onUpload={fileList => {
2314
+ console.log('[DEBUG] FileManager onUpload 触发', {
2315
+ filesCount: fileList.length,
2316
+ hasSetUploadedFilesRef: !!setUploadedFilesRef.current,
2317
+ });
2318
+
2319
+ const fileArray = Array.from(fileList);
2320
+
2321
+ // 应用上传限制配置验证
2322
+ // 优先使用 senderConfigRef.current,但如果它没有上传限制字段,则使用 dynamicSenderConfig
2323
+ let configSource: any = senderConfigRef.current;
2324
+ if (!configSource ||
2325
+ (configSource.upload_size_limit === undefined &&
2326
+ configSource.upload_number_limit === undefined &&
2327
+ (!configSource.upload_file_type_limit || configSource.upload_file_type_limit.length === 0))) {
2328
+ configSource = dynamicSenderConfig;
2329
+ }
2330
+ const existingFiles = uploadedFilesRef.current || [];
2331
+
2332
+ // 检查是否有上传限制配置(只要有一个限制配置存在就执行验证)
2333
+ const hasUploadLimitConfig =
2334
+ configSource &&
2335
+ (configSource.upload_size_limit !== undefined ||
2336
+ (configSource.upload_number_limit !== undefined && configSource.upload_number_limit > 0) ||
2337
+ (configSource.upload_file_type_limit && configSource.upload_file_type_limit.length > 0));
2338
+
2339
+ console.log('[DEBUG] FileManager 验证配置检查', {
2340
+ hasUploadLimitConfig,
2341
+ configSource,
2342
+ upload_number_limit: configSource?.upload_number_limit,
2343
+ existingFilesCount: existingFiles.length,
2344
+ newFilesCount: fileArray.length,
2345
+ });
2346
+
2347
+ if (hasUploadLimitConfig && configSource) {
2348
+ // 提取上传限制相关配置
2349
+ const uploadConfig = {
2350
+ upload_size_limit: configSource.upload_size_limit,
2351
+ upload_number_limit: configSource.upload_number_limit,
2352
+ upload_file_type_limit: configSource.upload_file_type_limit,
2353
+ upload_limit_message: configSource.upload_limit_message,
2354
+ };
2355
+
2356
+ console.log('[DEBUG] FileManager 执行文件验证', {
2357
+ uploadConfig,
2358
+ existingFilesCount: existingFiles.length,
2359
+ newFilesCount: fileArray.length,
2360
+ totalCount: existingFiles.length + fileArray.length,
2361
+ });
2362
+
2363
+ const validationResult = validateFileUpload(
2364
+ fileArray,
2365
+ existingFiles,
2366
+ uploadConfig
2367
+ );
2368
+
2369
+ console.log('[DEBUG] FileManager 验证结果', {
2370
+ valid: validationResult.valid,
2371
+ errors: validationResult.errors,
2372
+ rejectedFilesCount: validationResult.rejectedFiles.length,
2373
+ });
2374
+
2375
+ if (!validationResult.valid) {
2376
+ // 显示错误提示
2377
+ showValidationErrors(validationResult);
2378
+ // 只保留通过验证的文件
2379
+ const validFiles = fileArray.filter(
2380
+ file => !validationResult.rejectedFiles.includes(file)
2381
+ );
2382
+
2383
+ if (validFiles.length === 0) {
2384
+ // 所有文件都被拒绝,直接返回
2385
+ return;
2386
+ }
2387
+
2388
+ // 使用通过验证的文件
2389
+ fileArray.splice(0, fileArray.length, ...validFiles);
2390
+ }
2391
+ }
2392
+
2393
+ // 处理 FileManager 中的文件上传
2394
+ if (setUploadedFilesRef.current) {
2395
+ const fileListWithType = fileArray.map(file => ({
2396
+ file,
2397
+ type: file.type.startsWith('image/') ? ('image' as const) : ('document' as const),
2398
+ status: 'pending' as const, // 添加 status 字段,确保文件可以正确显示
2399
+ name: file.name, // 添加 name 字段,方便 FilesDisplay 使用
2400
+ }));
2401
+ console.log(
2402
+ '[DEBUG] 通过 setUploadedFilesRef 设置文件(来自 FileManager)',
2403
+ fileListWithType
2404
+ );
2405
+ setUploadedFilesRef.current(fileListWithType);
2406
+ } else {
2407
+ console.warn('[DEBUG] setUploadedFilesRef 未设置,使用 fileInput 方式');
2408
+ // 如果 ref 还没有设置,通过触发 fileInput 来处理
2409
+ if (fileInputRef.current) {
2410
+ // 创建一个 DataTransfer 对象来模拟文件选择
2411
+ const dataTransfer = new DataTransfer();
2412
+ fileArray.forEach(file => {
2413
+ dataTransfer.items.add(file);
2414
+ });
2415
+ fileInputRef.current.files = dataTransfer.files;
2416
+ const event = new Event('change', { bubbles: true });
2417
+ fileInputRef.current.dispatchEvent(event);
2418
+ }
2419
+ }
2420
+ }}
2421
+ /> */}
2422
+ <ConfigProvider
2423
+ drawer={{
2424
+ styles: {
2425
+ body: { padding: 0 },
2426
+ header: { display: 'none' },
2427
+ },
2428
+ }}
2429
+ >
2430
+ <Drawer
2431
+ autoFocus={false}
2432
+ onClose={() => setOpen(false)}
2433
+ open={open}
2434
+ width={drawerType === 'graphPreview' ? '100%' : 720}
2435
+ className="overflow-x-hidden"
2436
+ destroyOnClose
2437
+ >
2438
+ {drawerType === 'preview' && (
2439
+ <DrawerPreview curFileInfo={curFileInfo} onClose={setOpen} token={token} />
2440
+ )}
2441
+ </Drawer>
2442
+ </ConfigProvider>
2443
+ </div>
2444
+ );
2445
+ };
2446
+ }
2447
+
2448
+ // 重新导出 registerPDFWorker,方便用户从 chat 模块引入
2449
+ export { registerPDFWorker } from '@mxmweb/fviewer';