@liujitcn/kratos-admin-system 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +137 -0
  2. package/dist/package/declarations/index.d.ts +1 -0
  3. package/dist/package/declarations/src/ai.d.ts +10 -0
  4. package/dist/package/declarations/src/api/base/ai_message.d.ts +27 -0
  5. package/dist/package/declarations/src/api/base/ai_session.d.ts +17 -0
  6. package/dist/package/declarations/src/api/base/ai_tool.d.ts +7 -0
  7. package/dist/package/declarations/src/api/base/oauth.d.ts +13 -0
  8. package/dist/package/declarations/src/api/base/sse.d.ts +31 -0
  9. package/dist/package/declarations/src/api/system/auth.d.ts +17 -0
  10. package/dist/package/declarations/src/api/system/base_api.d.ts +22 -0
  11. package/dist/package/declarations/src/api/system/base_area.d.ts +19 -0
  12. package/dist/package/declarations/src/api/system/base_config.d.ts +20 -0
  13. package/dist/package/declarations/src/api/system/base_dept.d.ts +21 -0
  14. package/dist/package/declarations/src/api/system/base_dict.d.ts +32 -0
  15. package/dist/package/declarations/src/api/system/base_job.d.ts +28 -0
  16. package/dist/package/declarations/src/api/system/base_log.d.ts +9 -0
  17. package/dist/package/declarations/src/api/system/base_menu.d.ts +21 -0
  18. package/dist/package/declarations/src/api/system/base_migration.d.ts +10 -0
  19. package/dist/package/declarations/src/api/system/base_post.d.ts +21 -0
  20. package/dist/package/declarations/src/api/system/base_role.d.ts +23 -0
  21. package/dist/package/declarations/src/api/system/base_tenant.d.ts +21 -0
  22. package/dist/package/declarations/src/api/system/base_user.d.ts +23 -0
  23. package/dist/package/declarations/src/api/system/code_gen.d.ts +14 -0
  24. package/dist/package/declarations/src/api/system/code_gen_column.d.ts +12 -0
  25. package/dist/package/declarations/src/api/system/code_gen_proto.d.ts +10 -0
  26. package/dist/package/declarations/src/api/system/code_gen_sse.d.ts +8 -0
  27. package/dist/package/declarations/src/api/system/code_gen_table.d.ts +18 -0
  28. package/dist/package/declarations/src/api/system/project_document.d.ts +9 -0
  29. package/dist/package/declarations/src/components/Ai.vue.d.ts +3 -0
  30. package/dist/package/declarations/src/index.d.ts +3 -0
  31. package/dist/package/declarations/src/module.d.ts +2 -0
  32. package/dist/package/declarations/src/rpc/base/v1/ai_message.d.ts +82 -0
  33. package/dist/package/declarations/src/rpc/base/v1/ai_session.d.ts +176 -0
  34. package/dist/package/declarations/src/rpc/base/v1/ai_tool.d.ts +59 -0
  35. package/dist/package/declarations/src/rpc/base/v1/oauth.d.ts +167 -0
  36. package/dist/package/declarations/src/rpc/base/v1/sse.d.ts +13 -0
  37. package/dist/package/declarations/src/rpc/common/v1/common.d.ts +57 -0
  38. package/dist/package/declarations/src/rpc/common/v1/enum.d.ts +58 -0
  39. package/dist/package/declarations/src/rpc/common/v1/types.d.ts +60 -0
  40. package/dist/package/declarations/src/rpc/google/protobuf/empty.d.ts +11 -0
  41. package/dist/package/declarations/src/rpc/google/protobuf/timestamp.d.ts +107 -0
  42. package/dist/package/declarations/src/rpc/system/admin/v1/auth.d.ts +156 -0
  43. package/dist/package/declarations/src/rpc/system/admin/v1/base_api.d.ts +193 -0
  44. package/dist/package/declarations/src/rpc/system/admin/v1/base_area.d.ts +82 -0
  45. package/dist/package/declarations/src/rpc/system/admin/v1/base_config.d.ts +112 -0
  46. package/dist/package/declarations/src/rpc/system/admin/v1/base_dept.d.ts +112 -0
  47. package/dist/package/declarations/src/rpc/system/admin/v1/base_dict.d.ts +215 -0
  48. package/dist/package/declarations/src/rpc/system/admin/v1/base_job.d.ts +177 -0
  49. package/dist/package/declarations/src/rpc/system/admin/v1/base_log.d.ts +87 -0
  50. package/dist/package/declarations/src/rpc/system/admin/v1/base_menu.d.ts +151 -0
  51. package/dist/package/declarations/src/rpc/system/admin/v1/base_migration.d.ts +64 -0
  52. package/dist/package/declarations/src/rpc/system/admin/v1/base_post.d.ts +112 -0
  53. package/dist/package/declarations/src/rpc/system/admin/v1/base_role.d.ts +128 -0
  54. package/dist/package/declarations/src/rpc/system/admin/v1/base_tenant.d.ts +112 -0
  55. package/dist/package/declarations/src/rpc/system/admin/v1/base_user.d.ts +161 -0
  56. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen.d.ts +155 -0
  57. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_column.d.ts +143 -0
  58. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_proto.d.ts +86 -0
  59. package/dist/package/declarations/src/rpc/system/admin/v1/code_gen_table.d.ts +149 -0
  60. package/dist/package/declarations/src/rpc/system/admin/v1/project_document.d.ts +64 -0
  61. package/dist/package/declarations/src/rpc/system/common/v1/enum.d.ts +67 -0
  62. package/dist/package/declarations/src/views/ai/chat/attachment.d.ts +14 -0
  63. package/dist/package/declarations/src/views/ai/chat/components/AiMarkdown.vue.d.ts +13 -0
  64. package/dist/package/declarations/src/views/ai/chat/components/ChatPanel.vue.d.ts +45 -0
  65. package/dist/package/declarations/src/views/ai/chat/components/SessionPanel.vue.d.ts +34 -0
  66. package/dist/package/declarations/src/views/ai/chat/components/XSender.vue.d.ts +12 -0
  67. package/dist/package/declarations/src/views/ai/chat/index.vue.d.ts +4 -0
  68. package/dist/package/declarations/src/views/ai/chat/message.d.ts +59 -0
  69. package/dist/package/declarations/src/views/ai/chat/stream.d.ts +10 -0
  70. package/dist/package/declarations/src/views/ai/chat/types.d.ts +115 -0
  71. package/dist/package/declarations/src/views/base/api/index.vue.d.ts +3 -0
  72. package/dist/package/declarations/src/views/base/area/index.vue.d.ts +3 -0
  73. package/dist/package/declarations/src/views/base/config/index.vue.d.ts +3 -0
  74. package/dist/package/declarations/src/views/base/dept/index.vue.d.ts +3 -0
  75. package/dist/package/declarations/src/views/base/dict/index.vue.d.ts +3 -0
  76. package/dist/package/declarations/src/views/base/dict/item.vue.d.ts +3 -0
  77. package/dist/package/declarations/src/views/base/job/index.vue.d.ts +3 -0
  78. package/dist/package/declarations/src/views/base/job/log.vue.d.ts +3 -0
  79. package/dist/package/declarations/src/views/base/log/index.vue.d.ts +3 -0
  80. package/dist/package/declarations/src/views/base/menu/index.vue.d.ts +3 -0
  81. package/dist/package/declarations/src/views/base/migration/index.vue.d.ts +4 -0
  82. package/dist/package/declarations/src/views/base/post/index.vue.d.ts +3 -0
  83. package/dist/package/declarations/src/views/base/role/index.vue.d.ts +3 -0
  84. package/dist/package/declarations/src/views/base/tenant/index.vue.d.ts +3 -0
  85. package/dist/package/declarations/src/views/base/user/components/dept-tree.vue.d.ts +19 -0
  86. package/dist/package/declarations/src/views/base/user/index.vue.d.ts +3 -0
  87. package/dist/package/declarations/src/views/profile/components/base.vue.d.ts +13 -0
  88. package/dist/package/declarations/src/views/profile/components/password.vue.d.ts +3 -0
  89. package/dist/package/declarations/src/views/profile/components/security.vue.d.ts +15 -0
  90. package/dist/package/declarations/src/views/profile/index.vue.d.ts +3 -0
  91. package/dist/package/declarations/src/views/tool/api-doc/index.vue.d.ts +4 -0
  92. package/dist/package/declarations/src/views/tool/code-gen/code-preview/index.vue.d.ts +3 -0
  93. package/dist/package/declarations/src/views/tool/code-gen/columns/index.vue.d.ts +3 -0
  94. package/dist/package/declarations/src/views/tool/code-gen/columns/option-copy.d.ts +25 -0
  95. package/dist/package/declarations/src/views/tool/code-gen/components/CodeGenProgressDialog.vue.d.ts +16 -0
  96. package/dist/package/declarations/src/views/tool/code-gen/components/CodePreviewPane.vue.d.ts +9 -0
  97. package/dist/package/declarations/src/views/tool/code-gen/config.d.ts +32 -0
  98. package/dist/package/declarations/src/views/tool/code-gen/preview/capabilities.d.ts +20 -0
  99. package/dist/package/declarations/src/views/tool/code-gen/preview/data.d.ts +42 -0
  100. package/dist/package/declarations/src/views/tool/code-gen/preview/index.vue.d.ts +3 -0
  101. package/dist/package/declarations/src/views/tool/code-gen/proto/index.vue.d.ts +3 -0
  102. package/dist/package/declarations/src/views/tool/code-gen/table/index.vue.d.ts +3 -0
  103. package/dist/package/declarations/src/views/tool/project-doc/index.vue.d.ts +4 -0
  104. package/dist/package/src/ai.ts +16 -0
  105. package/dist/package/src/api/base/ai_message.ts +139 -0
  106. package/dist/package/src/api/base/ai_session.ts +77 -0
  107. package/dist/package/src/api/base/ai_tool.ts +22 -0
  108. package/dist/package/src/api/base/oauth.ts +47 -0
  109. package/dist/package/src/api/base/sse.ts +192 -0
  110. package/dist/package/src/api/system/auth.ts +65 -0
  111. package/dist/package/src/api/system/base_api.ts +95 -0
  112. package/dist/package/src/api/system/base_area.ts +73 -0
  113. package/dist/package/src/api/system/base_config.ts +82 -0
  114. package/dist/package/src/api/system/base_dept.ts +83 -0
  115. package/dist/package/src/api/system/base_dict.ts +144 -0
  116. package/dist/package/src/api/system/base_job.ts +125 -0
  117. package/dist/package/src/api/system/base_log.ts +32 -0
  118. package/dist/package/src/api/system/base_menu.ts +83 -0
  119. package/dist/package/src/api/system/base_migration.ts +35 -0
  120. package/dist/package/src/api/system/base_post.ts +83 -0
  121. package/dist/package/src/api/system/base_role.ts +93 -0
  122. package/dist/package/src/api/system/base_tenant.ts +83 -0
  123. package/dist/package/src/api/system/base_user.ts +93 -0
  124. package/dist/package/src/api/system/code_gen.ts +56 -0
  125. package/dist/package/src/api/system/code_gen_column.ts +43 -0
  126. package/dist/package/src/api/system/code_gen_proto.ts +32 -0
  127. package/dist/package/src/api/system/code_gen_sse.ts +33 -0
  128. package/dist/package/src/api/system/code_gen_table.ts +74 -0
  129. package/dist/package/src/api/system/project_document.ts +33 -0
  130. package/dist/package/src/components/Ai.vue +57 -0
  131. package/dist/package/src/index.ts +3 -0
  132. package/dist/package/src/module.ts +17 -0
  133. package/dist/package/src/rpc/base/v1/ai_message.ts +98 -0
  134. package/dist/package/src/rpc/base/v1/ai_session.ts +214 -0
  135. package/dist/package/src/rpc/base/v1/ai_tool.ts +74 -0
  136. package/dist/package/src/rpc/base/v1/oauth.ts +199 -0
  137. package/dist/package/src/rpc/base/v1/sse.ts +22 -0
  138. package/dist/package/src/rpc/common/v1/common.ts +71 -0
  139. package/dist/package/src/rpc/common/v1/enum.ts +71 -0
  140. package/dist/package/src/rpc/common/v1/types.ts +77 -0
  141. package/dist/package/src/rpc/google/protobuf/empty.ts +19 -0
  142. package/dist/package/src/rpc/google/protobuf/timestamp.ts +115 -0
  143. package/dist/package/src/rpc/system/admin/v1/auth.ts +208 -0
  144. package/dist/package/src/rpc/system/admin/v1/base_api.ts +241 -0
  145. package/dist/package/src/rpc/system/admin/v1/base_area.ts +105 -0
  146. package/dist/package/src/rpc/system/admin/v1/base_config.ts +140 -0
  147. package/dist/package/src/rpc/system/admin/v1/base_dept.ts +140 -0
  148. package/dist/package/src/rpc/system/admin/v1/base_dict.ts +249 -0
  149. package/dist/package/src/rpc/system/admin/v1/base_job.ts +206 -0
  150. package/dist/package/src/rpc/system/admin/v1/base_log.ts +101 -0
  151. package/dist/package/src/rpc/system/admin/v1/base_menu.ts +195 -0
  152. package/dist/package/src/rpc/system/admin/v1/base_migration.ts +81 -0
  153. package/dist/package/src/rpc/system/admin/v1/base_post.ts +134 -0
  154. package/dist/package/src/rpc/system/admin/v1/base_role.ts +151 -0
  155. package/dist/package/src/rpc/system/admin/v1/base_tenant.ts +134 -0
  156. package/dist/package/src/rpc/system/admin/v1/base_user.ts +190 -0
  157. package/dist/package/src/rpc/system/admin/v1/code_gen.ts +176 -0
  158. package/dist/package/src/rpc/system/admin/v1/code_gen_column.ts +170 -0
  159. package/dist/package/src/rpc/system/admin/v1/code_gen_proto.ts +104 -0
  160. package/dist/package/src/rpc/system/admin/v1/code_gen_table.ts +179 -0
  161. package/dist/package/src/rpc/system/admin/v1/project_document.ts +79 -0
  162. package/dist/package/src/rpc/system/common/v1/enum.ts +80 -0
  163. package/dist/package/src/typings/swagger-ui-dist.d.ts +28 -0
  164. package/dist/package/src/typings/x-markdown-vue.d.ts +1 -0
  165. package/dist/package/src/views/ai/chat/attachment.ts +65 -0
  166. package/dist/package/src/views/ai/chat/components/AiMarkdown.vue +162 -0
  167. package/dist/package/src/views/ai/chat/components/ChatPanel.vue +1861 -0
  168. package/dist/package/src/views/ai/chat/components/SessionPanel.vue +316 -0
  169. package/dist/package/src/views/ai/chat/components/XSender.vue +554 -0
  170. package/dist/package/src/views/ai/chat/index.vue +848 -0
  171. package/dist/package/src/views/ai/chat/message.ts +416 -0
  172. package/dist/package/src/views/ai/chat/stream.ts +115 -0
  173. package/dist/package/src/views/ai/chat/types.ts +126 -0
  174. package/dist/package/src/views/base/api/index.vue +455 -0
  175. package/dist/package/src/views/base/area/index.vue +269 -0
  176. package/dist/package/src/views/base/config/index.vue +530 -0
  177. package/dist/package/src/views/base/dept/index.vue +461 -0
  178. package/dist/package/src/views/base/dict/index.vue +304 -0
  179. package/dist/package/src/views/base/dict/item.vue +373 -0
  180. package/dist/package/src/views/base/job/index.vue +535 -0
  181. package/dist/package/src/views/base/job/log.vue +202 -0
  182. package/dist/package/src/views/base/log/index.vue +317 -0
  183. package/dist/package/src/views/base/menu/index.vue +944 -0
  184. package/dist/package/src/views/base/migration/index.vue +791 -0
  185. package/dist/package/src/views/base/post/index.vue +324 -0
  186. package/dist/package/src/views/base/role/index.vue +684 -0
  187. package/dist/package/src/views/base/tenant/index.vue +328 -0
  188. package/dist/package/src/views/base/user/components/dept-tree.vue +75 -0
  189. package/dist/package/src/views/base/user/index.vue +783 -0
  190. package/dist/package/src/views/profile/components/base.vue +320 -0
  191. package/dist/package/src/views/profile/components/password.vue +269 -0
  192. package/dist/package/src/views/profile/components/security.vue +443 -0
  193. package/dist/package/src/views/profile/index.vue +199 -0
  194. package/dist/package/src/views/tool/api-doc/index.vue +275 -0
  195. package/dist/package/src/views/tool/code-gen/code-preview/index.vue +233 -0
  196. package/dist/package/src/views/tool/code-gen/columns/index.vue +1456 -0
  197. package/dist/package/src/views/tool/code-gen/columns/option-copy.ts +125 -0
  198. package/dist/package/src/views/tool/code-gen/components/CodeGenProgressDialog.vue +328 -0
  199. package/dist/package/src/views/tool/code-gen/components/CodePreviewPane.vue +42 -0
  200. package/dist/package/src/views/tool/code-gen/config.ts +175 -0
  201. package/dist/package/src/views/tool/code-gen/preview/capabilities.ts +34 -0
  202. package/dist/package/src/views/tool/code-gen/preview/data.ts +331 -0
  203. package/dist/package/src/views/tool/code-gen/preview/index.vue +831 -0
  204. package/dist/package/src/views/tool/code-gen/proto/index.vue +703 -0
  205. package/dist/package/src/views/tool/code-gen/table/index.vue +864 -0
  206. package/dist/package/src/views/tool/project-doc/index.vue +561 -0
  207. package/package.json +60 -0
  208. package/tsconfig.json +13 -0
@@ -0,0 +1,848 @@
1
+ <template>
2
+ <div class="ai-page" :class="{ 'is-session-collapsed': sessionPanelCollapsed }">
3
+ <SessionPanel
4
+ v-if="!sessionPanelCollapsed"
5
+ v-model:active="activeSessionID"
6
+ v-model:keyword="sessionKeyword"
7
+ :sessions="filteredSessions"
8
+ @change="handleSessionChange"
9
+ @action="handleSessionAction"
10
+ @create="handleCreateSession"
11
+ @toggle-collapse="toggleSessionPanel"
12
+ />
13
+ <div v-else class="agent-session-collapsed">
14
+ <button class="agent-session-collapsed__toggle" type="button" aria-label="展开会话栏" @click="toggleSessionPanel">
15
+ <el-icon><DArrowRight /></el-icon>
16
+ </button>
17
+ <span class="agent-session-collapsed__label">最近对话</span>
18
+ </div>
19
+ <ChatPanel
20
+ :active-session="activeSession"
21
+ :messages="currentMessages"
22
+ :sending="currentSessionSending"
23
+ :shortcuts="starterShortcuts"
24
+ :loading-shortcuts="loadingShortcuts"
25
+ @submit="handleSubmit"
26
+ @message-action="handleMessageAction"
27
+ @message-edit="handleEditMessage"
28
+ >
29
+ <template #flow-blocks="{ message, activeFlowMessageId }">
30
+ <component
31
+ v-if="aiFlowBlocks && message.blocks?.length"
32
+ :is="aiFlowBlocks"
33
+ :message="message"
34
+ :active-flow-message-id="activeFlowMessageId"
35
+ @flow-action="handleFlowAction"
36
+ />
37
+ </template>
38
+ </ChatPanel>
39
+ </div>
40
+ </template>
41
+
42
+ <script setup lang="ts">
43
+ import "vue-element-plus-x/styles/index.css";
44
+ import { computed, onBeforeUnmount, onMounted, ref } from "vue";
45
+ import { DArrowRight } from "@element-plus/icons-vue";
46
+ import { ElMessage, ElMessageBox } from "element-plus";
47
+ import { getAdminAiExtension } from "@liujitcn/kratos-admin-system";
48
+ import { defAiMessageService } from "@liujitcn/kratos-admin-system/api/base/ai_message";
49
+ import { defAiSessionService } from "@liujitcn/kratos-admin-system/api/base/ai_session";
50
+ import { defAiToolService } from "@liujitcn/kratos-admin-system/api/base/ai_tool";
51
+ import type { AiAction } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_message";
52
+ import type { AiSession } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_session";
53
+ import type { AiShortcut } from "@liujitcn/kratos-admin-system/rpc/base/v1/ai_tool";
54
+ import { AiMessageStatus, Terminal } from "@liujitcn/kratos-admin-system/rpc/common/v1/enum";
55
+ import ChatPanel from "./components/ChatPanel.vue";
56
+ import SessionPanel from "./components/SessionPanel.vue";
57
+ import {
58
+ appendStreamingDelta,
59
+ createLocalUserMessage,
60
+ createThinkingMessage,
61
+ ensureStreamingMessage,
62
+ hasStreamingDelta,
63
+ markAIMessageRegenerating,
64
+ markStreamingError,
65
+ markThinkingMessageFailed,
66
+ markSpeakingMessage,
67
+ normalizeMessageList,
68
+ normalizeSession,
69
+ normalizeSessionList,
70
+ resolveTimestamp,
71
+ replacePendingMessages,
72
+ sortMessages
73
+ } from "./message";
74
+ import { readAiEventStream } from "./stream";
75
+ import type {
76
+ AiStreamEvent,
77
+ AiStreamPayload,
78
+ ChatMessageAction,
79
+ ChatMessageEditPayload,
80
+ ChatMessageItem,
81
+ SessionAction,
82
+ SubmitPayload
83
+ } from "./types";
84
+
85
+ defineOptions({
86
+ name: "AiChat"
87
+ });
88
+
89
+ const sessionKeyword = ref("");
90
+ const activeSessionID = ref("");
91
+ const sessionPanelCollapsed = ref(false);
92
+ const sendingSessionMap = ref<Record<string, boolean>>({});
93
+ const loadingSessions = ref(false);
94
+ const loadingSessionID = ref("");
95
+ const loadingShortcuts = ref(false);
96
+ const sessions = ref<AiSession[]>([]);
97
+ const starterShortcuts = ref<AiShortcut[]>([]);
98
+ const messages = ref<Record<string, ChatMessageItem[]>>({});
99
+ const pendingDeltaMap = new Map<string, AiStreamPayload>();
100
+ const runningStreamTaskMap = new Map<string, AiStreamTask>();
101
+ let pendingDeltaFrame = 0;
102
+ let speakingMessageID = "";
103
+
104
+ /** 当前会话的流式任务状态。 */
105
+ type AiStreamTask = {
106
+ /** 用于取消当前 Fetch 与 ReadableStream 读取。 */
107
+ controller: AbortController;
108
+ /** 是否已收到 finish 或 error 事件。 */
109
+ finished: boolean;
110
+ };
111
+
112
+ const filteredSessions = computed(() => {
113
+ const keyword = sessionKeyword.value.trim();
114
+ if (!keyword) return sessions.value;
115
+ return sessions.value.filter(item => item.title.includes(keyword) || item.summary.includes(keyword));
116
+ });
117
+
118
+ const activeSession = computed(() => sessions.value.find(item => item.id === activeSessionID.value) ?? sessions.value[0]);
119
+
120
+ const currentMessages = computed(() => messages.value[activeSessionID.value] ?? []);
121
+
122
+ const currentSessionSending = computed(() => isSessionSending(activeSessionID.value));
123
+
124
+ const aiFlowBlocks = computed(() => getAdminAiExtension().flowBlocks);
125
+
126
+ /** 切换会话时同步当前活动会话。 */
127
+ function handleSessionChange(item: AiSession) {
128
+ activeSessionID.value = item.id;
129
+ if (isSessionSending(item.id) && messages.value[item.id]?.length) return;
130
+ void loadMessages(item.id);
131
+ }
132
+
133
+ /** 切换会话侧栏折叠状态。 */
134
+ function toggleSessionPanel() {
135
+ sessionPanelCollapsed.value = !sessionPanelCollapsed.value;
136
+ }
137
+
138
+ /** 处理会话项菜单动作,接入真实重命名与删除接口。 */
139
+ async function handleSessionAction(payload: { action: SessionAction; item: AiSession }) {
140
+ try {
141
+ if (payload.action === "rename") {
142
+ await handleRenameSession(payload.item);
143
+ return;
144
+ }
145
+ await handleDeleteSession(payload.item);
146
+ } catch (error) {
147
+ if (error === "cancel" || error === "close") {
148
+ ElMessage.info("已取消操作");
149
+ }
150
+ }
151
+ }
152
+
153
+ /** 主动创建新的会话,并切换到刚创建的会话。 */
154
+ async function handleCreateSession() {
155
+ const sessionID = await createSession();
156
+ if (!sessionID) return;
157
+ activeSessionID.value = sessionID;
158
+ messages.value[sessionID] = [];
159
+ ElMessage.success("已创建新会话");
160
+ }
161
+
162
+ /** 提交用户输入并同步消息流。 */
163
+ async function handleSubmit(payload: SubmitPayload) {
164
+ void sendAiPayload(payload);
165
+ }
166
+
167
+ /** 处理业务模块提供的结构化流程动作。 */
168
+ async function handleFlowAction(action: AiAction, label?: string) {
169
+ const sessionID = activeSessionID.value;
170
+ if (!sessionID || isSessionSending(sessionID)) return;
171
+ await sendAiPayload({
172
+ text: label || "继续",
173
+ attachments: [],
174
+ action
175
+ });
176
+ }
177
+
178
+ /** 执行真实发送流程,复用于输入框提交和本地失败重发。 */
179
+ async function sendAiPayload(payload: SubmitPayload) {
180
+ const sessionID = await ensureActiveSession();
181
+ if (!sessionID) {
182
+ return false;
183
+ }
184
+ // 同一个会话内仍然串行发送,避免历史上下文和消息顺序被并发请求打乱。
185
+ if (isSessionSending(sessionID)) return false;
186
+
187
+ const localUserMessage = createLocalUserMessage(payload);
188
+ const thinkingMessage = createThinkingMessage({ sessionID });
189
+ messages.value[sessionID] = sortMessages([...(messages.value[sessionID] ?? []), localUserMessage, thinkingMessage]);
190
+ setSessionSending(sessionID, true);
191
+ void runAiStreamTask(sessionID, payload);
192
+ return true;
193
+ }
194
+
195
+ /** 后台消费单个会话的 direct stream,不阻塞其他会话输入。 */
196
+ async function runAiStreamTask(sessionID: string, payload: SubmitPayload) {
197
+ const controller = new AbortController();
198
+ const task: AiStreamTask = {
199
+ controller,
200
+ finished: false
201
+ };
202
+ runningStreamTaskMap.set(sessionID, task);
203
+ try {
204
+ const response = await defAiMessageService.StreamAiMessage(
205
+ {
206
+ session_id: sessionID,
207
+ content: payload.text,
208
+ attachments: payload.attachments.map(item => ({
209
+ id: item.id,
210
+ name: item.name,
211
+ size: item.size,
212
+ url: item.url,
213
+ mime_type: item.mime_type
214
+ })),
215
+ action: payload.action
216
+ },
217
+ {
218
+ signal: controller.signal
219
+ }
220
+ );
221
+
222
+ if (!response.body) {
223
+ throw new Error("AI 助手流式响应为空");
224
+ }
225
+ await readAiEventStream(
226
+ response.body,
227
+ event => {
228
+ handleAiStreamEvent(event, task);
229
+ },
230
+ controller.signal
231
+ );
232
+ if (!task.finished && !controller.signal.aborted) {
233
+ throw new Error("AI 助手流式响应未完整返回");
234
+ }
235
+ } catch (error) {
236
+ if (controller.signal.aborted) return;
237
+ messages.value[sessionID] = markThinkingMessageFailed(messages.value[sessionID] ?? [], {
238
+ sessionID
239
+ });
240
+ const message = error instanceof Error ? error.message : "AI 助手请求失败";
241
+ ElMessage.error(message);
242
+ } finally {
243
+ if (runningStreamTaskMap.get(sessionID) === task) {
244
+ runningStreamTaskMap.delete(sessionID);
245
+ setSessionSending(sessionID, false);
246
+ }
247
+ }
248
+ }
249
+
250
+ /** 处理聊天气泡上的重试、分支、朗读、复制和删除操作。 */
251
+ async function handleMessageAction(payload: { action: ChatMessageAction; item: ChatMessageItem }) {
252
+ if (payload.action === "copy") {
253
+ await handleCopyMessage(payload.item);
254
+ return;
255
+ }
256
+ if (payload.action === "delete") {
257
+ await handleDeleteMessage(payload.item);
258
+ return;
259
+ }
260
+ if (payload.action === "branch") {
261
+ await handleBranchMessage(payload.item);
262
+ return;
263
+ }
264
+ if (payload.action === "speak") {
265
+ handleSpeakMessage(payload.item);
266
+ return;
267
+ }
268
+ await handleRetryMessage(payload.item);
269
+ }
270
+
271
+ /** 编辑当前用户消息文本,并重新生成同一轮助手输出。 */
272
+ async function handleEditMessage(payload: ChatMessageEditPayload) {
273
+ const sessionID = activeSessionID.value;
274
+ const messageID = String(payload.item.id ?? "");
275
+ if (!sessionID || isSessionSending(sessionID) || !messageID || payload.item.role !== "user" || payload.item.localOnly) return;
276
+
277
+ setSessionSending(sessionID, true);
278
+ stopSpeaking();
279
+ try {
280
+ const current = messages.value[sessionID] ?? [];
281
+ const updatedUserMessages = current.map<ChatMessageItem>(message => {
282
+ if (String(message.id) !== messageID || message.role !== "user") return message;
283
+ return {
284
+ ...message,
285
+ content: payload.content,
286
+ input_content: {
287
+ kind: message.input_content?.kind || "text",
288
+ content: payload.content
289
+ }
290
+ };
291
+ });
292
+ messages.value[sessionID] = markAIMessageRegenerating(updatedUserMessages, sessionID, messageID);
293
+
294
+ const response = await defAiMessageService.UpdateAiMessage({
295
+ session_id: sessionID,
296
+ message_id: messageID,
297
+ content: payload.content
298
+ });
299
+ const currentMessages = messages.value[sessionID] ?? [];
300
+ messages.value[sessionID] = replacePendingMessages(currentMessages, normalizeMessageList(response.messages));
301
+ if (response.session) upsertSession(normalizeSession(response.session));
302
+ ElMessage.success("已更新并重新生成");
303
+ } catch (error) {
304
+ await loadMessages(sessionID, { force: true });
305
+ const message = error instanceof Error ? error.message : "更新消息失败";
306
+ ElMessage.error(message);
307
+ } finally {
308
+ setSessionSending(sessionID, false);
309
+ }
310
+ }
311
+
312
+ /** 复制当前消息正文,保留用户原始输入或助手 Markdown 文本。 */
313
+ async function handleCopyMessage(item: ChatMessageItem) {
314
+ try {
315
+ await navigator.clipboard.writeText(String(item.content ?? ""));
316
+ ElMessage.success("消息已复制");
317
+ } catch {
318
+ ElMessage.error("当前浏览器不支持复制");
319
+ }
320
+ }
321
+
322
+ /** 删除当前会话中的消息,并同步后端持久化状态。 */
323
+ async function handleDeleteMessage(item: ChatMessageItem) {
324
+ const sessionID = activeSessionID.value;
325
+ if (!sessionID) return;
326
+ if (speakingMessageID === resolveMessageBubbleKey(item)) {
327
+ stopSpeaking();
328
+ }
329
+ if (item.localOnly) {
330
+ const messageKey = resolveMessageBubbleKey(item);
331
+ messages.value[sessionID] = (messages.value[sessionID] ?? []).filter(message => {
332
+ if (item.role === "user") return !message.localOnly;
333
+ return resolveMessageBubbleKey(message) !== messageKey;
334
+ });
335
+ ElMessage.success("消息已删除");
336
+ return;
337
+ }
338
+ await defAiMessageService.DeleteAiMessage({
339
+ session_id: sessionID,
340
+ message_id: String(item.id)
341
+ });
342
+ messages.value[sessionID] = (messages.value[sessionID] ?? []).filter(message => String(message.id) !== String(item.id));
343
+ ElMessage.success("消息已删除");
344
+ }
345
+
346
+ /** 重试失败的一轮消息,或重新生成助手输出。 */
347
+ async function handleRetryMessage(item: ChatMessageItem) {
348
+ const sessionID = activeSessionID.value;
349
+ if (!sessionID || isSessionSending(sessionID)) return;
350
+
351
+ try {
352
+ let response;
353
+ if (item.localOnly) {
354
+ const payload = resolveLocalRetryPayload(item);
355
+ if (!payload) {
356
+ ElMessage.warning("未找到可重新发送的本地消息");
357
+ return;
358
+ }
359
+ messages.value[sessionID] = (messages.value[sessionID] ?? []).filter(message => !message.localOnly);
360
+ if (await sendAiPayload(payload)) {
361
+ ElMessage.success("已重新发送");
362
+ }
363
+ return;
364
+ }
365
+ setSessionSending(sessionID, true);
366
+ if (item.role === "user") {
367
+ if (item.status !== AiMessageStatus.FAILED_AAMS) {
368
+ ElMessage.warning("只有发送失败的消息可以重新发送");
369
+ return;
370
+ }
371
+ response = await defAiMessageService.RetryAiUserMessage({
372
+ session_id: sessionID,
373
+ message_id: String(item.id)
374
+ });
375
+ } else {
376
+ messages.value[sessionID] = markAIMessageRegenerating(messages.value[sessionID] ?? [], sessionID, String(item.id));
377
+ response = await defAiMessageService.RegenerateAiMessage({
378
+ session_id: sessionID,
379
+ message_id: String(item.id)
380
+ });
381
+ }
382
+ const current = messages.value[sessionID] ?? [];
383
+ messages.value[sessionID] = replacePendingMessages(current, normalizeMessageList(response.messages));
384
+ if (response.session) upsertSession(normalizeSession(response.session));
385
+ ElMessage.success(item.role === "user" ? "已重新发送" : "已重新生成");
386
+ } catch (error) {
387
+ if (item.role !== "user") await loadMessages(sessionID, { force: true });
388
+ const message = error instanceof Error ? error.message : "重新生成失败";
389
+ ElMessage.error(message);
390
+ } finally {
391
+ if (!item.localOnly) {
392
+ setSessionSending(sessionID, false);
393
+ }
394
+ }
395
+ }
396
+
397
+ /** 从本地失败气泡还原可重新提交的输入内容。 */
398
+ function resolveLocalRetryPayload(item: ChatMessageItem): SubmitPayload | undefined {
399
+ if (item.role === "user") {
400
+ return {
401
+ text: String(item.content ?? ""),
402
+ attachments: item.attachments ?? []
403
+ };
404
+ }
405
+
406
+ const sortedList = sortMessages(messages.value[activeSessionID.value] ?? []);
407
+ const targetIndex = sortedList.findIndex(message => resolveMessageBubbleKey(message) === resolveMessageBubbleKey(item));
408
+ const endIndex = targetIndex >= 0 ? targetIndex - 1 : sortedList.length - 1;
409
+ for (let index = endIndex; index >= 0; index--) {
410
+ const message = sortedList[index];
411
+ if (message.localOnly && message.role === "user") {
412
+ return {
413
+ text: String(message.content ?? ""),
414
+ attachments: message.attachments ?? []
415
+ };
416
+ }
417
+ }
418
+ return undefined;
419
+ }
420
+
421
+ /** 从当前消息处创建一个新的持久化分支会话。 */
422
+ async function handleBranchMessage(item: ChatMessageItem) {
423
+ const sourceSessionID = activeSessionID.value;
424
+ if (!sourceSessionID) return;
425
+ stopSpeaking();
426
+ const response = await defAiSessionService.CreateAiSessionBranch({
427
+ source_session_id: sourceSessionID,
428
+ anchor_message_id: String(item.id),
429
+ title: buildBranchSessionTitle(item),
430
+ terminal: Terminal.TERMINAL_ADMIN
431
+ });
432
+ const branchSession = normalizeSession(response.session);
433
+ upsertSession(branchSession);
434
+ messages.value[branchSession.id] = normalizeMessageList(response.messages);
435
+ activeSessionID.value = branchSession.id;
436
+ ElMessage.success("已创建分支会话");
437
+ }
438
+
439
+ /** 朗读或停止朗读当前助手输出。 */
440
+ function handleSpeakMessage(item: ChatMessageItem) {
441
+ if (item.role === "user") return;
442
+ if (!window.speechSynthesis) {
443
+ ElMessage.warning("当前浏览器不支持朗读");
444
+ return;
445
+ }
446
+ const messageKey = resolveMessageBubbleKey(item);
447
+ if (speakingMessageID === messageKey) {
448
+ stopSpeaking();
449
+ return;
450
+ }
451
+ stopSpeaking();
452
+ const utterance = new SpeechSynthesisUtterance(String(item.content ?? ""));
453
+ utterance.lang = "zh-CN";
454
+ utterance.onend = () => clearSpeakingState(messageKey);
455
+ utterance.onerror = () => clearSpeakingState(messageKey);
456
+ speakingMessageID = messageKey;
457
+ markAllMessagesSpeaking(speakingMessageID);
458
+ window.speechSynthesis.speak(utterance);
459
+ }
460
+
461
+ /** 生成当前气泡的前端稳定键,用于朗读态和渲染态关联。 */
462
+ function resolveMessageBubbleKey(item: ChatMessageItem) {
463
+ return String(item.key ?? `${item.id}:${item.role}`);
464
+ }
465
+
466
+ /** 停止浏览器朗读,并清理气泡朗读态。 */
467
+ function stopSpeaking() {
468
+ if (window.speechSynthesis) {
469
+ window.speechSynthesis.cancel();
470
+ }
471
+ speakingMessageID = "";
472
+ markAllMessagesSpeaking();
473
+ }
474
+
475
+ /** 朗读事件结束后,只清理当前朗读消息对应的状态。 */
476
+ function clearSpeakingState(messageID: string) {
477
+ if (speakingMessageID && speakingMessageID !== messageID) return;
478
+ speakingMessageID = "";
479
+ markAllMessagesSpeaking();
480
+ }
481
+
482
+ /** 同步所有已加载会话中的朗读态,避免切换会话后状态残留。 */
483
+ function markAllMessagesSpeaking(messageID?: string) {
484
+ Object.keys(messages.value).forEach(sessionID => {
485
+ messages.value[sessionID] = markSpeakingMessage(messages.value[sessionID] ?? [], messageID);
486
+ });
487
+ }
488
+
489
+ /** 判断指定会话是否存在未完成的发送任务。 */
490
+ function isSessionSending(sessionID: string) {
491
+ return Boolean(sessionID && sendingSessionMap.value[sessionID]);
492
+ }
493
+
494
+ /** 更新单个会话的发送状态,避免后台会话阻塞当前会话输入。 */
495
+ function setSessionSending(sessionID: string, sending: boolean) {
496
+ if (!sessionID) return;
497
+ const nextMap = { ...sendingSessionMap.value };
498
+ if (sending) {
499
+ nextMap[sessionID] = true;
500
+ } else {
501
+ delete nextMap[sessionID];
502
+ }
503
+ sendingSessionMap.value = nextMap;
504
+ }
505
+
506
+ /** 取消指定会话仍在后台读取的流式任务。 */
507
+ function cancelSessionStreamTask(sessionID: string) {
508
+ const task = runningStreamTaskMap.get(sessionID);
509
+ if (!task) return;
510
+ task.finished = true;
511
+ task.controller.abort();
512
+ runningStreamTaskMap.delete(sessionID);
513
+ setSessionSending(sessionID, false);
514
+ }
515
+
516
+ /** 取消页面内所有后台流式任务。 */
517
+ function cancelAllStreamTasks() {
518
+ Array.from(runningStreamTaskMap.keys()).forEach(sessionID => cancelSessionStreamTask(sessionID));
519
+ }
520
+
521
+ /** 处理 AI 助手流式文本增量。 */
522
+ function handleAiDelta(payload: AiStreamPayload) {
523
+ if (!hasStreamingDelta(payload)) return;
524
+ queueAiDelta(payload);
525
+ }
526
+
527
+ /** 处理 AI 助手流式结束事件。 */
528
+ function handleAiFinish(payload: AiStreamPayload, task?: AiStreamTask) {
529
+ const sessionID = String(payload.session_id ?? "");
530
+ if (!sessionID) return;
531
+ if (task) task.finished = true;
532
+ flushAiDelta();
533
+ const nextMessages = normalizeMessageList(payload.messages as never[]);
534
+ const current = messages.value[sessionID] ?? [];
535
+ const messageID = String(payload.message_id ?? "");
536
+ const streamKey = messageID ? `${sessionID}:${messageID}` : "";
537
+ const hasLocalStreamingMessages = current.some(item => item.streamKey === streamKey && item.localOnly);
538
+ messages.value[sessionID] =
539
+ nextMessages.length || !hasLocalStreamingMessages ? replacePendingMessages(current, nextMessages, payload) : current;
540
+ if (payload.session) {
541
+ upsertSession(normalizeSession(payload.session as AiSession));
542
+ }
543
+ }
544
+
545
+ /** 处理 AI 助手流式异常事件。 */
546
+ function handleAiError(payload: AiStreamPayload, task?: AiStreamTask) {
547
+ const sessionID = String(payload.session_id ?? "");
548
+ if (!sessionID || !messages.value[sessionID]) return;
549
+ if (task) task.finished = true;
550
+ flushAiDelta();
551
+ const nextMessages = normalizeMessageList(payload.messages as never[]);
552
+ if (nextMessages.length) {
553
+ messages.value[sessionID] = replacePendingMessages(messages.value[sessionID] ?? [], nextMessages, payload);
554
+ return;
555
+ }
556
+ const streamingMessages = ensureStreamingMessage(messages.value[sessionID] ?? [], payload);
557
+ messages.value[sessionID] = markStreamingError(streamingMessages, payload);
558
+ }
559
+
560
+ /** 根据解析结果派发 AI 助手 direct stream 事件。 */
561
+ function handleAiStreamEvent(event: AiStreamEvent, task?: AiStreamTask) {
562
+ switch (event.event) {
563
+ case "delta":
564
+ handleAiDelta(event.payload);
565
+ break;
566
+ case "finish":
567
+ handleAiFinish(event.payload, task);
568
+ break;
569
+ case "error":
570
+ handleAiError(event.payload, task);
571
+ break;
572
+ }
573
+ }
574
+
575
+ /** 合并同一帧内的流式分片,减少频繁重排导致的卡顿。 */
576
+ function queueAiDelta(payload: AiStreamPayload) {
577
+ const sessionID = String(payload.session_id ?? "");
578
+ const messageID = String(payload.message_id ?? "");
579
+ if (!sessionID || !messageID || !messages.value[sessionID]) return;
580
+
581
+ const key = `${sessionID}:${messageID}`;
582
+ const cachedPayload = pendingDeltaMap.get(key);
583
+ pendingDeltaMap.set(key, {
584
+ ...payload,
585
+ delta: `${cachedPayload?.delta ?? ""}${payload.delta ?? ""}`
586
+ });
587
+
588
+ if (pendingDeltaFrame) return;
589
+ pendingDeltaFrame = window.requestAnimationFrame(() => {
590
+ pendingDeltaFrame = 0;
591
+ flushAiDelta();
592
+ });
593
+ }
594
+
595
+ /** 立即刷新已缓存的流式分片。 */
596
+ function flushAiDelta() {
597
+ if (pendingDeltaFrame) {
598
+ window.cancelAnimationFrame(pendingDeltaFrame);
599
+ pendingDeltaFrame = 0;
600
+ }
601
+ if (pendingDeltaMap.size === 0) return;
602
+
603
+ const payloadList = Array.from(pendingDeltaMap.values());
604
+ pendingDeltaMap.clear();
605
+ for (const payload of payloadList) {
606
+ const sessionID = String(payload.session_id ?? "");
607
+ if (!sessionID || !messages.value[sessionID]) continue;
608
+ const streamingMessages = ensureStreamingMessage(messages.value[sessionID] ?? [], payload);
609
+ messages.value[sessionID] = appendStreamingDelta(streamingMessages, payload);
610
+ }
611
+ }
612
+
613
+ /** 首次打开时加载会话列表,并拉取当前会话消息。 */
614
+ async function ensureSessionsLoaded() {
615
+ if (loadingSessions.value || sessions.value.length > 0) return;
616
+
617
+ loadingSessions.value = true;
618
+ try {
619
+ void loadAiShortcuts();
620
+ const response = await defAiSessionService.ListAiSession({ terminal: Terminal.TERMINAL_ADMIN });
621
+ sessions.value = normalizeSessionList(response?.sessions);
622
+ const sessionID = await ensureActiveSession();
623
+ if (sessionID) await loadMessages(sessionID);
624
+ } catch {
625
+ sessions.value = [];
626
+ } finally {
627
+ loadingSessions.value = false;
628
+ }
629
+ }
630
+
631
+ /** 加载当前管理端可用快捷入口。 */
632
+ async function loadAiShortcuts() {
633
+ if (loadingShortcuts.value) return;
634
+ loadingShortcuts.value = true;
635
+ try {
636
+ const response = await defAiToolService.ListAiShortcut({ terminal: Terminal.TERMINAL_ADMIN });
637
+ starterShortcuts.value = normalizeStarterShortcuts(response.shortcuts);
638
+ } catch {
639
+ starterShortcuts.value = [];
640
+ } finally {
641
+ loadingShortcuts.value = false;
642
+ }
643
+ }
644
+
645
+ /** 加载指定会话的消息记录。 */
646
+ async function loadMessages(sessionID: string, options?: { force?: boolean }) {
647
+ if (!sessionID) return;
648
+ if (!options?.force && isSessionSending(sessionID) && messages.value[sessionID]?.length) return;
649
+
650
+ loadingSessionID.value = sessionID;
651
+ try {
652
+ const response = await defAiMessageService.ListAiMessage({ session_id: sessionID });
653
+ if (loadingSessionID.value !== sessionID) return;
654
+ messages.value[sessionID] = normalizeMessageList(response?.messages);
655
+ } catch {
656
+ if (loadingSessionID.value === sessionID) {
657
+ messages.value[sessionID] = [];
658
+ }
659
+ } finally {
660
+ if (loadingSessionID.value === sessionID) {
661
+ loadingSessionID.value = "";
662
+ }
663
+ }
664
+ }
665
+
666
+ /** 重命名当前会话。 */
667
+ async function handleRenameSession(item: AiSession) {
668
+ const { value } = await ElMessageBox.prompt(`请输入新的会话名称\n当前名称:${item.title}`, "重命名会话", {
669
+ confirmButtonText: "确认",
670
+ cancelButtonText: "取消",
671
+ inputValue: item.title,
672
+ inputPattern: /\S+/,
673
+ inputErrorMessage: "请输入会话名称"
674
+ });
675
+
676
+ const response = await defAiSessionService.UpdateAiSession({ id: item.id, title: value.trim() });
677
+ upsertSession(normalizeSession(response.session));
678
+ ElMessage.success("会话已重命名");
679
+ }
680
+
681
+ /** 删除当前会话,并自动切换到剩余会话。 */
682
+ async function handleDeleteSession(item: AiSession) {
683
+ await ElMessageBox.confirm(`是否删除该会话?\n会话名称:${item.title}`, "删除会话", {
684
+ confirmButtonText: "确认删除",
685
+ cancelButtonText: "取消",
686
+ type: "warning"
687
+ });
688
+
689
+ await defAiSessionService.DeleteAiSession({ id: item.id });
690
+ cancelSessionStreamTask(item.id);
691
+ sessions.value = sessions.value.filter(session => session.id !== item.id);
692
+ delete messages.value[item.id];
693
+
694
+ if (activeSessionID.value === item.id) {
695
+ activeSessionID.value = sessions.value[0]?.id ?? "";
696
+ }
697
+ const nextSessionID = await ensureActiveSession();
698
+ if (nextSessionID) await loadMessages(nextSessionID);
699
+
700
+ ElMessage.success("会话已删除");
701
+ }
702
+
703
+ /** 保证当前存在可用会话;当列表为空时自动创建首个会话。 */
704
+ async function ensureActiveSession() {
705
+ if (!activeSessionID.value && sessions.value.length > 0) {
706
+ activeSessionID.value = sessions.value[0].id;
707
+ }
708
+ if (activeSessionID.value) return activeSessionID.value;
709
+
710
+ activeSessionID.value = (await createSession()) ?? "";
711
+ return activeSessionID.value;
712
+ }
713
+
714
+ /** 创建新的助手会话,并同步到本地列表。 */
715
+ async function createSession(options?: { title?: string }) {
716
+ const response = await defAiSessionService.CreateAiSession({
717
+ title: options?.title || "新对话",
718
+ terminal: Terminal.TERMINAL_ADMIN
719
+ });
720
+ const normalizedSession = normalizeSession(response.session);
721
+ upsertSession(normalizedSession);
722
+ return normalizedSession.id;
723
+ }
724
+
725
+ /** 使用当前消息内容生成一个易识别的分支会话标题。 */
726
+ function buildBranchSessionTitle(item: ChatMessageItem) {
727
+ const content = String(item.input_content?.content || item.content || "新对话")
728
+ .replace(/\s+/g, " ")
729
+ .trim();
730
+ return `分支:${content.slice(0, 18) || "新对话"}`;
731
+ }
732
+
733
+ /** 归一化快捷入口,保证空态展示稳定排序。 */
734
+ function normalizeStarterShortcuts(list?: AiShortcut[] | null) {
735
+ return [...(list ?? [])]
736
+ .filter(item => Boolean(item?.key && (item.title || item.prompt)))
737
+ .map(item => ({
738
+ ...item,
739
+ title: item.title || item.prompt,
740
+ prompt: item.prompt || item.title,
741
+ required_tools: Array.isArray(item.required_tools) ? item.required_tools : [],
742
+ sort: Number(item.sort || 0),
743
+ group: String((item as AiShortcut & { group?: string }).group ?? "")
744
+ }))
745
+ .sort((left, right) => left.sort - right.sort);
746
+ }
747
+
748
+ /** 更新或插入会话,并按更新时间排序。 */
749
+ function upsertSession(session: AiSession) {
750
+ const nextList = sessions.value.filter(item => item.id !== session.id);
751
+ nextList.unshift(session);
752
+ sessions.value = nextList.sort((left, right) => {
753
+ const leftTime = resolveTimestamp(left.updated_at);
754
+ const rightTime = resolveTimestamp(right.updated_at);
755
+ return rightTime - leftTime;
756
+ });
757
+ }
758
+
759
+ /** 页面加载后主动准备首个会话,避免进入菜单后仍需额外点击。 */
760
+ onMounted(() => {
761
+ void loadAiShortcuts();
762
+ void ensureSessionsLoaded();
763
+ });
764
+
765
+ onBeforeUnmount(() => {
766
+ stopSpeaking();
767
+ cancelAllStreamTasks();
768
+ if (pendingDeltaFrame) {
769
+ window.cancelAnimationFrame(pendingDeltaFrame);
770
+ pendingDeltaFrame = 0;
771
+ }
772
+ pendingDeltaMap.clear();
773
+ });
774
+ </script>
775
+
776
+ <style scoped lang="scss">
777
+ .ai-page {
778
+ box-sizing: border-box;
779
+ display: grid;
780
+ grid-template-columns: 320px minmax(0, 1fr);
781
+ height: 100%;
782
+ min-height: 0;
783
+ overflow: hidden;
784
+ background: var(--admin-page-card-bg);
785
+ border: 1px solid var(--admin-page-divider-strong);
786
+ border-radius: var(--admin-page-radius);
787
+ box-shadow: var(--admin-page-shadow);
788
+ }
789
+ .ai-page.is-session-collapsed {
790
+ grid-template-columns: 44px minmax(0, 1fr);
791
+ }
792
+ .agent-session-collapsed {
793
+ display: flex;
794
+ flex-direction: column;
795
+ gap: 12px;
796
+ align-items: center;
797
+ min-height: 0;
798
+ padding: 16px 8px;
799
+ background: var(--admin-page-card-bg);
800
+ border-right: 1px solid var(--admin-page-divider-strong);
801
+ }
802
+ .agent-session-collapsed__toggle {
803
+ display: inline-flex;
804
+ align-items: center;
805
+ justify-content: center;
806
+ width: 28px;
807
+ height: 28px;
808
+ color: var(--admin-page-text-secondary);
809
+ cursor: pointer;
810
+ background: var(--el-fill-color-light);
811
+ border: 0;
812
+ border-radius: var(--admin-page-radius);
813
+ transition:
814
+ color 0.2s ease,
815
+ background-color 0.2s ease;
816
+ &:hover {
817
+ color: var(--el-color-primary);
818
+ background: var(--el-color-primary-light-9);
819
+ }
820
+ }
821
+ .agent-session-collapsed__label {
822
+ font-size: 12px;
823
+ line-height: 16px;
824
+ color: var(--admin-page-text-secondary);
825
+ user-select: none;
826
+ writing-mode: vertical-rl;
827
+ }
828
+
829
+ @media screen and (width <= 1200px) {
830
+ .ai-page {
831
+ grid-template-columns: 264px minmax(0, 1fr);
832
+ }
833
+ .ai-page.is-session-collapsed {
834
+ grid-template-columns: 44px minmax(0, 1fr);
835
+ }
836
+ }
837
+
838
+ @media screen and (width <= 768px) {
839
+ .ai-page {
840
+ grid-template-columns: 1fr;
841
+ height: 100%;
842
+ min-height: 0;
843
+ }
844
+ .agent-session-collapsed {
845
+ display: none;
846
+ }
847
+ }
848
+ </style>